/**
* Roboliq: Automation for liquid-handling robots
* @copyright 2017, ETH Zurich, Ellis Whitehead
* @license GPL-3.0
*/
/**
* Roboliq's default directives.
* @module config/roboliqDirectiveHandlers
*/
var _ = require('lodash');
var assert = require('assert');
var math = require('mathjs');
var random = require('random-js');
import commandHelper from '../commandHelper.js';
var Design = require('../design.js');
var expect = require('../expect.js');
var misc = require('../misc.js');
var wellsParser = require('../parsers/wellsParser.js');
// TODO: analyze wells string (call wellsParser)
// TODO: zipMerge to merge destinations into mix items (but error somehow if not enough destinations)
// TODO: extractKey list of destinations from items
// TODO: extractValue list of destinations from items
// TODO: extract unique list of destinations, make it a well string?
function handleDirective(spec, data) {
return misc.handleDirective(spec, data);
}
function directive_createWellAssignments(spec, data) {
return expect.try("#createWellAssignments", () => {
const parsed = commandHelper.parseParams(spec, data, {
properties: {
list: {type: "array"},
wells: {type: "Wells"}
},
required: ["list", "wells"]
});
return _.take(parsed.value.wells, parsed.value.list.length);
});
}
function directive_data(spec, data) {
// console.log("directive_data: "+JSON.stringify(spec, null, '\t'))
// console.log("DATA: "+JSON.stringify(data.objects.DATA, null, '\t'))
return expect.try("data()", () => {
const updatedSCOPEDATA = commandHelper.updateSCOPEDATA({data: spec}, data);
// console.log({updateDATA: updatedSCOPEDATA.DATA})
let result = Design.query(updatedSCOPEDATA.DATA, spec);
// console.log("result0: "+JSON.stringify(result))
if (spec.templateGroup) {
result = _.map(result, DATA => {
const SCOPE = _.merge({}, updatedSCOPEDATA.SCOPE, Design.getCommonValues(DATA));
return commandHelper.substituteDeep(spec.templateGroup, data, SCOPE, DATA);
});
// console.log("result1: "+JSON.stringify(result))
}
else if (spec.hasOwnProperty("map") || spec.template) {
const template = _.get(spec, "map", spec.template);
// console.log("result: "+JSON.stringify(result, null, '\t'))
result = _.map(result, DATA => _.map(DATA, row => {
const SCOPE = _.defaults({}, row, updatedSCOPEDATA.SCOPE);
// console.log("row: "+JSON.stringify(row, null, '\t'))
// console.log("SCOPE: "); console.log(SCOPE);
return commandHelper.substituteDeep(template, data, SCOPE, undefined);
}));
// console.log("result1: "+JSON.stringify(result))
}
// deprecated, use `map`
if (spec.value) {
result = _.map(result, DATA => _.map(DATA, row => {
if (_.startsWith(spec.value, "$`")) {
const SCOPE = _.merge({}, updatedSCOPEDATA.SCOPE, row);
return commandHelper.substituteDeep(spec.value, data, SCOPE, DATA);
}
else {
return row[spec.value];
}
}));
// console.log("result1: "+JSON.stringify(result))
}
if (spec.hasOwnProperty("summarize")) {
// console.log("result: "+JSON.stringify(result, null, '\t'))
result = _.map(result, DATA => {
const SCOPE = _.clone(updatedSCOPEDATA.SCOPE);
const columns = getColumns(DATA);
_.forEach(columns, key => {
SCOPE[key] = _.map(DATA, key);
});
// console.log(SCOPE);
// console.log("SCOPE: "+JSON.stringify(SCOPE, null, '\t'))
return commandHelper.substituteDeep(spec.summarize, data, SCOPE, DATA, false);
});
// console.log("result1: "+JSON.stringify(result))
}
result = (spec.groupBy) ? result : _.flatten(result);
// console.log("result2: "+JSON.stringify(result))
if (spec.flatten) {
result = _.flatten(result);
// console.log("result3: "+JSON.stringify(result))
}
if (spec.head) {
result = _.head(result);
// console.log("result4: "+JSON.stringify(result))
}
else if (spec.unique) {
result = _.uniq(result);
}
if (spec.join) {
result = result.join(spec.join);
// console.log("result5: "+JSON.stringify(result))
}
if (spec.orderBy) {
result = _.orderBy(result, spec.orderBy);
}
if (spec.reverse) {
result = _.reverse(result);
}
// console.log("result: "+JSON.stringify(result, null, '\t'))
return result;
});
}
function getColumns(DATA) {
// Get column names
const columnMap = {};
_.forEach(DATA, row => _.forEach(_.keys(row), key => { columnMap[key] = true; } ));
const columns = _.keys(columnMap);
// console.log({columns})
return columns;
}
function directive_destinationWells(spec, data) {
expect.truthy({}, _.isString(spec), "#destinationWells: expected string, received "+spec);
return directive_wells(spec, data);
}
function directive_for(spec, data) {
// console.log({spec})
expect.paramsRequired(spec, ['factors', 'output']);
var views = directive_factorialCols(spec.factors, data);
//console.log("views:", views);
return _.map(views, function(view) {
var rendered = misc.renderTemplate(spec.output, view, data);
//console.log("rendered:", rendered);
var rendered2 = misc.handleDirectiveDeep(rendered, data);
//console.log("rendered2:", rendered2);
return rendered2;
});
}
function directive_factorialArrays(spec, data) {
//console.log("genList:", spec);
assert(_.isArray(spec));
var lists = _.map(spec, function(elem) { return handleDirective(elem, data); });
return combineLists(lists);
}
function combineLists(elem) {
//console.log("combineLists: ", elem);
var list = [];
if (_.isArray(elem) && !_.isEmpty(elem)) {
list = elem[0];
if (!_.isArray(list))
list = [list];
for (var i = 1; i < elem.length; i++) {
//console.log("list@"+i, list);
list = _(list).map(function(x) {
//console.log("x", x);
if (!_.isArray(x))
x = [x];
return elem[i].map(function(y) { return x.concat(y); });
}).flatten().value();
}
}
return list;
}
function directive_factorialCols(spec, data) {
//console.log("genFactorialCols:", spec);
assert(_.isPlainObject(spec) || _.isArray(spec));
var variables = (_.isPlainObject(spec))
? _.toPairs(spec)
: _(spec).map(_.toPairs).flatten().value();
var lists = _.map(variables, function(pair) {
var key = pair[0];
var values = pair[1];
if (!_.isArray(values)) {
var obj1 = {};
obj1[key] = values;
return [obj1];
}
else {
return values.map(function(value) {
var obj1 = {};
obj1[key] = value;
return obj1;
});
}
})
var result = directive_factorialMerge(lists, data);
//console.log("genFactorialCols result:", result);
return result;
}
function directive_gradient(data, data_) {
if (!_.isArray(data))
data = [data];
var list = [];
_.forEach(data, function(data) {
assert(data.volume);
assert(data.count);
assert(data.count >= 2);
var decimals = _.isNumber(data.decimals) ? data.decimals : 2;
//console.log("decimals:", decimals);
var volumeTotal = math.round(math.eval(data.volume).toNumber('ul'), decimals);
// Pick volumes
var volumes = Array(data.count);
for (var i = 0; i < data.count / 2; i++) {
volumes[i] = math.round(volumeTotal * i / (data.count - 1), decimals);
volumes[data.count - i - 1] = math.round(volumeTotal - volumes[i], decimals);
}
if ((data.count % 2) === 1)
volumes[Math.floor(data.count / 2)] = math.round(volumeTotal / 2, decimals);
//console.log("volumes:", volumes);
// Create items
for (var i = 0; i < data.count; i++) {
var volume2 = volumes[i];
var volume1 = math.round(volumeTotal - volume2, decimals);
var l = [];
l.push((volume1 > 0) ? {source: data.source1, volume: math.unit(volume1, 'ul').format({precision: 14})} : null);
l.push((volume2 > 0) ? {source: data.source2, volume: math.unit(volume2, 'ul').format({precision: 14})} : null);
//if (l.length > 0)
list.push(l);
}
});
return list;
}
/**
* Merge an array of objects, or combinatorially merge an array of arrays of objects.
*
* @param {Array} spec The array of objects or arrays of objects to merge.
* @return {Object|Array} The object or array resulting from combinatorial merging.
*/
function directive_factorialMerge(spec, data) {
//console.log("#merge", spec);
if (_.isEmpty(spec)) return spec;
//console.log("genMerge lists:", lists);
var result = genMerge2(spec, data, {}, 0, []);
// If all elements were objects rather than arrays, return an object:
/*if (_.every(spec, _.isPlainObject)) {
assert(result.length == 1);
result = result[0];
}*/
//console.log("genMerge result:", result);
return result;
}
/**
* Helper function for factorial merging of arrays of objects.
* For example, the first element of the first array is merged with the first element of the second array,
* added to the `acc` list, then the first element of the first array is merged with the second element of the second array, and so on.
* @param {array} spec array of objects, may be nested arbitrarily deep, i.e. array of arrays of objects
* @param {object} data [description]
* @param {object} obj0 accumulated result of the current merge, will be added to `acc` once the end of the `spec` list is reached
* @param {number} index index of current item in `spec`
* @param {array} acc accumulated list of merged objects
* @return {array} Returns a factorial list of merged objects.
*/
function genMerge2(spec, data, obj0, index, acc) {
//console.log("genMerge2", spec, obj0, index, acc);
assert(_.isArray(spec));
var list = _.map(spec, function(elem) {
if (!_.isArray(elem))
elem = [elem];
return handleDirective(elem, data);
});
if (index >= spec.length) {
acc.push(obj0);
return acc;
}
var elem = spec[index];
if (!_.isArray(elem))
elem = [elem];
for (var j = 0; j < elem.length; j++) {
var elem2 = handleDirective(elem[j], data);
//console.log("elem2:", elem2)
if (_.isArray(elem2)) {
genMerge2(elem2, data, obj1, 0, acc);
}
else if (elem2 !== null) {
assert(_.isPlainObject(elem2));
var obj1 = _.merge({}, obj0, elem[j]);
genMerge2(list, data, obj1, index + 1, acc);
}
}
return acc;
}
//function expandArrays
function directive_factorialMixtures(spec, data) {
// Get mixutre items
var items;
if (_.isArray(spec))
items = spec;
else {
expect.paramsRequired(spec, ['items']);
items = misc.getVariableValue(spec.items, data.objects);
}
assert(_.isArray(items));
var items2 = _.map(items, function(item) {
return (_.isPlainObject(item))
? directive_factorialCols(item, data)
: item;
});
var combined = combineLists(items2);
if (spec.replicates && spec.replicates > 1) {
combined = directive_replicate({count: spec.replicates, value: combined}, data);
}
return combined;
}
// function directive_include_jsonl(spec, data) {
// assert(_.isString(spec));
// const filename = spec;
// console.log("files: "+JSON.stringify(Object.keys(data)))
// assert(data.files.hasOwnProperty(filename));
// const filedata = data.files[spec];
// console.log(JSON.stringify(filename));
// assert(false);
// }
function directive_length(spec, data) {
if (_.isArray(spec))
return spec.length;
else if (_.isString(spec)) {
var value = misc.getObjectsValue(spec, data.objects)
if (_.isPlainObject(value) && value.hasOwnProperty('value'))
value = value.value;
expect.truthy({}, _.isArray(value), '#length expected an array, received: '+spec);
return value.length;
}
else {
expect.truthy({}, false, '#length expected an array, received: '+spec);
}
}
function directive_merge(spec, data) {
assert(_.isArray(spec));
var list = _.map(spec, function(x) { return handleDirective(x, data); });
return _.merge.apply(null, [{}].concat(list));
}
function directive_pipetteMixtures(spec, data) {
// Get mixutre items
var items;
if (_.isArray(spec))
items = spec;
else {
expect.paramsRequired(spec, ['items']);
items = misc.getVariableValue(spec.items, data.objects);
}
assert(_.isArray(items));
var items2 = _.map(items, function(item) {
return (_.isPlainObject(item))
? directive_factorialCols(item, data)
: item;
});
var combined = combineLists(items2);
if (spec.replicates && spec.replicates > 1) {
combined = directive_replicate({count: spec.replicates, value: combined}, data);
}
//console.log(1)
//console.log(JSON.stringify(combined, null, '\t'))
// Check and set volumes
const volumePerMixture = (spec.volume)
? math.eval(spec.volume) : undefined;
//console.log({volumePerMixture})
//console.log({combined})
combined.forEach(l => {
let volumeTotal = math.unit(0, 'l');
let missingVolumeIndex = -1;
//console.log({l})
// Find total volume of components and whether any components are missing the volume parameter
_.forEach(l, (x, i) => {
//console.log({x, i, and: x.hasOwnProperty('volume')})
if (x) {
if (!x.hasOwnProperty('volume')) {
assert(volumePerMixture, "missing volume parameter: "+JSON.stringify(x));
assert(missingVolumeIndex < 0, "only one mixture element may omit the volume parameter: "+JSON.stringify(l));
missingVolumeIndex = i;
//console.log({missingVolumeIndex})
}
else {
volumeTotal = math.add(volumeTotal, math.eval(x.volume));
}
}
});
// If one of the components needs to have its volume set:
if (missingVolumeIndex >= 0) {
assert(volumePerMixture);
//console.log({volumePerMixture, volumeTotal, subtract: math.subtract(volumePerMixture, volumeTotal).format({precision: 10})})
l[missingVolumeIndex] = _.merge({}, l[missingVolumeIndex], {
volume: math.subtract(volumePerMixture, volumeTotal).format({precision: 13})
});
}
else if (!_.isUndefined(volumePerMixture)) {
const t1 = volumePerMixture.format({precision: 14});
const t2 = volumeTotal.format({precision: 14});
if (t1 !== t2) {
console.log("WARNING: volume in mixture should sum to "+t1+" rather than "+t2+": "+JSON.stringify(l));
}
}
});
//console.log(2)
//console.log(JSON.stringify(combined, null, '\t'))
_.forEach(spec.transformations || [], t => {
if (t.name === 'shuffle') {
var mt = random.engines.mt19937();
assert(_.isNumber(t.seed), "`shuffle` requires a numeric `seed` paramter");
mt.seed(t.seed);
random.shuffle(mt, combined);
}
});
//console.log(3)
//console.log(JSON.stringify(combined, null, '\t'))
_.forEach(spec.transformationsPerWell || [], t => {
if (t.name === 'sortByVolumeMax') {
combined = _.map(combined, l => {
const offset = 0;
const count = t.count || l.length - offset;
return _.take(l, offset)
.concat(_.sortBy(_.take(l, count), x => -math.eval(x.volume).toNumber('l')))
.concat(_.drop(l, offset + count));
});
}
});
//console.log(4)
//console.log(JSON.stringify(combined, null, '\t'))
return combined;
}
function directive_replaceLabware(spec, data) {
expect.paramsRequired(spec, ['list', 'new']);
var list = misc.getVariableValue(spec.list, data.objects);
//if (!_.isArray(list)) console.log("list:", list)
assert(_.isArray(list), "expected a list, received: "+JSON.stringify(list));
assert(_.isString(spec.new));
var l1 = _.flatten(_.map(list, function(s) {
var l2 = wellsParser.parse(s);
return _.map(l2, function(x) {
//console.log("s:", s, "x:", x);
//console.log(x.hasOwnProperty('labware'), !spec.old, x.labware === spec.old);
if (x.hasOwnProperty('labware') && (!spec.old || x.labware === spec.old)) {
x.labware = spec.new;
}
return x;
});
}));
var l2 = wellsParser.processParserResult(l1, data.objects);
return l2;
}
function directive_replicate(spec) {
assert(_.isPlainObject(spec));
assert(_.isNumber(spec.count));
assert(spec.value);
var depth = spec.depth || 0;
assert(depth >= 0);
var step = function(x, count, depth) {
//console.log("step:", x, count, depth);
if (_.isArray(x) && depth > 0) {
if (depth === 1)
return _.flatten(_.map(x, function(y) { return step(y, count, depth - 1); }));
else
return _.map(x, function(y) { return step(y, count, depth - 1); });
}
else {
return _.flatten(_.fill(Array(count), x));
}
}
return step(spec.value, spec.count, depth);
}
function directive_tableCols(table, data) {
//console.log("genTableCols:", table)
assert(_.isPlainObject(table));
assert(!_.isEmpty(table));
var ns1 = _.uniq(_.map(table, function(x) { return _.isArray(x) ? x.length : 1; }));
var ns = _.filter(ns1, function(n) { return n > 1; });
assert(ns1.length > 0);
assert(ns.length <= 1);
var n = (ns.length === 1) ? ns[0] : 1;
var list = Array(n);
for (var i = 0; i < n; i++) {
list[i] = _.mapValues(table, function(value) {
return (_.isArray(value)) ? value[i] : value;
});
}
return list;
}
function directive_tableRows(table, data) {
//console.log("genTableRows:", table)
assert(_.isArray(table));
var list = [];
var names = [];
var defaults = {};
_.forEach(table, function(row) {
// Object are for default values that will apply to the following rows
if (_.isArray(row)) {
// First array in table is the column names
if (names.length === 0) {
names = row;
}
else {
assert(row.length === names.length);
var obj = _.clone(defaults);
for (var i = 0; i < names.length; i++) {
obj[names[i]] = row[i];
}
list.push(obj);
}
}
else {
assert(_.isPlainObject(row));
defaults = _.merge(defaults, row);
//console.log("defaults:", defaults)
}
});
return list;
}
function directive_take(spec, data) {
//console.log("#take:", spec);
expect.paramsRequired(spec, ['list', 'count']);
var list = misc.getVariableValue(spec.list, data.objects);
var count = misc.getVariableValue(spec.count, data.objects);
return _.take(list, count);
}
function directive_wells(spec, data) {
return wellsParser.parse(spec, data.objects);
}
function directive_zipMerge(spec, data) {
assert(_.isArray(spec));
if (spec.length <= 1)
return spec;
//console.log('spec:', spec);
var zipped = _.zip.apply(null, spec);
//console.log('zipped:', zipped);
var merged = _.map(zipped, function(l) { return _.merge.apply(null, [{}].concat(l)); });
//console.log('spec:', spec);
return merged;
}
//
// from design.js
//
function directive_allocateWells(spec, data) {
assert(spec.N, "You must specify a positive value for parameter `N` in `allocateWells()`")
// console.log("directive_allocateWells: "+JSON.stringify(spec))
const design = {
design: {
".*": spec.N,
"x=allocateWells": spec
}
};
const table = Design.flattenDesign(design);
return _.map(table, "x");
}
module.exports = {
"createPipetteMixtureList": directive_pipetteMixtures,
"createWellAssignments": directive_createWellAssignments,
"data": directive_data,
"destinationWells": directive_destinationWells,
"factorialArrays": directive_factorialArrays,
"factorialCols": directive_factorialCols,
"factorialMerge": directive_factorialMerge,
"factorialMixtures": directive_factorialMixtures,
"for": directive_for,
"gradient": directive_gradient,
// "include_jsonl": directive_include_jsonl,
"length": directive_length,
"merge": directive_merge,
"replaceLabware": directive_replaceLabware,
"replicate": directive_replicate,
"tableCols": directive_tableCols,
"tableRows": directive_tableRows,
"take": directive_take,
//"#wells": genWells,
"undefined": function() { return undefined; },
"zipMerge": directive_zipMerge,
// From design.js:
"allocateWells": directive_allocateWells,
};