UNPKG

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
"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