/**
* Roboliq: Automation for liquid-handling robots
* @copyright 2017, ETH Zurich, Ellis Whitehead
* @license GPL-3.0
*/
/**
* A module of functions for querying and manipulating well contents.
* @module WellContents
*/
import _ from 'lodash';
var assert = require('assert');
var math = require('mathjs');
import expect from './expectCore.js';
var misc = require('./misc.js');
var wellsParser = require('./parsers/wellsParser.js');
export const emptyVolume = math.unit(0, 'ul');
export const unknownVolume = math.eval('Infinity l');
/**
* Validate well contents. Throws an error if they aren't valid.
*
* @param {array} contents - The well contents.
*/
export function checkContents(contents) {
if (_.isUndefined(contents)) {
// ok
}
else if (!_.isArray(contents)) {
assert(false, "expected well contents to be represented by an array: "+JSON.stringify(contents));
}
else if (contents.length == 0) {
// ok
}
else {
//console.log(contents)
var volume = math.eval(contents[0]);
if (contents.length == 1) {
// FIXME: remove 'false, ' from here!
assert.equal(volume.toNumber('l'), 0, "when the contents array has only one element, that element must be 0: "+JSON.stringify(contents));
}
else if (contents.length == 2) {
assert(_.isString(contents[1]), "second element of contents should be a string: "+JSON.stringify(contents));
}
else {
for (var i = 1; i < contents.length; i++) {
//try {
checkContents(contents[i]);
//} catch (e) {
//
//}
}
}
}
}
/**
* Tries to find the contents array for the given syringe.
*
* @param {string} syringeName name of the syringe
* @param {object} data the data object passed to command handlers
* @param {object} effects an optional effects object for effects which have taken place during the command handler and aren't in the data object
* @return {WellContents} the contents array if found, otherwise null
*//*
export function getSyringeContents(syringeName, data, effects) {
//console.log({syringeName})
const contentsName = `${syringeName}.contents`;
// Check for well or labware contents in effects object
if (!_.isEmpty(effects)) {
if (effects.hasOwnProperty(contentsName))
return effects[contentsName];
}
let contents = misc.findObjectsValue(contentsName, data.objects, effects);
checkContents(contents);
return contents;
}*/
/**
* Tries to find the contents array for the given well.
*
* @param {string} wellName name of the well
* @param {object} data the data object passed to command handlers
* @param {object} effects an optional effects object for effects which have taken place during the command handler and aren't in the data object
* @return {WellContents} the contents array if found, otherwise null
*/
export function getWellContents(wellName, data, effects) {
//console.log({wellName})
var wellInfo = wellsParser.parseOne(wellName);
assert(wellInfo.wellId, "missing `wellId`: "+JSON.stringify(wellInfo));
var labwareContentsName = wellInfo.labware+".contents";
var wellContentsName = wellInfo.labware+".contents."+wellInfo.wellId;
// Check for well or labware contents in effects object
if (!_.isEmpty(effects)) {
if (effects.hasOwnProperty(wellContentsName))
return effects[wellContentsName];
if (effects.hasOwnProperty(labwareContentsName))
return effects[labwareContentsName];
}
var contents = misc.findObjectsValue(wellContentsName, data.objects, effects);
if (!_.isEmpty(contents)) return contents;
contents = misc.findObjectsValue(labwareContentsName, data.objects, effects);
if (_.isArray(contents)) {
expect.try({objectName: wellName}, () => checkContents(contents));
return contents;
}
return [];
}
/**
* Get the volume of the contents array.
* @param {array} contents The well contents array
* @return {object} the mathjs volume if found, otherwise 0ul
*/
export function getVolume(contents) {
checkContents(contents);
if (!_.isEmpty(contents)) {
const volume = math.eval(contents[0]);
if (math.unit('l').equalBase(volume)) return volume;
}
return emptyVolume;
}
/**
* Check whether the contents are empty.
* They are empty if the contents are undefined, an empty array,
* or the array begins with a number that mathjs considers equal to 0.
*
* @param {WellContents} contents
* @return {Boolean} true if the contents are empty
*/
export function isEmpty(contents) {
const volume = getVolume(contents);
return math.equal(volume.toNumber('l'), 0);
}
/**
* Get the volume of the given well.
* @param {string} wellName name of the well
* @param {object} data the data object passed to command handlers
* @param {object} effects an optional effects object for effects which have taken place during the command handler and aren't in the data object
* @return {object} the mathjs volume if found, otherwise 0ul
*/
export function getWellVolume(wellName, data, effects) {
var contents = getWellContents(wellName, data, effects);
if (!_.isEmpty(contents)) {
var volume = math.eval(contents[0]);
if (math.unit('l').equalBase(volume)) return volume;
}
return emptyVolume;
}
/**
* Get an object representing the effects of pipetting.
* @param {string} wellName fully qualified object name of the well
* @param {object} data The data object passed to command handlers.
* @param {object} effects The effects object for effects which have taken place during the command handler and aren't in the data object
* @return {array} [content, contentName], where content will be null if not found
*/
export function getContentsAndName(wellName, data, effects) {
//console.log("getContentsAndName", wellName)
if (!effects) effects = {};
//var i = wellName.indexOf('(');
//var wellId = if (i >= 0) {}
var wellInfo = wellsParser.parseOne(wellName);
var labwareName;
//console.log("wellInfo", wellInfo);
if (wellInfo.source) {
labwareName = wellInfo.source;
}
else {
assert(wellInfo.wellId);
labwareName = wellInfo.labware;
// Check for contents of well
var contentsName = labwareName+".contents."+wellInfo.wellId;
//console.log("contentsName", contentsName, effects[contentsName], _.get(data.objects, contentsName))
var contents = effects[contentsName] || misc.findObjectsValue(contentsName, data.objects, effects);
checkContents(contents);
if (contents)
return [contents, contentsName];
}
// Check for contents of labware
//console.log("labwareName", labwareName);
var contentsName = labwareName+".contents";
//console.log("contentsName", contentsName)
var contents = effects[contentsName] || misc.findObjectsValue(contentsName, data.objects, effects);
// If contents is an array, then we have the correct contents;
// Otherwise, we have a map of well contents, but no entry for the current well yet
if (_.isArray(contents)) {
checkContents(contents);
return [contents, contentsName];
}
return [undefined, wellInfo.labware+".contents."+wellInfo.wellId];
}
/**
* Convert the contents array encoding to a map of substances to amounts
* @param {array} contents The well contents array
* @return {object} map of substance name to the volume or amount of that substance in the well
*/
export function flattenContents(contents) {
if (_.isUndefined(contents)) {
return {};
}
checkContents(contents);
//console.log("flattenContents:", contents);
assert(_.isArray(contents), "Expected 'contents' to be an array: "+JSON.stringify(contents));
// The first element always holds the volume in the well.
// If the array has exactly one element, the volume should be 0l.
if (contents.length <= 1) {
return {};
}
// If the array has exactly two elements, the second element is the name of the substance.
else if (contents.length == 2) {
assert(_.isString(contents[1]), "second element of contents should be a string: "+JSON.stringify(contents));
var volume = math.eval(contents[0]).format({precision: 14});
return _.fromPairs([[contents[1], volume]]);
}
// If the array has more than two elements, each element after the volume has the same
// structure as the top array and they represent the mixture originally dispensed in the well.
else {
/*const maps = _(contents).tail().map(contents2 => {
const flattened = flattenContents(contents2);
//console.log({flattened});
const x = _.mapValues(flattened, value => math.eval(value))
//console.log({x});
return x;
}).value();*/
var maps = _.map(_.tail(contents), contents => _.mapValues(flattenContents(contents), value => math.eval(value)));
//console.log("maps: "+JSON.stringify(maps));
var merger = function(a, b) { return (_.isUndefined(a)) ? b : math.add(a, b); };
var mergeArgs = _.flatten([{}, maps, merger]);
//console.log("mergeArgs: "+mergeArgs);
var merged = _.mergeWith.apply(_, mergeArgs);
//console.log("merged: "+JSON.stringify(merged));
var total = math.eval(contents[0]);
var subTotal = _.reduce(merged, function(total, n) { return math.add(total, n); }, emptyVolume);
//console.log("total: "+total);
//console.log("subTotal: "+subTotal);
//var factor = math.fraction(total, subTotal);
try {
const result = _.mapValues(merged, function(v) {
//console.log({v, totalNumber: total.toNumber("l")})
const numerator = math.multiply(v, total.toNumber("l"));
const result = math.divide(numerator, subTotal.toNumber("l"))
return result.format({precision: 4});
});
return result;
} catch (e) {
console.log({total: total.toNumber("l"), subTotal: subTotal.toNumber("l"), factor})
console.log(JSON.stringify(contents))
throw e;
}
}
}
export function mergeContents(contents) {
const flat = flattenContents(contents);
const pairs = _.toPairs(flat);
if (pairs.length === 0) {
return [];
}
else if (pairs.length === 1) {
const l = pairs[0];
return [l[1], l[0]];
}
else {
const volumes1 = _.values(flat);
const volumes2 = volumes1.map(s => math.eval(s));
const sum = math.sum(volumes2);
const contents2 = [sum.format({precision: 14})].concat(pairs.map(l => [l[1], l[0]]));
return contents2;
}
}
/**
* Add source contents to destination contents at the given volume.
* @param {array} srcContents - current contents of the source well
* @param {array} dstContents - current contents of the destination well
* @param {string} volume - a string representing the volume to transfer
* @return {array} an array whose first element is the new source contents and whose second element is the new destination contents.
*/
export function transferContents(srcContents, dstContents, volume) {
assert(_.isArray(srcContents));
checkContents(srcContents);
if (_.isString(volume))
volume = math.eval(volume);
const volumeText = volume.format({precision: 14});
//console.log({dstContents})
if (_.isUndefined(dstContents) || _.isEmpty(dstContents) || !_.isArray(dstContents))
dstContents = [];
//console.log({dstContents})
checkContents(dstContents);
const srcContentsToAppend = [volumeText].concat(_.tail(srcContents));
let dstContents2;
// If the destination is empty:
if (dstContents.length <= 1) {
dstContents2 = srcContentsToAppend;
}
else {
const dstVolume = math.eval(dstContents[0]);
const totalVolumeText = math.add(dstVolume, volume).format({precision: 14});
// If the destination currently only contains one substance:
if (dstContents.length === 2) {
dstContents2 = [totalVolumeText, dstContents, srcContentsToAppend];
}
// Otherwise add source to destination contents
else {
const dstSumOfComponents = math.sum(_.map(_.tail(dstContents), l => math.eval(l[0])));
if (math.equal(dstVolume, dstSumOfComponents)) {
dstContents2 = _.flatten([totalVolumeText, _.tail(dstContents), [srcContentsToAppend]]);
}
else {
dstContents2 = _.flatten([totalVolumeText, [dstContents], [srcContentsToAppend]]);
}
}
}
//console.log("dstContents", dstContents);
// Decrease volume of source
const srcVolume0 = math.eval(srcContents[0]);
const srcVolume1 = math.chain(srcVolume0).subtract(volume).done();
const srcContents2 = [srcVolume1.format({precision: 14})].concat(_.tail(srcContents));
checkContents(srcContents2);
checkContents(dstContents2);
return [srcContents2, dstContents2];
}