/** * 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); }