/**
* Roboliq: Automation for liquid-handling robots
* @copyright 2017, ETH Zurich, Ellis Whitehead
* @license GPL-3.0
*/
/**
* Module for converting an EvowareConfigSpec to a general Roboliq configuration protocol.
* @module
*/
const _ = require('lodash');
const assert = require('assert');
const math = require('mathjs');
const commandHelper = require('roboliq-processor/dist/commandHelper.js');
const expect = require('roboliq-processor/dist/expect.js');
const evowareEquipment = require('./equipment/evoware.js');
// For validate():
const Validator = require('jsonschema').Validator;
const YAML = require('yamljs');
/**
* Convert an EvowareConfigSpec into the format of a Roboliq Protocol.
* This function exists, because it is much simpler to write an
* EvowareConfigSpec than the equivalent Protocol.
* @param {EvowareConfigSpec} spec - specification of an Evoware robot configuration
* @param {Object} [data] - (not currently used) protocol data loaded before this configuration
* @return {Protocol} - an Evoware robot configuration in the format of a Protocol.
*/
function makeProtocol(spec, data = {objects: {}, predicates: []}) {
// Validate
const validation = validate(spec);
if (!_.isEmpty(validation.errors)) {
return {
errors: {
EvowareConfigSpec: validation.errors
}
};
}
const namespace = [spec.namespace, spec.name].join(".");
const agent = [spec.namespace, spec.name, "evoware"].join(".");
const predicates = [];
const output = { roboliq: "v1", predicates, commandHandlers: evowareEquipment.getCommandHandlers() };
function getAgentName() {
return [namespace, "evoware"].join(".");
}
function getEquipmentName(equipmentName) {
assert(equipmentName, "equipmentName is undefined");
return [namespace, equipmentName].join(".");
}
function getSiteName(siteName) {
assert(siteName, "siteName is undefined");
return [namespace, "site", siteName].join(".");
}
// Return fully qualified model name - first lookup to see whether the
// model is specific to this robot, and if so, use that name. Otherwise
// use the model name for lab, as defined by spec.namespace.
function getModelName(base) {
assert(base, "modelName is undefined");
const model1 = [spec.namespace, spec.name, "model", base];
const model2 = [spec.namespace, "model", base];
const isOnlyOnRobot = _.has(output.objects, model1);
return (isOnlyOnRobot) ? model1.join(".") : model2.join(".");
}
function lookupSyringe(base) {
assert(base, "lookupSyringe: base name must be defined");
const id1 = [spec.namespace, spec.name, "liha", "syringe", base.toString()];
const id2 = base.toString();
const id =
_.has(output.objects, id1) ? id1 :
_.has(data.objects, id2) ? id2 :
undefined;
assert(!_.isUndefined(id), "syringe not found: "+base);
return id.join(".");
}
function lookupTipModel(base) {
assert(base, "lookupTipModel: base name must be defined");
const id1 = [spec.namespace, spec.name, "liha", "tipModel", base];
const id2 = [spec.namespace, "tipModel", base];
const id3 = base;
// console.log({id1, id2, id3, b1: _.has(output.objects, id1), b2: _.has(data.objects, id2), b3: _.has(data.objects, id3)})
const id =
_.has(output.objects, id1) ? id1 :
_.has(data.objects, id2) ? id2 :
_.has(data.objects, id3) ? id3 :
undefined;
assert(!_.isUndefined(id), "tipModel not found: "+base);
return id.join(".");
}
let siteModelCount = 0;
function addSiteModelCompatibilities(siteModelCompatibilities, output) {
if (_.isUndefined(output.predicates))
output.predicates = [];
// Add predicates for siteModelCompatibilities
_.forEach(siteModelCompatibilities, compat => {
siteModelCount++;
const siteModel = `${namespace}.siteModel${siteModelCount}`;
output.predicates.push({isSiteModel: {model: siteModel}});
_.forEach(compat.sites, site => {
output.predicates.push({siteModel: {site: helpers.getSiteName(site), siteModel}});
});
_.forEach(compat.models, labwareModel => {
output.predicates.push({stackable: {below: siteModel, above: helpers.getModelName(labwareModel)}})
});
});
}
const helpers = {
getAgentName,
getEquipmentName,
getSiteName,
getModelName,
lookupSyringe,
lookupTipModel,
addSiteModelCompatibilities,
};
output.schemas = evowareEquipment.getSchemas();
_.set(output, ["roboliq"], "v1");
_.set(output, ["objects", spec.namespace, "type"], "Namespace");
_.set(output, ["objects", spec.namespace, "model", "type"], "Namespace");
_.set(output, ["objects", spec.namespace, spec.name, "type"], "Namespace");
_.set(output, ["objects", spec.namespace, spec.name, "evoware", "type"], "EvowareRobot");
_.set(output, ["objects", spec.namespace, spec.name, "evoware", "config"], spec.config);
_.set(output, ["objects", spec.namespace, spec.name, "site", "type"], "Namespace");
_.set(output, ["objects", spec.namespace, spec.name, "liha", "type"], "Pipetter");
// Add 5 timers
_.forEach(_.range(5), i => {
const equipment = [spec.namespace, spec.name, `timer${i+1}`].join(".");
_.set(output.objects, equipment, {
type: "Timer",
evowareId: i+1
});
output.predicates.push({ "timer.canAgentEquipment": {agent, equipment} });
});
// Add bench sites (equipment sites will be added by the equipment modules)
_.forEach(spec.sites, (value, key) => {
_.set(output, ["objects", spec.namespace, spec.name, "site", key], _.merge({type: "Site"}, value));
});
// Add explicitly defined models to spec.namespace
_.forEach(spec.models, (value, key) => {
_.set(output, ["objects", spec.namespace, "model", key], value);
});
// Add predicates for siteModelCompatibilities
addSiteModelCompatibilities(spec.siteModelCompatibilities, output);
// Lid and plate stacking
_.forEach(spec.lidStacking, lidsModels => {
_.forEach(lidsModels.lids, lid => {
_.forEach(lidsModels.models, model => {
const below = getModelName(model);
const above = getModelName(lid);
predicates.push({stackable: {below, above}});
});
});
});
handleEquipment(spec, helpers, namespace, agent, output);
handleRomas(spec, helpers, namespace, agent, output);
handleLiha(spec, helpers, namespace, agent, output);
output.objectToPredicateConverters = evowareEquipment.objectToPredicateConverters;
if (spec.planAlternativeChoosers) {
output.planAlternativeChoosers = spec.planAlternativeChoosers;
// console.log({planAlternativeChoosers: output.planAlternativeChoosers})
}
// User-defined commandHandlers
_.forEach(spec.commandHandlers, (fn, key) => {
output.commandHandlers[key] = fn;
});
return output;
}
/**
* Create the predictates to be added to Roboliq's robot
* configuration for Evoware's RoMa relationships.
*
* Expect specs of this form:
* ``{<transporter>: {<program>: [site names]}}``
* @param {Object} spec
* @param {Roma[]} spec.romas
*/
function handleRomas(spec, helpers, namespace, agent, output) {
let siteCliqueId = 1;
_.forEach(spec.romas, (roma, i) => {
// Add the roma object
_.set(output, ["objects", spec.namespace, spec.name, `roma${i+1}`], {type: "Transporter", evowareRoma: i});
const equipment = [spec.namespace, spec.name, `roma${i+1}`].join(".");
_.forEach(roma.safeVectorCliques, safeVectorClique => {
const siteClique = `${namespace}.siteClique${siteCliqueId}`;
siteCliqueId++;
const program = safeVectorClique.vector;
// Add the site clique predicates
_.forEach(safeVectorClique.clique, base => {
const site = helpers.getSiteName(base);
output.predicates.push({"siteCliqueSite": {siteClique, site}});
});
// Add the transporter predicates
output.predicates.push({
"transporter.canAgentEquipmentProgramSites": {
agent,
equipment,
program,
siteClique
}
});
});
});
}
function handleLiha(spec, helpers, namespace, agent, output) {
if (!spec.liha) return;
const equipment = [spec.namespace, spec.name, "liha"].join(".");
const tipModelToSyringes = {};
if (_.isPlainObject(spec.liha.tipModels)) {
const tipModels = _.mapValues(spec.liha.tipModels, x => _.merge({type: "TipModel"}, x));
// console.log(tipModels)
_.set(output.objects, [spec.namespace, spec.name, "liha", "tipModel"], tipModels);
// console.log({stuff: _.get(output, ["object", spec.namespace, spec.name, "liha", "tipModel"])});
}
output.schemas[`pipetter.cleanTips|${agent}|${equipment}`] = {
description: "Clean the pipetter tips.",
properties: {
agent: {description: "Agent identifier", type: "Agent"},
equipment: {description: "Equipment identifier", type: "Equipment"},
program: {description: "Program identifier", type: "string"},
items: {
description: "List of which syringes to clean at which intensity",
type: "array",
items: {
type: "object",
properties: {
syringe: {description: "Syringe identifier", type: "Syringe"},
intensity: {description: "Intensity of the cleaning", type: "pipetter.CleaningIntensity"}
},
required: ["syringe", "intensity"]
}
}
},
required: ["agent", "equipment", "items"]
};
// Add syringes
_.set(output.objects, [spec.namespace, spec.name, "liha", "syringe"], {});
_.forEach(spec.liha.syringes, (syringeSpec, i) => {
// console.log({syringeSpec})
const syringe = [spec.namespace, spec.name, "liha", "syringe", (i+1).toString()].join(".");
const syringeObj = {
type: "Syringe",
row: i + 1
};
// console.log({syringeObj})
_.set(output.objects, syringe, syringeObj);
// Handle permanent tips
if (syringeSpec.tipModelPermanent) {
const tipModel = helpers.lookupTipModel(syringeSpec.tipModelPermanent);
syringeObj.tipModel = tipModel;
syringeObj.tipModelPermanent = tipModel;
tipModelToSyringes[tipModel] = (tipModelToSyringes[tipModel] || []).concat([syringe]);
}
else {
assert(false, "roboliq-evoware currently only supports fixed tips; please contact the software developer to add support for disposable tips.")
}
});
// Handle tipModelToSyringes mapping for non-permantent tips
_.forEach(spec.liha.tipModelToSyringes, (syringes0, tipModel0) => {
const tipModel = helpers.lookupTipModel(syringeSpec.tipModelPermanent);
const syringes = syringes0.map(helpers.lookupSyringe);
tipModelToSyringes[tipModel] = (tipModelToSyringes[tipModel] || []).concat(syringes);
});
// console.log({tipModelToSyringes})
_.set(output.objects, [spec.namespace, spec.name, "liha", "tipModelToSyringes"], tipModelToSyringes);
// Handle washPrograms
if (spec.liha.washPrograms) {
const washPrograms = _.merge({type: "Namespace"}, _.mapValues(spec.liha.washPrograms, x => _.merge({type: "EvowareWashProgram"}, x)));
_.set(output.objects, [spec.namespace, spec.name, "washProgram"], washPrograms);
}
// Add system liquid
const syringeCount = spec.liha.syringes.length;
assert(syringeCount <= 8, "roboliq-evoware has only been configured to handle 8-syringe LiHas; please contact the software developer to accommodate your needs.");
_.set(output.objects, [spec.namespace, spec.name, "systemLiquidLabwareModel"], {
"type": "PlateModel",
"description": "dummy labware model representing the system liquid source",
"rows": syringeCount,
"columns": 1,
"evowareName": "SystemLiquid"
});
_.set(output.objects, [spec.namespace, spec.name, "systemLiquid"], {
"type": "Liquid",
"wells": _.map(_.range(syringeCount), i => `${spec.namespace}.${spec.name}.systemLiquidLabware(${String.fromCharCode(65 + i)}01)`)
});
_.set(output.objects, [spec.namespace, spec.name, "systemLiquidLabware"], {
"type": "Plate",
"description": "dummy labware representing the system liquid source",
"model": `${namespace}.systemLiquidLabwareModel`,
"location": helpers.getSiteName("SYSTEM"),
"contents": ["Infinity l", "systemLiquid"]
});
// Equipment predicates
output.predicates.push({
"pipetter.canAgentEquipment": {
agent,
equipment
}
});
// Syringe predicates
_.forEach(spec.liha.syringes, (syringeSpec, i) => {
output.predicates.push({
"pipetter.canAgentEquipmentSyringe": {
agent,
equipment,
syringe: `ourlab.mario.liha.syringe.${i+1}`
}
})
});
// Site predicates
_.forEach(spec.liha.sites, site0 => {
const site = helpers.getSiteName(site0);
output.predicates.push({
"pipetter.canAgentEquipmentSite": {
agent,
equipment,
site
}
});
});
// Command handler for `pipetter.cleanTips`
output.commandHandlers[`pipetter.cleanTips|${agent}|${equipment}`] = makeCleanTipsHandler(namespace);
}
function makeCleanTipsHandler(namespace) {
return function cleanTips(params, parsed, data) {
//console.log("pipetter.cleanTips|ourlab.mario.evoware|ourlab.mario.liha")
//console.log(JSON.stringify(parsed, null, ' '))
const cleaningIntensities = data.schemas["pipetter.CleaningIntensity"].enum;
const syringeNameToItems = _.map(parsed.value.items, (item, index) => [parsed.objectName[`items.${index}.syringe`], item]);
//console.log(syringeNameToItems);
const expansionList = [];
const sub = function(syringeNames, volume) {
const syringeNameToItems2 = _.filter(syringeNameToItems, ([syringeName, ]) =>
_.includes(syringeNames, syringeName)
);
//console.log({syringeNameToItems2})
if (!_.isEmpty(syringeNameToItems2)) {
const value = _.max(_.map(syringeNameToItems2, ([, item]) => cleaningIntensities.indexOf(item.intensity)));
if (value >= 0) {
const intensity = cleaningIntensities[value];
const syringes = _.map(syringeNameToItems2, ([syringeName, ]) => syringeName);
expansionList.push({
command: "pipetter._washTips",
agent: parsed.objectName.agent,
equipment: parsed.objectName.equipment,
program: `${namespace}.washProgram.${intensity}_${volume}`,
intensity: intensity,
syringes: syringeNames
});
}
}
}
// Get list of syringes on the liha
const syringesName = `${namespace}.liha.syringe`;
const syringesObj = _.get(data.objects, syringesName);
assert(syringesObj, "didn't find LiHa syringes "+syringesName);
// Lists of [syringeName, tipModelName, programCode]
const l = _.map(syringesObj, (syringeObj, syringeName0) => {
const syringeName = `${namespace}.liha.syringe.${syringeName0}`;
// console.log({syringeObj})
const tipModelName = syringeObj.tipModel;
const tipModelObj = _.get(data.objects, tipModelName);
assert(tipModelObj, "didn't find tipModel "+tipModelName);
return [syringeName, tipModelName, tipModelObj.programCode];
});
// console.log({l})
// Group by program code, and call `sub()`
const m = _.groupBy(l, x => x[2]);
_.forEach(m, (l, programCode) => {
sub(l.map(x => x[0]), programCode);
})
return {expansion: expansionList};
};
}
function handleEquipment(spec, helpers, namespace, agent, output) {
_.forEach(spec.equipment, (value, key) => {
// console.log({key})
const module = require(__dirname+"/equipment/"+value.module);
const protocol = module.configure(helpers, key, value.params);
// console.log(key+": "+JSON.stringify(protocol, null, '\t'))
// console.log(key+".objects: "+JSON.stringify(protocol.objects, null, '\t'))
_.merge(output, _.omit(protocol, "predicates"));
// console.log("output.objects: "+JSON.stringify(output.objects, null, '\t'))
if (!_.isEmpty(protocol.predicates))
output.predicates.push(...protocol.predicates);
});
}
/*
function test() {
const evowareSpec = require('/Users/ellisw/src/roboliq/config/bsse-mario-new.js');
const orig = require('/Users/ellisw/src/roboliq/config/bsse-mario.js');
const protocol = process(evowareSpec);
// console.log(JSON.stringify(protocol, null, '\t'));
const diff = require('deep-diff');
// console.log("isSiteModel predicates: "+JSON.stringify(_.filter(protocol.predicates, x => Object.keys(x)[0] == "isSiteModel")));
// console.log("siteCliqueSite predicates: "+JSON.stringify(_.filter(protocol.predicates, x => Object.keys(x)[0] == "siteCliqueSite"), null, '\t'));
protocol.predicates = _.fromPairs(_.sortBy(protocol.predicates.map(x => [JSON.stringify(x), x]), x => x[0]));
orig.predicates = _.fromPairs(_.sortBy(orig.predicates.map(x => [JSON.stringify(x), x]), x => x[0]));
const diffs = diff(_.omit(orig, "objectToPredicateConverters"), _.omit(protocol, "objectToPredicateConverters"));
const diffs2 = _.filter(diffs, d => (
(d.kind == "E" && d.path[0] == "commandHandlers") ? false
: (d.kind == "E" && d.path[0] == "planHandlers") ? false
: true
));
console.log(JSON.stringify(diffs2, null, '\t'));
}
*/
/**
* Validates a EvowareConfigSpec against the JSON schema.
* @param {EvowareConfigSpec} evowareSpec - evoware config spec
* @return {object} - returns the validation results from the npm package `jsonschema`
*/
function validate(evowareSpec) {
const v = new Validator();
const schemas = YAML.load(__dirname+"/schemas/EvowareConfigSpec.yaml");
// console.log(JSON.stringify(schemas, null, '\t'));
_.forEach(schemas, (schema, name) => {
const id = "/"+name;
v.addSchema(_.merge({id}, schema), id);
});
// console.log(JSON.stringify(evowareSpec, null, '\t'));
// console.log(evowareSpec);
// See: http://json-schema.org/example2.html
// See: https://spacetelescope.github.io/understanding-json-schema/structuring.html
// TODO: raise error on unknown type
// TODO: add some extra types, such as `function`, see
// https://www.npmjs.com/package/jsonschema
// https://www.npmjs.com/package/jsonschema-extra
const result = v.validate(evowareSpec, schemas.EvowareConfigSpec);
// console.log(result);
return result;
}
module.exports = {
makeProtocol,
validate
};