ai-planning-val
Version:
Javascript/typescript wrapper for VAL (AI Planning plan validation and evaluation tools from KCL Planning department and the planning community around the ICAPS conference).
497 lines • 25.1 kB
JavaScript
"use strict";
/* --------------------------------------------------------------------------------------------
* Copyright (c) Jan Dolejsi. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ValStep = exports.ValStepExitCode = exports.ValStepError = void 0;
const process = __importStar(require("child_process"));
const events_1 = require("events");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const pddl_workspace_1 = require("pddl-workspace");
const HappeningsToValStep_1 = require("./HappeningsToValStep");
const valUtils_1 = require("./valUtils");
class ValStepError extends Error {
constructor(message, domain, problem, valStepInput) {
super(message);
this.message = message;
this.domain = domain;
this.problem = problem;
this.valStepInput = valStepInput;
}
}
exports.ValStepError = ValStepError;
class ValStepExitCode extends Error {
constructor(message) {
super(message);
}
}
exports.ValStepExitCode = ValStepExitCode;
/**
* Wraps the Valstep executable.
* @see https://github.com/KCL-Planning/VAL/blob/master/applications/README.md#valstep
*/
class ValStep extends events_1.EventEmitter {
constructor(domainInfo, problemInfo) {
super();
this.domainInfo = domainInfo;
this.problemInfo = problemInfo;
this.valStepInput = '';
this.outputBuffer = '';
/** Default file name */
this.VALSTEP_EXE = 'ValStep';
this.valStepOutputPattern = /^(?:(?:\? )?Posted action \d+\s+)*(?:\? )+Seeing (\d+) changed lits\s*([\s\S]*)\s+\?\s*$/m;
this.valStepLiteralsPattern = /([\w-]+(?: [\w-]+)*) - now (true|false|[+-]?\d+\.?\d*(?:e[+-]?\d+)?)/g;
this.variableValues = problemInfo.getInits().map(v => pddl_workspace_1.TimedVariableValue.copy(v));
this.initialValues = this.variableValues.map(v => pddl_workspace_1.TimedVariableValue.copy(v));
this.happeningsConvertor = new HappeningsToValStep_1.HappeningsToValStep();
}
/**
* Subscribe to the state update event.
* @param callback state update callback
* @returns `this`
*/
onStateUpdated(callback) {
return this.on(ValStep.NEW_HAPPENING_EFFECTS, callback);
}
/**
* Subscribe to the state update event (once).
* @param callback state update callback
* @returns `this`
*/
onceStateUpdated(callback) {
return this.once(ValStep.NEW_HAPPENING_EFFECTS, callback);
}
/**
* Executes series of plan happenings in one batch without waiting for incremental effect evaluation.
* @param happenings plan happenings to play
* @param options ValStep execution options
* @returns final variable values, or undefined in case the ValStep fails
*/
executeBatch(happenings, options) {
return __awaiter(this, void 0, void 0, function* () {
if (this.childProcess) {
throw new Error(`This ValStep instance was already used. Create new one`);
}
this.valStepInput = this.convertHappeningsToValStepInput(happenings);
if (options === null || options === void 0 ? void 0 : options.verbose) {
console.log("ValStep >>>" + this.valStepInput);
}
let args = yield this.createValStepArgs();
const valStepsPath = yield valUtils_1.Util.toFile(this.valStepInput, { prefix: 'valSteps', suffix: '.valsteps' });
args = ['-i', valStepsPath, ...args];
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
this.logValStepCommand(options, args);
const child = that.childProcess = process.spawn(this.createValCommand(options), args, options);
let outputtingProblem = false;
if (!child.stdout) {
reject(new Error(`ValStep child process has no 'stdout'`));
console.log(child.kill() ? "ValStep killed" : "ValStep not killed yet.");
return;
}
child.stdout.on('data', output => {
const outputString = output.toString("utf8");
if (options === null || options === void 0 ? void 0 : options.verbose) {
console.log("ValStep <<<" + outputString);
}
if (outputtingProblem) {
that.outputBuffer += outputString;
}
else if (outputString.indexOf('(define (problem') >= 0) {
that.outputBuffer = outputString.substr(outputString.indexOf('(define (problem'));
outputtingProblem = true;
}
});
child.on("error", error => reject(new ValStepError(error.message, this.domainInfo, this.problemInfo, this.valStepInput)));
child.on("close", (code, signal) => __awaiter(this, void 0, void 0, function* () {
if (code !== 0) {
console.log(`ValStep exit code: ${code}, signal: ${signal}.`);
}
const eventualProblem = that.outputBuffer;
const newValues = yield that.extractInitialState(eventualProblem);
// shift the time of the values to the plan makespan
newValues === null || newValues === void 0 ? void 0 : newValues.forEach(v => v.update(that.happeningsConvertor.makespan, v.getVariableValue()));
resolve(newValues);
}));
}));
});
}
createValCommand(options) {
var _a;
return (_a = options === null || options === void 0 ? void 0 : options.valStepPath) !== null && _a !== void 0 ? _a : this.VALSTEP_EXE;
}
logValStepCommand(options, args) {
if (options === null || options === void 0 ? void 0 : options.verbose) {
console.log(`ValStep command: ${this.createValCommand(options)}\nValStep args: ${args.join(' ')}\nValStep cwd: ${options.cwd}`);
}
}
convertHappeningsToValStepInput(happenings) {
const groupedHappenings = pddl_workspace_1.utils.Util.groupBy(happenings, (h) => h.getTime());
let valStepInput = '';
[...groupedHappenings.keys()]
.sort((a, b) => a - b)
.forEach((time, batchId) => {
const happeningGroup = groupedHappenings.get(time);
if (happeningGroup) {
const valSteps = this.happeningsConvertor.convert(happeningGroup, batchId);
valStepInput += valSteps;
}
else {
console.warn(`Did not find happening group corresponding to time ${time}.`);
}
});
valStepInput += ValStep.QUIT_INSTRUCTION;
return valStepInput;
}
/**
* Executes series of plan happenings capturing the PDDL problem that VAL outputs at the end.
* @param happenings plan happenings to play
* @param options ValStep execution options
* @returns final variable values, or null/undefined in case the tool fails
*/
execute(happenings, options) {
return __awaiter(this, void 0, void 0, function* () {
if (this.childProcess) {
throw new Error(`This ValStep instance was already used. Create new one`);
}
const args = yield this.createValStepArgs();
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
this.logValStepCommand(options, args);
const child = that.childProcess = process.execFile(this.createValCommand(options), args, { cwd: options === null || options === void 0 ? void 0 : options.cwd, timeout: 2000, maxBuffer: 2 * 1024 * 1024 }, (error, stdout, stderr) => __awaiter(this, void 0, void 0, function* () {
if (error) {
reject(new ValStepError(error.message, this.domainInfo, this.problemInfo, this.valStepInput));
return;
}
if (options === null || options === void 0 ? void 0 : options.verbose) {
console.log(stdout);
console.log(stderr);
}
const eventualProblem = that.outputBuffer;
const newValues = yield that.extractInitialState(eventualProblem);
resolve(newValues);
}));
let outputtingProblem = false;
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', output => {
if (options === null || options === void 0 ? void 0 : options.verbose) {
console.log("ValStep <<<" + output);
}
if (outputtingProblem) {
this.outputBuffer += output;
}
else if (output.indexOf('(define (problem') >= 0) {
this.outputBuffer = output.substr(output.indexOf('(define (problem'));
outputtingProblem = true;
}
});
const groupedHappenings = pddl_workspace_1.utils.Util.groupBy(happenings, (h) => h.getTime());
for (const time of groupedHappenings.keys()) {
const happeningGroup = groupedHappenings.get(time);
if (happeningGroup) {
const valSteps = this.happeningsConvertor.convert(happeningGroup);
this.valStepInput += valSteps;
try {
if (!((_b = child.stdin) === null || _b === void 0 ? void 0 : _b.write(valSteps))) {
reject('Failed to post happenings to valstep');
return;
}
if (options === null || options === void 0 ? void 0 : options.verbose) {
console.log("ValStep >>>" + valSteps);
}
}
catch (err) {
if (options === null || options === void 0 ? void 0 : options.verbose) {
console.log("ValStep input causing error: " + valSteps);
}
reject('Sending happenings to valstep caused error: ' + err);
return;
}
}
else {
console.warn(`Did not find happening group for time ${time}.`);
}
}
this.valStepInput += ValStep.QUIT_INSTRUCTION;
if (options === null || options === void 0 ? void 0 : options.verbose) {
console.log("ValStep >>> " + ValStep.QUIT_INSTRUCTION);
}
(_c = child.stdin) === null || _c === void 0 ? void 0 : _c.write(ValStep.QUIT_INSTRUCTION);
}));
});
}
/**
* Parses the problem file and extracts the initial state.
* @param problemText problem file content output by ValStep
* @returns variable values array, or null if the tool failed
*/
extractInitialState(problemText) {
return __awaiter(this, void 0, void 0, function* () {
const problemInfo = yield pddl_workspace_1.parser.PddlProblemParser.parseText(problemText);
if (!problemInfo) {
return undefined;
}
return problemInfo.getInits();
});
}
startValStep(options) {
return __awaiter(this, void 0, void 0, function* () {
if (this.childProcess) {
throw new Error(`This ValStep instance was already used. Create new one`);
}
const args = yield this.createValStepArgs();
this.logValStepCommand(options, args);
return this.childProcess = process.execFile(this.createValCommand(options), args, options);
});
}
/**
* Executes series of plan happenings, while waiting for each burst of happenings (scheduled at the same time) to evaluate effects.
* @param happenings plan happenings to play
* @param options ValStep execution options
* @returns final variable values
*/
executeIncrementally(happenings, options) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
if (this.childProcess) {
throw new Error(`This ValStep instance was already used. Create new one`);
}
const child = yield this.startValStep(options);
// subscribe to the child process standard output stream and concatenate it till it is complete
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', output => {
if (options === null || options === void 0 ? void 0 : options.verbose) {
console.log("ValStep <<<" + output);
}
this.outputBuffer += output;
if (this.isOutputComplete(this.outputBuffer)) {
const variableValues = this.parseEffects(this.outputBuffer);
this.outputBuffer = ''; // reset the output buffer
this.emit(ValStep.HAPPENING_EFFECTS_EVALUATED, variableValues);
}
});
// subscribe to the process exit event to be able to report possible crashes
child.on("error", err => this.throwValStepError(err));
child.on("exit", (code, signal) => this.throwValStepExitCode(code, signal));
const groupedHappenings = pddl_workspace_1.utils.Util.groupBy(happenings, (h) => h.getTime());
for (const time of groupedHappenings.keys()) {
const happeningGroup = groupedHappenings.get(time);
if (happeningGroup) {
yield this.postHappenings(happeningGroup, options);
}
else {
console.warn(`Could not find happening group for time ${time}.`);
}
}
(_b = child.stdin) === null || _b === void 0 ? void 0 : _b.write('q\n');
return this.variableValues;
});
}
createValStepArgs() {
return __awaiter(this, void 0, void 0, function* () {
// copy editor content to temp files to avoid using out-of-date content on disk
try {
const domainFilePath = yield valUtils_1.Util.toPddlFile(this.domainInfo.getCompiledText(), { prefix: 'domain' });
const problemFilePath = yield valUtils_1.Util.toPddlFile(this.problemInfo.getCompiledText(), { prefix: 'problem' }); // todo: this is where we are sending un-pre-processed problem text when rendering plan
const args = [domainFilePath, problemFilePath];
return args;
}
catch (err) {
console.log(err);
throw err;
}
});
}
/**
* Posts happening interactively.
* @param happenings happenings group (typically sharing the same timestamp)
* @param options execution options
*/
postHappenings(happenings, options) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.childProcess) {
this.childProcess = yield this.startValStep(options);
}
const childProcess = this.childProcess;
const valSteps = this.happeningsConvertor.convert(happenings);
this.valStepInput += valSteps;
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
return new Promise((resolve, reject) => {
var _a, _b;
const lastHappening = happenings[happenings.length - 1];
const lastHappeningTime = lastHappening.getTime();
const timeOut = setTimeout(lastHappeningTime1 => {
childProcess.kill();
reject(`ValStep did not respond to happenings @ ${lastHappeningTime1}`);
return;
}, (_a = options === null || options === void 0 ? void 0 : options.timeout) !== null && _a !== void 0 ? _a : 500, lastHappeningTime);
// subscribe to the valstep child process updates
that.once(ValStep.HAPPENING_EFFECTS_EVALUATED, (effectValues) => {
clearTimeout(timeOut);
const newValues = effectValues.filter(v => that.applyIfNew(lastHappeningTime, v));
if (newValues.length > 0) {
this.emit(ValStep.NEW_HAPPENING_EFFECTS, happenings, newValues);
}
resolve(true);
});
try {
if (!((_b = childProcess.stdin) === null || _b === void 0 ? void 0 : _b.write(valSteps))) {
reject('Cannot post happenings to valstep');
}
if (options === null || options === void 0 ? void 0 : options.verbose) {
console.log("ValStep >>>" + valSteps);
}
}
catch (err) {
if (options === null || options === void 0 ? void 0 : options.verbose) {
console.log("ValStep intput causing error: " + valSteps);
}
reject('Cannot post happenings to valstep: ' + err);
}
});
});
}
applyIfNew(time, value) {
const currentValue = this.variableValues.find(v => v.getVariableName().toLowerCase() === value.getVariableName().toLowerCase());
if (currentValue === undefined) {
this.variableValues.push(pddl_workspace_1.TimedVariableValue.from(time, value));
return true;
}
else {
if (value.getValue() === currentValue.getValue()) {
return false;
}
else {
currentValue.update(time, value);
return true;
}
}
}
throwValStepExitCode(code, signal) {
if (code !== null && code !== 0) {
throw new ValStepExitCode(`ValStep exit code ${code} and signal ${signal}`);
}
}
throwValStepError(err) {
throw new ValStepError(`ValStep failed with error ${err.name} and message ${err.message}`, this.domainInfo, this.problemInfo, this.valStepInput);
}
isOutputComplete(output) {
var _a, _b;
this.valStepOutputPattern.lastIndex = 0;
const match = this.valStepOutputPattern.exec(output);
if (match && match[2]) {
const expectedChangedLiterals = parseInt(match[1]);
const changedLiterals = match[2];
if (expectedChangedLiterals === 0) {
return true;
} // the happening did not have any effects
this.valStepLiteralsPattern.lastIndex = 0;
const actualChangedLiterals = (_b = (_a = changedLiterals.match(this.valStepLiteralsPattern)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
return expectedChangedLiterals <= actualChangedLiterals; // functions are not included in the expected count
}
else {
return false;
}
}
parseEffects(happeningsEffectText) {
const effectValues = [];
this.valStepOutputPattern.lastIndex = 0;
const match = this.valStepOutputPattern.exec(happeningsEffectText);
if (match) {
const changedLiterals = match[2];
this.valStepLiteralsPattern.lastIndex = 0;
let match1;
while (match1 = this.valStepLiteralsPattern.exec(changedLiterals)) {
const variableName = match1[1];
const valueAsString = match1[2];
let value;
if (valueAsString === "true") {
value = true;
}
else if (valueAsString === "false") {
value = false;
}
else if (!isNaN(parseFloat(valueAsString))) {
value = parseFloat(valueAsString);
}
else {
console.warn(`Unexpected variable value: '${valueAsString}' in ${changedLiterals}`);
value = Number.NaN;
}
effectValues.push(new pddl_workspace_1.VariableValue(variableName, value));
}
return effectValues;
}
else {
throw new Error(`ValStep output does not parse: ${happeningsEffectText}`);
}
}
getUpdatedValues() {
return this.variableValues
.filter(value1 => this.changedFromInitial(value1));
}
changedFromInitial(value1) {
return !this.initialValues.some(value2 => value1.sameValue(value2));
}
static storeError(err, targetDirectoryFsPath, valStepPath) {
return __awaiter(this, void 0, void 0, function* () {
const targetDir = targetDirectoryFsPath;
const caseDir = 'valstep-' + new Date().toISOString().split(':').join('-');
const casePath = path.join(targetDir, caseDir);
yield pddl_workspace_1.utils.afs.mkdirIfDoesNotExist(casePath, 0o644);
const domainFile = "domain.pddl";
const problemFile = "problem.pddl";
const inputFile = "happenings.valsteps";
yield fs.promises.writeFile(path.join(casePath, domainFile), err.domain.getCompiledText(), { encoding: "utf-8" });
yield fs.promises.writeFile(path.join(casePath, problemFile), err.problem.getCompiledText(), { encoding: "utf-8" });
yield fs.promises.writeFile(path.join(casePath, inputFile), err.valStepInput, { encoding: "utf-8" });
const command = `:: The purpose of this batch file is to be able to reproduce the valstep error
type ${inputFile} | ${pddl_workspace_1.utils.Util.q(valStepPath)} ${domainFile} ${problemFile}
:: or for latest version of ValStep:
${pddl_workspace_1.utils.Util.q(valStepPath)} -i ${inputFile} ${domainFile} ${problemFile}`;
yield fs.promises.writeFile(path.join(casePath, "run.cmd"), command, { encoding: "utf-8" });
return casePath;
});
}
}
exports.ValStep = ValStep;
ValStep.QUIT_INSTRUCTION = 'q\n';
ValStep.HAPPENING_EFFECTS_EVALUATED = Symbol("HAPPENING_EFFECTS_EVALUATED");
ValStep.NEW_HAPPENING_EFFECTS = Symbol("NEW_HAPPENING_EFFECTS");
//# sourceMappingURL=ValStep.js.map