opentrons 8.1.0a0__py2.py3-none-any.whl → 8.2.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 (230) hide show
  1. opentrons/cli/analyze.py +71 -7
  2. opentrons/config/__init__.py +9 -0
  3. opentrons/config/advanced_settings.py +22 -0
  4. opentrons/config/defaults_ot3.py +14 -36
  5. opentrons/config/feature_flags.py +4 -0
  6. opentrons/config/types.py +6 -17
  7. opentrons/drivers/absorbance_reader/abstract.py +27 -3
  8. opentrons/drivers/absorbance_reader/async_byonoy.py +207 -154
  9. opentrons/drivers/absorbance_reader/driver.py +24 -15
  10. opentrons/drivers/absorbance_reader/hid_protocol.py +79 -50
  11. opentrons/drivers/absorbance_reader/simulator.py +32 -6
  12. opentrons/drivers/types.py +23 -1
  13. opentrons/execute.py +2 -2
  14. opentrons/hardware_control/api.py +18 -10
  15. opentrons/hardware_control/backends/controller.py +3 -2
  16. opentrons/hardware_control/backends/flex_protocol.py +11 -5
  17. opentrons/hardware_control/backends/ot3controller.py +18 -50
  18. opentrons/hardware_control/backends/ot3simulator.py +7 -6
  19. opentrons/hardware_control/instruments/ot2/pipette_handler.py +22 -82
  20. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -2
  21. opentrons/hardware_control/module_control.py +43 -2
  22. opentrons/hardware_control/modules/__init__.py +7 -1
  23. opentrons/hardware_control/modules/absorbance_reader.py +230 -83
  24. opentrons/hardware_control/modules/errors.py +7 -0
  25. opentrons/hardware_control/modules/heater_shaker.py +8 -3
  26. opentrons/hardware_control/modules/magdeck.py +12 -3
  27. opentrons/hardware_control/modules/mod_abc.py +27 -2
  28. opentrons/hardware_control/modules/tempdeck.py +15 -7
  29. opentrons/hardware_control/modules/thermocycler.py +69 -3
  30. opentrons/hardware_control/modules/types.py +11 -5
  31. opentrons/hardware_control/modules/update.py +11 -5
  32. opentrons/hardware_control/modules/utils.py +3 -1
  33. opentrons/hardware_control/ot3_calibration.py +6 -6
  34. opentrons/hardware_control/ot3api.py +126 -89
  35. opentrons/hardware_control/poller.py +15 -11
  36. opentrons/hardware_control/protocols/__init__.py +1 -7
  37. opentrons/hardware_control/protocols/instrument_configurer.py +14 -2
  38. opentrons/hardware_control/protocols/liquid_handler.py +5 -0
  39. opentrons/motion_planning/__init__.py +2 -0
  40. opentrons/motion_planning/waypoints.py +32 -0
  41. opentrons/protocol_api/__init__.py +2 -1
  42. opentrons/protocol_api/_liquid.py +87 -1
  43. opentrons/protocol_api/_parameter_context.py +10 -1
  44. opentrons/protocol_api/core/engine/deck_conflict.py +0 -297
  45. opentrons/protocol_api/core/engine/instrument.py +29 -25
  46. opentrons/protocol_api/core/engine/labware.py +10 -2
  47. opentrons/protocol_api/core/engine/module_core.py +129 -17
  48. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +355 -0
  49. opentrons/protocol_api/core/engine/protocol.py +55 -2
  50. opentrons/protocol_api/core/instrument.py +2 -0
  51. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
  52. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +5 -2
  53. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -0
  54. opentrons/protocol_api/core/module.py +22 -4
  55. opentrons/protocol_api/core/protocol.py +5 -2
  56. opentrons/protocol_api/instrument_context.py +52 -20
  57. opentrons/protocol_api/labware.py +13 -1
  58. opentrons/protocol_api/module_contexts.py +68 -13
  59. opentrons/protocol_api/protocol_context.py +38 -4
  60. opentrons/protocol_api/validation.py +5 -3
  61. opentrons/protocol_engine/__init__.py +10 -9
  62. opentrons/protocol_engine/actions/__init__.py +5 -0
  63. opentrons/protocol_engine/actions/actions.py +42 -25
  64. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  65. opentrons/protocol_engine/clients/sync_client.py +7 -1
  66. opentrons/protocol_engine/clients/transports.py +1 -1
  67. opentrons/protocol_engine/commands/__init__.py +0 -4
  68. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +41 -11
  69. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +161 -0
  70. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +53 -9
  71. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +160 -0
  72. opentrons/protocol_engine/commands/absorbance_reader/read.py +196 -0
  73. opentrons/protocol_engine/commands/aspirate.py +29 -16
  74. opentrons/protocol_engine/commands/aspirate_in_place.py +32 -15
  75. opentrons/protocol_engine/commands/blow_out.py +63 -14
  76. opentrons/protocol_engine/commands/blow_out_in_place.py +55 -13
  77. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +2 -5
  78. opentrons/protocol_engine/commands/calibration/calibrate_module.py +3 -4
  79. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +2 -5
  80. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +6 -4
  81. opentrons/protocol_engine/commands/command.py +28 -17
  82. opentrons/protocol_engine/commands/command_unions.py +37 -24
  83. opentrons/protocol_engine/commands/comment.py +5 -3
  84. opentrons/protocol_engine/commands/configure_for_volume.py +11 -14
  85. opentrons/protocol_engine/commands/configure_nozzle_layout.py +9 -15
  86. opentrons/protocol_engine/commands/custom.py +5 -3
  87. opentrons/protocol_engine/commands/dispense.py +42 -20
  88. opentrons/protocol_engine/commands/dispense_in_place.py +32 -14
  89. opentrons/protocol_engine/commands/drop_tip.py +68 -15
  90. opentrons/protocol_engine/commands/drop_tip_in_place.py +52 -11
  91. opentrons/protocol_engine/commands/get_tip_presence.py +5 -3
  92. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +6 -6
  93. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +6 -6
  94. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +6 -6
  95. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +8 -6
  96. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +8 -4
  97. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +6 -4
  98. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +6 -6
  99. opentrons/protocol_engine/commands/home.py +11 -5
  100. opentrons/protocol_engine/commands/liquid_probe.py +146 -88
  101. opentrons/protocol_engine/commands/load_labware.py +19 -5
  102. opentrons/protocol_engine/commands/load_liquid.py +18 -7
  103. opentrons/protocol_engine/commands/load_module.py +43 -6
  104. opentrons/protocol_engine/commands/load_pipette.py +18 -17
  105. opentrons/protocol_engine/commands/magnetic_module/disengage.py +6 -6
  106. opentrons/protocol_engine/commands/magnetic_module/engage.py +6 -4
  107. opentrons/protocol_engine/commands/move_labware.py +106 -19
  108. opentrons/protocol_engine/commands/move_relative.py +15 -3
  109. opentrons/protocol_engine/commands/move_to_addressable_area.py +29 -4
  110. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +13 -4
  111. opentrons/protocol_engine/commands/move_to_coordinates.py +11 -5
  112. opentrons/protocol_engine/commands/move_to_well.py +37 -10
  113. opentrons/protocol_engine/commands/pick_up_tip.py +50 -29
  114. opentrons/protocol_engine/commands/pipetting_common.py +39 -15
  115. opentrons/protocol_engine/commands/prepare_to_aspirate.py +62 -15
  116. opentrons/protocol_engine/commands/reload_labware.py +13 -4
  117. opentrons/protocol_engine/commands/retract_axis.py +6 -3
  118. opentrons/protocol_engine/commands/save_position.py +2 -3
  119. opentrons/protocol_engine/commands/set_rail_lights.py +5 -3
  120. opentrons/protocol_engine/commands/set_status_bar.py +5 -3
  121. opentrons/protocol_engine/commands/temperature_module/deactivate.py +6 -4
  122. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +3 -4
  123. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +6 -6
  124. opentrons/protocol_engine/commands/thermocycler/__init__.py +19 -0
  125. opentrons/protocol_engine/commands/thermocycler/close_lid.py +8 -8
  126. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +6 -4
  127. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +6 -4
  128. opentrons/protocol_engine/commands/thermocycler/open_lid.py +8 -4
  129. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +165 -0
  130. opentrons/protocol_engine/commands/thermocycler/run_profile.py +6 -6
  131. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +3 -4
  132. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +3 -4
  133. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +6 -4
  134. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +6 -4
  135. opentrons/protocol_engine/commands/touch_tip.py +19 -7
  136. opentrons/protocol_engine/commands/unsafe/__init__.py +30 -0
  137. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +6 -4
  138. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -4
  139. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +5 -3
  140. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +194 -0
  141. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +75 -0
  142. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +5 -3
  143. opentrons/protocol_engine/commands/verify_tip_presence.py +5 -5
  144. opentrons/protocol_engine/commands/wait_for_duration.py +5 -3
  145. opentrons/protocol_engine/commands/wait_for_resume.py +5 -3
  146. opentrons/protocol_engine/create_protocol_engine.py +41 -8
  147. opentrons/protocol_engine/engine_support.py +2 -1
  148. opentrons/protocol_engine/error_recovery_policy.py +14 -3
  149. opentrons/protocol_engine/errors/__init__.py +18 -0
  150. opentrons/protocol_engine/errors/exceptions.py +114 -2
  151. opentrons/protocol_engine/execution/__init__.py +2 -0
  152. opentrons/protocol_engine/execution/command_executor.py +22 -13
  153. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  154. opentrons/protocol_engine/execution/door_watcher.py +1 -1
  155. opentrons/protocol_engine/execution/equipment.py +2 -1
  156. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  157. opentrons/protocol_engine/execution/gantry_mover.py +4 -2
  158. opentrons/protocol_engine/execution/hardware_stopper.py +3 -3
  159. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +1 -4
  160. opentrons/protocol_engine/execution/labware_movement.py +6 -3
  161. opentrons/protocol_engine/execution/movement.py +8 -3
  162. opentrons/protocol_engine/execution/pipetting.py +7 -4
  163. opentrons/protocol_engine/execution/queue_worker.py +6 -2
  164. opentrons/protocol_engine/execution/run_control.py +1 -1
  165. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +1 -1
  166. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +2 -1
  167. opentrons/protocol_engine/execution/tip_handler.py +77 -43
  168. opentrons/protocol_engine/notes/__init__.py +14 -2
  169. opentrons/protocol_engine/notes/notes.py +18 -1
  170. opentrons/protocol_engine/plugins.py +1 -1
  171. opentrons/protocol_engine/protocol_engine.py +54 -31
  172. opentrons/protocol_engine/resources/__init__.py +2 -0
  173. opentrons/protocol_engine/resources/deck_data_provider.py +58 -5
  174. opentrons/protocol_engine/resources/file_provider.py +157 -0
  175. opentrons/protocol_engine/resources/fixture_validation.py +5 -0
  176. opentrons/protocol_engine/resources/labware_validation.py +10 -0
  177. opentrons/protocol_engine/state/__init__.py +0 -70
  178. opentrons/protocol_engine/state/addressable_areas.py +1 -1
  179. opentrons/protocol_engine/state/command_history.py +21 -2
  180. opentrons/protocol_engine/state/commands.py +110 -31
  181. opentrons/protocol_engine/state/files.py +59 -0
  182. opentrons/protocol_engine/state/frustum_helpers.py +440 -0
  183. opentrons/protocol_engine/state/geometry.py +359 -15
  184. opentrons/protocol_engine/state/labware.py +166 -63
  185. opentrons/protocol_engine/state/liquids.py +1 -1
  186. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +19 -3
  187. opentrons/protocol_engine/state/modules.py +167 -85
  188. opentrons/protocol_engine/state/motion.py +16 -9
  189. opentrons/protocol_engine/state/pipettes.py +157 -317
  190. opentrons/protocol_engine/state/state.py +30 -1
  191. opentrons/protocol_engine/state/state_summary.py +3 -0
  192. opentrons/protocol_engine/state/tips.py +69 -114
  193. opentrons/protocol_engine/state/update_types.py +408 -0
  194. opentrons/protocol_engine/state/wells.py +236 -0
  195. opentrons/protocol_engine/types.py +90 -0
  196. opentrons/protocol_reader/file_format_validator.py +83 -15
  197. opentrons/protocol_runner/json_translator.py +21 -5
  198. opentrons/protocol_runner/legacy_command_mapper.py +27 -6
  199. opentrons/protocol_runner/legacy_context_plugin.py +27 -71
  200. opentrons/protocol_runner/protocol_runner.py +6 -3
  201. opentrons/protocol_runner/run_orchestrator.py +26 -6
  202. opentrons/protocols/advanced_control/mix.py +3 -5
  203. opentrons/protocols/advanced_control/transfers.py +125 -56
  204. opentrons/protocols/api_support/constants.py +1 -1
  205. opentrons/protocols/api_support/definitions.py +1 -1
  206. opentrons/protocols/api_support/labware_like.py +4 -4
  207. opentrons/protocols/api_support/tip_tracker.py +2 -2
  208. opentrons/protocols/api_support/types.py +15 -2
  209. opentrons/protocols/api_support/util.py +30 -42
  210. opentrons/protocols/duration/errors.py +1 -1
  211. opentrons/protocols/duration/estimator.py +50 -29
  212. opentrons/protocols/execution/dev_types.py +2 -2
  213. opentrons/protocols/execution/execute_json_v4.py +15 -10
  214. opentrons/protocols/execution/execute_python.py +8 -3
  215. opentrons/protocols/geometry/planning.py +12 -12
  216. opentrons/protocols/labware.py +17 -33
  217. opentrons/simulate.py +3 -3
  218. opentrons/types.py +30 -3
  219. opentrons/util/logging_config.py +34 -0
  220. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/METADATA +5 -4
  221. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/RECORD +227 -215
  222. opentrons/protocol_engine/commands/absorbance_reader/measure.py +0 -94
  223. opentrons/protocol_engine/commands/configuring_common.py +0 -26
  224. opentrons/protocol_runner/thread_async_queue.py +0 -174
  225. /opentrons/protocol_engine/state/{abstract_store.py → _abstract_store.py} +0 -0
  226. /opentrons/protocol_engine/state/{move_types.py → _move_types.py} +0 -0
  227. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/LICENSE +0 -0
  228. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/WHEEL +0 -0
  229. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/entry_points.txt +0 -0
  230. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0a0.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,13 @@
1
1
  """Protocol API module implementation logic."""
2
2
  from __future__ import annotations
3
3
 
4
- from typing import Optional, List
4
+ from typing import Optional, List, Dict, Union
5
5
 
6
6
  from opentrons.hardware_control import SynchronousAdapter, modules as hw_modules
7
7
  from opentrons.hardware_control.modules.types import (
8
8
  ModuleModel,
9
9
  TemperatureStatus,
10
10
  MagneticStatus,
11
- ThermocyclerStep,
12
11
  SpeedStatus,
13
12
  module_model_from_string,
14
13
  )
@@ -16,15 +15,18 @@ from opentrons.drivers.types import (
16
15
  HeaterShakerLabwareLatchStatus,
17
16
  ThermocyclerLidStatus,
18
17
  )
19
- from opentrons.types import DeckSlotName
18
+
20
19
  from opentrons.protocol_engine import commands as cmd
20
+ from opentrons.protocol_engine.types import ABSMeasureMode
21
+ from opentrons.types import DeckSlotName
21
22
  from opentrons.protocol_engine.clients import SyncClient as ProtocolEngineClient
22
23
  from opentrons.protocol_engine.errors.exceptions import (
23
24
  LabwareNotLoadedOnModuleError,
24
25
  NoMagnetEngageHeightError,
26
+ CannotPerformModuleAction,
25
27
  )
26
28
 
27
- from opentrons.protocols.api_support.types import APIVersion
29
+ from opentrons.protocols.api_support.types import APIVersion, ThermocyclerStep
28
30
 
29
31
  from ... import validation
30
32
  from ..module import (
@@ -324,15 +326,13 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore):
324
326
  cmd.thermocycler.WaitForLidTemperatureParams(moduleId=self.module_id)
325
327
  )
326
328
 
327
- def execute_profile(
329
+ def _execute_profile_pre_221(
328
330
  self,
329
331
  steps: List[ThermocyclerStep],
330
332
  repetitions: int,
331
- block_max_volume: Optional[float] = None,
333
+ block_max_volume: Optional[float],
332
334
  ) -> None:
333
- """Execute a Thermocycler Profile."""
334
- self._repetitions = repetitions
335
- self._step_count = len(steps)
335
+ """Execute a thermocycler profile using thermocycler/runProfile and flattened steps."""
336
336
  engine_steps = [
337
337
  cmd.thermocycler.RunProfileStepParams(
338
338
  celsius=step["temperature"],
@@ -349,6 +349,49 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore):
349
349
  )
350
350
  )
351
351
 
352
+ def _execute_profile_post_221(
353
+ self,
354
+ steps: List[ThermocyclerStep],
355
+ repetitions: int,
356
+ block_max_volume: Optional[float],
357
+ ) -> None:
358
+ """Execute a thermocycler profile using thermocycler/runExtendedProfile."""
359
+ engine_steps: List[
360
+ Union[cmd.thermocycler.ProfileCycle, cmd.thermocycler.ProfileStep]
361
+ ] = [
362
+ cmd.thermocycler.ProfileCycle(
363
+ repetitions=repetitions,
364
+ steps=[
365
+ cmd.thermocycler.ProfileStep(
366
+ celsius=step["temperature"],
367
+ holdSeconds=step["hold_time_seconds"],
368
+ )
369
+ for step in steps
370
+ ],
371
+ )
372
+ ]
373
+ self._engine_client.execute_command(
374
+ cmd.thermocycler.RunExtendedProfileParams(
375
+ moduleId=self.module_id,
376
+ profileElements=engine_steps,
377
+ blockMaxVolumeUl=block_max_volume,
378
+ )
379
+ )
380
+
381
+ def execute_profile(
382
+ self,
383
+ steps: List[ThermocyclerStep],
384
+ repetitions: int,
385
+ block_max_volume: Optional[float] = None,
386
+ ) -> None:
387
+ """Execute a Thermocycler Profile."""
388
+ self._repetitions = repetitions
389
+ self._step_count = len(steps)
390
+ if self.api_version >= APIVersion(2, 21):
391
+ return self._execute_profile_post_221(steps, repetitions, block_max_volume)
392
+ else:
393
+ return self._execute_profile_pre_221(steps, repetitions, block_max_volume)
394
+
352
395
  def deactivate_lid(self) -> None:
353
396
  """Turn off the heated lid."""
354
397
  self._engine_client.execute_command(
@@ -523,23 +566,92 @@ class AbsorbanceReaderCore(ModuleCore, AbstractAbsorbanceReaderCore):
523
566
  """Absorbance Reader core logic implementation for Python protocols."""
524
567
 
525
568
  _sync_module_hardware: SynchronousAdapter[hw_modules.AbsorbanceReader]
526
- _initialized_value: Optional[int] = None
569
+ _initialized_value: Optional[List[int]] = None
570
+ _ready_to_initialize: bool = False
527
571
 
528
- def initialize(self, wavelength: int) -> None:
572
+ def initialize(
573
+ self,
574
+ mode: ABSMeasureMode,
575
+ wavelengths: List[int],
576
+ reference_wavelength: Optional[int] = None,
577
+ ) -> None:
529
578
  """Initialize the Absorbance Reader by taking zero reading."""
579
+ if not self._ready_to_initialize:
580
+ raise CannotPerformModuleAction(
581
+ "Cannot perform Initialize action on Absorbance Reader without calling `.close_lid()` first."
582
+ )
583
+
584
+ # TODO: check that the wavelengths are within the supported wavelengths
530
585
  self._engine_client.execute_command(
531
586
  cmd.absorbance_reader.InitializeParams(
532
587
  moduleId=self.module_id,
533
- sampleWavelength=wavelength,
588
+ measureMode=mode,
589
+ sampleWavelengths=wavelengths,
590
+ referenceWavelength=reference_wavelength,
534
591
  ),
535
592
  )
536
- self._initialized_value = wavelength
593
+ self._initialized_value = wavelengths
537
594
 
538
- def initiate_read(self) -> None:
539
- """Initiate read on the Absorbance Reader."""
595
+ def read(self, filename: Optional[str] = None) -> Dict[int, Dict[str, float]]:
596
+ """Initiate a read on the Absorbance Reader, and return the results. During Analysis, this will return a measurement of zero for all wells."""
597
+ wavelengths = self._engine_client.state.modules.get_absorbance_reader_substate(
598
+ self.module_id
599
+ ).configured_wavelengths
600
+ if wavelengths is None:
601
+ raise CannotPerformModuleAction(
602
+ "Cannot perform Read action on Absorbance Reader without calling `.initialize(...)` first."
603
+ )
540
604
  if self._initialized_value:
541
605
  self._engine_client.execute_command(
542
- cmd.absorbance_reader.MeasureAbsorbanceParams(
543
- moduleId=self.module_id, sampleWavelength=self._initialized_value
606
+ cmd.absorbance_reader.ReadAbsorbanceParams(
607
+ moduleId=self.module_id, fileName=filename
544
608
  )
545
609
  )
610
+ if not self._engine_client.state.config.use_virtual_modules:
611
+ read_result = (
612
+ self._engine_client.state.modules.get_absorbance_reader_substate(
613
+ self.module_id
614
+ ).data
615
+ )
616
+ if read_result is not None:
617
+ return read_result
618
+ raise CannotPerformModuleAction(
619
+ "Absorbance Reader failed to return expected read result."
620
+ )
621
+
622
+ # When using virtual modules, return all zeroes
623
+ virtual_asbsorbance_result: Dict[int, Dict[str, float]] = {}
624
+ for wavelength in wavelengths:
625
+ converted_values = (
626
+ self._engine_client.state.modules.convert_absorbance_reader_data_points(
627
+ data=[0] * 96
628
+ )
629
+ )
630
+ virtual_asbsorbance_result[wavelength] = converted_values
631
+ return virtual_asbsorbance_result
632
+
633
+ def close_lid(
634
+ self,
635
+ ) -> None:
636
+ """Close the Absorbance Reader's lid."""
637
+ self._engine_client.execute_command(
638
+ cmd.absorbance_reader.CloseLidParams(
639
+ moduleId=self.module_id,
640
+ )
641
+ )
642
+ self._ready_to_initialize = True
643
+
644
+ def open_lid(self) -> None:
645
+ """Close the Absorbance Reader's lid."""
646
+ self._engine_client.execute_command(
647
+ cmd.absorbance_reader.OpenLidParams(
648
+ moduleId=self.module_id,
649
+ )
650
+ )
651
+
652
+ def is_lid_on(self) -> bool:
653
+ """Returns True if the Absorbance Reader's lid is currently on the Reader slot."""
654
+ abs_state = self._engine_client.state.modules.get_absorbance_reader_substate(
655
+ self.module_id
656
+ )
657
+ return abs_state.is_lid_on
@@ -0,0 +1,355 @@
1
+ """A Protocol-Engine-friendly wrapper for opentrons.motion_planning.deck_conflict."""
2
+ from __future__ import annotations
3
+ import logging
4
+ from typing import (
5
+ Optional,
6
+ Tuple,
7
+ Union,
8
+ List,
9
+ )
10
+
11
+ from opentrons_shared_data.errors.exceptions import MotionPlanningFailureError
12
+ from opentrons_shared_data.module import FLEX_TC_LID_COLLISION_ZONE
13
+
14
+ from opentrons.hardware_control import CriticalPoint
15
+ from opentrons.motion_planning import adjacent_slots_getters
16
+
17
+ from opentrons.protocol_engine import (
18
+ StateView,
19
+ DeckSlotLocation,
20
+ OnLabwareLocation,
21
+ WellLocation,
22
+ LiquidHandlingWellLocation,
23
+ PickUpTipWellLocation,
24
+ DropTipWellLocation,
25
+ )
26
+ from opentrons.protocol_engine.types import (
27
+ StagingSlotLocation,
28
+ )
29
+ from opentrons.types import DeckSlotName, StagingSlotName, Point
30
+ from . import point_calculations
31
+
32
+
33
+ class PartialTipMovementNotAllowedError(MotionPlanningFailureError):
34
+ """Error raised when trying to perform a partial tip movement to an illegal location."""
35
+
36
+ def __init__(self, message: str) -> None:
37
+ super().__init__(
38
+ message=message,
39
+ )
40
+
41
+
42
+ class UnsuitableTiprackForPipetteMotion(MotionPlanningFailureError):
43
+ """Error raised when trying to perform a pipette movement to a tip rack, based on adapter status."""
44
+
45
+ def __init__(self, message: str) -> None:
46
+ super().__init__(
47
+ message=message,
48
+ )
49
+
50
+
51
+ _log = logging.getLogger(__name__)
52
+
53
+ _FLEX_TC_LID_BACK_LEFT_PT = Point(
54
+ x=FLEX_TC_LID_COLLISION_ZONE["back_left"]["x"],
55
+ y=FLEX_TC_LID_COLLISION_ZONE["back_left"]["y"],
56
+ z=FLEX_TC_LID_COLLISION_ZONE["back_left"]["z"],
57
+ )
58
+
59
+ _FLEX_TC_LID_FRONT_RIGHT_PT = Point(
60
+ x=FLEX_TC_LID_COLLISION_ZONE["front_right"]["x"],
61
+ y=FLEX_TC_LID_COLLISION_ZONE["front_right"]["y"],
62
+ z=FLEX_TC_LID_COLLISION_ZONE["front_right"]["z"],
63
+ )
64
+
65
+
66
+ def check_safe_for_pipette_movement(
67
+ engine_state: StateView,
68
+ pipette_id: str,
69
+ labware_id: str,
70
+ well_name: str,
71
+ well_location: Union[
72
+ WellLocation,
73
+ LiquidHandlingWellLocation,
74
+ PickUpTipWellLocation,
75
+ DropTipWellLocation,
76
+ ],
77
+ ) -> None:
78
+ """Check if the labware is safe to move to with a pipette in partial tip configuration.
79
+
80
+ Args:
81
+ engine_state: engine state view
82
+ pipette_id: ID of the pipette to be moved
83
+ labware_id: ID of the labware we are moving to
84
+ well_name: Name of the well to move to
85
+ well_location: exact location within the well to move to
86
+ """
87
+ # TODO (spp, 2023-02-06): remove this check after thorough testing.
88
+ # This function is capable of checking for movement conflict regardless of
89
+ # nozzle configuration.
90
+ if not engine_state.pipettes.get_is_partially_configured(pipette_id):
91
+ return
92
+
93
+ if isinstance(well_location, DropTipWellLocation):
94
+ # convert to WellLocation
95
+ well_location = engine_state.geometry.get_checked_tip_drop_location(
96
+ pipette_id=pipette_id,
97
+ labware_id=labware_id,
98
+ well_location=well_location,
99
+ partially_configured=True,
100
+ )
101
+ well_location_point = engine_state.geometry.get_well_position(
102
+ labware_id=labware_id, well_name=well_name, well_location=well_location
103
+ )
104
+ primary_nozzle = engine_state.pipettes.get_primary_nozzle(pipette_id)
105
+
106
+ destination_cp = _get_critical_point_to_use(engine_state, labware_id)
107
+
108
+ pipette_bounds_at_well_location = (
109
+ engine_state.pipettes.get_pipette_bounds_at_specified_move_to_position(
110
+ pipette_id=pipette_id,
111
+ destination_position=well_location_point,
112
+ critical_point=destination_cp,
113
+ )
114
+ )
115
+ if not _is_within_pipette_extents(
116
+ engine_state=engine_state,
117
+ pipette_id=pipette_id,
118
+ pipette_bounding_box_at_loc=pipette_bounds_at_well_location,
119
+ ):
120
+ raise PartialTipMovementNotAllowedError(
121
+ f"Requested motion with the {primary_nozzle} nozzle partial configuration"
122
+ f" is outside of robot bounds for the pipette."
123
+ )
124
+
125
+ labware_slot = engine_state.geometry.get_ancestor_slot_name(labware_id)
126
+
127
+ surrounding_slots = adjacent_slots_getters.get_surrounding_slots(
128
+ slot=labware_slot.as_int(), robot_type=engine_state.config.robot_type
129
+ )
130
+
131
+ if _will_collide_with_thermocycler_lid(
132
+ engine_state=engine_state,
133
+ pipette_bounds=pipette_bounds_at_well_location,
134
+ surrounding_regular_slots=surrounding_slots.regular_slots,
135
+ ):
136
+ raise PartialTipMovementNotAllowedError(
137
+ f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
138
+ f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
139
+ f" will result in collision with thermocycler lid in deck slot A1."
140
+ )
141
+
142
+ for regular_slot in surrounding_slots.regular_slots:
143
+ if _slot_has_potential_colliding_object(
144
+ engine_state=engine_state,
145
+ pipette_bounds=pipette_bounds_at_well_location,
146
+ surrounding_slot=regular_slot,
147
+ ):
148
+ raise PartialTipMovementNotAllowedError(
149
+ f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
150
+ f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
151
+ f" will result in collision with items in deck slot {regular_slot}."
152
+ )
153
+ for staging_slot in surrounding_slots.staging_slots:
154
+ if _slot_has_potential_colliding_object(
155
+ engine_state=engine_state,
156
+ pipette_bounds=pipette_bounds_at_well_location,
157
+ surrounding_slot=staging_slot,
158
+ ):
159
+ raise PartialTipMovementNotAllowedError(
160
+ f"Moving to {engine_state.labware.get_display_name(labware_id)} in slot"
161
+ f" {labware_slot} with {primary_nozzle} nozzle partial configuration"
162
+ f" will result in collision with items in staging slot {staging_slot}."
163
+ )
164
+
165
+
166
+ def _get_critical_point_to_use(
167
+ engine_state: StateView, labware_id: str
168
+ ) -> Optional[CriticalPoint]:
169
+ """Return the critical point to use when accessing the given labware."""
170
+ # TODO (spp, 2024-09-17): looks like Y_CENTER of column is the same as its XY_CENTER.
171
+ # I'm using this if-else ladder to be consistent with what we do in
172
+ # `MotionPlanning.get_movement_waypoints_to_well()`.
173
+ # We should probably use only XY_CENTER in both places.
174
+ if engine_state.labware.get_should_center_column_on_target_well(labware_id):
175
+ return CriticalPoint.Y_CENTER
176
+ elif engine_state.labware.get_should_center_pipette_on_target_well(labware_id):
177
+ return CriticalPoint.XY_CENTER
178
+ return None
179
+
180
+
181
+ def _slot_has_potential_colliding_object(
182
+ engine_state: StateView,
183
+ pipette_bounds: Tuple[Point, Point, Point, Point],
184
+ surrounding_slot: Union[DeckSlotName, StagingSlotName],
185
+ ) -> bool:
186
+ """Return the slot, if any, that has an item that the pipette might collide into."""
187
+ # Check if slot overlaps with pipette position
188
+ slot_pos = engine_state.addressable_areas.get_addressable_area_position(
189
+ addressable_area_name=surrounding_slot.id,
190
+ do_compatibility_check=False,
191
+ )
192
+ slot_bounds = engine_state.addressable_areas.get_addressable_area_bounding_box(
193
+ addressable_area_name=surrounding_slot.id,
194
+ do_compatibility_check=False,
195
+ )
196
+ slot_back_left_coords = Point(slot_pos.x, slot_pos.y + slot_bounds.y, slot_pos.z)
197
+ slot_front_right_coords = Point(slot_pos.x + slot_bounds.x, slot_pos.y, slot_pos.z)
198
+
199
+ # If slot overlaps with pipette bounds
200
+ if point_calculations.are_overlapping_rectangles(
201
+ rectangle1=(pipette_bounds[0], pipette_bounds[1]),
202
+ rectangle2=(slot_back_left_coords, slot_front_right_coords),
203
+ ):
204
+ # Check z-height of items in overlapping slot
205
+ if isinstance(surrounding_slot, DeckSlotName):
206
+ slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
207
+ DeckSlotLocation(slotName=surrounding_slot)
208
+ )
209
+ else:
210
+ slot_highest_z = engine_state.geometry.get_highest_z_in_slot(
211
+ StagingSlotLocation(slotName=surrounding_slot)
212
+ )
213
+ return slot_highest_z >= pipette_bounds[0].z
214
+ return False
215
+
216
+
217
+ def _will_collide_with_thermocycler_lid(
218
+ engine_state: StateView,
219
+ pipette_bounds: Tuple[Point, Point, Point, Point],
220
+ surrounding_regular_slots: List[DeckSlotName],
221
+ ) -> bool:
222
+ """Return whether the pipette might collide with thermocycler's lid/clips on a Flex.
223
+
224
+ If any of the pipette's bounding vertices lie inside the no-go zone of the thermocycler-
225
+ which is the area that's to the left, back and below the thermocycler's lid's
226
+ protruding clips, then we will mark the movement for possible collision.
227
+
228
+ This could cause false raises for the case where an 8-channel is accessing the
229
+ thermocycler labware in a location such that the pipette is in the area between
230
+ the clips but not touching either clips. But that's a tradeoff we'll need to make
231
+ between a complicated check involving accurate positions of all entities involved
232
+ and a crude check that disallows all partial tip movements around the thermocycler.
233
+ """
234
+ # TODO (spp, 2024-02-27): Improvements:
235
+ # - make the check dynamic according to lid state:
236
+ # - if lid is open, check if pipette is in no-go zone
237
+ # - if lid is closed, use the closed lid height to check for conflict
238
+ if (
239
+ DeckSlotName.SLOT_A1 in surrounding_regular_slots
240
+ and engine_state.modules.is_flex_deck_with_thermocycler()
241
+ ):
242
+ return (
243
+ point_calculations.are_overlapping_rectangles(
244
+ rectangle1=(_FLEX_TC_LID_BACK_LEFT_PT, _FLEX_TC_LID_FRONT_RIGHT_PT),
245
+ rectangle2=(pipette_bounds[0], pipette_bounds[1]),
246
+ )
247
+ and pipette_bounds[0].z <= _FLEX_TC_LID_BACK_LEFT_PT.z
248
+ )
249
+
250
+ return False
251
+
252
+
253
+ def check_safe_for_tip_pickup_and_return(
254
+ engine_state: StateView,
255
+ pipette_id: str,
256
+ labware_id: str,
257
+ ) -> None:
258
+ """Check if the presence or absence of a tiprack adapter might cause any pipette movement issues.
259
+
260
+ A 96 channel pipette will pick up tips using cam action when it's configured
261
+ to use ALL nozzles. For this, the tiprack needs to be on the Flex 96 channel tiprack adapter
262
+ or similar or the tips will not be picked up.
263
+
264
+ On the other hand, if the pipette is configured with partial nozzle configuration,
265
+ it uses the usual pipette presses to pick the tips up, in which case, having the tiprack
266
+ on the Flex 96 channel tiprack adapter (or similar) will cause the pipette to
267
+ crash against the adapter posts.
268
+
269
+ In order to check if the 96-channel can move and pickup/drop tips safely, this method
270
+ checks for the height attribute of the tiprack adapter rather than checking for the
271
+ specific official adapter since users might create custom labware &/or definitions
272
+ compatible with the official adapter.
273
+ """
274
+ if not engine_state.pipettes.get_channels(pipette_id) == 96:
275
+ # Adapters only matter to 96 ch.
276
+ return
277
+
278
+ is_partial_config = engine_state.pipettes.get_is_partially_configured(pipette_id)
279
+ tiprack_name = engine_state.labware.get_display_name(labware_id)
280
+ tiprack_parent = engine_state.labware.get_location(labware_id)
281
+ if isinstance(tiprack_parent, OnLabwareLocation): # tiprack is on an adapter
282
+ is_96_ch_tiprack_adapter = engine_state.labware.get_has_quirk(
283
+ labware_id=tiprack_parent.labwareId, quirk="tiprackAdapterFor96Channel"
284
+ )
285
+ tiprack_height = engine_state.labware.get_dimensions(labware_id).z
286
+ adapter_height = engine_state.labware.get_dimensions(tiprack_parent.labwareId).z
287
+ if is_partial_config and tiprack_height < adapter_height:
288
+ raise PartialTipMovementNotAllowedError(
289
+ f"{tiprack_name} cannot be on an adapter taller than the tip rack"
290
+ f" when picking up fewer than 96 tips."
291
+ )
292
+ elif not is_partial_config and not is_96_ch_tiprack_adapter:
293
+ raise UnsuitableTiprackForPipetteMotion(
294
+ f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter"
295
+ f" in order to pick up or return all 96 tips simultaneously."
296
+ )
297
+
298
+ elif (
299
+ not is_partial_config
300
+ ): # tiprack is not on adapter and pipette is in full config
301
+ raise UnsuitableTiprackForPipetteMotion(
302
+ f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter"
303
+ f" in order to pick up or return all 96 tips simultaneously."
304
+ )
305
+
306
+
307
+ def _is_within_pipette_extents(
308
+ engine_state: StateView,
309
+ pipette_id: str,
310
+ pipette_bounding_box_at_loc: Tuple[Point, Point, Point, Point],
311
+ ) -> bool:
312
+ """Whether a given point is within the extents of a configured pipette on the specified robot."""
313
+ channels = engine_state.pipettes.get_channels(pipette_id)
314
+ robot_extents = engine_state.geometry.absolute_deck_extents
315
+ (
316
+ pip_back_left_bound,
317
+ pip_front_right_bound,
318
+ pip_back_right_bound,
319
+ pip_front_left_bound,
320
+ ) = pipette_bounding_box_at_loc
321
+
322
+ # Given the padding values accounted for against the deck extents,
323
+ # a pipette is within extents when all of the following are true:
324
+
325
+ # Each corner slot full pickup case:
326
+ # A1: Front right nozzle is within the rear and left-side padding limits
327
+ # D1: Back right nozzle is within the front and left-side padding limits
328
+ # A3 Front left nozzle is within the rear and right-side padding limits
329
+ # D3: Back left nozzle is within the front and right-side padding limits
330
+ # Thermocycler Column A2: Front right nozzle is within padding limits
331
+
332
+ if channels == 96:
333
+ return (
334
+ pip_front_right_bound.y
335
+ <= robot_extents.deck_extents.y + robot_extents.padding_rear
336
+ and pip_front_right_bound.x >= robot_extents.padding_left_side
337
+ and pip_back_right_bound.y >= robot_extents.padding_front
338
+ and pip_back_right_bound.x >= robot_extents.padding_left_side
339
+ and pip_front_left_bound.y
340
+ <= robot_extents.deck_extents.y + robot_extents.padding_rear
341
+ and pip_front_left_bound.x
342
+ <= robot_extents.deck_extents.x + robot_extents.padding_right_side
343
+ and pip_back_left_bound.y >= robot_extents.padding_front
344
+ and pip_back_left_bound.x
345
+ <= robot_extents.deck_extents.x + robot_extents.padding_right_side
346
+ )
347
+ # For 8ch pipettes we only check the rear and front extents
348
+ return (
349
+ pip_front_right_bound.y
350
+ <= robot_extents.deck_extents.y + robot_extents.padding_rear
351
+ and pip_back_right_bound.y >= robot_extents.padding_front
352
+ and pip_front_left_bound.y
353
+ <= robot_extents.deck_extents.y + robot_extents.padding_rear
354
+ and pip_back_left_bound.y >= robot_extents.padding_front
355
+ )
@@ -2,11 +2,17 @@
2
2
  from __future__ import annotations
3
3
  from typing import Dict, Optional, Type, Union, List, Tuple, TYPE_CHECKING
4
4
 
5
+ from opentrons_shared_data.liquid_classes import LiquidClassDefinitionDoesNotExist
6
+
5
7
  from opentrons.protocol_engine import commands as cmd
6
8
  from opentrons.protocol_engine.commands import LoadModuleResult
7
9
  from opentrons_shared_data.deck.types import DeckDefinitionV5, SlotDefV3
8
10
  from opentrons_shared_data.labware.labware_definition import LabwareDefinition
9
11
  from opentrons_shared_data.labware.types import LabwareDefinition as LabwareDefDict
12
+ from opentrons_shared_data import liquid_classes
13
+ from opentrons_shared_data.liquid_classes.liquid_class_definition import (
14
+ LiquidClassSchemaV1,
15
+ )
10
16
  from opentrons_shared_data.pipette.types import PipetteNameType
11
17
  from opentrons_shared_data.robot.types import RobotType
12
18
 
@@ -51,7 +57,7 @@ from opentrons.protocol_engine.errors import (
51
57
 
52
58
  from ... import validation
53
59
  from ..._types import OffDeckType
54
- from ..._liquid import Liquid
60
+ from ..._liquid import Liquid, LiquidClass
55
61
  from ...disposal_locations import TrashBin, WasteChute
56
62
  from ..protocol import AbstractProtocol
57
63
  from ..labware import LabwareLoadParams
@@ -103,6 +109,7 @@ class ProtocolCore(
103
109
  str, Union[ModuleCore, NonConnectedModuleCore]
104
110
  ] = {}
105
111
  self._disposal_locations: List[Union[Labware, TrashBin, WasteChute]] = []
112
+ self._defined_liquid_class_defs_by_name: Dict[str, LiquidClassSchemaV1] = {}
106
113
  self._load_fixed_trash()
107
114
 
108
115
  @property
@@ -311,7 +318,6 @@ class ProtocolCore(
311
318
 
312
319
  return labware_core
313
320
 
314
- # TODO (spp, 2022-12-14): https://opentrons.atlassian.net/browse/RLAB-237
315
321
  def move_labware(
316
322
  self,
317
323
  labware_core: LabwareCore,
@@ -441,10 +447,40 @@ class ProtocolCore(
441
447
  existing_module_ids=list(self._module_cores_by_id.keys()),
442
448
  )
443
449
 
450
+ # When the protocol engine is created, we add Module Lids as part of the deck fixed labware
451
+ # If a valid module exists in the deck config. For analysis, we add the labware here since
452
+ # deck fixed labware is not created under the same conditions. We also need to inject the Module
453
+ # lids when the module isnt already on the deck config, like when adding a new
454
+ # module during a protocol setup.
455
+ self._load_virtual_module_lid(module_core)
456
+
444
457
  self._module_cores_by_id[module_core.module_id] = module_core
445
458
 
446
459
  return module_core
447
460
 
461
+ def _load_virtual_module_lid(
462
+ self, module_core: Union[ModuleCore, NonConnectedModuleCore]
463
+ ) -> None:
464
+ if isinstance(module_core, AbsorbanceReaderCore):
465
+ substate = self._engine_client.state.modules.get_absorbance_reader_substate(
466
+ module_core.module_id
467
+ )
468
+ if substate.lid_id is None:
469
+ lid = self._engine_client.execute_command_without_recovery(
470
+ cmd.LoadLabwareParams(
471
+ loadName="opentrons_flex_lid_absorbance_plate_reader_module",
472
+ location=ModuleLocation(moduleId=module_core.module_id),
473
+ namespace="opentrons",
474
+ version=1,
475
+ displayName="Absorbance Reader Lid",
476
+ )
477
+ )
478
+
479
+ self._engine_client.add_absorbance_reader_lid(
480
+ module_id=module_core.module_id,
481
+ lid_id=lid.labwareId,
482
+ )
483
+
448
484
  def _create_non_connected_module_core(
449
485
  self, load_module_result: LoadModuleResult
450
486
  ) -> NonConnectedModuleCore:
@@ -723,6 +759,23 @@ class ProtocolCore(
723
759
  ),
724
760
  )
725
761
 
762
+ def define_liquid_class(self, name: str) -> LiquidClass:
763
+ """Define a liquid class for use in transfer functions."""
764
+ try:
765
+ # Check if we have already loaded this liquid class' definition
766
+ liquid_class_def = self._defined_liquid_class_defs_by_name[name]
767
+ except KeyError:
768
+ try:
769
+ # Fetching the liquid class data from file and parsing it
770
+ # is an expensive operation and should be avoided.
771
+ # Calling this often will degrade protocol execution performance.
772
+ liquid_class_def = liquid_classes.load_definition(name)
773
+ self._defined_liquid_class_defs_by_name[name] = liquid_class_def
774
+ except LiquidClassDefinitionDoesNotExist:
775
+ raise ValueError(f"Liquid class definition not found for '{name}'.")
776
+
777
+ return LiquidClass.create(liquid_class_def)
778
+
726
779
  def get_labware_location(
727
780
  self, labware_core: LabwareCore
728
781
  ) -> Union[str, LabwareCore, ModuleCore, NonConnectedModuleCore, OffDeckType]:
@@ -33,6 +33,7 @@ class AbstractInstrument(ABC, Generic[WellCoreType]):
33
33
  rate: float,
34
34
  flow_rate: float,
35
35
  in_place: bool,
36
+ is_meniscus: Optional[bool] = None,
36
37
  ) -> None:
37
38
  """Aspirate a given volume of liquid from the specified location.
38
39
  Args:
@@ -55,6 +56,7 @@ class AbstractInstrument(ABC, Generic[WellCoreType]):
55
56
  flow_rate: float,
56
57
  in_place: bool,
57
58
  push_out: Optional[float],
59
+ is_meniscus: Optional[bool] = None,
58
60
  ) -> None:
59
61
  """Dispense a given volume of liquid into the specified location.
60
62
  Args: