Skip to content

Commit

Permalink
Fix Placement (#798) (#801)
Browse files Browse the repository at this point in the history
* set default placement type to pattern

* change sorting for placement samples table

* more tweaks

* fix sorting

* reverse comparison

* default to column direction

* correct comment

* allow wrap around

* use reverse of offsets only

* use enum index
  • Loading branch information
nafiz1001 authored May 24, 2024
1 parent c0a7d12 commit 86d0e9b
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 70 deletions.
26 changes: 17 additions & 9 deletions frontend/src/components/placementVisuals/PlacementSamplesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { fetchSamples } from "../../modules/cache/cache";
import { selectCell, selectContainer } from "../../modules/placement/selectors";
import { selectActiveDestinationContainer, selectActiveSourceContainer } from "../../modules/labworkSteps/selectors";
import store from "../../store";
import { compareArray, coordinatesToOffsets, offsetsToCoordinates } from "../../utils/functions";
export interface PlacementSamplesTableProps {
container: string | null
}
Expand Down Expand Up @@ -80,7 +81,7 @@ const PlacementSamplesTable = ({ container: containerName }: PlacementSamplesTab
sample,
selected: cell.selected,
name,
coordinates: cell.coordinates
coordinates: cell.coordinates,
})
return samples
}, [] as Samples)
Expand Down Expand Up @@ -132,17 +133,24 @@ const PlacementSamplesTable = ({ container: containerName }: PlacementSamplesTab
const sortedSamples = [...samples]
sortedSamples.sort((a, b) => {
const MAX = 100
const HALF = MAX/2
const QUARTER = HALF/2

let orderA = MAX
let orderB = MAX
if (a.selected) orderA -= HALF
if (b.selected) orderB -= HALF
if (a.sample && b.sample) {
if (a.sample > b.sample) orderB -= QUARTER
if (a.sample < b.sample) orderA -= QUARTER

if (a.selected) orderA -= MAX/2
if (b.selected) orderB -= MAX/2

if (container) {
const aOffsets = a.coordinates ? coordinatesToOffsets(container.spec, a.coordinates) : []
const bOffsets = b.coordinates ? coordinatesToOffsets(container.spec, b.coordinates) : []
const arrayComparison = compareArray(aOffsets.reverse(), bOffsets.reverse())
if (arrayComparison > 0) orderB -= MAX/4
if (arrayComparison < 0) orderA -= MAX/4
}

if (a.sample > b.sample) orderB -= MAX/8
if (a.sample < b.sample) orderA -= MAX/8

return orderA - orderB
})
return sortedSamples.reduce((sortedSamples, s) => {
Expand All @@ -156,7 +164,7 @@ const PlacementSamplesTable = ({ container: containerName }: PlacementSamplesTab
}
return sortedSamples
}, [] as SortedSample[])
}, [samples])
}, [samples, container])

const selectionProps: TableRowSelection<SortedSample> = {
selectedRowKeys: placementSelectedSamples,
Expand Down
107 changes: 46 additions & 61 deletions frontend/src/modules/placement/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Draft, PayloadAction, createSlice, original } from "@reduxjs/toolkit"
import { Container, Sample } from "../../models/frontend_models"
import { CoordinateAxis, CoordinateSpec } from "../../models/fms_api_models"
import { CellIdentifier, CellState, CellWithParentIdentifier, CellWithParentState, ContainerIdentifier, ContainerState, ParentContainerState, PlacementDirections, PlacementGroupOptions, PlacementOptions, PlacementState, PlacementType, TubesWithoutParentState } from "./models"
import { compareArray, coordinatesToOffsets, offsetsToCoordinates } from "../../utils/functions"

export type LoadContainerPayload = LoadParentContainerPayload | LoadTubesWithoutParentPayload
export interface MouseOnCellPayload extends CellWithParentIdentifier {
Expand Down Expand Up @@ -38,7 +39,7 @@ export interface PlaceAllSourcePayload {
const initialState: PlacementState = {
containers: [] as PlacementState['containers'],
placementType: PlacementType.GROUP,
placementDirection: PlacementDirections.ROW,
placementDirection: PlacementDirections.COLUMN,
} as const

const slice = createSlice({
Expand Down Expand Up @@ -363,42 +364,6 @@ function placeCell(sourceCell: Draft<CellState>, destCell: Draft<CellWithParentS
destCell.placedFrom = sourceCell
}

function coordinatesToOffsets(spec: CoordinateSpec, coordinates: string) {
const offsets: number[] = []
const originalCoordinates = coordinates
for (const axis of spec) {
if (coordinates.length === 0) {
throw new Error(`Cannot convert coordinates ${originalCoordinates} with spec ${JSON.stringify(spec)} to offsets at axis ${axis}`)
}
const offset = axis.findIndex((coordinate) => coordinates.startsWith(coordinate))
if (offset < 0) {
throw new Error(`Cannot convert coordinates ${originalCoordinates} with spec ${JSON.stringify(spec)} to offsets at axis ${axis}`)
}
offsets.push(offset)
coordinates = coordinates.slice(axis[offset].length)
}

return offsets
}

function offsetsToCoordinates(offsets: readonly number[], spec: CoordinateSpec) {
if (spec.length !== offsets.length) {
throw new Error(`Cannot convert offsets ${JSON.stringify(offsets)} to coordinates with spec ${JSON.stringify(spec)}`)
}

const coordinates: string[] = []
for (let i = 0; i < spec.length; i++) {
if (offsets[i] < 0) {
throw new Error('Numbers in offsets argument cannot be negative')
}
if (offsets[i] >= spec[i].length) {
throw new Error(`Cannot convert offset ${JSON.stringify(offsets)} to coordinates with spec ${JSON.stringify(spec)} at axis ${i}`)
}
coordinates.push(spec[i][offsets[i]])
}
return coordinates.join('')
}

function placementDestinationLocations(state: PlacementState, sources: Draft<CellState>[], destination: Draft<CellWithParentState>, placementOptions: PlacementOptions): Draft<CellWithParentState>[] {
/**
* If this function encounters an error, it will set state.error and return an array that might be less than the length of sources arguments
Expand All @@ -412,13 +377,17 @@ function placementDestinationLocations(state: PlacementState, sources: Draft<Cel
const destinationContainer = getParentContainer(state, destination)
const destinationStartingOffsets = coordinatesToOffsets(destinationContainer.spec, destination.coordinates)

const [ axisRow, axisCol ] = destinationContainer.spec
const height = axisRow?.length ?? 1
const width = axisCol?.length ?? 1

const sourceContainerNames = new Set(sources.map((s) => s.parentContainerName))
if (sourceContainerNames.size > 1) {
throw new Error('Cannot use pattern placement type with more than one source container')
}

switch (placementOptions.type) {
case PlacementType.PATTERN: {
const sourceContainerNames = new Set(sources.map((s) => s.parentContainerName))
if (sourceContainerNames.size > 1) {
throw new Error('Cannot use pattern placement type with more than one source container')
}

const sourceOffsetsList = sources.map((source) => {
const parentContainer = getParentContainer(state, source)
if (!source.coordinates)
Expand All @@ -442,30 +411,46 @@ function placementDestinationLocations(state: PlacementState, sources: Draft<Cel
break
}
case PlacementType.GROUP: {
// it is possible to place samples from multiple containers in one shot

// sort source location indices by sample id
const sourceIndices = [...sources.keys()].sort((indexA, indexB) => {
const relativeOffsetByIndices = [...sources.keys()].sort((indexA, indexB) => {
const a = sources[indexA]
const b = sources[indexB]
const sampleA = a.sample
const sampleB = b.sample
if (sampleA === null) {
throw new Error(`Cell at coordinates ${atCellLocations(a)} has no sample`)
const offsetsA = a.coordinates ? coordinatesToOffsets(getContainer(state, a).spec, a.coordinates) : []
const offsetsB = b.coordinates ? coordinatesToOffsets(getContainer(state, b).spec, b.coordinates) : []
const comparison = compareArray(offsetsA.reverse(), offsetsB.reverse())
return comparison
}).reduce<Record<number, number>>((relativeOffsetByIndices, sortedIndex, index) => {
relativeOffsetByIndices[sortedIndex] = index
return relativeOffsetByIndices
}, {})

for (const sourceIndex in sources) {
const relativeOffset = relativeOffsetByIndices[sourceIndex]
const [startingRow, startingCol] = destinationStartingOffsets

const { ROW, COLUMN } = PlacementDirections
const finalOffsets = {
[ROW]: startingRow,
[COLUMN]: startingCol
}
if (sampleB === null) {
throw new Error(`Cell at coordinates ${atCellLocations(b)} has no sample`)

if (placementOptions.direction === PlacementDirections.ROW) {
finalOffsets[COLUMN] += relativeOffset
const finalColBeforeWrap = finalOffsets[1]
if (finalColBeforeWrap >= width) {
finalOffsets[COLUMN] = finalColBeforeWrap % width
finalOffsets[ROW] += Math.floor(finalColBeforeWrap / width)
}
} else if (placementOptions.direction === PlacementDirections.COLUMN) {
finalOffsets[ROW] += relativeOffset
const finalRowBeforeWrap = finalOffsets[0]
if (finalRowBeforeWrap >= height) {
finalOffsets[ROW] = finalRowBeforeWrap % height
finalOffsets[COLUMN] += Math.floor(finalRowBeforeWrap / height)
}
}
return sampleA - sampleB
})

newOffsetsList.push(
...sourceIndices.map(
(index) => destinationStartingOffsets.map(
(offset, axis) => offset + (placementOptions.direction === PlacementDirections.ROW && axis == 1 ? index : 0) + (placementOptions.direction === PlacementDirections.COLUMN && axis == 0 ? index : 0)
)
)
)
newOffsetsList.push([ finalOffsets[ROW], finalOffsets[COLUMN] ])
}
}
}

Expand Down
48 changes: 48 additions & 0 deletions frontend/src/utils/functions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CoordinateSpec } from "../models/fms_api_models"

export function constVal<T>(x: T) {
return () => x
}
Expand All @@ -8,4 +10,50 @@ export function isNullish(nullable: any): nullable is null | undefined {

export function isDefined(nullable: any) {
return !isNullish(nullable)
}

export function coordinatesToOffsets(spec: CoordinateSpec, coordinates: string) {
const offsets: number[] = []
const originalCoordinates = coordinates
for (const axis of spec) {
if (coordinates.length === 0) {
throw new Error(`Cannot convert coordinates ${originalCoordinates} with spec ${JSON.stringify(spec)} to offsets at axis ${axis}`)
}
const offset = axis.findIndex((coordinate) => coordinates.startsWith(coordinate))
if (offset < 0) {
throw new Error(`Cannot convert coordinates ${originalCoordinates} with spec ${JSON.stringify(spec)} to offsets at axis ${axis}`)
}
offsets.push(offset)
coordinates = coordinates.slice(axis[offset].length)
}

return offsets
}

export function offsetsToCoordinates(offsets: readonly number[], spec: CoordinateSpec) {
if (spec.length !== offsets.length) {
throw new Error(`Cannot convert offsets ${JSON.stringify(offsets)} to coordinates with spec ${JSON.stringify(spec)}`)
}

const coordinates: string[] = []
for (let i = 0; i < spec.length; i++) {
if (offsets[i] < 0) {
throw new Error('Numbers in offsets argument cannot be negative')
}
if (offsets[i] >= spec[i].length) {
throw new Error(`Cannot convert offset ${JSON.stringify(offsets)} to coordinates with spec ${JSON.stringify(spec)} at axis ${i}`)
}
coordinates.push(spec[i][offsets[i]])
}
return coordinates.join('')
}

export function compareArray(a: readonly number[], b: readonly number[]): number {
if (a.length > b.length) return 1
if (a.length < b.length) return -1
for (let i = 0; i < a.length; i++) {
if (a[i] < b[i]) return -1
if (a[i] > b[i]) return 1
}
return 0
}

0 comments on commit 86d0e9b

Please sign in to comment.