opentrons 8.3.0a2__py2.py3-none-any.whl → 8.3.0a5__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 (57) hide show
  1. opentrons/hardware_control/api.py +5 -1
  2. opentrons/hardware_control/ot3api.py +18 -8
  3. opentrons/hardware_control/protocols/liquid_handler.py +4 -1
  4. opentrons/hardware_control/protocols/motion_controller.py +1 -0
  5. opentrons/legacy_commands/commands.py +37 -0
  6. opentrons/legacy_commands/types.py +39 -0
  7. opentrons/protocol_api/core/engine/instrument.py +109 -0
  8. opentrons/protocol_api/core/instrument.py +27 -0
  9. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +50 -0
  10. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +24 -0
  11. opentrons/protocol_api/instrument_context.py +141 -0
  12. opentrons/protocol_api/labware.py +17 -2
  13. opentrons/protocol_engine/commands/__init__.py +40 -0
  14. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +1 -1
  15. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +1 -1
  16. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +1 -1
  17. opentrons/protocol_engine/commands/absorbance_reader/read.py +4 -1
  18. opentrons/protocol_engine/commands/air_gap_in_place.py +1 -1
  19. opentrons/protocol_engine/commands/command_unions.py +39 -0
  20. opentrons/protocol_engine/commands/evotip_dispense.py +156 -0
  21. opentrons/protocol_engine/commands/evotip_seal_pipette.py +331 -0
  22. opentrons/protocol_engine/commands/evotip_unseal_pipette.py +160 -0
  23. opentrons/protocol_engine/commands/get_next_tip.py +1 -1
  24. opentrons/protocol_engine/commands/liquid_probe.py +63 -12
  25. opentrons/protocol_engine/commands/load_labware.py +5 -0
  26. opentrons/protocol_engine/commands/load_lid.py +1 -1
  27. opentrons/protocol_engine/commands/load_lid_stack.py +1 -1
  28. opentrons/protocol_engine/commands/load_liquid_class.py +1 -1
  29. opentrons/protocol_engine/commands/move_labware.py +9 -0
  30. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +1 -1
  31. opentrons/protocol_engine/commands/robot/move_axes_relative.py +1 -1
  32. opentrons/protocol_engine/commands/robot/move_axes_to.py +1 -1
  33. opentrons/protocol_engine/commands/robot/move_to.py +1 -1
  34. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +1 -1
  35. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +1 -1
  36. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +1 -1
  37. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +1 -1
  38. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +1 -1
  39. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +1 -1
  40. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +1 -1
  41. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +1 -1
  42. opentrons/protocol_engine/errors/__init__.py +4 -0
  43. opentrons/protocol_engine/errors/exceptions.py +26 -0
  44. opentrons/protocol_engine/execution/gantry_mover.py +5 -0
  45. opentrons/protocol_engine/execution/hardware_stopper.py +7 -7
  46. opentrons/protocol_engine/execution/tip_handler.py +30 -9
  47. opentrons/protocol_engine/resources/labware_validation.py +13 -0
  48. opentrons/protocol_engine/state/commands.py +6 -2
  49. opentrons/protocol_engine/state/frustum_helpers.py +13 -44
  50. opentrons/protocol_engine/state/labware.py +13 -1
  51. opentrons/protocol_engine/state/pipettes.py +5 -0
  52. {opentrons-8.3.0a2.dist-info → opentrons-8.3.0a5.dist-info}/METADATA +4 -4
  53. {opentrons-8.3.0a2.dist-info → opentrons-8.3.0a5.dist-info}/RECORD +57 -54
  54. {opentrons-8.3.0a2.dist-info → opentrons-8.3.0a5.dist-info}/LICENSE +0 -0
  55. {opentrons-8.3.0a2.dist-info → opentrons-8.3.0a5.dist-info}/WHEEL +0 -0
  56. {opentrons-8.3.0a2.dist-info → opentrons-8.3.0a5.dist-info}/entry_points.txt +0 -0
  57. {opentrons-8.3.0a2.dist-info → opentrons-8.3.0a5.dist-info}/top_level.txt +0 -0
@@ -778,6 +778,7 @@ class API(
778
778
  position: Mapping[Axis, float],
779
779
  speed: Optional[float] = None,
780
780
  max_speeds: Optional[Dict[Axis, float]] = None,
781
+ expect_stalls: bool = False,
781
782
  ) -> None:
782
783
  """Moves the effectors of the specified axis to the specified position.
783
784
  The effector of the x,y axis is the center of the carriage.
@@ -1248,7 +1249,10 @@ class API(
1248
1249
  await self.prepare_for_aspirate(mount)
1249
1250
 
1250
1251
  async def tip_drop_moves(
1251
- self, mount: top_types.Mount, home_after: bool = True
1252
+ self,
1253
+ mount: top_types.Mount,
1254
+ home_after: bool = True,
1255
+ ignore_plunger: bool = False,
1252
1256
  ) -> None:
1253
1257
  spec, _ = self.plan_check_drop_tip(mount, home_after)
1254
1258
 
@@ -1189,7 +1189,7 @@ class OT3API(
1189
1189
  speed: Optional[float] = None,
1190
1190
  critical_point: Optional[CriticalPoint] = None,
1191
1191
  max_speeds: Union[None, Dict[Axis, float], OT3AxisMap[float]] = None,
1192
- _expect_stalls: bool = False,
1192
+ expect_stalls: bool = False,
1193
1193
  ) -> None:
1194
1194
  """Move the critical point of the specified mount to a location
1195
1195
  relative to the deck, at the specified speed."""
@@ -1233,7 +1233,7 @@ class OT3API(
1233
1233
  target_position,
1234
1234
  speed=speed,
1235
1235
  max_speeds=checked_max,
1236
- expect_stalls=_expect_stalls,
1236
+ expect_stalls=expect_stalls,
1237
1237
  )
1238
1238
 
1239
1239
  async def move_axes( # noqa: C901
@@ -1241,6 +1241,7 @@ class OT3API(
1241
1241
  position: Mapping[Axis, float],
1242
1242
  speed: Optional[float] = None,
1243
1243
  max_speeds: Optional[Dict[Axis, float]] = None,
1244
+ expect_stalls: bool = False,
1244
1245
  ) -> None:
1245
1246
  """Moves the effectors of the specified axis to the specified position.
1246
1247
  The effector of the x,y axis is the center of the carriage.
@@ -1296,7 +1297,11 @@ class OT3API(
1296
1297
  if axis not in absolute_positions:
1297
1298
  absolute_positions[axis] = position_value
1298
1299
 
1299
- await self._move(target_position=absolute_positions, speed=speed)
1300
+ await self._move(
1301
+ target_position=absolute_positions,
1302
+ speed=speed,
1303
+ expect_stalls=expect_stalls,
1304
+ )
1300
1305
 
1301
1306
  async def move_rel(
1302
1307
  self,
@@ -1306,7 +1311,7 @@ class OT3API(
1306
1311
  max_speeds: Union[None, Dict[Axis, float], OT3AxisMap[float]] = None,
1307
1312
  check_bounds: MotionChecks = MotionChecks.NONE,
1308
1313
  fail_on_not_homed: bool = False,
1309
- _expect_stalls: bool = False,
1314
+ expect_stalls: bool = False,
1310
1315
  ) -> None:
1311
1316
  """Move the critical point of the specified mount by a specified
1312
1317
  displacement in a specified direction, at the specified speed."""
@@ -1348,7 +1353,7 @@ class OT3API(
1348
1353
  speed=speed,
1349
1354
  max_speeds=checked_max,
1350
1355
  check_bounds=check_bounds,
1351
- expect_stalls=_expect_stalls,
1356
+ expect_stalls=expect_stalls,
1352
1357
  )
1353
1358
 
1354
1359
  async def _cache_and_maybe_retract_mount(self, mount: OT3Mount) -> None:
@@ -2320,11 +2325,16 @@ class OT3API(
2320
2325
  instrument.working_volume = tip_volume
2321
2326
 
2322
2327
  async def tip_drop_moves(
2323
- self, mount: Union[top_types.Mount, OT3Mount], home_after: bool = False
2328
+ self,
2329
+ mount: Union[top_types.Mount, OT3Mount],
2330
+ home_after: bool = False,
2331
+ ignore_plunger: bool = False,
2324
2332
  ) -> None:
2325
2333
  realmount = OT3Mount.from_mount(mount)
2326
-
2327
- await self._move_to_plunger_bottom(realmount, rate=1.0, check_current_vol=False)
2334
+ if ignore_plunger is False:
2335
+ await self._move_to_plunger_bottom(
2336
+ realmount, rate=1.0, check_current_vol=False
2337
+ )
2328
2338
 
2329
2339
  if self.gantry_load == GantryLoad.HIGH_THROUGHPUT:
2330
2340
  spec = self._pipette_handler.plan_ht_drop_tip()
@@ -183,7 +183,10 @@ class LiquidHandler(
183
183
  ...
184
184
 
185
185
  async def tip_drop_moves(
186
- self, mount: MountArgType, home_after: bool = True
186
+ self,
187
+ mount: MountArgType,
188
+ home_after: bool = True,
189
+ ignore_plunger: bool = False,
187
190
  ) -> None:
188
191
  ...
189
192
 
@@ -171,6 +171,7 @@ class MotionController(Protocol[MountArgType]):
171
171
  position: Mapping[Axis, float],
172
172
  speed: Optional[float] = None,
173
173
  max_speeds: Optional[Dict[Axis, float]] = None,
174
+ expect_stalls: bool = False,
174
175
  ) -> None:
175
176
  """Moves the effectors of the specified axis to the specified position.
176
177
  The effector of the x,y axis is the center of the carriage.
@@ -299,3 +299,40 @@ def move_to_disposal_location(
299
299
  "name": command_types.MOVE_TO_DISPOSAL_LOCATION,
300
300
  "payload": {"instrument": instrument, "location": location, "text": text},
301
301
  }
302
+
303
+
304
+ def seal(
305
+ instrument: InstrumentContext,
306
+ location: Well,
307
+ ) -> command_types.SealCommand:
308
+ location_text = stringify_location(location)
309
+ text = f"Sealing to {location_text}"
310
+ return {
311
+ "name": command_types.SEAL,
312
+ "payload": {"instrument": instrument, "location": location, "text": text},
313
+ }
314
+
315
+
316
+ def unseal(
317
+ instrument: InstrumentContext,
318
+ location: Well,
319
+ ) -> command_types.UnsealCommand:
320
+ location_text = stringify_location(location)
321
+ text = f"Unsealing from {location_text}"
322
+ return {
323
+ "name": command_types.UNSEAL,
324
+ "payload": {"instrument": instrument, "location": location, "text": text},
325
+ }
326
+
327
+
328
+ def resin_tip_dispense(
329
+ instrument: InstrumentContext,
330
+ flow_rate: float | None,
331
+ ) -> command_types.PressurizeCommand:
332
+ if flow_rate is None:
333
+ flow_rate = 10 # The Protocol Engine default for Resin Tip Dispense
334
+ text = f"Pressurize pipette to dispense from resin tip at {flow_rate}uL/s."
335
+ return {
336
+ "name": command_types.PRESSURIZE,
337
+ "payload": {"instrument": instrument, "text": text},
338
+ }
@@ -43,6 +43,10 @@ TOUCH_TIP: Final = "command.TOUCH_TIP"
43
43
  RETURN_TIP: Final = "command.RETURN_TIP"
44
44
  MOVE_TO: Final = "command.MOVE_TO"
45
45
  MOVE_TO_DISPOSAL_LOCATION: Final = "command.MOVE_TO_DISPOSAL_LOCATION"
46
+ SEAL: Final = "command.SEAL"
47
+ UNSEAL: Final = "command.UNSEAL"
48
+ PRESSURIZE: Final = "command.PRESSURIZE"
49
+
46
50
 
47
51
  # Modules #
48
52
 
@@ -535,11 +539,40 @@ class MoveLabwareCommandPayload(TextOnlyPayload):
535
539
  pass
536
540
 
537
541
 
542
+ class SealCommandPayload(TextOnlyPayload):
543
+ instrument: InstrumentContext
544
+ location: Union[None, Location, Well]
545
+
546
+
547
+ class UnsealCommandPayload(TextOnlyPayload):
548
+ instrument: InstrumentContext
549
+ location: Union[None, Location, Well]
550
+
551
+
552
+ class PressurizeCommandPayload(TextOnlyPayload):
553
+ instrument: InstrumentContext
554
+
555
+
538
556
  class MoveLabwareCommand(TypedDict):
539
557
  name: Literal["command.MOVE_LABWARE"]
540
558
  payload: MoveLabwareCommandPayload
541
559
 
542
560
 
561
+ class SealCommand(TypedDict):
562
+ name: Literal["command.SEAL"]
563
+ payload: SealCommandPayload
564
+
565
+
566
+ class UnsealCommand(TypedDict):
567
+ name: Literal["command.UNSEAL"]
568
+ payload: UnsealCommandPayload
569
+
570
+
571
+ class PressurizeCommand(TypedDict):
572
+ name: Literal["command.PRESSURIZE"]
573
+ payload: PressurizeCommandPayload
574
+
575
+
543
576
  Command = Union[
544
577
  DropTipCommand,
545
578
  DropTipInDisposalLocationCommand,
@@ -588,6 +621,9 @@ Command = Union[
588
621
  MoveToCommand,
589
622
  MoveToDisposalLocationCommand,
590
623
  MoveLabwareCommand,
624
+ SealCommand,
625
+ UnsealCommand,
626
+ PressurizeCommand,
591
627
  ]
592
628
 
593
629
 
@@ -637,6 +673,9 @@ CommandPayload = Union[
637
673
  MoveToCommandPayload,
638
674
  MoveToDisposalLocationCommandPayload,
639
675
  MoveLabwareCommandPayload,
676
+ SealCommandPayload,
677
+ UnsealCommandPayload,
678
+ PressurizeCommandPayload,
640
679
  ]
641
680
 
642
681
 
@@ -48,6 +48,8 @@ if TYPE_CHECKING:
48
48
  from opentrons.protocol_api._liquid import LiquidClass
49
49
 
50
50
  _DISPENSE_VOLUME_VALIDATION_ADDED_IN = APIVersion(2, 17)
51
+ _RESIN_TIP_DEFAULT_VOLUME = 400
52
+ _RESIN_TIP_DEFAULT_FLOW_RATE = 10.0
51
53
 
52
54
 
53
55
  class InstrumentCore(AbstractInstrument[WellCore]):
@@ -678,6 +680,113 @@ class InstrumentCore(AbstractInstrument[WellCore]):
678
680
  location=location, mount=self.get_mount()
679
681
  )
680
682
 
683
+ def resin_tip_seal(
684
+ self, location: Location, well_core: WellCore, in_place: Optional[bool] = False
685
+ ) -> None:
686
+ labware_id = well_core.labware_id
687
+ well_name = well_core.get_name()
688
+ well_location = (
689
+ self._engine_client.state.geometry.get_relative_pick_up_tip_well_location(
690
+ labware_id=labware_id,
691
+ well_name=well_name,
692
+ absolute_point=location.point,
693
+ )
694
+ )
695
+
696
+ self._engine_client.execute_command(
697
+ cmd.EvotipSealPipetteParams(
698
+ pipetteId=self._pipette_id,
699
+ labwareId=labware_id,
700
+ wellName=well_name,
701
+ wellLocation=well_location,
702
+ )
703
+ )
704
+
705
+ def resin_tip_unseal(self, location: Location, well_core: WellCore) -> None:
706
+ well_name = well_core.get_name()
707
+ labware_id = well_core.labware_id
708
+
709
+ if location is not None:
710
+ relative_well_location = (
711
+ self._engine_client.state.geometry.get_relative_well_location(
712
+ labware_id=labware_id,
713
+ well_name=well_name,
714
+ absolute_point=location.point,
715
+ )
716
+ )
717
+
718
+ well_location = DropTipWellLocation(
719
+ origin=DropTipWellOrigin(relative_well_location.origin.value),
720
+ offset=relative_well_location.offset,
721
+ )
722
+ else:
723
+ well_location = DropTipWellLocation()
724
+
725
+ pipette_movement_conflict.check_safe_for_pipette_movement(
726
+ engine_state=self._engine_client.state,
727
+ pipette_id=self._pipette_id,
728
+ labware_id=labware_id,
729
+ well_name=well_name,
730
+ well_location=well_location,
731
+ )
732
+ self._engine_client.execute_command(
733
+ cmd.EvotipUnsealPipetteParams(
734
+ pipetteId=self._pipette_id,
735
+ labwareId=labware_id,
736
+ wellName=well_name,
737
+ wellLocation=well_location,
738
+ )
739
+ )
740
+
741
+ self._protocol_core.set_last_location(location=location, mount=self.get_mount())
742
+
743
+ def resin_tip_dispense(
744
+ self,
745
+ location: Location,
746
+ well_core: WellCore,
747
+ volume: Optional[float] = None,
748
+ flow_rate: Optional[float] = None,
749
+ ) -> None:
750
+ """
751
+ Args:
752
+ volume: The volume of liquid to dispense, in microliters. Defaults to 400uL.
753
+ location: The exact location to dispense to.
754
+ well_core: The well to dispense to, if applicable.
755
+ flow_rate: The flow rate in µL/s to dispense at. Defaults to 10.0uL/S.
756
+ """
757
+ if isinstance(location, (TrashBin, WasteChute)):
758
+ raise ValueError("Trash Bin and Waste Chute have no Wells.")
759
+ well_name = well_core.get_name()
760
+ labware_id = well_core.labware_id
761
+ if volume is None:
762
+ volume = _RESIN_TIP_DEFAULT_VOLUME
763
+ if flow_rate is None:
764
+ flow_rate = _RESIN_TIP_DEFAULT_FLOW_RATE
765
+
766
+ well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
767
+ labware_id=labware_id,
768
+ well_name=well_name,
769
+ absolute_point=location.point,
770
+ is_meniscus=None,
771
+ )
772
+ pipette_movement_conflict.check_safe_for_pipette_movement(
773
+ engine_state=self._engine_client.state,
774
+ pipette_id=self._pipette_id,
775
+ labware_id=labware_id,
776
+ well_name=well_name,
777
+ well_location=well_location,
778
+ )
779
+ self._engine_client.execute_command(
780
+ cmd.EvotipDispenseParams(
781
+ pipetteId=self._pipette_id,
782
+ labwareId=labware_id,
783
+ wellName=well_name,
784
+ wellLocation=well_location,
785
+ volume=volume,
786
+ flowRate=flow_rate,
787
+ )
788
+ )
789
+
681
790
  def get_mount(self) -> Mount:
682
791
  """Get the mount the pipette is attached to."""
683
792
  return self._engine_client.state.pipettes.get(
@@ -180,6 +180,33 @@ class AbstractInstrument(ABC, Generic[WellCoreType]):
180
180
  ) -> None:
181
181
  ...
182
182
 
183
+ @abstractmethod
184
+ def resin_tip_seal(
185
+ self,
186
+ location: types.Location,
187
+ well_core: WellCoreType,
188
+ in_place: Optional[bool] = False,
189
+ ) -> None:
190
+ ...
191
+
192
+ @abstractmethod
193
+ def resin_tip_unseal(
194
+ self,
195
+ location: types.Location,
196
+ well_core: WellCoreType,
197
+ ) -> None:
198
+ ...
199
+
200
+ @abstractmethod
201
+ def resin_tip_dispense(
202
+ self,
203
+ location: types.Location,
204
+ well_core: WellCoreType,
205
+ volume: Optional[float] = None,
206
+ flow_rate: Optional[float] = None,
207
+ ) -> None:
208
+ ...
209
+
183
210
  @abstractmethod
184
211
  def get_mount(self) -> types.Mount:
185
212
  ...
@@ -308,6 +308,30 @@ class LegacyInstrumentCore(AbstractInstrument[LegacyWellCore]):
308
308
  ) -> None:
309
309
  raise APIVersionError(api_element="Dropping tips in a trash bin or waste chute")
310
310
 
311
+ def resin_tip_seal(
312
+ self,
313
+ location: types.Location,
314
+ well_core: WellCore,
315
+ in_place: Optional[bool] = False,
316
+ ) -> None:
317
+ raise APIVersionError(api_element="Sealing resin tips.")
318
+
319
+ def resin_tip_unseal(
320
+ self,
321
+ location: types.Location,
322
+ well_core: WellCore,
323
+ ) -> None:
324
+ raise APIVersionError(api_element="Unsealing resin tips.")
325
+
326
+ def resin_tip_dispense(
327
+ self,
328
+ location: types.Location,
329
+ well_core: WellCore,
330
+ volume: Optional[float] = None,
331
+ flow_rate: Optional[float] = None,
332
+ ) -> None:
333
+ raise APIVersionError(api_element="Dispensing liquid from resin tips.")
334
+
311
335
  def home(self) -> None:
312
336
  """Home the mount"""
313
337
  self._protocol_interface.get_hardware().home_z(
@@ -400,6 +424,32 @@ class LegacyInstrumentCore(AbstractInstrument[LegacyWellCore]):
400
424
  location=location, mount=location_cache_mount
401
425
  )
402
426
 
427
+ def evotip_seal(
428
+ self,
429
+ location: types.Location,
430
+ well_core: LegacyWellCore,
431
+ in_place: Optional[bool] = False,
432
+ ) -> None:
433
+ """This will never be called because it was added in API 2.22."""
434
+ assert False, "evotip_seal only supported in API 2.22 & later"
435
+
436
+ def evotip_unseal(
437
+ self, location: types.Location, well_core: WellCore, home_after: Optional[bool]
438
+ ) -> None:
439
+ """This will never be called because it was added in API 2.22."""
440
+ assert False, "evotip_unseal only supported in API 2.22 & later"
441
+
442
+ def evotip_dispense(
443
+ self,
444
+ location: types.Location,
445
+ well_core: WellCore,
446
+ volume: Optional[float] = None,
447
+ flow_rate: Optional[float] = None,
448
+ push_out: Optional[float] = None,
449
+ ) -> None:
450
+ """This will never be called because it was added in API 2.22."""
451
+ assert False, "evotip_dispense only supported in API 2.22 & later"
452
+
403
453
  def get_mount(self) -> types.Mount:
404
454
  """Get the mount this pipette is attached to."""
405
455
  return self._mount
@@ -276,6 +276,30 @@ class LegacyInstrumentCoreSimulator(AbstractInstrument[LegacyWellCore]):
276
276
  ) -> None:
277
277
  raise APIVersionError(api_element="Dropping tips in a trash bin or waste chute")
278
278
 
279
+ def resin_tip_seal(
280
+ self,
281
+ location: types.Location,
282
+ well_core: WellCore,
283
+ in_place: Optional[bool] = False,
284
+ ) -> None:
285
+ raise APIVersionError(api_element="Sealing resin tips.")
286
+
287
+ def resin_tip_unseal(
288
+ self,
289
+ location: types.Location,
290
+ well_core: WellCore,
291
+ ) -> None:
292
+ raise APIVersionError(api_element="Unsealing resin tips.")
293
+
294
+ def resin_tip_dispense(
295
+ self,
296
+ location: types.Location,
297
+ well_core: WellCore,
298
+ volume: Optional[float] = None,
299
+ flow_rate: Optional[float] = None,
300
+ ) -> None:
301
+ raise APIVersionError(api_element="Dispensing liquid from resin tips.")
302
+
279
303
  def home(self) -> None:
280
304
  self._protocol_interface.set_last_location(None)
281
305
 
@@ -1712,6 +1712,147 @@ class InstrumentContext(publisher.CommandPublisher):
1712
1712
 
1713
1713
  return self
1714
1714
 
1715
+ @requires_version(2, 22)
1716
+ def resin_tip_seal(
1717
+ self,
1718
+ location: Union[labware.Well, labware.Labware],
1719
+ ) -> InstrumentContext:
1720
+ """Seal resin tips onto the pipette.
1721
+
1722
+ The location provided should contain resin tips. Sealing the
1723
+ tip will perform a `pick up` action but there will be no tip tracking
1724
+ associated with the pipette.
1725
+
1726
+ :param location: A location containing resin tips, must be a Labware or a Well.
1727
+
1728
+ :type location: :py:class:`~.types.Location`
1729
+ """
1730
+ if isinstance(location, labware.Labware):
1731
+ well = location.wells()[0]
1732
+ else:
1733
+ well = location
1734
+
1735
+ with publisher.publish_context(
1736
+ broker=self.broker,
1737
+ command=cmds.seal(
1738
+ instrument=self,
1739
+ location=well,
1740
+ ),
1741
+ ):
1742
+ self._core.resin_tip_seal(
1743
+ location=well.top(), well_core=well._core, in_place=False
1744
+ )
1745
+ return self
1746
+
1747
+ @requires_version(2, 22)
1748
+ def resin_tip_unseal(
1749
+ self,
1750
+ location: Union[labware.Well, labware.Labware],
1751
+ ) -> InstrumentContext:
1752
+ """Release resin tips from the pipette.
1753
+
1754
+ The location provided should be a valid location to drop resin tips.
1755
+
1756
+ :param location: A location containing that can accept tips.
1757
+
1758
+ :type location: :py:class:`~.types.Location`
1759
+
1760
+ :param home_after:
1761
+ Whether to home the pipette after dropping the tip. If not specified
1762
+ defaults to ``True`` on a Flex. The plunger will not home on an unseal.
1763
+
1764
+ When ``False``, the pipette does not home its plunger. This can save a few
1765
+ seconds, but is not recommended. Homing helps the robot track the pipette's
1766
+ position.
1767
+
1768
+ """
1769
+ if isinstance(location, labware.Labware):
1770
+ well = location.wells()[0]
1771
+ else:
1772
+ well = location
1773
+
1774
+ with publisher.publish_context(
1775
+ broker=self.broker,
1776
+ command=cmds.unseal(
1777
+ instrument=self,
1778
+ location=well,
1779
+ ),
1780
+ ):
1781
+ self._core.resin_tip_unseal(location=well.top(), well_core=well._core)
1782
+
1783
+ return self
1784
+
1785
+ @requires_version(2, 22)
1786
+ def resin_tip_dispense(
1787
+ self,
1788
+ location: types.Location,
1789
+ volume: Optional[float] = None,
1790
+ rate: Optional[float] = None,
1791
+ ) -> InstrumentContext:
1792
+ """Dispense a volume from resin tips into a labware.
1793
+
1794
+ The location provided should contain resin tips labware as well as a
1795
+ receptical for dispensed liquid. Dispensing from tip will perform a
1796
+ `dispense` action of the specified volume at a desired flow rate.
1797
+
1798
+ :param location: A location containing resin tips.
1799
+ :type location: :py:class:`~.types.Location`
1800
+
1801
+ :param volume: Will default to maximum, recommended to use the default.
1802
+ The volume, in µL, that the pipette will prepare to handle.
1803
+ :type volume: float
1804
+
1805
+ :param rate: Will default to 10.0, recommended to use the default. How quickly
1806
+ a pipette dispenses liquid. The speed in µL/s is calculated as
1807
+ ``rate`` multiplied by :py:attr:`flow_rate.dispense<flow_rate>`.
1808
+ :type rate: float
1809
+
1810
+ """
1811
+ well: Optional[labware.Well] = None
1812
+ last_location = self._get_last_location_by_api_version()
1813
+
1814
+ try:
1815
+ target = validation.validate_location(
1816
+ location=location, last_location=last_location
1817
+ )
1818
+ except validation.NoLocationError as e:
1819
+ raise RuntimeError(
1820
+ "If dispense is called without an explicit location, another"
1821
+ " method that moves to a location (such as move_to or "
1822
+ "aspirate) must previously have been called so the robot "
1823
+ "knows where it is."
1824
+ ) from e
1825
+
1826
+ if isinstance(target, validation.WellTarget):
1827
+ well = target.well
1828
+ if target.location:
1829
+ move_to_location = target.location
1830
+ elif well.parent._core.is_fixed_trash():
1831
+ move_to_location = target.well.top()
1832
+ else:
1833
+ move_to_location = target.well.bottom(
1834
+ z=self._well_bottom_clearances.dispense
1835
+ )
1836
+ else:
1837
+ raise RuntimeError(
1838
+ "A well must be specified when using `resin_tip_dispense`."
1839
+ )
1840
+
1841
+ with publisher.publish_context(
1842
+ broker=self.broker,
1843
+ command=cmds.resin_tip_dispense(
1844
+ instrument=self,
1845
+ flow_rate=rate,
1846
+ ),
1847
+ ):
1848
+ self._core.resin_tip_dispense(
1849
+ move_to_location,
1850
+ well_core=well._core,
1851
+ volume=volume,
1852
+ flow_rate=rate,
1853
+ )
1854
+ return self
1855
+
1715
1856
  @requires_version(2, 18)
1716
1857
  def _retract(
1717
1858
  self,
@@ -115,8 +115,23 @@ class Well:
115
115
  @property
116
116
  @requires_version(2, 0)
117
117
  def has_tip(self) -> bool:
118
- """Whether this well contains a tip. Always ``False`` if the parent labware
119
- isn't a tip rack."""
118
+ """Whether this well contains an unused tip.
119
+
120
+ From API v2.2 on:
121
+
122
+ - Returns ``False`` if:
123
+
124
+ - the well has no tip present, or
125
+ - the well has a tip that's been used by the protocol previously
126
+
127
+ - Returns ``True`` if the well has an unused tip.
128
+
129
+ Before API v2.2:
130
+
131
+ - Returns ``True`` as long as the well has a tip, even if it is used.
132
+
133
+ Always ``False`` if the parent labware isn't a tip rack.
134
+ """
120
135
  return self._core.has_tip()
121
136
 
122
137
  @has_tip.setter