Source: EvowareCarrierFile.js

/**
 * Roboliq: Automation for liquid-handling robots
 * @copyright 2017, ETH Zurich, Ellis Whitehead
 * @license GPL-3.0
 */

/**
 * Loads data from an Evoware carrier file.
 * @module
 */

import _ from 'lodash';
import assert from 'assert';
//import fs from 'fs';
//import iconv from 'iconv-lite';
import {sprintf} from 'sprintf-js';
import * as EvowareUtils from './EvowareUtils.js';


/**
 * Tuple for location refered to by carrier+grid+site indexes
 * @class module:evoware/EvowareCarrierFile.CarrierGridSiteIndex
 * @property {integer} carrierId - ID for the carrier
 * @property {integer} gridIndex - 1-based index of grid
 * @property {integer} siteIndex -  0-based index of site
 */

export class CarrierGridSiteIndex {
	constructor(carrierId, gridIndex, siteIndex) {
		this.carrierId = carrierId;
		this.gridIndex = gridIndex;
		this.siteIndex = siteIndex;
	}
}

/**
 * Tuple for location refered to by carrier+site index
 * @class module:evoware/EvowareCarrierFile.CarrierSiteIndex
 * @property {integer} carrierId - ID for the carrier
 * @property {integer} siteIndex -  0-based index of site
 */

export class CarrierSiteIndex {
	constructor(carrierId, siteIndex) {
		this.carrierId = carrierId;
		this.siteIndex = siteIndex;
	}
}

/**
 * A base type for evoware models, one of Carrier, LabwareModel, or Vector.
 * @typedef {object} EvowareModel
 * @property {string} type - the type of model
 */

/**
 * A Carrier object
 * @class module:evoware/EvowareCarrierFile.Carrier
 * @property {string} type - should be "Carrier"
 * @property {string} name
 * @property {integer} id
 * @property {integer} siteCount
 * @property {string} [deviceName]
 * @property {string} [partNo]
 * @property {array} [vectors] - array of vector names for this carrier
 */
export class Carrier {
	constructor(name, id, siteCount, deviceName, partNo) {
		this.type = "Carrier";
		this.name = name;
		this.id = id;
		this.siteCount = siteCount;
		this.deviceName = deviceName;
		this.partNo = partNo;
		this.vectors = [];
	}
}

/**
 * An evoware labware model
 * @class module:evoware/EvowareCarrierFile.LabwareModel
 * @property {string} type - should be "LabwareModel"
 * @property {string} name
 * @property {integer} rows
 * @property {integer} cols
 * @property {number} ul - maximum volume of wells
 * @property {array} sites - list of CarrierSiteIndexes where this labware can be placed.
 */

export class LabwareModel {
	constructor(name, rows, cols, ul, sites) {
		this.type = "LabwareModel";
		this.name = name;
		this.rows = rows;
		this.cols = cols;
		this.ul = ul;
		this.sites = sites;
	}
}

/**
 * A tranporter "vector", related to movements that the RoMas can make
 * @class module:evoware/EvowareCarrierFile.Vector
 * @property {string} type - should be "Vector"
 * @property {integer} carrierId - which carrier this vector is for
 * @property {string} clazz - Wide, Narrow, or user-defined
 * @property {integer} romaId - which RoMa this vector is for
 */

export class Vector {
	constructor(carrierId, clazz, romaId) {
		this.type = "Vector";
		this.carrierId = carrierId;
		this.clazz = clazz;
		this.romaId = romaId;
	}
}

/**
 * An object representing an evoware carrier file
 *
 * @class module:evoware/EvowareCarrierFile.EvowareCarrierData
 * @property {object} models - map from model name to model data
 * @property {object} idToName - map of model ID to model name
 * @property {object} carrierIdToVectors - map of carrier ID to list of Vectors
 */

export class EvowareCarrierData {
	constructor(carrierModels, labwareModels) {
		this.carrierModels = carrierModels;
		this.labwareModels = labwareModels;
		this.carrierIdToName = _(carrierModels).map(x => [x.id, x.name]).fromPairs().value();
		this.labwareIdToName = _(labwareModels).map(x => [x.id, x.name]).fromPairs().value();
	}

	getCarrierByName(carrierName) {
		return this.carrierModels[carrierName];
	}

	getCarrierById(id) {
		const name = this.carrierIdToName[id];
		return this.carrierModels[name];
	}

	/**
	 * Print debug output: carrier id, carrier name.
	 */
	printCarriersById() {
		const l0 = _(this.carrierModels).map(model => [model.id, model.name]).value();
		const l = _.sortBy(l0, x => x[0]);
		//console.log({l})
		l.forEach(([id, name]) => {
			console.log(sprintf("%03d\t%s", id, name));
		});
	}
}

/*
case class CarrierSite(
	carrier: Carrier,
	iSite: Int
)
*/

/**
 * Load an evoware carrier file and return its model data.
 * @param  {string} filename - path to the carrier file
 * @return {EvowareCarrierData}
 */
export function load(filename) {
	const modelList = loadEvowareModels(filename);
	const data = makeEvowareCarrierData(modelList);
	//console.log({data});
	return data;
}

/**
 * Create an EvowareCarrierData object from an array of evoware models.
 * @param  {array} modelList - array of evoware models
 * @return {EvowareCarrierData}
 */
function makeEvowareCarrierData(modelList) {
	// Create maps/lists for the various model types
	const carrierModels = {};
	const labwareModels = {};
	const vectors = [];
	_.forEach(modelList, model => {
		if (model.type === "Carrier") carrierModels[model.name] = model;
		else if (model.type === "LabwareModel") labwareModels[model.name] = model;
		else vectors.push(model);
	});
	// Create map from ID to name
	const idToName = _(carrierModels).filter(x => x.type === "Carrier").map(model => [model.id, model.name]).fromPairs().value();
	// Add vectors to carriers
	const carrierIdToVectors = _.groupBy(vectors, 'carrierId');
	_.forEach(carrierIdToVectors, (vectors, carrierId) => {
		const carrierName = idToName[carrierId];
		const carrier = carrierModels[carrierName];
		carrier.vectors = vectors.map(x => x.name);
	});
	//console.log({modelList, models, idToName, carrierIdToVectors})

	return new EvowareCarrierData(
		carrierModels,
		labwareModels
	);
}

/**
 * Parses the file `Carrier.cfg` into a list of `EvowareModels`.
 * @param {string} filename - path to the carrier file
 * @return {array} an array of EvowareModels (e.g. Carriers, Vectors, EvowareLabwareModels)
 */
function loadEvowareModels(filename) {
	const models = [];

	const lines = new EvowareUtils.EvowareSemicolonFile(filename, 4);

	// Find models in the carrier file
	while (lines.hasNext()) {
		const model = parseModel(lines);
		//console.log({model})
		if (!_.isUndefined(model))
			models.push(model)
		//assert(lineIndex2 > lineIndex);
		//console.log({lineIndex2})
	}

	return models;
}

/**
 * Parse the line and return a model, if relevant.
 * @param {EvowareSemicolonFile} lines - array of lines from the Carrier.cfg
 * @return {array} an optional model.
 */
function parseModel(lines) {
	const [lineKind, l] = lines.nextSplit();
	//console.log({lineKind, l})
	switch (lineKind) {
		case 13: return parse13(l, lines);
		case 15: return parse15(l, lines);
		case 17: return parse17(l, lines);
		// NOTE: There are also 23 and 25 lines, but I don't know what they're for.
		default: return undefined;
	}
}

/**
 * Parse a carrier object; carrier lines begin with "13"
 *
 * @param {array} l - array of string representing the elements of the current line
 * @param {EvowareSemicolonFile} lines - array of lines from the Carrier.cfg
 * @return {Carrier} a Carrier.
 */
function parse13(l, lines) {
	const sName = l[0];
	const l1 = l[1].split("/");
	const sId = l1[0];
	//val sBarcode = l1(1)
	const id = parseInt(sId);
	const nSites = parseInt(l[4]);
	const deviceNameList = parse998(lines.peekAhead(nSites + 1));
	const deviceName = (deviceNameList.length != 1) ? undefined : deviceNameList[0];
	const partNoList = parse998(lines.peekAhead(nSites + 3));
	const partNo = (partNoList.length != 1) ? undefined : partNoList[0];
	lines.skip(nSites + 6);
	return new Carrier(sName, id, nSites, deviceName, partNo);
}

/**
 * Parse a labware object; labware lines begin with "15"
 *
 * @param {array} l - array of string representing the elements of the current line
 * @param {EvowareSemicolonFile} lines - array of lines from the Carrier.cfg
 * @return {LabwareModel} a new LabwareModel.
 */
function parse15(l, lines) {
	const sName = l[0];
	const ls2 = l[2].split("/");
	const nCols = parseInt(ls2[0]);
	const nRows = parseInt(ls2[1]);
	//const nCompartments = ls2(2).toInt
	const ls4 = l[4].split("/")
	const zBottom = parseInt(ls4[0]);
	const zDispense = parseInt(ls4[2]);
	const nArea = Number(l[5]); // mm^2
	const nDepthOfBottom = Number(l[15]); // mm
	//const nTipsPerWell = l(6).toDouble
	//const nDepth = l(15).toDouble // mm
	const nCarriers = parseInt(l[20]);
	// shape: flat, round, v-shaped (if nDepth == 0, then flat, if > 0 then v-shaped, if < 0 then round
	// labware can have lid

	// negative values for rounded bottom, positive for cone, 0 for flat
	const [nDepthOfCone, nDepthOfRound] = (nDepthOfBottom > 0)
	 	? [nDepthOfBottom, 0.0]
		: [0.0, -nDepthOfBottom];
	const r = Math.sqrt(nArea / Math.PI);
	// Calculate the volume in microliters
	const ul = ((zBottom - zDispense) / 10.0 - nDepthOfCone - nDepthOfRound) * nArea +
		// Volume of a cone: (1/3)*area*height
		(nDepthOfCone * nArea / 3) +
		// Volume of a half-sphere:
		((4.0 / 6.0) * Math.PI * r * r * r);

	const lsCarrier = lines.take(nCarriers);
	const sites = _.flatten(lsCarrier.map(s => {
		const ls = parse998(s); // split line, but drop the "998" prefix
		const idCarrier = parseInt(ls[0]);
		const sitemask = ls[1];
		const [, , site_li] = EvowareUtils.parseEncodedIndexes(sitemask);
		//console.log({sitemask, site_li})
		return site_li.map(site_i => new CarrierSiteIndex(idCarrier, site_i));
	}));

	lines.skip(10);
	return new LabwareModel(sName, nRows, nCols, ul, sites);
}

/**
 * Parse a vector object; vector lines begin with "17"
 *
 * @param {array} l - array of string representing the elements of the current line
 * @param {EvowareSemicolonFile} lines - array of lines from the Carrier.cfg
 * @return {Vector} a new Vector, if any
 */
function parse17(l, lines) {
	//println("parse17: "+l.toList)
	const l0 = l[0].split("_");
	if (l0.length < 3)
		return undefined;

	const sClass = l0[1];
	const iRoma = parseInt(l0[2]) - 1;
	const nSteps = parseInt(l[3]);
	const idCarrier = parseInt(l[4]);
	const model = (nSteps > 2) ? new Vector(idCarrier, sClass, iRoma) : undefined;
	lines.skip(nSteps);
	return model;
}

/**
 * Parse a line with the expected lineType=998.  Discards the linetype and just returns a list of strings elements.
 * @param  {string} s - the line
 * @return {array} array of line elements
 */
function parse998(s) {
	const [lineType, l] = EvowareUtils.splitSemicolons(s);
	assert(lineType === 998);
	return _.initial(l);
}