app/frontend/javascript/shared/transfersLayouts.js
// This files contains the logic for building transfers using different
// layouts.
//
// A resulting transfer is an object of the form:
// { request: <request object>,
// well: <well object>,
// plateObj: { plate: <plate object>,
// state: <state string>,
// index: <plate index integer>
// },
// targetWell: <position string>
// }
import { requestIsLibraryCreation, requestIsActive } from './requestHelpers.js'
import { indexToName, nameToIndex, quadrantTargetFor } from './wellHelpers.js'
import buildArray from './buildArray.js'
const quadrantOffsets = {
rowOffset: [0, 1, 0, 1],
colOffset: [0, 0, 1, 1],
}
// Used to calculate QuadrantStamp transfers. Combines four plates into one.
// Receives an array of requestsWithPlates and returns an object containing an array of
// valid transfers and an array of duplicated transfers (i.e. a transfer whose
// both source and destination match a transfer already present in the
// validTransfers array will be put in the duplicatedTransfer array).
// For each request, the target well is calculated using quadrantTargetFor
// function with quadrantOffsets.
//
// Resulting plate eg. where P1-4 is Plate 1-4
// +--+--+--+--+--+--+--~
// |P1|P3|P1|P3|P1|P3|P1
// |A1|A1|A2|A2|A3|A3|A4
// +--+--+--+--+--+--+--~
// |P2|P4|P2|P4|P2|P4|P1
// |A1|A1|A2|A2|A3|A3|A4
// +--+--+--+--+--+--+--~
// |P1|P3|P1|P3|P1|P3|P1
// |B1|B1|B2|B2|B3|B3|B4
// +--+--+--+--+--+--+--~
// |P2|P4|P2|P4|P2|P4|P1
// |B1|B1|B2|B2|B3|B3|B4
const quadrantTransfers = function (requestsWithPlates) {
const transfersSet = new Set()
const validTransfers = []
const duplicatedTransfers = []
for (let i = 0; i < requestsWithPlates.length; i++) {
const { request, well, plateObj } = requestsWithPlates[i]
if (request === undefined) {
continue
}
const targetWell = quadrantTargetFor(
plateObj.index,
well.position.name,
quadrantOffsets.rowOffset,
quadrantOffsets.colOffset
)
const transfer = {
request: request,
well: well,
plateObj: plateObj,
targetWell: targetWell,
}
const transferStr = well.position.name + targetWell
if (transfersSet.has(transferStr)) {
duplicatedTransfers.push(transfer)
} else {
transfersSet.add(transferStr)
validTransfers.push(transfer)
}
}
return {
validTransfers: validTransfers,
duplicatedTransfers: duplicatedTransfers,
}
}
// Creates a two dimensional matrix where each rows contains a spatial
// representation of the wells of the corresponding plate.
//
// E.g. given two 96-wells plates containing the requests for the following wells:
// Plate 1: {A1, B1, A2} (Note: requests are not ordered)
// Plate 2: {A1, A2, B2}
//
// The matrix will look like:
// [
// [A1, B1, , , , , , , A2, ...],
// [A1, , , , , , , , A2, B2, ...]
// ]
//
// If a well contains more than one request, the extra ones will be stored in
// duplicatedRequests array.
//
// Note: Indexes are calculated for 96 wells plates only.
const buildPlatesMatrix = function (requestsWithPlates, maxPlates, maxWellsPerPlate) {
const platesMatrix = buildArray(maxPlates, () => new Array(maxWellsPerPlate))
const duplicatedRequests = []
for (let i = 0; i < requestsWithPlates.length; i++) {
const { request, well, plateObj } = requestsWithPlates[i]
if (request === undefined) {
continue
}
const wellIndex = nameToIndex(well.position.name, 8)
if (platesMatrix[plateObj.index][wellIndex] === undefined) {
platesMatrix[plateObj.index][wellIndex] = requestsWithPlates[i]
} else {
duplicatedRequests.push(requestsWithPlates[i])
}
}
return { platesMatrix: platesMatrix, duplicatedRequests: duplicatedRequests }
}
// Creates an object with an array of three dimensions, grouping the requests by source plate, library
// type and well index.
//
// E.g. given two 96-wells plates containing the requests for the following wells:
// Plate 1: {P1-A1-Lib1, P1-A1-Lib2, P1-B1-Lib1, P1-A2-Lib1} (Note: requests are not ordered)
// Plate 2: {P2-A1-Lib1, P2-A2-Lib1, P2-A2-Lib1, P2-B2-Lib1}
// Being transferred into two destination plates
//
// The resulting object will look like this:
// {
// 'Plate1': {
// {'Lib1': {'A1': P1-A1-Lib1,'B1': P1-B1-Lib1, 'A2': P1-A2-Lib1]},
// {'Lib2': {'A1': P1-A1-Lib1}}
// },
// 'Plate2': {
// {'Lib1': {'A1': P2-A1-Lib1, 'A2': P2-A2-Lib1, 'B2': P2-B2-Lib1]}
// }
// }
// If a well contains more than one copy of the same library request, the extra ones will be stored in
// a duplicatedRequests array:
// [P2-A2-Lib1]
//
const buildLibrarySplitPlatesMatrix = function (requestsWithPlates) {
const platesMatrix = []
const duplicatedRequests = []
for (const requestObj of requestsWithPlates) {
const { request, well, plateObj } = requestObj
if (request === undefined) {
continue
}
const wellIndex = nameToIndex(well.position.name, 8)
platesMatrix[plateObj.index] ??= {}
platesMatrix[plateObj.index][request.library_type] ??= []
if (typeof platesMatrix[plateObj.index][request.library_type][wellIndex] === 'undefined') {
// We classify requests by source plate, library type and well position at source
platesMatrix[plateObj.index][request.library_type][wellIndex] = requestObj
} else {
// All extra requests in the same source plate, library type and position
// go to the duplication list
duplicatedRequests.push(requestObj)
}
}
return { platesMatrix: platesMatrix, duplicatedRequests: duplicatedRequests }
}
// Used to calculate sequential transfers.
// Receives an array of transfer requests and return an array of transfers
// where the target well is calculated from the index of the request in
// transferRequests array.
//
// Note: The target well name is calculated for a 96 well plate target.
const buildSequentialTransfersArray = function (transferRequests) {
const transfers = new Array(transferRequests.length)
for (let i = 0; i < transferRequests.length; i++) {
const requestWithPlate = transferRequests[i]
transfers[i] = {
request: requestWithPlate.request,
well: requestWithPlate.well,
plateObj: requestWithPlate.plateObj,
targetWell: indexToName(i, 8),
}
}
return transfers
}
// Gets a list of requests and create as many destinations as different library types we have defined in the requests.
// For every source plate a number of wells at the destination is assigned so the wells will be transferred as stamp into
// those list of wells reserved in column order. Eg:
// Default value is 24 wells, so first plate will get the first 3 columns, second plate columns 4 to 6, etc; the position
// will be a stamp from the source into those columns (so A2 in plate 2 will go to A5 in each destination plate)
// Args:
// - transferRequests: each request from a source plate that we want to connect with a destination plate
// - numberOfWellsForEachSourcePlateInColumnOrder: number of wells that every source plate will have reserved in each
// destination plate (default 24 wells (3 columns))
//
const buildSequentialLibrarySplitTransfersArray = function (
transferRequests,
numberOfWellsForEachSourcePlateInColumnOrder = 24
) {
const libraryTypes = []
return transferRequests.map((requestWithPlate) => {
const libraryType = requestWithPlate.request.library_type
if (!libraryTypes.includes(libraryType)) {
libraryTypes.push(libraryType)
}
const incrementalWellsForPlate = requestWithPlate.plateObj.index * numberOfWellsForEachSourcePlateInColumnOrder
const wellIndex = nameToIndex(requestWithPlate.well.position.name, 8) + incrementalWellsForPlate
const plateIndex = libraryTypes.indexOf(libraryType)
return {
request: requestWithPlate.request,
well: requestWithPlate.well,
plateObj: requestWithPlate.plateObj,
targetWell: indexToName(wellIndex, 8),
targetPlate: plateIndex,
}
})
}
// Takes an array with 96 elements (for a 96 well plate)
// Returns an array with elements just for those wells that have been populated (by scanning into the UI)
// including the target well coordinate.
// Example of one element of the output array:
// {
// targetWell: "A1",
// tubeObj:
// {
// index: 0,
// tube:
// {
// id: "1",
// labware_barcode:
// {
// human_barcode: "NT4R",
// ...
// },
// links: {self: "http://localhost:3000/api/v2/tubes/9"},
// receptacle:
// {
// type: "tubes",
// uuid: "db4f1e22-1230-11ec-962c-38f9d3dee0d3"
// }
// }
// }
// }
const buildSequentialTubesTransfersArray = function (transferRequests) {
const transfers = new Array()
for (let i = 0; i < transferRequests.length; i++) {
if (transferRequests[i] === undefined) {
continue
}
const tubeTransferReq = transferRequests[i]
transfers.push({
tubeObj: { tube: tubeTransferReq.labware, index: tubeTransferReq.index },
targetWell: indexToName(i, 8),
})
}
return transfers
}
// Used to calculate sequential transfers. Combines ten 96 well plates into one
// 96 well plate.
// Receives an array of requestsWithPlates and return an object containing
// valid transfers and duplicated transfers (i.e. a transfer whose
// both source and destination match a transfer already present in the
// validTransfers array will be put in the duplicatedTransfer array).
//
// Resulting plate Eg.
//
// Plate 1 Plate 2 Dest. Plate
// +--+--+--~ +--+--+--~ +----+----+----~
// |A1| |A3 |A1| | |P1A1|P1A3|P2D2
// +--+--+--~ +--+--+--~ +----+----+----~
// | | | | |B2|B3 |P1C1|P1D3|P2B3
// +--+--+--~ + +--+--+--~ => +----+----+----~
// |C1|C2| | | | |P1D1|P2A1|P2D3
// +--+--+--~ +--+--+--~ +----+----+----~
// |D1| |D3 | |D2|D3 |P1C2|P2B2|
const sequentialTransfers = function (requestsWithPlates) {
const { platesMatrix, duplicatedRequests } = buildPlatesMatrix(requestsWithPlates, 10, 96)
const transferRequests = platesMatrix.flat()
const validTransfers = buildSequentialTransfersArray(transferRequests)
const duplicatedTransfers = buildSequentialTransfersArray(duplicatedRequests)
return {
validTransfers: validTransfers,
duplicatedTransfers: duplicatedTransfers,
}
}
// Returns only the active library creation requests from a list of requests as input
// Args:
// requestsWithPlates - Array of requests
// Returns:
// Array of active library creation requests
const libraryRequestsWithPlates = function (requestsWithPlates) {
return requestsWithPlates.filter((obj) => requestIsLibraryCreation(obj.request) && requestIsActive(obj.request))
}
// TransferFunction that defines the creation of as many destination plates as library types have been defined
// in the library creation requests at the input plates.
// The number of input plates that are going to be transferred is limited to 4.
// From each input plate it will pick up the first 3 columns (24 wells, wells A1 to H3).
// These columns will be stamped into successive sets of 3 columns on the destination plate(s),
// source 1 to columns 1-3, source 2 to columns 4-6, source 3 to columns 7-9, and source 4 to columns 10-12.
// Returns an object with the list of valid transfers and a list of duplicates, where a duplicate is a request
// from same source plate, same library type and same well at input.
// Args:
// requestsWithPlates - Array of requests from the input plates
// Returns:
// Object with key 'validTransfers' with the list of requests that are not duplicates, and 'duplicatedTransfers'
// with the duplicated requests
const stampLibrarySplitTransfers = function (requestsWithPlates) {
const filteredOnlyLibraryRequestsWithPlates = libraryRequestsWithPlates(requestsWithPlates)
const { platesMatrix, duplicatedRequests } = buildLibrarySplitPlatesMatrix(filteredOnlyLibraryRequestsWithPlates)
const transferRequests = platesMatrix.flatMap((x) => Object.values(x)).flat()
const validTransfers = buildSequentialLibrarySplitTransfersArray(transferRequests, 24)
const duplicatedTransfers = buildSequentialLibrarySplitTransfersArray(duplicatedRequests, 24)
return { validTransfers, duplicatedTransfers }
}
// Mapping of allowed transfer functions
const transferFunctions = {
quadrant: quadrantTransfers,
sequential: sequentialTransfers,
sequentialLibrarySplit: stampLibrarySplitTransfers,
}
// Receives an array of requestsWithPlates and a transfer layout name (either
// 'quadrant' or 'sequential').
// Returns an object containing an array of valid transfers and an array of
// duplicated transfers.
// Throws an error if the transfers layout string is not mapped to a transfer
// function.
const transfersFromRequests = function (requestsWithPlates, transfersLayout) {
const transferFunction = transferFunctions[transfersLayout]
if (transferFunction === undefined) {
throw `Invalid transfers layout name: ${transfersLayout}`
}
const { validTransfers, duplicatedTransfers } = transferFunction(requestsWithPlates)
return { valid: validTransfers, duplicated: duplicatedTransfers }
}
// Receives an array of potential transfers and a transfer layout name
// (valid options: 'sequentialtubes').
// Returns an object containing an array of valid transfers
// Throws an error if the transfers layout string is not mapped to a transfer
// function.
// Inputs:
// validTubes: [
// { index: 0, state: 'valid', tube: {} },
// { index: 1, state: 'valid', tube: {} },
// ...etc...
// ]
//
const transfersForTubes = function (validTubes) {
const maxTubes = 96
const tubeArray = new Array(maxTubes)
for (let i = 0; i < validTubes.length; i++) {
const tubeObj = validTubes[i]
if (tubeArray[tubeObj.index] === undefined) {
tubeArray[tubeObj.index] = validTubes[i]
}
}
const validTransfers = buildSequentialTubesTransfersArray(tubeArray)
const duplicatedTransfers = []
return { valid: validTransfers, duplicated: duplicatedTransfers }
}
export {
transfersFromRequests,
transfersForTubes,
buildPlatesMatrix,
buildLibrarySplitPlatesMatrix,
buildSequentialLibrarySplitTransfersArray,
}