From 2b6f72d762b79c8600acb20a7812bf3df5bc73b8 Mon Sep 17 00:00:00 2001 From: David Chau Date: Thu, 23 Oct 2025 18:47:46 -0400 Subject: [PATCH 1/3] refactor(protocol-designer): make HydratedFormData.tipRack a LabwareEntity --- protocol-designer/src/form-types.ts | 4 ++-- protocol-designer/src/steplist/fieldLevel/index.ts | 13 +++++++++++++ protocol-designer/src/steplist/formLevel/errors.ts | 6 ++++-- .../formLevel/stepFormToArgs/mixFormToArgs.ts | 5 +++-- .../stepFormToArgs/moveLiquidFormToArgs.ts | 10 ++++------ .../stepFormToArgs/test/mixFormToArgs.test.ts | 2 +- .../test/moveLiquidFormToArgs.test.ts | 2 +- .../stepFormToArgs/test/stepFormToArgs.test.ts | 4 ++-- .../src/steplist/formLevel/test/warnings.test.ts | 4 +++- .../src/steplist/formLevel/warnings.tsx | 2 +- 10 files changed, 34 insertions(+), 18 deletions(-) diff --git a/protocol-designer/src/form-types.ts b/protocol-designer/src/form-types.ts index 4b1b19e842b..0c138b77c3a 100644 --- a/protocol-designer/src/form-types.ts +++ b/protocol-designer/src/form-types.ts @@ -281,7 +281,7 @@ export interface HydratedMoveLiquidFormData extends AnnotationFields { nozzles: NozzleConfigurationStyle | null path: PathOption pipette: PipetteEntity - tipRack: string + tipRack: LabwareEntity volume: number pushOut_volume: number | null pushOut_checkbox: boolean @@ -375,7 +375,7 @@ export interface HydratedMixFormData extends AnnotationFields { nozzles: NozzleConfigurationStyle | null pipette: PipetteEntity // can be null if user deletes pipette stepType: 'mix' - tipRack: string + tipRack: LabwareEntity volume: number wells: string[] aspirate_delay_seconds?: number | null diff --git a/protocol-designer/src/steplist/fieldLevel/index.ts b/protocol-designer/src/steplist/fieldLevel/index.ts index bd07f8cb550..cecba113745 100644 --- a/protocol-designer/src/steplist/fieldLevel/index.ts +++ b/protocol-designer/src/steplist/fieldLevel/index.ts @@ -20,6 +20,7 @@ import type { import type { InvariantContext, LabwareEntities, + LabwareEntity, PipetteEntity, StagingAreaEntities, TrashBinEntities, @@ -61,6 +62,15 @@ const getLabwareOrAdditionalEquipmentEntity = ( } else return null } +const getTipRackEntityByURI = ( + state: InvariantContext, + tiprackDefURI: string +): LabwareEntity | undefined => { + return Object.values(state.labwareEntities).find( + lw => lw.labwareDefURI == tiprackDefURI + ) +} + const getIsStackingLocation = ( newLocation: string, labwareEntities: LabwareEntities @@ -287,6 +297,9 @@ const stepFieldHelperMap: Record = { pipette: { hydrate: getPipetteEntity, }, + tipRack: { + hydrate: getTipRackEntityByURI, + }, times: { maskValue: composeMaskers(maskToInteger, onlyPositiveNumbers, defaultTo(0)), castValue: Number, diff --git a/protocol-designer/src/steplist/formLevel/errors.ts b/protocol-designer/src/steplist/formLevel/errors.ts index 3996ac38894..2e4b9221f95 100644 --- a/protocol-designer/src/steplist/formLevel/errors.ts +++ b/protocol-designer/src/steplist/formLevel/errors.ts @@ -768,7 +768,8 @@ export const volumeTooHigh = ( } const volume = Number(fields.volume) - const pipetteCapacity = getPipetteCapacity(pipette, tipRack) + // TODO: refactor getPipetteCapacity() to use tiprack def directly instead of tiprackDefURI + const pipetteCapacity = getPipetteCapacity(pipette, tipRack.labwareDefURI) if ( !Number.isNaN(volume) && !Number.isNaN(pipetteCapacity) && @@ -1418,13 +1419,14 @@ export const conditioningVolumeOutOfRange = ( ) { return null } + // TODO: refactor getMaxConditioningVolume() to use tiprack def directly instead of tiprackDefUri const maxConditioningVolume = getMaxConditioningVolume({ transferVolume: Number(volume), disposalVolume: disposalVolume_checkbox === true ? Number(disposalVolume_volume) : 0, pipetteSpecs: pipette.spec, labwareEntities: labwareEntities ?? {}, - tiprackDefUri: tipRack, + tiprackDefUri: tipRack.labwareDefURI, }) return conditioning_checkbox && conditioning_volume > maxConditioningVolume ? CONDITIONING_VOLUME_OUT_OF_RANGE diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts index 9dc285fb43a..bcf139098b1 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts @@ -36,10 +36,11 @@ export const mixFormToArgs = ( pushOut_checkbox, pushOut_volume, } = hydratedFormData + // TODO: refactor getMatchingTipLiquidSpecs() to use tiprack def directly instead of tiprackDefURI const matchingTipLiquidSpecs = getMatchingTipLiquidSpecs( pipette, hydratedFormData.volume, - hydratedFormData.tipRack + hydratedFormData.tipRack.labwareDefURI ) const unorderedWells = hydratedFormData.wells || [] const orderedWells = getOrderedWells( @@ -110,7 +111,7 @@ export const mixFormToArgs = ( offsetFromBottomMm, blowoutOffsetFromTopMm, aspirateDelaySeconds, - tipRack: hydratedFormData.tipRack, + tipRack: hydratedFormData.tipRack.labwareDefURI, dispenseDelaySeconds, // TODO(jr, 7/26/24): wire up wellNames dropTipLocation: dropTip_location, diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts index 6ee6a68e8e2..f39357f2b92 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts @@ -74,11 +74,8 @@ const getCheckedPath = ( contextualState: InvariantContext, path: PathOption ): PathOption => { - const { pipette, tipRack, volume } = hydratedFormData + const { pipette, tipRack: tiprackEntity, volume } = hydratedFormData const { spec: pipetteSpecs } = pipette - const tiprackEntity = Object.values(contextualState.labwareEntities).find( - lw => lw.labwareDefURI === tipRack - ) // should not hit if (tiprackEntity == null) { console.error('Tiprack for transfer has no associated labware entity.') @@ -297,10 +294,11 @@ export const moveLiquidFormToArgs = ( 'dispense_airGap_checkbox', 'dispense_airGap_volume' ) + // TODO: refactor getMatchingTipLiquidSpecs() to use tiprack def directly instead of tiprackDefURI const matchingTipLiquidSpecs = getMatchingTipLiquidSpecs( hydratedFormData.pipette, hydratedFormData.volume, - tipRack + tipRack.labwareDefURI ) const conditioningVolume = hydratedFormData.conditioning_checkbox === true && @@ -314,7 +312,7 @@ export const moveLiquidFormToArgs = ( volume, sourceLabware: sourceLabware.id, destLabware: destLabware.id, - tipRack: tipRack, + tipRack: tipRack.labwareDefURI, aspirateFlowRateUlSec: hydratedFormData.aspirate_flowRate || matchingTipLiquidSpecs.defaultAspirateFlowRate.default, diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/mixFormToArgs.test.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/mixFormToArgs.test.ts index 68e60f765b5..f65d0e61569 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/mixFormToArgs.test.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/mixFormToArgs.test.ts @@ -40,7 +40,7 @@ beforeEach(() => { blowout_checkbox: false, blowout_location: null, mix_mmFromBottom: 0.5, - tipRack: 'mockTiprack', + tipRack: { labwareDefURI: 'mockTiprack' } as any, pipette: { id: 'pipetteId', spec: fixtureP10SingleV2Specs, diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/moveLiquidFormToArgs.test.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/moveLiquidFormToArgs.test.ts index 273677b8e2c..79ff5faf316 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/moveLiquidFormToArgs.test.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/moveLiquidFormToArgs.test.ts @@ -72,7 +72,7 @@ describe('move liquid step form -> command creator args', () => { type: sourceLabwareType, def: sourceLabwareDef, }, - tipRack: 'tiprack1Id', + tipRack: { labwareDefURI: 'tiprack1Id' } as any, aspirate_wells: [ASPIRATE_WELL], aspirate_wellOrder_first: 'l2r', aspirate_wellOrder_second: 't2b', diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/stepFormToArgs.test.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/stepFormToArgs.test.ts index d53b8e6347f..4b863997737 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/stepFormToArgs.test.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/stepFormToArgs.test.ts @@ -62,7 +62,7 @@ describe('form casting', () => { dispense_airGap_checkbox: false, dropTip_location: 'some location', nozzles: null, - tipRack: 'some tiprack', + tipRack: {} as LabwareEntity, liquidClassesSupported: true, aspirate_retract_position_reference: POSITION_REFERENCE_BOTTOM, aspirate_submerge_mmFromBottom: 1, @@ -115,7 +115,7 @@ describe('form casting', () => { dropTip_location: 'some location', mix_touchTip_checkbox: false, nozzles: null, - tipRack: 'some tiprack', + tipRack: {} as LabwareEntity, liquidClassesSupported: true, pushOut_checkbox: false, pushOut_volume: null, diff --git a/protocol-designer/src/steplist/formLevel/test/warnings.test.ts b/protocol-designer/src/steplist/formLevel/test/warnings.test.ts index e557633e176..5686cf1e6e2 100644 --- a/protocol-designer/src/steplist/formLevel/test/warnings.test.ts +++ b/protocol-designer/src/steplist/formLevel/test/warnings.test.ts @@ -185,7 +185,9 @@ describe('liquid class compatibility', () => { pipette: { spec: { channels: 1, liquids: { default: { maxVolume: 1000 } } }, }, - tipRack: 'opentrons/opentrons_flex_96_tiprack_1000ul/1', + tipRack: { + labwareDefURI: 'opentrons/opentrons_flex_96_tiprack_1000ul/1', + }, liquidClass: 'glycerol_50', path: 'singleDispense', } diff --git a/protocol-designer/src/steplist/formLevel/warnings.tsx b/protocol-designer/src/steplist/formLevel/warnings.tsx index 2fb3d056d3d..acf40763251 100644 --- a/protocol-designer/src/steplist/formLevel/warnings.tsx +++ b/protocol-designer/src/steplist/formLevel/warnings.tsx @@ -299,7 +299,7 @@ export const incompatibleLiquidClass: ( } const tipRackObject = pipetteObject.byTipType.find( - ({ tiprack }) => tiprack === tipRack + ({ tiprack }) => tiprack === tipRack.labwareDefURI ) if (tipRackObject == null) { From 3220fe075d52d01219d2d0e36039b1e97183601d Mon Sep 17 00:00:00 2001 From: David Chau Date: Thu, 23 Oct 2025 19:15:56 -0400 Subject: [PATCH 2/3] Lint --- protocol-designer/src/steplist/fieldLevel/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol-designer/src/steplist/fieldLevel/index.ts b/protocol-designer/src/steplist/fieldLevel/index.ts index cecba113745..a22121deee2 100644 --- a/protocol-designer/src/steplist/fieldLevel/index.ts +++ b/protocol-designer/src/steplist/fieldLevel/index.ts @@ -67,7 +67,7 @@ const getTipRackEntityByURI = ( tiprackDefURI: string ): LabwareEntity | undefined => { return Object.values(state.labwareEntities).find( - lw => lw.labwareDefURI == tiprackDefURI + lw => lw.labwareDefURI === tiprackDefURI ) } From daade2f9abe4ad0dbf456a13c438bc3a04a404df Mon Sep 17 00:00:00 2001 From: David Chau Date: Thu, 23 Oct 2025 23:17:37 -0400 Subject: [PATCH 3/3] Handle null --- protocol-designer/src/steplist/formLevel/errors.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol-designer/src/steplist/formLevel/errors.ts b/protocol-designer/src/steplist/formLevel/errors.ts index 2e4b9221f95..3c732064918 100644 --- a/protocol-designer/src/steplist/formLevel/errors.ts +++ b/protocol-designer/src/steplist/formLevel/errors.ts @@ -769,7 +769,7 @@ export const volumeTooHigh = ( const volume = Number(fields.volume) // TODO: refactor getPipetteCapacity() to use tiprack def directly instead of tiprackDefURI - const pipetteCapacity = getPipetteCapacity(pipette, tipRack.labwareDefURI) + const pipetteCapacity = getPipetteCapacity(pipette, tipRack?.labwareDefURI) if ( !Number.isNaN(volume) && !Number.isNaN(pipetteCapacity) && @@ -1426,7 +1426,7 @@ export const conditioningVolumeOutOfRange = ( disposalVolume_checkbox === true ? Number(disposalVolume_volume) : 0, pipetteSpecs: pipette.spec, labwareEntities: labwareEntities ?? {}, - tiprackDefUri: tipRack.labwareDefURI, + tiprackDefUri: tipRack?.labwareDefURI, }) return conditioning_checkbox && conditioning_volume > maxConditioningVolume ? CONDITIONING_VOLUME_OUT_OF_RANGE