UNPKG

liquid-data-handler

Version:

Transform plate layouts according to liquid handling instructions

485 lines 24.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const lodash_1 = __importStar(require("lodash")); const array_object_transformer_1 = require("array-object-transformer"); // import PlateTransformer from "plate-data-transfer/dist/tsc/main"; const PlateTransformerClass_1 = __importDefault(require("./PlateTransformerClass")); const getNextPlateInstructions_1 = __importDefault(require("./getNextPlateInstructions")); const InstructionKeys_1 = require("./keys/InstructionKeys"); const LayoutKeys_1 = require("./keys/LayoutKeys"); // import { TransferInstructionsType } from "plate-data-transfer"; const well_id_formatter_1 = require("well-id-formatter"); const sufSep = "_suftag_"; function getAllIndexes(arr, val) { var indexes = [], i = -1; while ((i = arr.indexOf(val, i + 1)) !== -1) { indexes.push(i); } return indexes; } class liquidDataHander { constructor(inputLayouts, inputInstructions, platesInfo, keySet, clashColumnStrategies = []) { this.inputInstructions = []; this.inputLayouts = []; //each plate in its own this.instructions = []; this.layouts = []; this.layoutsSuffix = []; this.finalDestPlates = []; this.discoveredClashes = []; this.layoutGroups = []; this.removeVolumes = false; this.inputLayouts = inputLayouts; this.inputInstructions = inputInstructions; this.plateInfo = platesInfo.map((plate) => (Object.assign(Object.assign({}, plate), { suffix: `${sufSep}${plate.suffix}` }))); this.keySet = keySet; this.layouts = []; this.layoutsSuffix = []; //layouts but colnames all have plate suffix this.columnSuffixNames = {}; this.instructions = []; this.clashStrategy = "suffix"; this.clashColumnStrategies = clashColumnStrategies; // not implemented yet this.finalDestPlates = []; this.discoveredClashes = []; this.layoutGroups = []; this.removeVolumes = false; //if user doesn't care about volumes //TODO this.setup(); } setup() { this.formatLayouts(); this.addMissingPlateLayouts(); this.detectLayoutGroups(); this.formatInstructions(); this.detectUnusedPlates(); this.assertUniqueSuffixes(); this.setLayoutSuffix(); } detectLayoutGroups() { //if a user uploads multiple layout plates, some of them will have the same column headers //here we idenitfy which plates are grouped with which this.layouts.forEach((layout) => { let plate_id = layout[0].Plate; let colnames = Object.keys(layout[0]).filter((colname) => ![LayoutKeys_1.PLATE_ID_KEY, LayoutKeys_1.WELL_ID_KEY].includes(colname)); //get colnames let colnamesString = colnames.sort().join(""); this.layoutGroups.push({ plate_id, group: colnamesString }); }); } fillMissingColumnsInPlateLayoutGroups() { //if 2+ destination plates have the same column names as input, //we should ensure they have the same column names as output //user will likely concatenate them. const volumeRegex = new RegExp(`volume${sufSep}`); let groupObj = {}; this.layoutGroups.forEach((origGroup) => { if (groupObj.hasOwnProperty(origGroup.group)) { groupObj[origGroup.group].push(origGroup.plate_id); } else if (origGroup.group !== "") { //don't add empty plates groupObj[origGroup.group] = [origGroup.plate_id]; } }); let plateGroups = Object.values(groupObj); plateGroups.forEach((plateGroup) => { let allGroupColnamesSet = new Set(); plateGroup.forEach((plateid) => { const layout = this.layouts.filter((d) => d[0].Plate === plateid)[0]; layout.forEach((row) => { [...Object.keys(row)].reduce((a, e) => a.add(e), allGroupColnamesSet); }); }); //all the colnames required for each plate in the group let allGroupColnamesArray = Array.from(allGroupColnamesSet); plateGroup.forEach((plateid) => { //iterate over plate in layout, and add on the colname if they do not exist this.layouts.forEach((layout) => { if (layout[0].Plate === plateid) { layout.forEach((row) => { allGroupColnamesArray.forEach((colname) => { if (!row.hasOwnProperty(colname)) { row[colname] = colname.match(volumeRegex) ? 0 : null; } }); }); } }); }); }); } fillMissingColumnsWithinPlateLayout() { //collect all the column names in this layout const volumeRegex = new RegExp(`volume${sufSep}`); this.layouts.forEach((layout) => { let allColnamesSet = new Set(); layout.forEach((row) => { [...Object.keys(row)].reduce((a, e) => a.add(e), allColnamesSet); let allColnamesArray = Array.from(allColnamesSet); //make sure each row has all column names layout.forEach((row) => { allColnamesArray.forEach((colname) => { if (colname.match(volumeRegex)) { //missing volumes should be 0, not null row[colname] = row.hasOwnProperty(colname) ? row[colname] : 0; } else { row[colname] = row.hasOwnProperty(colname) ? row[colname] : null; } }); }); }); }); } getClashReport() { this.runLiquidDataHandlerAllSuffix(); // this is resetting the source map this.discoverClashes(); return { discoveredClashes: this.discoveredClashes, defaultClashStrategy: this.clashStrategy, clashColumnStrategies: this.clashColumnStrategies, }; } getFinalPlateLayouts(clashColumnStrategies = [], defaultClashStrategy = "suffix") { this.runLiquidDataHandlerWithClashStrategies(clashColumnStrategies, defaultClashStrategy); this.fillMissingColumnsWithinPlateLayout(); this.fillMissingColumnsInPlateLayoutGroups(); this.removeSufTags(); // if (this.removeVolumes) { // //take out volumes if user doesn't care // //TODO // } return this.layouts; } removeSufTags() { //not removing user's suffix //removing internal suffix tag this.layouts.forEach((layout, i) => { let output = { Plate: [], Well: [] }; let collayout = (0, array_object_transformer_1.arrayToObject)(layout); Object.entries(collayout).forEach((arr) => { let newplatename = arr[0].replace(sufSep, ""); output[newplatename] = arr[1]; }); let newArray = (0, array_object_transformer_1.objectToArray)(output); this.layouts[i] = newArray; }); } runLiquidDataHandlerWithClashStrategies(clashColumnStrategies, defaultClashStrategy, defaultSeparator = " + ") { let plateInstructionChunk = (0, getNextPlateInstructions_1.default)(this.instructions); let stillRunning = true; while (stillRunning) { let nextChunk = plateInstructionChunk.next(); if (!nextChunk.done) { //get sourceLayout, getDestLayout, chunkInstructions sourcePlateInfo, destPlateInf let chunkInstructions = nextChunk.value; let { srcLayout, destLayout, srcPlateRel, destPlateRel } = this.getPlateTransformerParams(chunkInstructions, "layouts"); let transformer = new PlateTransformerClass_1.default(srcLayout, destLayout, chunkInstructions, srcPlateRel, destPlateRel, clashColumnStrategies); //get new destination plate layout transformer.assigncolumnClashStrategies(clashColumnStrategies); transformer.defaultClashStragty = defaultClashStrategy; transformer.defaultSeparator = defaultSeparator; let newDestLayout = transformer.transformPlates(null); //now update the destPlate with newDestLayout this.layouts.forEach((layout, i) => { if (layout[0]["Plate"] === newDestLayout[0]["Plate"]) { //the new layout is updates to include transfered wells this.layouts[i] = newDestLayout; } }); } else { //no more instructions stillRunning = false; } } } runLiquidDataHandlerAllSuffix() { let plateInstructionChunk = (0, getNextPlateInstructions_1.default)(this.instructions); let stillRunning = true; while (stillRunning) { let nextChunk = plateInstructionChunk.next(); if (!nextChunk.done) { //get sourceLayout, getDestLayout, chunkInstructions sourcePlateInfo, destPlateInf let chunkInstructions = nextChunk.value; let { srcLayout, destLayout, srcPlateRel, destPlateRel } = this.getPlateTransformerParams(chunkInstructions, "layoutsSuffix"); let transformer = new PlateTransformerClass_1.default(srcLayout, destLayout, chunkInstructions, srcPlateRel, destPlateRel); //get new destination plate layout let newDestLayout = transformer.transformPlates(null); //now update the destPlate with newDestLayout this.layoutsSuffix.forEach((layout, i) => { if (layout[0]["Plate"] === newDestLayout[0]["Plate"]) { //the new layout is updates to include transfered wells this.layoutsSuffix[i] = newDestLayout; } }); } else { //no more instructions stillRunning = false; } } } resolveClashes() { // if repeated columns do not have clashed values // then ignore // if } getClashingIndexes(repeatedSuffixColnames, columnLayout) { let arraysOfValuesFromRepeatedCols = repeatedSuffixColnames.map((cn) => columnLayout[cn]); //get an array of rows counting the number of non-null values. //clashing values have 2 or more. let numberValuesArray = lodash_1.default.zip(...arraysOfValuesFromRepeatedCols).map((arr) => (0, lodash_1.sum)(arr.map((val) => val !== null))); let clashingIndexes = numberValuesArray.reduce((acc, cur, idx) => { if (cur > 1) { acc.push(idx); } return acc; }, []); return clashingIndexes; } discoverClashes() { //after layoutSuffix is run, discover where there are clashes //then create a table for user to this.layoutsSuffix.forEach((layout) => { let plateid = layout[0]["Plate"]; let suffix = this.plateInfo.filter((plate) => plate.layoutId === plateid)[0]["suffix"]; if (suffix) { let columnLayout = (0, array_object_transformer_1.arrayToObject)(layout); let suffixColnames = Object.keys(columnLayout); let suffixRegEx = new RegExp(`^(.*)${sufSep}(.*)$`); let noSuffixColnames = suffixColnames.map((sufcol) => { let match = suffixRegEx.exec(sufcol); return match && match[1] ? match[1] : sufcol; }); let uniqNoSuffColnames = lodash_1.default.uniq(noSuffixColnames); if (uniqNoSuffColnames.length + 1 === noSuffixColnames.length) { } else { //create an object specifiy the potentially clashing column names uniqNoSuffColnames.forEach((noSuffixColname) => { if (noSuffixColname !== "volume_") { let indexes = getAllIndexes(noSuffixColnames, noSuffixColname); if (indexes.length > 1) { //the column name is present more than one time //get the repeated equivalents of the colnams with suffixes let repeatedSuffixColnames = indexes.map((idx) => suffixColnames[idx]); //just because it is repeated doesn't mean there are clashes in the actual wells //it could be that different plates transfer different into non-overlapping wells. //see if the actual values clash - if one has a value while the other is null, it isn't a real clash; let clashingIndexes = this.getClashingIndexes(repeatedSuffixColnames, columnLayout); this.discoveredClashes.push({ plate_id: plateid, colname: noSuffixColname, suffixColnames: repeatedSuffixColnames, isActualClash: clashingIndexes.length > 1, clashingIndexes, }); } } }); } } else { throw Error(`No suffix error on plate ${plateid}`); } }); } getPlateTransformerParams(chunkInstructions, layoutChoice) { //get source plate info let srcInstructionsPlateID = chunkInstructions[0]["Source Plate"]; let srcPlateIDArr = this.plateInfo.filter((info) => info["instructionId"] === srcInstructionsPlateID); if (srcPlateIDArr.length === 0) { throw Error(`Report Program Error: instruction plate ${srcInstructionsPlateID} not in plateInfo`); } let srcInfo = srcPlateIDArr[0]; let srcLayoutPlateID = srcInfo["layoutId"]; let srcLayoutArr = this[layoutChoice].filter((layout) => layout[0]["Plate"] === srcLayoutPlateID); if (!srcLayoutArr.length) { throw Error(`Source Plate ${srcLayoutPlateID} not in PlateInfo`); } let srcLayout = srcLayoutArr[0]; //get destination plate info let destInstructionsPlateID = chunkInstructions[0]["Destination Plate"]; let destPlateIDarr = this.plateInfo.filter((info) => info["instructionId"] === destInstructionsPlateID); if (destPlateIDarr.length === 0) { throw Error(`Report Program Error: instruction plate ${destInstructionsPlateID} not in plateInfo`); } let destInfo = destPlateIDarr[0]; let destLayoutPlateID = destInfo["layoutId"]; let destLayoutArr = this[layoutChoice].filter((layout) => layout[0]["Plate"] === destLayoutPlateID); if (!destLayoutArr) { throw Error(`Destination plate ${destLayoutPlateID} not in plateInfo`); } let destLayout = destLayoutArr[0]; let srcPlateRel = { plate_map_id: srcInfo.layoutId, plate_instruction_id: srcInfo.instructionId, plate_size: srcInfo.size, plate_suffix: srcInfo.suffix, }; let destPlateRel = { plate_map_id: destInfo.layoutId, plate_instruction_id: destInfo.instructionId, plate_size: destInfo.size, plate_suffix: destInfo.suffix, }; return { srcLayout, destLayout, srcPlateRel, destPlateRel, }; } detectUnusedPlates() { //throw errors for following senarios. let configLayoutPlates = this.plateInfo.map((plt) => plt.layoutId); let configInstructionPlates = this.plateInfo.map((plt) => plt.instructionId); // let configPlates = [...configInstructionPlates, ...configLayoutPlates]; let sourceInstructionPlates = lodash_1.default.uniq(this.instructions.map((i) => i["Source Plate"])); let destinationInstructionPlates = lodash_1.default.uniq(this.instructions.map((i) => i["Destination Plate"])); let instructionPlates = lodash_1.default.uniq([ ...sourceInstructionPlates, ...destinationInstructionPlates, ]); let layoutPlates = lodash_1.default.uniq(this.layouts.map((layout) => layout[0][LayoutKeys_1.PLATE_ID_KEY])); //a plate in config is not present in instructions configInstructionPlates.forEach((plate_name) => { if (!instructionPlates.includes(plate_name)) { throw Error(`plate id ${plate_name} is missing from transfer instructions.`); } }); //a plate in config is not present in layouts configLayoutPlates.forEach((plate_name) => { if (!layoutPlates.includes(plate_name)) { throw Error(`plate id "${plate_name}" is missing from plate layouts.`); } }); //a layout is not present in config layoutPlates.forEach((plate_name) => { if (!configLayoutPlates.includes(plate_name)) { throw Error(`layout plate id "${plate_name}" is missing from plate configuration object.`); } }); //an instructions plate is not present in config instructionPlates.forEach((plate_name) => { if (!configInstructionPlates.includes(plate_name)) { throw Error(`transfer instructions plate id "${plate_name}" is missing from plate configuration object.`); } }); } assertUniqueSuffixes() { let suffixes = this.plateInfo.map((p) => p.suffix); let uniq_suffixes = lodash_1.default.uniq(suffixes); if (uniq_suffixes.length < suffixes.length) { throw Error("Error: All plates must have a unique suffix."); } } setLayoutSuffix() { //create a copy of plate layouts with changed column names to contain plate suffix in all columns lodash_1.default.cloneDeep(this.layouts).forEach((layout) => { let columnLayout = (0, array_object_transformer_1.arrayToObject)(layout); let suffixArr = this.plateInfo.filter((plate) => plate.layoutId === columnLayout[LayoutKeys_1.PLATE_ID_KEY][0]); if (suffixArr.length === 0) { throw Error(`A plate layout is defined in config, but not : ${columnLayout[LayoutKeys_1.PLATE_ID_KEY][0]}`); } let suffix = this.plateInfo.filter((plate) => plate.layoutId === columnLayout[LayoutKeys_1.PLATE_ID_KEY][0])[0].suffix; if (suffix) { let colnames = Object.keys(columnLayout).map((col) => { let col_suff = [LayoutKeys_1.PLATE_ID_KEY, LayoutKeys_1.WELL_ID_KEY].includes(col) ? col : `${col}${suffix}`; return [col, col_suff]; }); //record the columns and column_suffixes this.columnSuffixNames[columnLayout[LayoutKeys_1.PLATE_ID_KEY][0]] = colnames; //change the column names to include the suffix colnames.forEach((colArr) => { columnLayout[colArr[1]] = columnLayout[colArr[0]]; if (![LayoutKeys_1.PLATE_ID_KEY, LayoutKeys_1.WELL_ID_KEY].includes(colArr[0])) { delete columnLayout[colArr[0]]; } }); let rowLayout = (0, array_object_transformer_1.objectToArray)(columnLayout); if (rowLayout[0][LayoutKeys_1.PLATE_ID_KEY] && rowLayout[0][LayoutKeys_1.WELL_ID_KEY]) { this.layoutsSuffix.push(rowLayout); } else { throw Error("Report This Error to www.github.com/svgpubs: Missing Plate and Well : this is a problem with the program. "); } } }); } get columnClashes() { //runLiquidDataHandler and records clashes return this.clashColumnStrategies; } set columnClashStragtegies(strats) { this.clashColumnStrategies = strats; } addMissingPlateLayouts() { this.plateInfo.forEach((plate) => { if (plate.isEmpty && plate.layoutId === "") { let newPlate = (0, well_id_formatter_1.getPlate)(plate.size).map((wellrow) => { return { Plate: plate.instructionId, Well: wellrow["padded"] }; }); plate.layoutId = plate.instructionId; //now it has a layout id //add the empty plate layout to this.layouts this.layouts.push(newPlate); } else if (plate.isEmpty || plate.layoutId === "") { throw Error(`Error: Plate Config plate "isEmpty = ${plate.isEmpty}"" but layoutId = ${plate.layoutId}. This is an error.`); } }); } formatLayouts() { // create empty plates for destinatios that are missing plate layouts var inputLayoutsCopy = lodash_1.default.cloneDeep(this.inputLayouts); //ensure well data has correct Plate and Well keys inputLayoutsCopy.forEach((layout, layoutIndex) => { this.layouts.push([]); layout.forEach((wellRow) => { let plateID = wellRow[this.keySet["plateIdKey"]]; let wellId = wellRow[this.keySet["wellIdKey"]]; delete wellRow[this.keySet["plateIdKey"]]; delete wellRow[this.keySet["wellIdKey"]]; this.layouts[layoutIndex].push(Object.assign(Object.assign({}, wellRow), { [LayoutKeys_1.PLATE_ID_KEY]: plateID, [LayoutKeys_1.WELL_ID_KEY]: wellId })); }); }); } formatInstructions() { var inputInstructionsCopy = lodash_1.default.cloneDeep(this.inputInstructions); let newRow; inputInstructionsCopy.forEach((inst) => { newRow = { [InstructionKeys_1.SRC_PLATE_KEY]: inst[this.keySet["sourePlateKey"]], [InstructionKeys_1.SRC_WELL_KEY]: inst[this.keySet["sourceWellKey"]], [InstructionKeys_1.VOL_KEY]: +inst[this.keySet["volumeKey"]], [InstructionKeys_1.DEST_PLATE_KEY]: inst[this.keySet["destPlateKey"]], [InstructionKeys_1.DEST_WELL_KEY]: inst[this.keySet["destWellKey"]], }; this.instructions.push(newRow); }); } } exports.default = liquidDataHander; //# sourceMappingURL=liquidDataHandler.js.map