opentrons 8.4.0a13__py2.py3-none-any.whl → 8.5.0a0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. opentrons/config/defaults_ot3.py +1 -1
  2. opentrons/legacy_commands/commands.py +16 -4
  3. opentrons/legacy_commands/robot_commands.py +51 -0
  4. opentrons/legacy_commands/types.py +91 -2
  5. opentrons/protocol_api/_liquid.py +60 -15
  6. opentrons/protocol_api/_liquid_properties.py +137 -90
  7. opentrons/protocol_api/_transfer_liquid_validation.py +10 -6
  8. opentrons/protocol_api/core/engine/instrument.py +172 -75
  9. opentrons/protocol_api/core/engine/protocol.py +13 -14
  10. opentrons/protocol_api/core/engine/robot.py +2 -2
  11. opentrons/protocol_api/core/engine/transfer_components_executor.py +157 -126
  12. opentrons/protocol_api/core/engine/well.py +16 -0
  13. opentrons/protocol_api/core/instrument.py +2 -2
  14. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -2
  15. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +1 -1
  16. opentrons/protocol_api/core/legacy/legacy_well_core.py +8 -0
  17. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -2
  18. opentrons/protocol_api/core/protocol.py +2 -2
  19. opentrons/protocol_api/core/well.py +8 -0
  20. opentrons/protocol_api/instrument_context.py +377 -86
  21. opentrons/protocol_api/labware.py +10 -0
  22. opentrons/protocol_api/protocol_context.py +79 -4
  23. opentrons/protocol_api/robot_context.py +48 -6
  24. opentrons/protocol_api/validation.py +15 -8
  25. opentrons/protocol_engine/commands/aspirate_while_tracking.py +0 -1
  26. opentrons/protocol_engine/commands/command_unions.py +10 -10
  27. opentrons/protocol_engine/commands/generate_command_schema.py +1 -1
  28. opentrons/protocol_engine/commands/get_next_tip.py +2 -2
  29. opentrons/protocol_engine/commands/pick_up_tip.py +9 -3
  30. opentrons/protocol_engine/commands/robot/__init__.py +20 -20
  31. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +34 -24
  32. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +29 -20
  33. opentrons/protocol_engine/commands/seal_pipette_to_tip.py +1 -1
  34. opentrons/protocol_engine/commands/unsafe/__init__.py +17 -1
  35. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +1 -2
  36. opentrons/protocol_engine/execution/labware_movement.py +9 -2
  37. opentrons/protocol_engine/execution/movement.py +12 -9
  38. opentrons/protocol_engine/execution/queue_worker.py +8 -1
  39. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +52 -19
  40. opentrons/protocol_engine/state/_well_math.py +2 -2
  41. opentrons/protocol_engine/state/commands.py +14 -28
  42. opentrons/protocol_engine/state/frustum_helpers.py +11 -7
  43. opentrons/protocol_engine/state/modules.py +1 -1
  44. opentrons/protocol_engine/state/pipettes.py +8 -0
  45. opentrons/protocol_engine/state/tips.py +46 -83
  46. opentrons/protocol_engine/state/update_types.py +8 -23
  47. opentrons/protocol_runner/legacy_command_mapper.py +11 -4
  48. opentrons/protocol_runner/run_orchestrator.py +1 -1
  49. opentrons/protocols/advanced_control/transfers/common.py +54 -11
  50. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +1 -1
  51. opentrons/protocols/api_support/definitions.py +1 -1
  52. opentrons/types.py +6 -6
  53. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a0.dist-info}/METADATA +4 -4
  54. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a0.dist-info}/RECORD +58 -57
  55. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a0.dist-info}/LICENSE +0 -0
  56. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a0.dist-info}/WHEEL +0 -0
  57. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a0.dist-info}/entry_points.txt +0 -0
  58. {opentrons-8.4.0a13.dist-info → opentrons-8.5.0a0.dist-info}/top_level.txt +0 -0
@@ -12,7 +12,6 @@ from opentrons_shared_data.liquid_classes.liquid_class_definition import (
12
12
  Coordinate,
13
13
  BlowoutLocation,
14
14
  )
15
- from opentrons_shared_data.pipette.types import LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP
16
15
 
17
16
  from opentrons.protocol_api._liquid_properties import (
18
17
  Submerge,
@@ -23,7 +22,7 @@ from opentrons.protocol_api._liquid_properties import (
23
22
  TouchTipProperties,
24
23
  )
25
24
  from opentrons.protocol_engine.errors import TouchTipDisabledError
26
- from opentrons.types import Location, Point
25
+ from opentrons.types import Location, Point, Mount
27
26
  from opentrons.protocols.advanced_control.transfers.transfer_liquid_utils import (
28
27
  LocationCheckDescriptors,
29
28
  )
@@ -119,11 +118,32 @@ class TransferComponentsExecutor:
119
118
  self,
120
119
  instrument_core: InstrumentCore,
121
120
  transfer_properties: TransferProperties,
122
- target_location: Location,
123
- target_well: WellCore,
121
+ target_location: Union[Location, TrashBin, WasteChute],
122
+ target_well: Optional[WellCore],
124
123
  tip_state: TipState,
125
124
  transfer_type: TransferType,
126
125
  ) -> None:
126
+ """Create a TransferComponentsExecutor instance.
127
+
128
+ One instance should be created to execute all the steps inside each of the
129
+ liquid class' transfer components- aspirate, dispense and multi-dispense.
130
+ The state of the TransferComponentsExecutor instance is expected to be valid
131
+ only for the component it was created.
132
+
133
+ For example, if we want to execute all the steps (submerge, dispense, retract, etc)
134
+ related to the 'dispense' component of a liquid-class based transfer, the class
135
+ will be used to initialize info about the dispense by assigning values
136
+ to class attributes as follows-
137
+ - target_location: the dispense location
138
+ - target_well: the well associated with dispense location, will be None when the
139
+ target_location argument is a TrashBin or WasteChute
140
+ - tip_state: the state of the tip before dispense component steps are executed
141
+ - transfer_type: whether the dispense component is being called as a part of a
142
+ 1-to-1 transfer or a consolidation or a distribution
143
+
144
+ These attributes will remain the same throughout the component's execution,
145
+ except `tip_state`, which will keep updating as fluids are handled.
146
+ """
127
147
  self._instrument = instrument_core
128
148
  self._transfer_properties = transfer_properties
129
149
  self._target_location = target_location
@@ -140,7 +160,6 @@ class TransferComponentsExecutor:
140
160
  self,
141
161
  submerge_properties: Submerge,
142
162
  post_submerge_action: Literal["aspirate", "dispense"],
143
- volume_for_pipette_mode_configuration: Optional[float],
144
163
  ) -> None:
145
164
  """Execute submerge steps.
146
165
 
@@ -148,56 +167,38 @@ class TransferComponentsExecutor:
148
167
  Should raise an error if this point is inside the liquid?
149
168
  For liquid meniscus this is easy to tell. Can’t be below meniscus
150
169
  For reference pos of anything else, do not allow submerge position to be below aspirate position
151
- 2. move to aspirate position at desired speed
170
+ 2. move to aspirate/dispense position at desired speed
152
171
  3. delay
172
+
173
+ If target location is a trash bin or waste chute, the pipette will move to the disposal location given,
174
+ remove air gap and delay
153
175
  """
154
- submerge_start_point = absolute_point_from_position_reference_and_offset(
155
- well=self._target_well,
156
- position_reference=submerge_properties.position_reference,
157
- offset=submerge_properties.offset,
158
- )
159
- submerge_start_location = Location(
160
- point=submerge_start_point, labware=self._target_location.labware
161
- )
162
- prep_before_moving_to_submerge = (
163
- post_submerge_action == "aspirate"
164
- and volume_for_pipette_mode_configuration is not None
165
- )
166
- if prep_before_moving_to_submerge:
167
- # Move to the tip probe start position
168
- self._instrument.move_to(
169
- location=Location(
170
- point=self._target_well.get_top(
171
- LIQUID_PROBE_START_OFFSET_FROM_WELL_TOP.z
172
- ),
173
- labware=self._target_location.labware,
174
- ),
176
+ submerge_start_location: Union[Location, TrashBin, WasteChute]
177
+ if isinstance(self._target_location, Location):
178
+ assert self._target_well is not None
179
+ submerge_start_point = absolute_point_from_position_reference_and_offset(
180
+ well=self._target_well,
181
+ well_volume_difference=0,
182
+ position_reference=submerge_properties.start_position.position_reference,
183
+ offset=submerge_properties.start_position.offset,
184
+ mount=self._instrument.get_mount(),
185
+ )
186
+ submerge_start_location = Location(
187
+ point=submerge_start_point, labware=self._target_location.labware
188
+ )
189
+ tx_utils.raise_if_location_inside_liquid(
190
+ location=submerge_start_location,
191
+ well_location=self._target_location,
175
192
  well_core=self._target_well,
176
- force_direct=False,
177
- minimum_z_height=None,
178
- speed=None,
193
+ location_check_descriptors=LocationCheckDescriptors(
194
+ location_type="submerge start",
195
+ pipetting_action=post_submerge_action,
196
+ ),
197
+ logger=log,
179
198
  )
180
- self._remove_air_gap(location=submerge_start_location)
181
- if (
182
- self._transfer_type != TransferType.MANY_TO_ONE
183
- and self._instrument.get_liquid_presence_detection()
184
- ):
185
- self._instrument.liquid_probe_with_recovery(
186
- well_core=self._target_well, loc=submerge_start_location
187
- )
188
- # TODO: do volume configuration + prepare for aspirate only if the mode needs to be changed
189
- self._instrument.configure_for_volume(volume_for_pipette_mode_configuration) # type: ignore[arg-type]
190
- self._instrument.prepare_to_aspirate()
191
- tx_utils.raise_if_location_inside_liquid(
192
- location=submerge_start_location,
193
- well_location=self._target_location,
194
- well_core=self._target_well,
195
- location_check_descriptors=LocationCheckDescriptors(
196
- location_type="submerge start",
197
- pipetting_action=post_submerge_action,
198
- ),
199
- logger=log,
200
- )
199
+ else:
200
+ submerge_start_location = self._target_location
201
+
201
202
  self._instrument.move_to(
202
203
  location=submerge_start_location,
203
204
  well_core=self._target_well,
@@ -205,21 +206,26 @@ class TransferComponentsExecutor:
205
206
  minimum_z_height=None,
206
207
  speed=None,
207
208
  )
208
- if not prep_before_moving_to_submerge:
209
- self._remove_air_gap(location=submerge_start_location)
210
- self._instrument.move_to(
211
- location=self._target_location,
212
- well_core=self._target_well,
213
- force_direct=True,
214
- minimum_z_height=None,
215
- speed=submerge_properties.speed,
216
- )
209
+ self._remove_air_gap(location=submerge_start_location)
210
+ if isinstance(self._target_location, Location):
211
+ self._instrument.move_to(
212
+ location=self._target_location,
213
+ well_core=self._target_well,
214
+ force_direct=True,
215
+ minimum_z_height=None,
216
+ speed=submerge_properties.speed,
217
+ )
218
+
217
219
  if submerge_properties.delay.enabled and submerge_properties.delay.duration:
218
220
  self._instrument.delay(submerge_properties.delay.duration)
219
221
 
220
222
  def aspirate_and_wait(self, volume: float) -> None:
221
223
  """Aspirate according to aspirate properties and wait if enabled."""
222
224
  # TODO: handle volume correction
225
+ assert (
226
+ isinstance(self._target_location, Location)
227
+ and self._target_well is not None
228
+ )
223
229
  aspirate_props = self._transfer_properties.aspirate
224
230
  correction_volume = aspirate_props.correction_by_volume.get_for_volume(volume)
225
231
  self._instrument.aspirate(
@@ -275,11 +281,15 @@ class TransferComponentsExecutor:
275
281
  NOTE: For most of our built-in definitions, we will keep _mix_ off because it is a very application specific thing.
276
282
  We should mention in our docs that users should adjust this property according to their application.
277
283
  """
278
- if not mix_properties.enabled:
284
+ if not mix_properties.enabled or not isinstance(
285
+ self._target_location, Location
286
+ ):
279
287
  return
280
288
  # Assertion only for mypy purposes
281
289
  assert (
282
- mix_properties.repetitions is not None and mix_properties.volume is not None
290
+ mix_properties.repetitions is not None
291
+ and mix_properties.volume is not None
292
+ and self._target_well is not None
283
293
  )
284
294
  push_out_vol = (
285
295
  self._transfer_properties.dispense.push_out_by_volume.get_for_volume(
@@ -337,11 +347,17 @@ class TransferComponentsExecutor:
337
347
  during a multi-dispense.
338
348
  """
339
349
  # TODO: Raise error if retract is below the meniscus
350
+ assert (
351
+ isinstance(self._target_location, Location)
352
+ and self._target_well is not None
353
+ )
340
354
  retract_props = self._transfer_properties.aspirate.retract
341
355
  retract_point = absolute_point_from_position_reference_and_offset(
342
356
  well=self._target_well,
343
- position_reference=retract_props.position_reference,
344
- offset=retract_props.offset,
357
+ well_volume_difference=0,
358
+ position_reference=retract_props.end_position.position_reference,
359
+ offset=retract_props.end_position.offset,
360
+ mount=self._instrument.get_mount(),
345
361
  )
346
362
  retract_location = Location(
347
363
  retract_point, labware=self._target_location.labware
@@ -371,7 +387,7 @@ class TransferComponentsExecutor:
371
387
  assert (
372
388
  touch_tip_props.speed is not None
373
389
  and touch_tip_props.z_offset is not None
374
- and touch_tip_props.mm_to_edge is not None
390
+ and touch_tip_props.mm_from_edge is not None
375
391
  )
376
392
  self._instrument.touch_tip(
377
393
  location=retract_location,
@@ -379,7 +395,7 @@ class TransferComponentsExecutor:
379
395
  radius=1,
380
396
  z_offset=touch_tip_props.z_offset,
381
397
  speed=touch_tip_props.speed,
382
- mm_from_edge=touch_tip_props.mm_to_edge,
398
+ mm_from_edge=touch_tip_props.mm_from_edge,
383
399
  )
384
400
  self._instrument.move_to(
385
401
  location=retract_location,
@@ -431,35 +447,47 @@ class TransferComponentsExecutor:
431
447
  - Prepare-to-aspirate (top of well)
432
448
  - Do air-gap (top of well)
433
449
  7. If drop tip, move to drop tip location, drop tip
450
+
451
+ If target location is a trash bin or waste chute, the retract movement step is skipped along with touch tip,
452
+ even if it is enabled.
434
453
  """
435
454
  # TODO: Raise error if retract is below the meniscus
436
-
437
455
  retract_props = self._transfer_properties.dispense.retract
438
- retract_point = absolute_point_from_position_reference_and_offset(
439
- well=self._target_well,
440
- position_reference=retract_props.position_reference,
441
- offset=retract_props.offset,
442
- )
443
- retract_location = Location(
444
- retract_point, labware=self._target_location.labware
445
- )
446
- tx_utils.raise_if_location_inside_liquid(
447
- location=retract_location,
448
- well_location=self._target_location,
449
- well_core=self._target_well,
450
- location_check_descriptors=LocationCheckDescriptors(
451
- location_type="retract end",
452
- pipetting_action="dispense",
453
- ),
454
- logger=log,
455
- )
456
- self._instrument.move_to(
457
- location=retract_location,
458
- well_core=self._target_well,
459
- force_direct=True,
460
- minimum_z_height=None,
461
- speed=retract_props.speed,
462
- )
456
+
457
+ retract_location: Union[Location, TrashBin, WasteChute]
458
+ if isinstance(self._target_location, Location):
459
+ assert self._target_well is not None
460
+ retract_point = absolute_point_from_position_reference_and_offset(
461
+ well=self._target_well,
462
+ well_volume_difference=0,
463
+ position_reference=retract_props.end_position.position_reference,
464
+ offset=retract_props.end_position.offset,
465
+ mount=self._instrument.get_mount(),
466
+ )
467
+ retract_location = Location(
468
+ retract_point, labware=self._target_location.labware
469
+ )
470
+ tx_utils.raise_if_location_inside_liquid(
471
+ location=retract_location,
472
+ well_location=self._target_location,
473
+ well_core=self._target_well,
474
+ location_check_descriptors=LocationCheckDescriptors(
475
+ location_type="retract end",
476
+ pipetting_action="dispense",
477
+ ),
478
+ logger=log,
479
+ )
480
+ self._instrument.move_to(
481
+ location=retract_location,
482
+ well_core=self._target_well,
483
+ force_direct=True,
484
+ minimum_z_height=None,
485
+ speed=retract_props.speed,
486
+ )
487
+ else:
488
+ retract_location = self._target_location
489
+
490
+ # TODO should we delay here for a trash despite not having a "retract"?
463
491
  retract_delay = retract_props.delay
464
492
  if retract_delay.enabled and retract_delay.duration:
465
493
  self._instrument.delay(retract_delay.duration)
@@ -487,7 +515,9 @@ class TransferComponentsExecutor:
487
515
  # then skip the final air gap if we have been told to do so.
488
516
  self._do_touch_tip_and_air_gap(
489
517
  touch_tip_properties=retract_props.touch_tip,
490
- location=retract_location,
518
+ location=retract_location
519
+ if isinstance(retract_location, Location)
520
+ else None,
491
521
  well=self._target_well,
492
522
  add_air_gap=False if is_final_air_gap and not add_final_air_gap else True,
493
523
  )
@@ -573,14 +603,19 @@ class TransferComponentsExecutor:
573
603
  and whether we are moving to another dispense or going back to the source.
574
604
  """
575
605
  # TODO: Raise error if retract is below the meniscus
576
-
606
+ assert (
607
+ isinstance(self._target_location, Location)
608
+ and self._target_well is not None
609
+ )
577
610
  assert self._transfer_properties.multi_dispense is not None
578
611
 
579
612
  retract_props = self._transfer_properties.multi_dispense.retract
580
613
  retract_point = absolute_point_from_position_reference_and_offset(
581
614
  well=self._target_well,
582
- position_reference=retract_props.position_reference,
583
- offset=retract_props.offset,
615
+ well_volume_difference=0,
616
+ position_reference=retract_props.end_position.position_reference,
617
+ offset=retract_props.end_position.offset,
618
+ mount=self._instrument.get_mount(),
584
619
  )
585
620
  retract_location = Location(
586
621
  retract_point, labware=self._target_location.labware
@@ -740,7 +775,7 @@ class TransferComponentsExecutor:
740
775
  assert (
741
776
  touch_tip_properties.speed is not None
742
777
  and touch_tip_properties.z_offset is not None
743
- and touch_tip_properties.mm_to_edge is not None
778
+ and touch_tip_properties.mm_from_edge is not None
744
779
  )
745
780
  # TODO:, check that when blow out is a non-dest-well,
746
781
  # whether the touch tip params from transfer props should be used for
@@ -753,7 +788,7 @@ class TransferComponentsExecutor:
753
788
  radius=1,
754
789
  z_offset=touch_tip_properties.z_offset,
755
790
  speed=touch_tip_properties.speed,
756
- mm_from_edge=touch_tip_properties.mm_to_edge,
791
+ mm_from_edge=touch_tip_properties.mm_from_edge,
757
792
  )
758
793
  except TouchTipDisabledError:
759
794
  # TODO: log a warning
@@ -803,43 +838,33 @@ class TransferComponentsExecutor:
803
838
  self._instrument.delay(delay_props.duration)
804
839
  self._tip_state.append_air_gap(air_gap_volume)
805
840
 
806
- def _remove_air_gap(self, location: Location) -> None:
841
+ def _remove_air_gap(self, location: Union[Location, TrashBin, WasteChute]) -> None:
807
842
  """Remove a previously added air gap."""
808
843
  last_air_gap = self._tip_state.last_liquid_and_air_gap_in_tip.air_gap
809
- if last_air_gap == 0:
810
- return
811
-
812
844
  dispense_props = self._transfer_properties.dispense
813
- correction_volume = dispense_props.correction_by_volume.get_for_volume(
814
- last_air_gap
815
- )
816
- # The minimum flow rate should be air_gap_volume per second
817
- flow_rate = max(
818
- dispense_props.flow_rate_by_volume.get_for_volume(last_air_gap),
819
- last_air_gap,
820
- )
821
- self._instrument.dispense(
845
+ self._instrument.remove_air_gap_during_transfer_with_liquid_class(
846
+ last_air_gap=last_air_gap,
847
+ dispense_props=dispense_props,
822
848
  location=location,
823
- well_core=None,
824
- volume=last_air_gap,
825
- rate=1,
826
- flow_rate=flow_rate,
827
- in_place=True,
828
- push_out=0,
829
- correction_volume=correction_volume,
830
849
  )
831
850
  self._tip_state.delete_air_gap(last_air_gap)
832
- dispense_delay = dispense_props.delay
833
- if dispense_delay.enabled and dispense_delay.duration:
834
- self._instrument.delay(dispense_delay.duration)
835
851
 
836
852
 
837
853
  def absolute_point_from_position_reference_and_offset(
838
854
  well: WellCore,
855
+ well_volume_difference: float,
839
856
  position_reference: PositionReference,
840
857
  offset: Coordinate,
858
+ mount: Mount,
841
859
  ) -> Point:
842
- """Return the absolute point, given the well, the position reference and offset."""
860
+ """Return the absolute point, given the well, the position reference and offset.
861
+
862
+ If using meniscus as the position reference, well_volume_difference should be specified.
863
+ `well_volume_difference` is the expected *difference* in well volume we want to consider
864
+ when estimating the height of the liquid meniscus after an aspirate/ dispense.
865
+ So, for liquid height estimation after an aspirate, well_volume_difference is
866
+ expected to be a -ve value while for a dispense, it will be a +ve value.
867
+ """
843
868
  match position_reference:
844
869
  case PositionReference.WELL_TOP:
845
870
  reference_point = well.get_top(0)
@@ -848,11 +873,17 @@ def absolute_point_from_position_reference_and_offset(
848
873
  case PositionReference.WELL_CENTER:
849
874
  reference_point = well.get_center()
850
875
  case PositionReference.LIQUID_MENISCUS:
851
- meniscus_point = well.get_meniscus()
852
- if not isinstance(meniscus_point, Point):
853
- reference_point = well.get_center()
876
+ estimated_liquid_height = well.estimate_liquid_height_after_pipetting(
877
+ mount=mount,
878
+ operation_volume=well_volume_difference,
879
+ )
880
+ if isinstance(estimated_liquid_height, (float, int)):
881
+ reference_point = well.get_bottom(z_offset=estimated_liquid_height)
854
882
  else:
855
- reference_point = meniscus_point
883
+ # If estimated liquid height gives a SimulatedProbeResult then
884
+ # assume meniscus is at well center.
885
+ # Will this cause more harm than good? Is there a better alternative to this?
886
+ reference_point = well.get_center()
856
887
  case _:
857
888
  raise ValueError(f"Unknown position reference {position_reference}")
858
889
  return reference_point + Point(offset.x, offset.y, offset.z)
@@ -223,3 +223,19 @@ class WellCore(AbstractWellCore):
223
223
  return self._engine_client.state.geometry.get_current_well_volume(
224
224
  labware_id=labware_id, well_name=well_name
225
225
  )
226
+
227
+ def height_from_volume(self, volume: LiquidTrackingType) -> LiquidTrackingType:
228
+ """Return the height in a well corresponding to a given volume."""
229
+ labware_id = self.labware_id
230
+ well_name = self._name
231
+ return self._engine_client.state.geometry.get_well_height_at_volume(
232
+ labware_id=labware_id, well_name=well_name, volume=volume
233
+ )
234
+
235
+ def volume_from_height(self, height: LiquidTrackingType) -> LiquidTrackingType:
236
+ """Return the volume contained in a well at any height."""
237
+ labware_id = self.labware_id
238
+ well_name = self._name
239
+ return self._engine_client.state.geometry.get_well_volume_at_height(
240
+ labware_id=labware_id, well_name=well_name, height=height
241
+ )
@@ -365,7 +365,7 @@ class AbstractInstrument(ABC, Generic[WellCoreType, LabwareCoreType]):
365
365
  liquid_class: LiquidClass,
366
366
  volume: float,
367
367
  source: List[Tuple[types.Location, WellCoreType]],
368
- dest: List[Tuple[types.Location, WellCoreType]],
368
+ dest: Union[List[Tuple[types.Location, WellCoreType]], TrashBin, WasteChute],
369
369
  new_tip: TransferTipPolicyV2,
370
370
  tip_racks: List[Tuple[types.Location, LabwareCoreType]],
371
371
  starting_tip: Optional[WellCoreType],
@@ -400,7 +400,7 @@ class AbstractInstrument(ABC, Generic[WellCoreType, LabwareCoreType]):
400
400
  liquid_class: LiquidClass,
401
401
  volume: float,
402
402
  source: List[Tuple[types.Location, WellCoreType]],
403
- dest: Tuple[types.Location, WellCoreType],
403
+ dest: Union[Tuple[types.Location, WellCoreType], TrashBin, WasteChute],
404
404
  new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
405
405
  tip_racks: List[Tuple[types.Location, LabwareCoreType]],
406
406
  starting_tip: Optional[WellCoreType],
@@ -605,7 +605,7 @@ class LegacyInstrumentCore(AbstractInstrument[LegacyWellCore, LegacyLabwareCore]
605
605
  liquid_class: LiquidClass,
606
606
  volume: float,
607
607
  source: List[Tuple[types.Location, LegacyWellCore]],
608
- dest: List[Tuple[types.Location, LegacyWellCore]],
608
+ dest: Union[List[Tuple[types.Location, LegacyWellCore]], TrashBin, WasteChute],
609
609
  new_tip: TransferTipPolicyV2,
610
610
  tip_racks: List[Tuple[types.Location, LegacyLabwareCore]],
611
611
  starting_tip: Optional[LegacyWellCore],
@@ -635,7 +635,7 @@ class LegacyInstrumentCore(AbstractInstrument[LegacyWellCore, LegacyLabwareCore]
635
635
  liquid_class: LiquidClass,
636
636
  volume: float,
637
637
  source: List[Tuple[types.Location, LegacyWellCore]],
638
- dest: Tuple[types.Location, LegacyWellCore],
638
+ dest: Union[Tuple[types.Location, LegacyWellCore], TrashBin, WasteChute],
639
639
  new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
640
640
  tip_racks: List[Tuple[types.Location, LegacyLabwareCore]],
641
641
  starting_tip: Optional[LegacyWellCore],
@@ -599,7 +599,7 @@ class LegacyProtocolCore(
599
599
  """Define a liquid to load into a well."""
600
600
  assert False, "define_liquid only supported on engine core"
601
601
 
602
- def define_liquid_class(self, name: str) -> LiquidClass:
602
+ def define_liquid_class(self, name: str, version: int) -> LiquidClass:
603
603
  """Define a liquid class."""
604
604
  assert False, "define_liquid_class is only supported on engine core"
605
605
 
@@ -143,6 +143,14 @@ class LegacyWellCore(AbstractWellCore):
143
143
  """Get the current well volume."""
144
144
  return 0.0
145
145
 
146
+ def height_from_volume(self, volume: LiquidTrackingType) -> LiquidTrackingType:
147
+ """Return the height in a well corresponding to a given volume."""
148
+ return 0.0
149
+
150
+ def volume_from_height(self, height: LiquidTrackingType) -> LiquidTrackingType:
151
+ """Return the volume contained in a well at any height."""
152
+ return 0.0
153
+
146
154
  # TODO(mc, 2022-10-28): is this used and/or necessary?
147
155
  def __repr__(self) -> str:
148
156
  """Use the well's display name as its repr."""
@@ -519,7 +519,7 @@ class LegacyInstrumentCoreSimulator(
519
519
  liquid_class: LiquidClass,
520
520
  volume: float,
521
521
  source: List[Tuple[types.Location, LegacyWellCore]],
522
- dest: List[Tuple[types.Location, LegacyWellCore]],
522
+ dest: Union[List[Tuple[types.Location, LegacyWellCore]], TrashBin, WasteChute],
523
523
  new_tip: TransferTipPolicyV2,
524
524
  tip_racks: List[Tuple[types.Location, LegacyLabwareCore]],
525
525
  starting_tip: Optional[LegacyWellCore],
@@ -549,7 +549,7 @@ class LegacyInstrumentCoreSimulator(
549
549
  liquid_class: LiquidClass,
550
550
  volume: float,
551
551
  source: List[Tuple[types.Location, LegacyWellCore]],
552
- dest: Tuple[types.Location, LegacyWellCore],
552
+ dest: Union[Tuple[types.Location, LegacyWellCore], TrashBin, WasteChute],
553
553
  new_tip: Literal[TransferTipPolicyV2.NEVER, TransferTipPolicyV2.ONCE],
554
554
  tip_racks: List[Tuple[types.Location, LegacyLabwareCore]],
555
555
  starting_tip: Optional[LegacyWellCore],
@@ -230,7 +230,7 @@ class AbstractProtocol(
230
230
  def get_last_location(
231
231
  self,
232
232
  mount: Optional[Mount] = None,
233
- ) -> Optional[Location]:
233
+ ) -> Optional[Union[Location, TrashBin, WasteChute]]:
234
234
  ...
235
235
 
236
236
  @abstractmethod
@@ -311,7 +311,7 @@ class AbstractProtocol(
311
311
  """Define a liquid to load into a well."""
312
312
 
313
313
  @abstractmethod
314
- def define_liquid_class(self, name: str) -> LiquidClass:
314
+ def define_liquid_class(self, name: str, version: int) -> LiquidClass:
315
315
  """Define a liquid class for use in transfer functions."""
316
316
 
317
317
  @abstractmethod
@@ -104,5 +104,13 @@ class AbstractWellCore(ABC):
104
104
  def get_liquid_volume(self) -> LiquidTrackingType:
105
105
  """Get the current volume within a well."""
106
106
 
107
+ @abstractmethod
108
+ def height_from_volume(self, volume: LiquidTrackingType) -> LiquidTrackingType:
109
+ """Return the height in a well corresponding to a given volume."""
110
+
111
+ @abstractmethod
112
+ def volume_from_height(self, height: LiquidTrackingType) -> LiquidTrackingType:
113
+ """Return the volume contained in a well at any height."""
114
+
107
115
 
108
116
  WellCoreType = TypeVar("WellCoreType", bound=AbstractWellCore)