UNPKG

liquid-data-handler

Version:

Transform plate layouts according to liquid handling instructions

302 lines 15.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const well_id_formatter_1 = require("well-id-formatter"); const lodash_1 = __importDefault(require("lodash")); class PlateTransformer { constructor(sourceMap, destMap, instructions, sourceRelationship, destRelationship, columnClashStrategies = []) { this.sourceMap = sourceMap; this.destMap = destMap; this.newDestMap = lodash_1.default.cloneDeep(destMap); this.instructions = instructions; this.sourceRelationship = sourceRelationship; this.destRelationship = destRelationship; this.defaultClashStragty = "suffix"; this.defaultSeparator = ", "; this.internalWellType = "padded"; this.columnClashStrategies = columnClashStrategies; this.newDestMapCols = Object.keys(this.newDestMap[0]); this.clashedColumns = []; this.validateAndFormatData(); } validateLayoutKeys() { const destKeysValid = this.destMap.every((well) => !!well["Well"] && !!well["Plate"]); const srcKeysValid = this.sourceMap.every((well) => !!well["Well"] && !!well["Plate"]); if (!(destKeysValid && srcKeysValid)) { throw Error(`PlateLayout Keys Invalid. Source and Destination Platelayouts both require "Plate" and "Well" keys.`); } } validateInstructionsKeys() { const instructionKeysValid = this.instructions.every((t) => t["Source Plate"] && t["Destination Well"] && t["Source Well"] && t["Volume"] && t["Destination Plate"]); if (!instructionKeysValid) { throw Error(`Transfer Instruction Keys Invalid. Keys must be "Source Plate", "Destination Plate", "Source Well", "Destination Well", and "Volume"`); } } validateAndFormatData() { this.validateInstructionsKeys(); this.validateLayoutKeys(); this.validateInstructionIDs(); this.validateMapIDs(); this.validateAndConvertWellFormats(this.internalWellType); this.fillMissingWells(); if (this.sourceMap.length !== +this.sourceRelationship.plate_size) { throw Error(`Source Map must have ${this.sourceRelationship.plate_size} rows, not ${this.sourceMap.length}`); } if (this.newDestMap.length !== this.destRelationship.plate_size) { throw Error(`Destination Map must have ${this.destRelationship.plate_size} rows, not ${this.newDestMap.length}`); } } setDefaultClashStrategy(strategy) { this.defaultClashStragty = strategy; } setClashConcatenationSeparator(separator) { this.defaultSeparator = separator; } assigncolumnClashStrategies(columnClashStrategies) { this.columnClashStrategies = columnClashStrategies; } fillMissingWells() { // if well data are missing, (ex, missing object for well A4), // insert them to make the plate complete, setting values to null //internalWellType should be sortable, either 'padded' or 'number', not unpadded // const sourceMapInternal: PlateMapRowType[] = _.cloneDeep(this.sourceMap); // const destMapInternal: PlateMapRowType[] = _.cloneDeep(this.newDestMap); //get complete array of source well ids const sourcePlateWells = (0, well_id_formatter_1.getPlateWells)(this.sourceRelationship.plate_size, this.internalWellType); //get complete array of dest well ids const destPlateWells = (0, well_id_formatter_1.getPlateWells)(this.destRelationship.plate_size, this.internalWellType); const sourceNewMapInternal = []; const destNewMapInternal = []; //create an object of each variable with the value set to null //from sourcemap const sourceMapRowInternalArr = Object.keys(this.sourceMap[0]) .filter((d) => d !== "Plate" && d !== "Well") .map((d) => [d, null]); const sourceMapRowInternalObj = lodash_1.default.fromPairs(sourceMapRowInternalArr); let idxCount = 0; sourcePlateWells.forEach((well) => { const srcMapRow = this.sourceMap[idxCount]; const srcMapRowWellIsCorrect = srcMapRow["Well"] === well; if (!srcMapRowWellIsCorrect) { // push an empty row into the new plate sourceNewMapInternal.push(Object.assign({ Plate: this.sourceRelationship.plate_map_id, Well: well }, sourceMapRowInternalObj)); } else { //if the well is correct idxCount += 1; sourceNewMapInternal.push(Object.assign({}, srcMapRow)); } }); //repeate steps for destination source const destMapRowInternalArr = Object.keys(this.newDestMap[0]) .filter((d) => d !== "Plate" && d !== "Well") .map((d) => [d, null]); const destMapRowInternalObj = lodash_1.default.fromPairs(destMapRowInternalArr); idxCount = 0; destPlateWells.forEach((well) => { const destMapRow = this.newDestMap[idxCount]; const destMapRowWell = destMapRow["Well"] === well; if (!destMapRowWell) { destNewMapInternal.push(Object.assign({ Plate: this.destRelationship.plate_map_id, Well: well }, destMapRowInternalObj)); } else { idxCount += 1; destNewMapInternal.push(Object.assign({}, destMapRow)); } }); //assign complete maps this.sourceMap = sourceNewMapInternal; this.newDestMap = destNewMapInternal; } validateAndConvertWellFormats(wellFormat) { let newWell; const destPlateSize = this.destRelationship.plate_size; const sourcePlateSize = this.sourceRelationship.plate_size; this.newDestMap = this.newDestMap.map((row) => { newWell = (0, well_id_formatter_1.wellIDFormatter)(row["Well"], wellFormat, destPlateSize); if (typeof newWell === "string") { row["Well"] = newWell; return row; } else { throw Error(`${row["Well"]} not recognized in ${destPlateSize} well plate`); } }); this.sourceMap = this.sourceMap.map((row) => { newWell = (0, well_id_formatter_1.wellIDFormatter)(row["Well"], wellFormat, sourcePlateSize); if (typeof newWell === "string") { row["Well"] = newWell; return row; } else { throw Error(`${row["Well"]} not recognized in ${sourcePlateSize} well plate`); } }); } transformPlates(wellformat) { let localClashedCols = []; if (this.columnClashStrategies.length) { localClashedCols = this.columnClashStrategies.map((cc) => cc.column_name); } const sourceCols = Object.keys(this.sourceMap[0]).filter((d) => d !== "Plate" && d !== "Well"); const destCols = Object.keys(this.destMap[0]).filter((d) => d !== "Plate" && d !== "Well"); // add source source columns to the new destination map //unless those columns already exist on the destination map. //initiate new destColumns if they do not already exist sourceCols.forEach((scol) => { // if (!destCols.includes(scol) && localClashedCols.includes(scol)) { if (!destCols.includes(scol) && !localClashedCols.includes(scol)) { //add a null col if the destCol doesn't have the source col and if it isn't a clashign col this.newDestMap = this.newDestMap.map((d) => { return Object.assign(Object.assign({}, d), { [scol]: null }); }); } }); //add the volumn column to destmap = the amount of volume transfered to dest plate from source plate const volColname = `volume${this.sourceRelationship.plate_suffix}`; sourceCols.forEach((scol) => { if (!destCols.includes(scol)) { this.newDestMap = this.newDestMap.map((d) => (Object.assign(Object.assign({}, d), { [volColname]: 0 }))); } }); //transfer source values to dest values this.instructions.forEach((inst) => { const sourceMapRow = Object.assign({}, this.sourceMap.filter((smap) => inst["Source Well"] === smap.Well)[0]); const destMapRow = lodash_1.default.find(this.newDestMap, (dmap) => inst["Destination Well"] === dmap.Well); //mutable row if (sourceMapRow) { //if we have a sourceMapRow sourceCols.forEach((scol) => { let isClashedCol = this.columnClashStrategies.some((strat) => strat.column_name === scol); if (destMapRow) { if (destMapRow[scol] === null && !isClashedCol) { //assign source col & value to destMap destMapRow[scol] = sourceMapRow[scol]; } else { console.log(sourceMapRow, destMapRow); //if value clashes: destMapRow[scol] already has been assigned a value this.mutateDestMapRowWithClashedValues(destMapRow, sourceMapRow, scol); } } }); //add the volume from source to destination well const instructionVolume = inst.Volume == null ? 0 : +inst.Volume; if (destMapRow) { let previousVolume = destMapRow[volColname]; previousVolume = !(previousVolume == null) && typeof previousVolume === "number" ? previousVolume : 0; //assign updated volume destMapRow[volColname] = instructionVolume + previousVolume; } } }); if (!(wellformat == null) && wellformat !== this.internalWellType) { //if there is a different well this.validateAndConvertWellFormats(wellformat); } return this.newDestMap; } assignNewColnameToDestMap(newColname) { this.newDestMap.forEach((row) => { row[newColname] = row[newColname] ? row[newColname] : null; }); } mutateDestMapRowWithClashedValues(destMapRow, sourceMapRow, colname) { if (!this.clashedColumns.includes(colname)) { this.clashedColumns.push(colname); } let newColname = null; //in case we add suffix to the colname let stragety = this.defaultClashStragty; let separator = this.defaultSeparator; //adjust strategy and separator if we have a specific clashStrategy for this colname const specificClashStrategy = this.columnClashStrategies.filter((cs) => cs.column_name === colname); if (specificClashStrategy.length) { stragety = specificClashStrategy[0].strategy; separator = specificClashStrategy[0].concatenation_separator; } switch (stragety) { case "concatenate": destMapRow[colname] = [destMapRow[colname], sourceMapRow[colname]].join(separator); break; case "keepDestination": break; case "keepSource": destMapRow[colname] = sourceMapRow[colname]; break; case "suffix": { newColname = `${colname}${this.sourceRelationship.plate_suffix}`; if (!this.newDestMapCols.includes(newColname)) { this.assignNewColnameToDestMap(newColname); this.newDestMapCols.push(newColname); } destMapRow[newColname] = sourceMapRow[colname]; break; } default: throw Error(`${stragety} not recognized as a clash strategy`); } return destMapRow; } validateMapIDs() { //ensures that the plate ids match //get all the plate ids from the dest map const mapDestIdArr = lodash_1.default.uniq(this.destMap.map((row) => row["Plate"])); //get all the plate ids from the source map const mapSourceIdArr = lodash_1.default.uniq(this.sourceMap.map((row) => row["Plate"])); //we only transfer one plate at a time. if (mapDestIdArr.length > 1) { throw Error(`PlateTransformerClass Destination Plate Maps contains more than one 'Plate': "${mapDestIdArr.join(" and ")}". Must only have one Plate.`); } if (mapSourceIdArr.length > 1) { throw Error(`PlateTransformerClass Source Plate Maps contains more than one 'Plate': "${mapSourceIdArr.join(" and ")}". Must only have one Plate.`); } //instructions and plate maps just have one plate id for source and destinatino const mapDestId = mapDestIdArr[0]; const mapSourceId = mapSourceIdArr[0]; //make sure that destination plate map Plate ID is found in plate relationships if (this.destRelationship.plate_map_id !== mapDestId) { throw Error(`Destination Plate Relationship Plate ID "${this.destRelationship.plate_map_id}" does NOT match Plate Map Plate ID "${mapDestId}"`); } //make sure that source Plate Map Plate ID is found in plate relationships if (this.sourceRelationship.plate_map_id !== mapSourceId) { throw Error(`Source Plate Relationship Plate ID "${this.sourceRelationship.plate_map_id}" does NOT match Plate Map Plate ID "${mapSourceId}"`); } } validateInstructionIDs() { //throw error if there are more than one source and one destination in instructions const instDestIdArr = lodash_1.default.uniq(this.instructions.map((inst) => inst["Destination Plate"])); const instSourceIdArr = lodash_1.default.uniq(this.instructions.map((inst) => inst["Source Plate"])); if (instDestIdArr.length > 1) { throw Error(`PlateTransformerClass Instructions contains more than one 'Destination Plate': "${instDestIdArr.join(" and ")}". Must only have one Destination Plate.`); } if (instSourceIdArr.length > 1) { throw Error(`PlateTransformerClass Instructions contains more than one 'Source Plate': "${instSourceIdArr.join(" and ")}". Must only have one Source Plate.`); } const instDestId = instDestIdArr[0]; const instSourceId = instSourceIdArr[0]; //make sure that instructions Destination Plate ID is found in destination plate relationships if (this.destRelationship.plate_instruction_id !== instDestId) { throw Error(`Destination Plate Relationship Plate ID "${this.destRelationship.plate_instruction_id}" does NOT match Destintation Plate in transfer instructions "${instDestId}"`); } //make sure that instruction Source Plate ID is found in plate relationships if (this.sourceRelationship.plate_instruction_id !== instSourceId) { throw Error(`Source Plate Relationship Plate ID "${this.sourceRelationship.plate_instruction_id}" does NOT match Source Plate in transfer instructions "${instSourceId}"`); } } } exports.default = PlateTransformer; //# sourceMappingURL=PlateTransformerClass.js.map