liquid-data-handler
Version:
Transform plate layouts according to liquid handling instructions
485 lines • 24.1 kB
JavaScript
;
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