/** * Roboliq: Automation for liquid-handling robots * @copyright 2017, ETH Zurich, Ellis Whitehead * @license GPL-3.0 */ /** * Module to load and save Evoware table files. * * @module */ import _ from 'lodash'; import assert from 'assert'; //import {sprintf} from 'sprintf-js'; import * as EvowareUtils from './EvowareUtils.js'; import * as EvowareCarrierFile from './EvowareCarrierFile.js'; import M from './Medley.js'; /** * Represent a hotel object in an Evoware table file * @class module:evoware/EvowareTableFile.HotelObject * @param {integer} parentCarrierId - carrier ID for the carrier holding this hotel * @param {integer} gridIndex - grid index of the hotel */ export class HotelObject { constructor(parentCarrierId, gridIndex) { this.parentCarrierId = parentCarrierId; this.gridIndex = gridIndex; } } /** * Represent an external object in an Evoware table file * @class module:evoware/EvowareTableFile.ExternalObject * @param {integer} n1 - Value of unknown significance (4 for System, 0 for others?) * @param {integer} n2 - I think this is the on-screen display index * @param {string} carrierName - carrier name for the carrier holding this hotel/object */ export class ExternalObject { constructor(n1, n2, carrierName) { this.n1 = n1; this.n2 = n2; this.carrierName = carrierName; } } /** * Parses an Evoware `.esc` script file, extracting the table setup. * @param {EvowareCarrierData} carrierData * @param {string} filename * @return {object} a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName} */ export function load(carrierData, filename) { const lines = new EvowareUtils.EvowareSemicolonFile(filename, 7); lines.next() // TODO: should we check whether this is equal to "--{ RPG }--"? //println(lsLine.takeWhile(_ != "--{ RPG }--").length) const [, l] = lines.nextSplit(); lines.skip(0); const tableFile = parse14(carrierData, l, lines); //println("parseFile: "+rest.takeWhile(_ != "--{ RPG }--")) return tableFile; } /** * Parse a table. * * @param {EvowareCarrierData} carrierData * @param {array} l - array of string representing the elements of the current line * @param {EvowareSemicolonFile} lines - array of lines from the Carrier.cfg * @return {object} a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName} */ function parse14(carrierData, l, lines) { //import configFile._ const carrierIdsInternal = parse14_getCarrierIds(_.initial(l)); //console.log("carrierIdsInternal: "+JSON.stringify(carrierIdsInternal)); const internalObjects = parse14_getLabwareObjects(carrierData, carrierIdsInternal, lines); //console.log("internalObjects: "+JSON.stringify(internalObjects)); const hotelObjects = parse14_getHotelObjects(lines); const externalObjects = parse14_getExternalObjects(lines); const externalSiteIdToLabwareModelName = parse14_getExternalLabwares(lines); const externalCarrierNameToGridIndexList = parse14_getExternalCarrierGrids(externalObjects, lines); // FIXME: for debug only //const gridToCarrierIdInternal = _(carrierIdsInternal).map((id, index) => [index.toString(), id]).filter(([, id]) => id > -1).fromPairs().value(); //console.log("gridToCarrierIdInternal: "+JSON.stringify(gridToCarrierIdInternal)); // ENDFIX function set(carrierName, gridIndex, siteIndex, propertyName, propertyValue) { const c = _.get(layout, carrierName, {}); if (_.isUndefined(gridIndex)) { M.setMut(c, propertyName, propertyValue); } else { const g = _.get(c, gridIndex, {}); if (_.isUndefined(siteIndex)) { M.setMut(g, propertyName, propertyValue); } else { const s = _.get(g, siteIndex, {}); M.setMut(s, propertyName, propertyValue); M.setMut(g, siteIndex, s); } M.setMut(c, gridIndex, g); } M.setMut(layout, carrierName, c); } // Get list of all carriers on the table const carrierAndGridList = []; // Internal carriers carrierIdsInternal.forEach((carrierId, gridIndex) => { if (carrierId > -1) { const carrier = carrierData.getCarrierById(carrierId); carrierAndGridList.push([carrier.name, gridIndex, "internal", true]); } }); // Hotel carriers hotelObjects.forEach(o => { const carrier = carrierData.getCarrierById(o.parentCarrierId); carrierAndGridList.push([carrier.name, o.gridIndex, "hotel", true]); }); // External objects externalObjects.forEach((external, i) => { const [carrierName, gridIndex] = externalCarrierNameToGridIndexList[i]; carrierAndGridList.push([carrierName, gridIndex, "external", _.pick(external, 'n1', 'n2')]); }); // Sort the list by gridIndex const carrierAndGridList1 = _.sortBy(carrierAndGridList, l => l[1]); //console.log(JSON.stringify(carrierAndGridList1, null, '\t')); // Populate the carrier/grid layout information in gridIndex-order const layout = {}; carrierAndGridList1.forEach(([carrierName, gridIndex, propertyName, propertyValue]) => { set(carrierName, gridIndex, undefined, propertyName, propertyValue); }); // Add to layout the internal site labels and labware internalObjects.forEach(([carrierName, gridIndex, siteIndex, label, labwareModelName]) => { if (!_.isEmpty(label)) set(carrierName, gridIndex, siteIndex, 'label', label); set(carrierName, gridIndex, siteIndex, 'labwareModelName', labwareModelName); }); // Add to layout the external site labware externalSiteIdToLabwareModelName.forEach(([carrierId, labwareModelName]) => { const carrier = carrierData.getCarrierById(carrierId); const result = _.find(externalCarrierNameToGridIndexList, ([carrierName,]) => carrierName === carrier.name); assert(!_.isUndefined(result)); const [, gridIndex] = result; set(carrier.name, gridIndex, 1, 'labwareModelName', labwareModelName); }); return layout; } /** * Extract array where the array index is the grid index and the value is the carrier ID. * This information is on the first line of the table definition. * A -1 value for the carrier ID means that there is no carrier at that grid. * @param {array} l - elements of line * @return {array} array of carrier IDs on this table */ function parse14_getCarrierIds(l) { return l.map(s => parseInt(s)); } /** * Get array of labwares on the table. * @param {EvowareCarrierData} carrierData * @param {EvowareSemicolonFile} lines - lines of table file * @return {array} an array of tuples (carrier name, gridIndex, siteIndex, site label, labware model name) */ function parse14_getLabwareObjects(carrierData, carrierIdsInternal, lines) { const result = []; carrierIdsInternal.forEach((carrierId, gridIndex) => { if (carrierId > -1) { const carrier = carrierData.getCarrierById(carrierId); const [n0, l0] = lines.nextSplit(); const [n1, l1] = lines.nextSplit(); //console.log({n0, l0, n1, l1, carrierId, carrierName: carrierData.carrierIdToName[carrierId], carrier}) assert(n0 === 998 && n1 === 998 && parseInt(l0[0]) === carrier.siteCount); //println(iGrid+": "+carrier) _.times(carrier.siteCount, siteIndex => { const labwareModelName = l0[siteIndex+1]; if (!_.isEmpty(labwareModelName)) { const item = [carrier.name, gridIndex, siteIndex+1, l1[siteIndex], labwareModelName]; result.push(item); } }); } else { lines.skip(1); } }); return result; } /** * Parse the hotel objects * @param {EvowareSemicolonFile} lines - lines of table file * @return {array} an array of HotelObjects */ function parse14_getHotelObjects(lines) { const [n0, l0] = lines.nextSplit(); assert(n0 === 998); const count = parseInt(l0[0]); return _.times(count, () => { const [n, l] = lines.nextSplit(); assert(n == 998); const id = parseInt(l[0]); const iGrid = parseInt(l[1]); return new HotelObject(id, iGrid); }); } /** * Parse the external objects. * @param {EvowareSemicolonFile} lines - lines of table file * @return {array} an array of external objects */ function parse14_getExternalObjects(lines) { const [n0, l0] = lines.nextSplit(); assert(n0 === 998); const count = parseInt(l0[0]); return _.times(count, () => { const [n, l] = lines.nextSplit(); assert(n == 998); const n1 = parseInt(l[0]); const n2 = parseInt(l[1]); const carrierName = l[2]; return new ExternalObject(n1, n2, carrierName); }); } /** * Parse labware on external sites * @param {EvowareSemicolonFile} lines - lines of table file * @return {object} list of tuples (carrier ID, labware model name) */ function parse14_getExternalLabwares(lines) { const [n0, l0] = lines.nextSplit(); assert(n0 === 998); const count = parseInt(l0[0]); return _.times(count, i => { const [n, l] = lines.nextSplit(); assert(n == 998); const carrierId = parseInt(l[0]); const labwareModelName = l[1]; return [carrierId, labwareModelName]; }); } function parse14_getExternalCarrierGrids(externalObjects, lines) { return externalObjects.map(external => { const [n, l] = lines.nextSplit(); assert(n === 998); //console.log("carrierName: "+external.carrierName); // we need to force the system liquid to be on grid -1 const gridIndex = (external.carrierName === "System") ? -1 : parseInt(l[0]); return [external.carrierName, gridIndex]; }); } /** * Create a string representation of an Evoware table layout * @param {EvowareCarrierData} carrierData - data loaded from an evoware carrier file * @param {object} table - a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName} * @return {string} string representation of table layout */ export function toStrings(carrierData, table) { const l1 = [ "00000000", "20000101_000000 No log in ", " ", "No user logged in ", "--{ RES }--", "V;200", "--{ CFG }--", "999;219;32;" ]; const s2 = toString_internalCarriers(carrierData, table); const l3 = toStrings_internalLabware(carrierData, table); const l4 = toStrings_hotels(carrierData, table); const l5 = toStrings_externals(carrierData, table); const l6 = [ "996;0;0;", "--{ RPG }--" ]; const l = _.flatten([l1, s2, l3, l4, l5, l6]); //console.log(l.join("\n")); return l; } /** * Create a string representation of the internal carriers * @param {EvowareCarrierData} carrierData - data loaded from an evoware carrier file * @param {object} table - a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName} * @return {string} string representation of internal carriers */ export function toString_internalCarriers(carrierData, table) { // Get list [[gridIndex, carrier.id]] for internal sites // [[a, b]] const gridToCarrierName_l = _(table).map((c, carrierName) => { //console.log({c, carrierName}) return _.map(c, (x, gridIndexText) => { if (x.internal) { try { const gridIndex = parseInt(gridIndexText); return [gridIndex, carrierName]; } catch(e) { // Do nothing } } return undefined; }); }).flatten().compact().value(); const gridToCarrierName_m = _.fromPairs(gridToCarrierName_l); //console.log({gridToCarrierId_l, gridToCarrierId_m}) const l = _.times(99, gridIndex => { const carrierName = gridToCarrierName_m[gridIndex]; return (_.isEmpty(carrierName)) ? -1 : carrierData.getCarrierByName(carrierName).id; }); return `14;${l.join(";")};`; } /** * Create a string representation of the internal labware * @param {EvowareCarrierData} carrierData - data loaded from an evoware carrier file * @param {object} table - a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName} * @return {string} string representation of internal labware */ export function toStrings_internalLabware(carrierData, table) { const items0 = []; _.forEach(table, (c, carrierName) => { _.forEach(c, (g, gridIndexText) => { if (g.internal === true) { const carrierId = _.get(carrierData.getCarrierByName(carrierName), 'id', -1); items0.push({carrierName, carrierId, gridIndex: parseInt(gridIndexText), g}); } }); }); // Sort by gridIndex const items = _.sortBy(items0, 'gridIndex'); //console.log("items:") //console.log(items) const gridIndexToItem = _(items).map(x => [x.gridIndex, x]).fromPairs().value(); //console.log({gridIndexToItem}) return _.flatten(_.times(99, gridIndex => { const item = gridIndexToItem[gridIndex]; if (_.isUndefined(item)) { return "998;0;"; } else { const carrier = carrierData.getCarrierByName(item.carrierName); //console.log({item, carrier}) //val sSiteCount = if (carrier.nSites > 0) carrier.nSites.toString else "" const namesAndLabels = _.times(carrier.siteCount, siteIndex => { //console.log({g: item.g}) const labwareModelName = _.get(item.g, [siteIndex + 1, 'labwareModelName'], ""); const label = _.get(item.g, [siteIndex + 1, 'label'], ""); return {labwareModelName, label}; }) return [ `998;${carrier.siteCount};${_.map(namesAndLabels, x => x.labwareModelName).join(';')};`, `998;${_.map(namesAndLabels, x => x.label).join(';')};`, ] } })); } /** * Create a string representation of the hotels * @param {EvowareCarrierData} carrierData - data loaded from an evoware carrier file * @param {object} table - a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName} * @return {string} string representation of hotels */ export function toStrings_hotels(carrierData, table) { const hotelItems0 = []; _.forEach(table, (c, carrierName) => { _.forEach(c, (g, gridIndexText) => { if (g.hotel === true) { const carrierId = carrierData.getCarrierByName(carrierName).id; hotelItems0.push([carrierId, parseInt(gridIndexText)]); } }); }); //console.log({hotelItems0}); const hotelItems = _.sortBy(hotelItems0, x => x[1]); return _.flatten([ `998;${hotelItems.length};`, hotelItems.map(([carrierId, gridIndex]) => `998;${carrierId};${gridIndex};`) ]); } /** * Create a string representation of external carriers * @param {EvowareCarrierData} carrierData - data loaded from an evoware carrier file * @param {object} table - a table layout, keys are carrier names, sub-keys are gridIndexes or properties, sub-sub-keys are siteIndexes or property, and sub-sub-sub-keys {label, labwareModelName} * @return {string} string representation of external carriers */ export function toStrings_externals(carrierData, table) { const items0 = []; _.forEach(table, (c, carrierName) => { _.forEach(c, (g, gridIndexText) => { if (g.external) { const carrierId = _.get(carrierData.getCarrierByName(carrierName), 'id', -1); items0.push({carrierName, carrierId, gridIndex: parseInt(gridIndexText), g}); } }); }); // Sort by carrierId const items = _.sortBy(items0, 'carrierId'); // Generate list of external objects and their carriers const l1 = _.flatten([ `998;${items.length};`, items.map(({carrierName, g}) => `998;${g.external.n1};${g.external.n2};${carrierName};`) ]); // Generate list of labware models const itemsWithLabware = items.filter(item => _.has(item, "g.1.labwareModelName")); const l2 = _.flatten([ `998;${itemsWithLabware.length};`, itemsWithLabware.map(({carrierId, g}) => `998;${carrierId};${_.get(g, "1.labwareModelName")};`) ]); // Generate grid list const l3 = items.map(({gridIndex}) => `998;${(gridIndex === -1) ? 1 : gridIndex};`); return _.concat(l1, l2, l3); }