/** * Roboliq: Automation for liquid-handling robots * @copyright 2017, ETH Zurich, Ellis Whitehead * @license GPL-3.0 */ /** * Module for compiling an instruction list (created by roboliq) to * an Evoware script. * @module */ import _ from 'lodash'; import naturalSort from 'javascript-natural-sort'; import path from 'path'; import M from './Medley.js'; import commandHelper from 'roboliq-processor/dist/commandHelper.js'; import * as EvowareTableFile from './EvowareTableFile.js'; import * as evowareHelper from './commands/evowareHelper.js'; import * as evoware from './commands/evoware.js'; import * as pipetter from './commands/pipetter.js'; import * as system from './commands/system.js'; import * as timer from './commands/timer.js'; import * as transporter from './commands/transporter.js'; const commandHandlers = { "evoware._execute": evoware._execute, "evoware._facts": evoware._facts, "evoware._raw": evoware._raw, "evoware._subroutine": evoware._subroutine, "evoware._userPrompt": evoware._userPrompt, "evoware._variable": evoware._variable, "pipetter._aspirate": pipetter._aspirate, "pipetter._dispense": pipetter._dispense, "pipetter._measureVolume": pipetter._measureVolume, "pipetter._mix": pipetter._mix, "pipetter._pipette": pipetter._pipette, "pipetter._washTips": pipetter._washTips, "system.runtimeExitLoop": system.runtimeExitLoop, "system.runtimeLoadVariables": system.runtimeLoadVariables, "system.runtimeSteps": system.runtimeSteps, "timer._sleep": timer._sleep, "timer._start": timer._start, "timer._wait": timer._wait, "transporter._moveLidFromContainerToSite": transporter._moveLidFromContainerToSite, "transporter._moveLidFromSiteToContainer": transporter._moveLidFromSiteToContainer, "transporter._movePlate": transporter._movePlate }; /** * Compile a protocol for a given evoware setup. * * @param {EvowareCarrierData} carrierData * @param {object} table - table object (see EvowareTableFile.load) * @param {roboliq:Protocol} protocol * @param {array} agents - string array of agent names that this script should generate script(s) for * @param {object} options - an optional map of options; e.g. set timing=false to avoid outputting time-logging instructions * @return {array} an array of {table, lines} items; one item is generated per required table layout. lines is an array of strings. */ export function compile(table, protocol, agents, options = {}) { // console.log(`compile:`) // console.log({options}) options = _.defaults(options, _.get(protocol.config, "evowareCompiler", {})); // console.log({options}) table = _.cloneDeep(table); const objects = _.cloneDeep(protocol.objects); const evowareVariables = {} const results = compileStep(table, protocol, agents, [], objects, evowareVariables, [], options); let lines = []; if (!_.isEmpty(results)) { // Prepend variables // console.log({evowareVariables}) const variableList = _.reverse(_.sortBy(_.toPairs(evowareVariables), x => x[0])); // console.log({variableList}) _.forEach(variableList, ([name, value]) => { // console.log({name, value, line: evowareHelper.createVariableLine(name, value)}) results.unshift({line: evowareHelper.createVariableLine(name, value)}); }); // Prepend token to call 'initRun' results.unshift({line: evowareHelper.createExecuteLine(options.variables.ROBOLIQ, ["initRun", options.variables.SCRIPTFILE], true)}); // Append token to reset the last moved ROMA results.push(transporter.moveLastRomaHome({objects})); lines = _(results).flattenDeep().map(x => x.line).compact().value(); if (!_.isEmpty(lines)) { // Prepend token to create the TEMPDIR if (_.some(lines, line => line.indexOf(options.variables.TEMPDIR) >= 0)) { lines.unshift(evowareHelper.createExecuteLine("cmd", ["/c", "mkdir", options.variables.TEMPDIR], true)); } // Prepend token to open HTML if (_.get(options, "checkBench", true)) { lines.unshift(evowareHelper.createUserPromptLine("Please check the bench setup and then confirm this dialog when you're done")); lines.unshift(evowareHelper.createExecuteLine(options.variables.BROWSER, [path.win32.dirname(options.variables.SCRIPTFILE)+"\\index.html"], false)); } } } return [{table, lines, tokenTree: results}]; } export function compileStep(table, protocol, agents, path, objects, evowareVariables, loopEndStack = [], options = {}) { // console.log(`compileStep: ${path.join(".")}`) // console.log({options}) try { const results = compileStepSub(table, protocol, agents, path, objects, evowareVariables, loopEndStack, options); return results; } catch (e) { console.log("ERROR: "+path.join(".")); console.log(JSON.stringify(_.get(protocol.steps, path))) console.log(e) console.log(e.stack) } return []; } function compileStepSub(table, protocol, agents, path, objects, evowareVariables, loopEndStack, options) { // console.log(`compileStepSub: ${path.join(".")}`) // console.log({options}) if (_.isUndefined(objects)) { objects = _.cloneDeep(protocol.objects); } const stepId = path.join("."); //console.log({steps: protocol.steps}) const step = (_.isEmpty(path)) ? protocol.steps : _.get(protocol.steps, path); // console.log({step}) if (_.isUndefined(step)) return undefined; if (stepId !== "" && protocol.COMPILER) { // Handle suspending if (protocol.COMPILER.suspendStepId) { const cmp = naturalSort(stepId, protocol.COMPILER.suspendStepId); // console.log({stepId, suspendStepId: protocol.COMPILER.suspendStepId, cmp}) // If we've passed the suspend step, quit compiling if (cmp > 0) { return undefined; } // If we're before the suspend step, skip unless the current step is a parent step else if (cmp < 0 && !_.startsWith(protocol.COMPILER.suspendStepId, stepId+".")) { return undefined; } } // Handle resuming if (protocol.COMPILER.resumeStepId) { const cmp = naturalSort(stepId, protocol.COMPILER.resumeStepId); // console.log({stepId, resumeStepId: protocol.COMPILER.resumeStepId, cmp}) // Skip until we've passed the resume step if (cmp <= 0) { return undefined; } } } const results = []; const commandHandler = commandHandlers[step.command]; let generatedCommandLines = false; let generatedTimingLogs = false; const agentMatch = _.isUndefined(step.agent) || _.includes(agents, step.agent); // If there is no command handler for this step, then handle sub-steps if (_.isUndefined(commandHandler) || !agentMatch) { // Find all sub-steps (properties that start with a digit) var keys = _.filter(_.keys(step), function(key) { var c = key[0]; return (c >= '0' && c <= '9'); }); // Sort them in "natural" order keys.sort(naturalSort); // console.log({keys}) const isLoop = _.includes(["system.repeat", "experiment.forEachGroup", "experiment.forEachRow"], step.command); const loopEndName = `_${stepId}End`; const loopEndStack2 = (isLoop) ? [loopEndName].concat(loopEndStack) : loopEndStack; let needLoopLabel = false; // Try to expand the substeps for (const key of keys) { const result1 = compileStep(table, protocol, agents, path.concat(key), objects, evowareVariables, loopEndStack2, options); // Possibly check whether we need a loop label if (isLoop && !needLoopLabel) { const result2 = _.flattenDeep(result1); needLoopLabel = _.some(result2, result => (result.line || "").indexOf(loopEndName) >= 0); } if (!_.isUndefined(result1)) { results.push(result1); } } if (needLoopLabel) { results.push({line: `Comment("${loopEndName}");`}); } } // Else, handle the step's command: else { const data = { objects, //predicates, //planHandlers: protocol.planHandlers, schemas: protocol.schemas, accesses: [], //files: filecache, protocol, path, loopEndStack, evowareVariables }; // Parse command options const schema = protocol.schemas[step.command]; const parsed = (schema) ? commandHelper.parseParams(step, data, schema) : {orig: step}; // Handle the command const result0 = commandHandler(step, parsed, data); // For all returned results: _.forEach(_.compact(result0), result1 => { // console.log("result1: "+JSON.stringify(result1)); results.push(result1); if (result1.effects) { _.forEach(result1.effects, (effect, path2) => { M.setMut(objects, path2, effect); }); } if (!_.isEmpty(result1.tableEffects)) { //console.log("tableEffects: "+JSON.stringify(result1.tableEffects)) _.forEach(result1.tableEffects, ([path2, value]) => { //console.log({path2, value}) // TODO: Need to check whether a table change is required because a different labware model is now used at a given site M.setMut(table, path2, value); }); } }); if (_.isEmpty(results)) { return []; } // Check whether command produced any output lines generatedCommandLines = _.find(_.flattenDeep(results), x => _.has(x, "line")); // Possibly wrap the instructions in calls to pathToRoboliqRuntimeCli in order to check timing // console.log({options, timing: _.get(options, "timing", true)}) if (generatedCommandLines && _.get(options, "timing", false) === true) { const agent = _.get(objects, step.agent); const exePath = "%{ROBOLIQ}";//options.variables.ROBOLIQ; // console.log({agent}) results.unshift({line: evowareHelper.createExecuteLine(exePath, ["begin", "--step", stepId, "--script", "%{SCRIPTFILE}"], false)}); results.push({line: evowareHelper.createExecuteLine(exePath, ["end", "--step", stepId, "--script", "%{SCRIPTFILE}"], false)}); generatedTimingLogs = true; } } // Process the protocol's effects const effects = _.get(protocol, ["effects", stepId]); if (!_.isUndefined(effects)) { _.forEach(effects, (effect, path2) => { M.setMut(objects, path2, effect); }); } const generatedComment = !_.isEmpty(step.description); if (generatedComment) { const hasInstruction = _.find(_.flattenDeep(results), x => _.has(x, "line")); const text = `${stepId}) ${step.description}`; results.unshift({line: `Comment("${text}");`}); } // Possibly wrap the instructions in a group const generatedGroup = (generatedTimingLogs) || (!generatedCommandLines && generatedComment && results.length > 1); if (generatedGroup) { const text = `Step ${stepId}`; results.unshift({line: `Group("${text}");`}); results.push({line: `GroupEnd();`}); } // console.log({stepId, results}) substitutePathVariables(results, options); return results; } function substitutePathVariables(results, options) { // console.log({results, options}) if (!options.variables) return; for (let i = 0; i < results.length; i++) { const result = results[i]; if (_.isArray(result)) { substitutePathVariables(result, options); } else { let line = results[i].line; if (line) { line = line.replace("%{ROBOLIQ}", options.variables.ROBOLIQ); line = line.replace("%{SCRIPTFILE}", options.variables.SCRIPTFILE); line = line.replace("%{SCRIPTDIR}", options.variables.SCRIPTDIR); line = line.replace("%{TEMPDIR}", options.variables.TEMPDIR); results[i].line = line; } } } }