Source: commands/pipetter/pipetterUtils.js

/**
 * A module of helper functions for the pipetter commands.
 * @module commands/pipetter/pipetterUtils
 */

var _ = require('lodash');
var assert = require('assert');
var math = require('mathjs');
import expect from '../../expect.js';
var misc = require('../../misc.js');
var wellsParser = require('../../parsers/wellsParser.js');
import * as WellContents from '../../WellContents.js';

/**
 * Get fully qualified syringe object name
 * @param {string|integer} syringeName - name or number of syringe
 * @param {object} data - The data object passed to command handlers.
 * @return {string} fully qualified syringe object name, if found
 */
export function getSyringeName(syringeName, equipmentName, data) {
	const syringeName2 = `${equipmentName}.syringe.${syringeName}`;
	if (_.isInteger(syringeName)) {
		return syringeName2;
	}
	else if (_.has(data.objects, syringeName)) {
		return syringeName;
	}
	else if (_.has(data.objects, syringeName2)) {
		return syringeName2;
	}
	return syringeName;
}

/**
 * Get an object representing the effects of pipetting, aspirating, or dispensing.
 * @param {object} params The parameters for the pipetter._aspirate command.
 * @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 effects caused by the `_aspirate`, `_dispense` or `_pipette` command.
 */
export function getEffects_pipette(parsed, data, effects) {
	const effects2 = (effects) ? _.cloneDeep(effects) : {};
	const effectsNew = {};

	function addEffect(name, obj) {
		effects2[name] = obj;
		effectsNew[name] = obj;
	}

	//console.log("getEffects_aspirate:\n"+JSON.stringify(parsed, null, '\t'));
	parsed.value.items.forEach((item, index) => {
		// console.log("item: "+JSON.stringify(item, null, '\t'));

		// Get initial contents of the syringe
		const syringe = _.isString(item.syringe) ? _.get(data.objects, item.syringe) : item.syringe;
		const syringeName = _.isString(item.syringe) ? item.syringe : parsed.objectName[`items.${index}.syringe`];
		const syringeContentsName = `${syringeName}.contents`;
		const syringeContents00 = effects2[syringeContentsName] || syringe.contents || [];
		const syringeContaminantsName = `${syringeName}.contaminants`;

		const volume = item.volume;

		const source = item.source;
		if (!_.isUndefined(source)) {
			const syringeContents0 = syringeContents00;
			const syringeContaminants0 = effects2[syringeContaminantsName] || syringe.contaminants || [];
			//console.log({syringeName, syringeContents0});
			// Get initial contents of the source well
			const [srcContents00, srcContentsName] = WellContents.getContentsAndName(source, data, effects2);
			const srcContents0 = (_.isEmpty(srcContents00))
				? ["Infinity l", source] : srcContents00;
			//console.log("srcContents0", srcContents0, srcContentsName);

			// Contents of source well and syringe after aspiration
			const [srcContents1a, syringeContents1a] = WellContents.transferContents(srcContents0, syringeContents0, item.volume);
			const srcContents1 = WellContents.mergeContents(srcContents1a);
			const syringeContents1 = WellContents.mergeContents(syringeContents1a);
			// console.log({srcContents1, syringeContents1a, syringeContents1});
			// Update content effect for source
			addEffect(srcContentsName, srcContents1);

			// Get list of syringe contaminants
			const contaminants1 = _.keys(WellContents.flattenContents(syringeContents1));
			//console.log({syringeContaminantsName, syringeContaminants0, contaminants1});
			// Update contaminant effects
			if (!_.isEqual(syringeContaminants0, contaminants1))
				addEffect(syringeContaminantsName, contaminants1);
			// Update content effect
			addEffect(syringeContentsName, syringeContents1);
			// Remove cleaned property
			//console.log(`syringe ${syringeName}: `+JSON.stringify(item.syringe))
			if (!_.isUndefined(syringe.cleaned))
				addEffect(`${syringeName}.cleaned`, null);

			// Update __WELLS__ effects for source
			const volume1 = math.eval(srcContents1[0]);
			const nameWELL = "__WELLS__."+srcContentsName;
			//console.log("nameWELL:", nameWELL)
			const well0 = misc.findObjectsValue(nameWELL, data.objects, effects2) || {
				isSource: true,
				volumeMin: srcContents0[0],
				volumeMax: srcContents0[0]
			};
			const well1 = _.merge({}, well0, {
				volumeMax: math.max(math.eval(well0.volumeMax), volume1).format({precision: 14}),
				volumeMin: math.min(math.eval(well0.volumeMin), volume1).format({precision: 14}),
				volumeRemoved: (well0.volumeRemoved)
					? math.chain(math.eval(well0.volumeRemoved)).add(volume).done().format({precision: 14})
					: volume.format({precision: 14})
			});
			//console.log({well0, well1});
			//console.log("x:\n"+JSON.stringify(x, null, '  '));
			addEffect(nameWELL, well1);
		}

		const destination = item.destination;
		if (!_.isUndefined(destination)) {
			const syringeContents0 = effects2[syringeContentsName] || syringe.contents || [];
			const syringeContaminants0 = effects2[syringeContaminantsName] || syringe.contaminants || [];
			//console.log({syringeName, syringeContents0});
			// Get initial contents of the destination well
			const [dstContents0, dstContentsName] = WellContents.getContentsAndName(destination, data, effects2);
			//console.log("dst contents", dstContents0, dstContentsName);

			expect.truthy({paramName: `items[${index}].syringe`}, !WellContents.isEmpty(syringeContents0), "syringe contents should not be empty when dispensing");
			// Final contents of source well and syringe
			const [syringeContents1, dstContents1] = WellContents.transferContents(syringeContents0, dstContents0, item.volume);
			//console.log({syringeContents1, dstContents1})

			//console.log({srcContents1, syringeContents1});
			// Check for contact with the destination contents by looking for
			// the word "wet" in the program name.
			const isWetContact = !_.isEmpty(parsed.value.program) && /(_wet_|\bwet\b|_wet\b)/.test(parsed.value.program.toLowerCase());
			if (isWetContact) {
				// Contaminate the syringe with source contents
				// FIXME: Contaminate the syringe with destination contents if there is wet contact, i.e., use dstContents1 instead of srcContents0 for flattenContents()
				const contaminantsB = _.keys(WellContents.flattenContents(dstContents0));
				const contaminants1 = _.uniq(syringeContaminants0.concat(contaminantsB));
				if (!_.isEqual(syringeContaminants0, contaminants1))
					addEffect(syringeContaminantsName, contaminants1);
			}

			// Update content effect
			// If content volume = zero, set to null
			const syringeContents2 = (WellContents.isEmpty(syringeContents1))
				? null : syringeContents1;
			if (!_.isEqual(syringeContents0, syringeContents2))
				addEffect(syringeContentsName, syringeContents2);

			// Update content effect for destination
			addEffect(dstContentsName, dstContents1);

			// Update __WELLS__ effects for destination
			// REFACTOR: lots of duplication with the same code in the source condition
			const volume0 = WellContents.getVolume(dstContents0);
			const volume1 = WellContents.getVolume(dstContents1);
			const nameWELL = "__WELLS__."+dstContentsName;
			//console.log({nameWELL, volume0, volume1})
			//console.log("nameWELL:", nameWELL)
			const well0 = misc.findObjectsValue(nameWELL, data.objects, effects2) || {
				isSource: false,
				volumeMin: volume0.format({precision: 14}),
				volumeMax: volume0.format({precision: 14})
			};
			//console.log({well0})
			const well1 = _.merge(well0, {
				volumeMax: math.max(math.eval(well0.volumeMax), volume1).format({precision: 14}),
				volumeMin: math.min(math.eval(well0.volumeMin), volume1).format({precision: 14}),
				volumeAdded: (well0.volumeAdded)
					? math.chain(math.eval(well0.volumeAdded)).add(volume).done().format({precision: 14})
					: volume.format({precision: 14})
			});
			//console.log("x:\n"+JSON.stringify(x, null, '  '));
			addEffect(nameWELL, well1);
		}

		if (!_.isUndefined(item.well)) {
			// console.log({item})
			const source = item.well;
			const syringeContents0 = syringeContents00;
			const syringeContaminants0 = effects2[syringeContaminantsName] || syringe.contaminants || [];
			//console.log({syringeName, syringeContents0});
			// Get initial contents of the source well
			const [srcContents00, srcContentsName] = WellContents.getContentsAndName(source, data, effects2);
			const srcContents0 = (_.isEmpty(srcContents00))
				? ["Infinity l", source] : srcContents00;
			//console.log("srcContents0", srcContents0, srcContentsName);

			// Contents of source well and syringe after aspiration
			const [srcContents1, syringeContents1] = WellContents.transferContents(srcContents0, syringeContents0, item.volume || WellContents.emptyVolume);
			//console.log({srcContents1, syringeContents1});

			// Get list of syringe contaminants
			const contaminants1 = _.keys(WellContents.flattenContents(syringeContents1));
			//console.log({syringeContaminantsName, syringeContaminants0, contaminants1});
			// Update contaminant effects
			if (!_.isEqual(syringeContaminants0, contaminants1))
				addEffect(syringeContaminantsName, contaminants1);
			// Remove cleaned property
			//console.log(`syringe ${syringeName}: `+JSON.stringify(item.syringe))
			if (!_.isUndefined(syringe.cleaned))
				addEffect(`${syringeName}.cleaned`, null);
		}

		// Prevent superfluous 'nulling' of syringe contents
		//console.log({syringeContentsName, syringeContents00, effects2: effects2[syringeContentsName], effectsNew: effectsNew[syringeContentsName]})
		if (effects2[syringeContentsName] === null && _.isEmpty(syringeContents00))
			delete effectsNew[syringeContentsName];
	});

	//console.log("effectsNew:\n"+JSON.stringify(effectsNew));

	return effectsNew;
}