opentrons 8.6.0__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.

Potentially problematic release.


This version of opentrons might be problematic. Click here for more details.

Files changed (601) hide show
  1. opentrons/__init__.py +150 -0
  2. opentrons/_version.py +34 -0
  3. opentrons/calibration_storage/__init__.py +54 -0
  4. opentrons/calibration_storage/deck_configuration.py +62 -0
  5. opentrons/calibration_storage/encoder_decoder.py +31 -0
  6. opentrons/calibration_storage/file_operators.py +142 -0
  7. opentrons/calibration_storage/helpers.py +103 -0
  8. opentrons/calibration_storage/ot2/__init__.py +34 -0
  9. opentrons/calibration_storage/ot2/deck_attitude.py +85 -0
  10. opentrons/calibration_storage/ot2/mark_bad_calibration.py +27 -0
  11. opentrons/calibration_storage/ot2/models/__init__.py +0 -0
  12. opentrons/calibration_storage/ot2/models/v1.py +149 -0
  13. opentrons/calibration_storage/ot2/pipette_offset.py +129 -0
  14. opentrons/calibration_storage/ot2/tip_length.py +281 -0
  15. opentrons/calibration_storage/ot3/__init__.py +31 -0
  16. opentrons/calibration_storage/ot3/deck_attitude.py +83 -0
  17. opentrons/calibration_storage/ot3/gripper_offset.py +156 -0
  18. opentrons/calibration_storage/ot3/models/__init__.py +0 -0
  19. opentrons/calibration_storage/ot3/models/v1.py +122 -0
  20. opentrons/calibration_storage/ot3/module_offset.py +138 -0
  21. opentrons/calibration_storage/ot3/pipette_offset.py +95 -0
  22. opentrons/calibration_storage/types.py +45 -0
  23. opentrons/cli/__init__.py +21 -0
  24. opentrons/cli/__main__.py +5 -0
  25. opentrons/cli/analyze.py +557 -0
  26. opentrons/config/__init__.py +631 -0
  27. opentrons/config/advanced_settings.py +871 -0
  28. opentrons/config/defaults_ot2.py +214 -0
  29. opentrons/config/defaults_ot3.py +499 -0
  30. opentrons/config/feature_flags.py +86 -0
  31. opentrons/config/gripper_config.py +55 -0
  32. opentrons/config/reset.py +203 -0
  33. opentrons/config/robot_configs.py +187 -0
  34. opentrons/config/types.py +183 -0
  35. opentrons/drivers/__init__.py +0 -0
  36. opentrons/drivers/absorbance_reader/__init__.py +11 -0
  37. opentrons/drivers/absorbance_reader/abstract.py +72 -0
  38. opentrons/drivers/absorbance_reader/async_byonoy.py +352 -0
  39. opentrons/drivers/absorbance_reader/driver.py +81 -0
  40. opentrons/drivers/absorbance_reader/hid_protocol.py +161 -0
  41. opentrons/drivers/absorbance_reader/simulator.py +84 -0
  42. opentrons/drivers/asyncio/__init__.py +0 -0
  43. opentrons/drivers/asyncio/communication/__init__.py +22 -0
  44. opentrons/drivers/asyncio/communication/async_serial.py +187 -0
  45. opentrons/drivers/asyncio/communication/errors.py +88 -0
  46. opentrons/drivers/asyncio/communication/serial_connection.py +557 -0
  47. opentrons/drivers/command_builder.py +102 -0
  48. opentrons/drivers/flex_stacker/__init__.py +13 -0
  49. opentrons/drivers/flex_stacker/abstract.py +214 -0
  50. opentrons/drivers/flex_stacker/driver.py +768 -0
  51. opentrons/drivers/flex_stacker/errors.py +68 -0
  52. opentrons/drivers/flex_stacker/simulator.py +309 -0
  53. opentrons/drivers/flex_stacker/types.py +367 -0
  54. opentrons/drivers/flex_stacker/utils.py +19 -0
  55. opentrons/drivers/heater_shaker/__init__.py +5 -0
  56. opentrons/drivers/heater_shaker/abstract.py +76 -0
  57. opentrons/drivers/heater_shaker/driver.py +204 -0
  58. opentrons/drivers/heater_shaker/simulator.py +94 -0
  59. opentrons/drivers/mag_deck/__init__.py +6 -0
  60. opentrons/drivers/mag_deck/abstract.py +44 -0
  61. opentrons/drivers/mag_deck/driver.py +208 -0
  62. opentrons/drivers/mag_deck/simulator.py +63 -0
  63. opentrons/drivers/rpi_drivers/__init__.py +33 -0
  64. opentrons/drivers/rpi_drivers/dev_types.py +94 -0
  65. opentrons/drivers/rpi_drivers/gpio.py +282 -0
  66. opentrons/drivers/rpi_drivers/gpio_simulator.py +127 -0
  67. opentrons/drivers/rpi_drivers/interfaces.py +15 -0
  68. opentrons/drivers/rpi_drivers/types.py +364 -0
  69. opentrons/drivers/rpi_drivers/usb.py +102 -0
  70. opentrons/drivers/rpi_drivers/usb_simulator.py +22 -0
  71. opentrons/drivers/serial_communication.py +151 -0
  72. opentrons/drivers/smoothie_drivers/__init__.py +4 -0
  73. opentrons/drivers/smoothie_drivers/connection.py +51 -0
  74. opentrons/drivers/smoothie_drivers/constants.py +121 -0
  75. opentrons/drivers/smoothie_drivers/driver_3_0.py +1933 -0
  76. opentrons/drivers/smoothie_drivers/errors.py +49 -0
  77. opentrons/drivers/smoothie_drivers/parse_utils.py +143 -0
  78. opentrons/drivers/smoothie_drivers/simulator.py +99 -0
  79. opentrons/drivers/smoothie_drivers/types.py +16 -0
  80. opentrons/drivers/temp_deck/__init__.py +10 -0
  81. opentrons/drivers/temp_deck/abstract.py +54 -0
  82. opentrons/drivers/temp_deck/driver.py +197 -0
  83. opentrons/drivers/temp_deck/simulator.py +57 -0
  84. opentrons/drivers/thermocycler/__init__.py +12 -0
  85. opentrons/drivers/thermocycler/abstract.py +99 -0
  86. opentrons/drivers/thermocycler/driver.py +395 -0
  87. opentrons/drivers/thermocycler/simulator.py +126 -0
  88. opentrons/drivers/types.py +107 -0
  89. opentrons/drivers/utils.py +222 -0
  90. opentrons/execute.py +742 -0
  91. opentrons/hardware_control/__init__.py +65 -0
  92. opentrons/hardware_control/__main__.py +77 -0
  93. opentrons/hardware_control/adapters.py +98 -0
  94. opentrons/hardware_control/api.py +1347 -0
  95. opentrons/hardware_control/backends/__init__.py +7 -0
  96. opentrons/hardware_control/backends/controller.py +400 -0
  97. opentrons/hardware_control/backends/errors.py +9 -0
  98. opentrons/hardware_control/backends/estop_state.py +164 -0
  99. opentrons/hardware_control/backends/flex_protocol.py +497 -0
  100. opentrons/hardware_control/backends/ot3controller.py +1930 -0
  101. opentrons/hardware_control/backends/ot3simulator.py +900 -0
  102. opentrons/hardware_control/backends/ot3utils.py +664 -0
  103. opentrons/hardware_control/backends/simulator.py +442 -0
  104. opentrons/hardware_control/backends/status_bar_state.py +240 -0
  105. opentrons/hardware_control/backends/subsystem_manager.py +431 -0
  106. opentrons/hardware_control/backends/tip_presence_manager.py +173 -0
  107. opentrons/hardware_control/backends/types.py +14 -0
  108. opentrons/hardware_control/constants.py +6 -0
  109. opentrons/hardware_control/dev_types.py +125 -0
  110. opentrons/hardware_control/emulation/__init__.py +0 -0
  111. opentrons/hardware_control/emulation/abstract_emulator.py +21 -0
  112. opentrons/hardware_control/emulation/app.py +56 -0
  113. opentrons/hardware_control/emulation/connection_handler.py +38 -0
  114. opentrons/hardware_control/emulation/heater_shaker.py +150 -0
  115. opentrons/hardware_control/emulation/magdeck.py +60 -0
  116. opentrons/hardware_control/emulation/module_server/__init__.py +8 -0
  117. opentrons/hardware_control/emulation/module_server/client.py +78 -0
  118. opentrons/hardware_control/emulation/module_server/helpers.py +130 -0
  119. opentrons/hardware_control/emulation/module_server/models.py +31 -0
  120. opentrons/hardware_control/emulation/module_server/server.py +110 -0
  121. opentrons/hardware_control/emulation/parser.py +74 -0
  122. opentrons/hardware_control/emulation/proxy.py +241 -0
  123. opentrons/hardware_control/emulation/run_emulator.py +68 -0
  124. opentrons/hardware_control/emulation/scripts/__init__.py +0 -0
  125. opentrons/hardware_control/emulation/scripts/run_app.py +54 -0
  126. opentrons/hardware_control/emulation/scripts/run_module_emulator.py +72 -0
  127. opentrons/hardware_control/emulation/scripts/run_smoothie.py +37 -0
  128. opentrons/hardware_control/emulation/settings.py +119 -0
  129. opentrons/hardware_control/emulation/simulations.py +133 -0
  130. opentrons/hardware_control/emulation/smoothie.py +192 -0
  131. opentrons/hardware_control/emulation/tempdeck.py +69 -0
  132. opentrons/hardware_control/emulation/thermocycler.py +128 -0
  133. opentrons/hardware_control/emulation/types.py +10 -0
  134. opentrons/hardware_control/emulation/util.py +38 -0
  135. opentrons/hardware_control/errors.py +43 -0
  136. opentrons/hardware_control/execution_manager.py +164 -0
  137. opentrons/hardware_control/instruments/__init__.py +5 -0
  138. opentrons/hardware_control/instruments/instrument_abc.py +39 -0
  139. opentrons/hardware_control/instruments/ot2/__init__.py +0 -0
  140. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +152 -0
  141. opentrons/hardware_control/instruments/ot2/pipette.py +777 -0
  142. opentrons/hardware_control/instruments/ot2/pipette_handler.py +995 -0
  143. opentrons/hardware_control/instruments/ot3/__init__.py +0 -0
  144. opentrons/hardware_control/instruments/ot3/gripper.py +420 -0
  145. opentrons/hardware_control/instruments/ot3/gripper_handler.py +173 -0
  146. opentrons/hardware_control/instruments/ot3/instrument_calibration.py +214 -0
  147. opentrons/hardware_control/instruments/ot3/pipette.py +858 -0
  148. opentrons/hardware_control/instruments/ot3/pipette_handler.py +1030 -0
  149. opentrons/hardware_control/module_control.py +332 -0
  150. opentrons/hardware_control/modules/__init__.py +69 -0
  151. opentrons/hardware_control/modules/absorbance_reader.py +373 -0
  152. opentrons/hardware_control/modules/errors.py +7 -0
  153. opentrons/hardware_control/modules/flex_stacker.py +948 -0
  154. opentrons/hardware_control/modules/heater_shaker.py +426 -0
  155. opentrons/hardware_control/modules/lid_temp_status.py +35 -0
  156. opentrons/hardware_control/modules/magdeck.py +233 -0
  157. opentrons/hardware_control/modules/mod_abc.py +245 -0
  158. opentrons/hardware_control/modules/module_calibration.py +93 -0
  159. opentrons/hardware_control/modules/plate_temp_status.py +61 -0
  160. opentrons/hardware_control/modules/tempdeck.py +299 -0
  161. opentrons/hardware_control/modules/thermocycler.py +731 -0
  162. opentrons/hardware_control/modules/types.py +417 -0
  163. opentrons/hardware_control/modules/update.py +255 -0
  164. opentrons/hardware_control/modules/utils.py +73 -0
  165. opentrons/hardware_control/motion_utilities.py +318 -0
  166. opentrons/hardware_control/nozzle_manager.py +422 -0
  167. opentrons/hardware_control/ot3_calibration.py +1171 -0
  168. opentrons/hardware_control/ot3api.py +3227 -0
  169. opentrons/hardware_control/pause_manager.py +31 -0
  170. opentrons/hardware_control/poller.py +112 -0
  171. opentrons/hardware_control/protocols/__init__.py +106 -0
  172. opentrons/hardware_control/protocols/asyncio_configurable.py +11 -0
  173. opentrons/hardware_control/protocols/calibratable.py +45 -0
  174. opentrons/hardware_control/protocols/chassis_accessory_manager.py +90 -0
  175. opentrons/hardware_control/protocols/configurable.py +48 -0
  176. opentrons/hardware_control/protocols/event_sourcer.py +18 -0
  177. opentrons/hardware_control/protocols/execution_controllable.py +33 -0
  178. opentrons/hardware_control/protocols/flex_calibratable.py +96 -0
  179. opentrons/hardware_control/protocols/flex_instrument_configurer.py +52 -0
  180. opentrons/hardware_control/protocols/gripper_controller.py +55 -0
  181. opentrons/hardware_control/protocols/hardware_manager.py +51 -0
  182. opentrons/hardware_control/protocols/identifiable.py +16 -0
  183. opentrons/hardware_control/protocols/instrument_configurer.py +206 -0
  184. opentrons/hardware_control/protocols/liquid_handler.py +266 -0
  185. opentrons/hardware_control/protocols/module_provider.py +16 -0
  186. opentrons/hardware_control/protocols/motion_controller.py +243 -0
  187. opentrons/hardware_control/protocols/position_estimator.py +45 -0
  188. opentrons/hardware_control/protocols/simulatable.py +10 -0
  189. opentrons/hardware_control/protocols/stoppable.py +9 -0
  190. opentrons/hardware_control/protocols/types.py +27 -0
  191. opentrons/hardware_control/robot_calibration.py +224 -0
  192. opentrons/hardware_control/scripts/README.md +28 -0
  193. opentrons/hardware_control/scripts/__init__.py +1 -0
  194. opentrons/hardware_control/scripts/gripper_control.py +208 -0
  195. opentrons/hardware_control/scripts/ot3gripper +7 -0
  196. opentrons/hardware_control/scripts/ot3repl +7 -0
  197. opentrons/hardware_control/scripts/repl.py +187 -0
  198. opentrons/hardware_control/scripts/tc_control.py +97 -0
  199. opentrons/hardware_control/scripts/update_module_fw.py +274 -0
  200. opentrons/hardware_control/simulator_setup.py +260 -0
  201. opentrons/hardware_control/thread_manager.py +431 -0
  202. opentrons/hardware_control/threaded_async_lock.py +97 -0
  203. opentrons/hardware_control/types.py +792 -0
  204. opentrons/hardware_control/util.py +234 -0
  205. opentrons/legacy_broker.py +53 -0
  206. opentrons/legacy_commands/__init__.py +1 -0
  207. opentrons/legacy_commands/commands.py +483 -0
  208. opentrons/legacy_commands/helpers.py +153 -0
  209. opentrons/legacy_commands/module_commands.py +276 -0
  210. opentrons/legacy_commands/protocol_commands.py +54 -0
  211. opentrons/legacy_commands/publisher.py +155 -0
  212. opentrons/legacy_commands/robot_commands.py +51 -0
  213. opentrons/legacy_commands/types.py +1186 -0
  214. opentrons/motion_planning/__init__.py +32 -0
  215. opentrons/motion_planning/adjacent_slots_getters.py +168 -0
  216. opentrons/motion_planning/deck_conflict.py +501 -0
  217. opentrons/motion_planning/errors.py +35 -0
  218. opentrons/motion_planning/types.py +42 -0
  219. opentrons/motion_planning/waypoints.py +218 -0
  220. opentrons/ordered_set.py +138 -0
  221. opentrons/protocol_api/__init__.py +105 -0
  222. opentrons/protocol_api/_liquid.py +157 -0
  223. opentrons/protocol_api/_liquid_properties.py +814 -0
  224. opentrons/protocol_api/_nozzle_layout.py +31 -0
  225. opentrons/protocol_api/_parameter_context.py +300 -0
  226. opentrons/protocol_api/_parameters.py +31 -0
  227. opentrons/protocol_api/_transfer_liquid_validation.py +108 -0
  228. opentrons/protocol_api/_types.py +43 -0
  229. opentrons/protocol_api/config.py +23 -0
  230. opentrons/protocol_api/core/__init__.py +23 -0
  231. opentrons/protocol_api/core/common.py +33 -0
  232. opentrons/protocol_api/core/core_map.py +74 -0
  233. opentrons/protocol_api/core/engine/__init__.py +22 -0
  234. opentrons/protocol_api/core/engine/_default_labware_versions.py +179 -0
  235. opentrons/protocol_api/core/engine/deck_conflict.py +400 -0
  236. opentrons/protocol_api/core/engine/exceptions.py +19 -0
  237. opentrons/protocol_api/core/engine/instrument.py +2391 -0
  238. opentrons/protocol_api/core/engine/labware.py +238 -0
  239. opentrons/protocol_api/core/engine/load_labware_params.py +73 -0
  240. opentrons/protocol_api/core/engine/module_core.py +1027 -0
  241. opentrons/protocol_api/core/engine/overlap_versions.py +20 -0
  242. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +358 -0
  243. opentrons/protocol_api/core/engine/point_calculations.py +64 -0
  244. opentrons/protocol_api/core/engine/protocol.py +1153 -0
  245. opentrons/protocol_api/core/engine/robot.py +139 -0
  246. opentrons/protocol_api/core/engine/stringify.py +74 -0
  247. opentrons/protocol_api/core/engine/transfer_components_executor.py +1006 -0
  248. opentrons/protocol_api/core/engine/well.py +241 -0
  249. opentrons/protocol_api/core/instrument.py +459 -0
  250. opentrons/protocol_api/core/labware.py +151 -0
  251. opentrons/protocol_api/core/legacy/__init__.py +11 -0
  252. opentrons/protocol_api/core/legacy/_labware_geometry.py +37 -0
  253. opentrons/protocol_api/core/legacy/deck.py +369 -0
  254. opentrons/protocol_api/core/legacy/labware_offset_provider.py +108 -0
  255. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +709 -0
  256. opentrons/protocol_api/core/legacy/legacy_labware_core.py +235 -0
  257. opentrons/protocol_api/core/legacy/legacy_module_core.py +592 -0
  258. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +612 -0
  259. opentrons/protocol_api/core/legacy/legacy_well_core.py +162 -0
  260. opentrons/protocol_api/core/legacy/load_info.py +67 -0
  261. opentrons/protocol_api/core/legacy/module_geometry.py +547 -0
  262. opentrons/protocol_api/core/legacy/well_geometry.py +148 -0
  263. opentrons/protocol_api/core/legacy_simulator/__init__.py +16 -0
  264. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +624 -0
  265. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +85 -0
  266. opentrons/protocol_api/core/module.py +484 -0
  267. opentrons/protocol_api/core/protocol.py +311 -0
  268. opentrons/protocol_api/core/robot.py +51 -0
  269. opentrons/protocol_api/core/well.py +116 -0
  270. opentrons/protocol_api/core/well_grid.py +45 -0
  271. opentrons/protocol_api/create_protocol_context.py +177 -0
  272. opentrons/protocol_api/deck.py +223 -0
  273. opentrons/protocol_api/disposal_locations.py +244 -0
  274. opentrons/protocol_api/instrument_context.py +3272 -0
  275. opentrons/protocol_api/labware.py +1579 -0
  276. opentrons/protocol_api/module_contexts.py +1447 -0
  277. opentrons/protocol_api/module_validation_and_errors.py +61 -0
  278. opentrons/protocol_api/protocol_context.py +1688 -0
  279. opentrons/protocol_api/robot_context.py +303 -0
  280. opentrons/protocol_api/validation.py +761 -0
  281. opentrons/protocol_engine/__init__.py +155 -0
  282. opentrons/protocol_engine/actions/__init__.py +65 -0
  283. opentrons/protocol_engine/actions/action_dispatcher.py +30 -0
  284. opentrons/protocol_engine/actions/action_handler.py +13 -0
  285. opentrons/protocol_engine/actions/actions.py +302 -0
  286. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  287. opentrons/protocol_engine/clients/__init__.py +5 -0
  288. opentrons/protocol_engine/clients/sync_client.py +174 -0
  289. opentrons/protocol_engine/clients/transports.py +197 -0
  290. opentrons/protocol_engine/commands/__init__.py +757 -0
  291. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +61 -0
  292. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +154 -0
  293. opentrons/protocol_engine/commands/absorbance_reader/common.py +6 -0
  294. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +151 -0
  295. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +154 -0
  296. opentrons/protocol_engine/commands/absorbance_reader/read.py +226 -0
  297. opentrons/protocol_engine/commands/air_gap_in_place.py +162 -0
  298. opentrons/protocol_engine/commands/aspirate.py +244 -0
  299. opentrons/protocol_engine/commands/aspirate_in_place.py +184 -0
  300. opentrons/protocol_engine/commands/aspirate_while_tracking.py +211 -0
  301. opentrons/protocol_engine/commands/blow_out.py +146 -0
  302. opentrons/protocol_engine/commands/blow_out_in_place.py +119 -0
  303. opentrons/protocol_engine/commands/calibration/__init__.py +60 -0
  304. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +166 -0
  305. opentrons/protocol_engine/commands/calibration/calibrate_module.py +117 -0
  306. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +96 -0
  307. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +156 -0
  308. opentrons/protocol_engine/commands/command.py +308 -0
  309. opentrons/protocol_engine/commands/command_unions.py +974 -0
  310. opentrons/protocol_engine/commands/comment.py +57 -0
  311. opentrons/protocol_engine/commands/configure_for_volume.py +108 -0
  312. opentrons/protocol_engine/commands/configure_nozzle_layout.py +115 -0
  313. opentrons/protocol_engine/commands/custom.py +67 -0
  314. opentrons/protocol_engine/commands/dispense.py +194 -0
  315. opentrons/protocol_engine/commands/dispense_in_place.py +179 -0
  316. opentrons/protocol_engine/commands/dispense_while_tracking.py +204 -0
  317. opentrons/protocol_engine/commands/drop_tip.py +232 -0
  318. opentrons/protocol_engine/commands/drop_tip_in_place.py +205 -0
  319. opentrons/protocol_engine/commands/flex_stacker/__init__.py +64 -0
  320. opentrons/protocol_engine/commands/flex_stacker/common.py +900 -0
  321. opentrons/protocol_engine/commands/flex_stacker/empty.py +293 -0
  322. opentrons/protocol_engine/commands/flex_stacker/fill.py +281 -0
  323. opentrons/protocol_engine/commands/flex_stacker/retrieve.py +339 -0
  324. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +328 -0
  325. opentrons/protocol_engine/commands/flex_stacker/store.py +339 -0
  326. opentrons/protocol_engine/commands/generate_command_schema.py +61 -0
  327. opentrons/protocol_engine/commands/get_next_tip.py +134 -0
  328. opentrons/protocol_engine/commands/get_tip_presence.py +87 -0
  329. opentrons/protocol_engine/commands/hash_command_params.py +38 -0
  330. opentrons/protocol_engine/commands/heater_shaker/__init__.py +102 -0
  331. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +83 -0
  332. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +82 -0
  333. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +84 -0
  334. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +110 -0
  335. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +125 -0
  336. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +90 -0
  337. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +102 -0
  338. opentrons/protocol_engine/commands/home.py +100 -0
  339. opentrons/protocol_engine/commands/identify_module.py +86 -0
  340. opentrons/protocol_engine/commands/labware_handling_common.py +29 -0
  341. opentrons/protocol_engine/commands/liquid_probe.py +464 -0
  342. opentrons/protocol_engine/commands/load_labware.py +210 -0
  343. opentrons/protocol_engine/commands/load_lid.py +154 -0
  344. opentrons/protocol_engine/commands/load_lid_stack.py +272 -0
  345. opentrons/protocol_engine/commands/load_liquid.py +95 -0
  346. opentrons/protocol_engine/commands/load_liquid_class.py +144 -0
  347. opentrons/protocol_engine/commands/load_module.py +223 -0
  348. opentrons/protocol_engine/commands/load_pipette.py +167 -0
  349. opentrons/protocol_engine/commands/magnetic_module/__init__.py +32 -0
  350. opentrons/protocol_engine/commands/magnetic_module/disengage.py +97 -0
  351. opentrons/protocol_engine/commands/magnetic_module/engage.py +119 -0
  352. opentrons/protocol_engine/commands/move_labware.py +546 -0
  353. opentrons/protocol_engine/commands/move_relative.py +102 -0
  354. opentrons/protocol_engine/commands/move_to_addressable_area.py +176 -0
  355. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +198 -0
  356. opentrons/protocol_engine/commands/move_to_coordinates.py +107 -0
  357. opentrons/protocol_engine/commands/move_to_well.py +119 -0
  358. opentrons/protocol_engine/commands/movement_common.py +338 -0
  359. opentrons/protocol_engine/commands/pick_up_tip.py +241 -0
  360. opentrons/protocol_engine/commands/pipetting_common.py +443 -0
  361. opentrons/protocol_engine/commands/prepare_to_aspirate.py +121 -0
  362. opentrons/protocol_engine/commands/pressure_dispense.py +155 -0
  363. opentrons/protocol_engine/commands/reload_labware.py +90 -0
  364. opentrons/protocol_engine/commands/retract_axis.py +75 -0
  365. opentrons/protocol_engine/commands/robot/__init__.py +70 -0
  366. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +96 -0
  367. opentrons/protocol_engine/commands/robot/common.py +18 -0
  368. opentrons/protocol_engine/commands/robot/move_axes_relative.py +101 -0
  369. opentrons/protocol_engine/commands/robot/move_axes_to.py +100 -0
  370. opentrons/protocol_engine/commands/robot/move_to.py +94 -0
  371. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +86 -0
  372. opentrons/protocol_engine/commands/save_position.py +109 -0
  373. opentrons/protocol_engine/commands/seal_pipette_to_tip.py +353 -0
  374. opentrons/protocol_engine/commands/set_rail_lights.py +67 -0
  375. opentrons/protocol_engine/commands/set_status_bar.py +89 -0
  376. opentrons/protocol_engine/commands/temperature_module/__init__.py +46 -0
  377. opentrons/protocol_engine/commands/temperature_module/deactivate.py +86 -0
  378. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +97 -0
  379. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +104 -0
  380. opentrons/protocol_engine/commands/thermocycler/__init__.py +152 -0
  381. opentrons/protocol_engine/commands/thermocycler/close_lid.py +87 -0
  382. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +80 -0
  383. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +80 -0
  384. opentrons/protocol_engine/commands/thermocycler/open_lid.py +87 -0
  385. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +171 -0
  386. opentrons/protocol_engine/commands/thermocycler/run_profile.py +124 -0
  387. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +140 -0
  388. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +100 -0
  389. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +93 -0
  390. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +89 -0
  391. opentrons/protocol_engine/commands/touch_tip.py +189 -0
  392. opentrons/protocol_engine/commands/unsafe/__init__.py +161 -0
  393. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +100 -0
  394. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +121 -0
  395. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +82 -0
  396. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +208 -0
  397. opentrons/protocol_engine/commands/unsafe/unsafe_stacker_close_latch.py +94 -0
  398. opentrons/protocol_engine/commands/unsafe/unsafe_stacker_manual_retrieve.py +295 -0
  399. opentrons/protocol_engine/commands/unsafe/unsafe_stacker_open_latch.py +91 -0
  400. opentrons/protocol_engine/commands/unsafe/unsafe_stacker_prepare_shuttle.py +136 -0
  401. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +77 -0
  402. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +90 -0
  403. opentrons/protocol_engine/commands/unseal_pipette_from_tip.py +153 -0
  404. opentrons/protocol_engine/commands/verify_tip_presence.py +100 -0
  405. opentrons/protocol_engine/commands/wait_for_duration.py +76 -0
  406. opentrons/protocol_engine/commands/wait_for_resume.py +75 -0
  407. opentrons/protocol_engine/create_protocol_engine.py +193 -0
  408. opentrons/protocol_engine/engine_support.py +28 -0
  409. opentrons/protocol_engine/error_recovery_policy.py +81 -0
  410. opentrons/protocol_engine/errors/__init__.py +191 -0
  411. opentrons/protocol_engine/errors/error_occurrence.py +182 -0
  412. opentrons/protocol_engine/errors/exceptions.py +1308 -0
  413. opentrons/protocol_engine/execution/__init__.py +50 -0
  414. opentrons/protocol_engine/execution/command_executor.py +216 -0
  415. opentrons/protocol_engine/execution/create_queue_worker.py +102 -0
  416. opentrons/protocol_engine/execution/door_watcher.py +119 -0
  417. opentrons/protocol_engine/execution/equipment.py +819 -0
  418. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  419. opentrons/protocol_engine/execution/gantry_mover.py +686 -0
  420. opentrons/protocol_engine/execution/hardware_stopper.py +147 -0
  421. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +207 -0
  422. opentrons/protocol_engine/execution/labware_movement.py +297 -0
  423. opentrons/protocol_engine/execution/movement.py +350 -0
  424. opentrons/protocol_engine/execution/pipetting.py +607 -0
  425. opentrons/protocol_engine/execution/queue_worker.py +86 -0
  426. opentrons/protocol_engine/execution/rail_lights.py +25 -0
  427. opentrons/protocol_engine/execution/run_control.py +33 -0
  428. opentrons/protocol_engine/execution/status_bar.py +34 -0
  429. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +188 -0
  430. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +81 -0
  431. opentrons/protocol_engine/execution/tip_handler.py +550 -0
  432. opentrons/protocol_engine/labware_offset_standardization.py +194 -0
  433. opentrons/protocol_engine/notes/__init__.py +17 -0
  434. opentrons/protocol_engine/notes/notes.py +59 -0
  435. opentrons/protocol_engine/plugins.py +104 -0
  436. opentrons/protocol_engine/protocol_engine.py +683 -0
  437. opentrons/protocol_engine/resources/__init__.py +26 -0
  438. opentrons/protocol_engine/resources/deck_configuration_provider.py +232 -0
  439. opentrons/protocol_engine/resources/deck_data_provider.py +94 -0
  440. opentrons/protocol_engine/resources/file_provider.py +161 -0
  441. opentrons/protocol_engine/resources/fixture_validation.py +68 -0
  442. opentrons/protocol_engine/resources/labware_data_provider.py +106 -0
  443. opentrons/protocol_engine/resources/labware_validation.py +73 -0
  444. opentrons/protocol_engine/resources/model_utils.py +32 -0
  445. opentrons/protocol_engine/resources/module_data_provider.py +44 -0
  446. opentrons/protocol_engine/resources/ot3_validation.py +21 -0
  447. opentrons/protocol_engine/resources/pipette_data_provider.py +379 -0
  448. opentrons/protocol_engine/slot_standardization.py +128 -0
  449. opentrons/protocol_engine/state/__init__.py +1 -0
  450. opentrons/protocol_engine/state/_abstract_store.py +27 -0
  451. opentrons/protocol_engine/state/_axis_aligned_bounding_box.py +50 -0
  452. opentrons/protocol_engine/state/_labware_origin_math.py +636 -0
  453. opentrons/protocol_engine/state/_move_types.py +83 -0
  454. opentrons/protocol_engine/state/_well_math.py +193 -0
  455. opentrons/protocol_engine/state/addressable_areas.py +699 -0
  456. opentrons/protocol_engine/state/command_history.py +309 -0
  457. opentrons/protocol_engine/state/commands.py +1164 -0
  458. opentrons/protocol_engine/state/config.py +39 -0
  459. opentrons/protocol_engine/state/files.py +57 -0
  460. opentrons/protocol_engine/state/fluid_stack.py +138 -0
  461. opentrons/protocol_engine/state/geometry.py +2408 -0
  462. opentrons/protocol_engine/state/inner_well_math_utils.py +548 -0
  463. opentrons/protocol_engine/state/labware.py +1432 -0
  464. opentrons/protocol_engine/state/liquid_classes.py +82 -0
  465. opentrons/protocol_engine/state/liquids.py +73 -0
  466. opentrons/protocol_engine/state/module_substates/__init__.py +45 -0
  467. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +35 -0
  468. opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +112 -0
  469. opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +115 -0
  470. opentrons/protocol_engine/state/module_substates/magnetic_block_substate.py +17 -0
  471. opentrons/protocol_engine/state/module_substates/magnetic_module_substate.py +65 -0
  472. opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +67 -0
  473. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +163 -0
  474. opentrons/protocol_engine/state/modules.py +1515 -0
  475. opentrons/protocol_engine/state/motion.py +373 -0
  476. opentrons/protocol_engine/state/pipettes.py +905 -0
  477. opentrons/protocol_engine/state/state.py +421 -0
  478. opentrons/protocol_engine/state/state_summary.py +36 -0
  479. opentrons/protocol_engine/state/tips.py +420 -0
  480. opentrons/protocol_engine/state/update_types.py +904 -0
  481. opentrons/protocol_engine/state/wells.py +290 -0
  482. opentrons/protocol_engine/types/__init__.py +310 -0
  483. opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
  484. opentrons/protocol_engine/types/command_annotations.py +53 -0
  485. opentrons/protocol_engine/types/deck_configuration.py +81 -0
  486. opentrons/protocol_engine/types/execution.py +96 -0
  487. opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
  488. opentrons/protocol_engine/types/instrument.py +47 -0
  489. opentrons/protocol_engine/types/instrument_sensors.py +47 -0
  490. opentrons/protocol_engine/types/labware.py +131 -0
  491. opentrons/protocol_engine/types/labware_movement.py +22 -0
  492. opentrons/protocol_engine/types/labware_offset_location.py +111 -0
  493. opentrons/protocol_engine/types/labware_offset_vector.py +16 -0
  494. opentrons/protocol_engine/types/liquid.py +40 -0
  495. opentrons/protocol_engine/types/liquid_class.py +59 -0
  496. opentrons/protocol_engine/types/liquid_handling.py +13 -0
  497. opentrons/protocol_engine/types/liquid_level_detection.py +191 -0
  498. opentrons/protocol_engine/types/location.py +194 -0
  499. opentrons/protocol_engine/types/module.py +310 -0
  500. opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
  501. opentrons/protocol_engine/types/run_time_parameters.py +133 -0
  502. opentrons/protocol_engine/types/tip.py +18 -0
  503. opentrons/protocol_engine/types/util.py +21 -0
  504. opentrons/protocol_engine/types/well_position.py +124 -0
  505. opentrons/protocol_reader/__init__.py +37 -0
  506. opentrons/protocol_reader/extract_labware_definitions.py +66 -0
  507. opentrons/protocol_reader/file_format_validator.py +152 -0
  508. opentrons/protocol_reader/file_hasher.py +27 -0
  509. opentrons/protocol_reader/file_identifier.py +284 -0
  510. opentrons/protocol_reader/file_reader_writer.py +90 -0
  511. opentrons/protocol_reader/input_file.py +16 -0
  512. opentrons/protocol_reader/protocol_files_invalid_error.py +6 -0
  513. opentrons/protocol_reader/protocol_reader.py +188 -0
  514. opentrons/protocol_reader/protocol_source.py +124 -0
  515. opentrons/protocol_reader/role_analyzer.py +86 -0
  516. opentrons/protocol_runner/__init__.py +26 -0
  517. opentrons/protocol_runner/create_simulating_orchestrator.py +118 -0
  518. opentrons/protocol_runner/json_file_reader.py +55 -0
  519. opentrons/protocol_runner/json_translator.py +314 -0
  520. opentrons/protocol_runner/legacy_command_mapper.py +852 -0
  521. opentrons/protocol_runner/legacy_context_plugin.py +116 -0
  522. opentrons/protocol_runner/protocol_runner.py +530 -0
  523. opentrons/protocol_runner/python_protocol_wrappers.py +179 -0
  524. opentrons/protocol_runner/run_orchestrator.py +496 -0
  525. opentrons/protocol_runner/task_queue.py +95 -0
  526. opentrons/protocols/__init__.py +6 -0
  527. opentrons/protocols/advanced_control/__init__.py +0 -0
  528. opentrons/protocols/advanced_control/common.py +38 -0
  529. opentrons/protocols/advanced_control/mix.py +60 -0
  530. opentrons/protocols/advanced_control/transfers/__init__.py +0 -0
  531. opentrons/protocols/advanced_control/transfers/common.py +180 -0
  532. opentrons/protocols/advanced_control/transfers/transfer.py +972 -0
  533. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +231 -0
  534. opentrons/protocols/api_support/__init__.py +0 -0
  535. opentrons/protocols/api_support/constants.py +8 -0
  536. opentrons/protocols/api_support/deck_type.py +110 -0
  537. opentrons/protocols/api_support/definitions.py +18 -0
  538. opentrons/protocols/api_support/instrument.py +151 -0
  539. opentrons/protocols/api_support/labware_like.py +233 -0
  540. opentrons/protocols/api_support/tip_tracker.py +175 -0
  541. opentrons/protocols/api_support/types.py +32 -0
  542. opentrons/protocols/api_support/util.py +403 -0
  543. opentrons/protocols/bundle.py +89 -0
  544. opentrons/protocols/duration/__init__.py +4 -0
  545. opentrons/protocols/duration/errors.py +5 -0
  546. opentrons/protocols/duration/estimator.py +628 -0
  547. opentrons/protocols/execution/__init__.py +0 -0
  548. opentrons/protocols/execution/dev_types.py +181 -0
  549. opentrons/protocols/execution/errors.py +40 -0
  550. opentrons/protocols/execution/execute.py +84 -0
  551. opentrons/protocols/execution/execute_json_v3.py +275 -0
  552. opentrons/protocols/execution/execute_json_v4.py +359 -0
  553. opentrons/protocols/execution/execute_json_v5.py +28 -0
  554. opentrons/protocols/execution/execute_python.py +169 -0
  555. opentrons/protocols/execution/json_dispatchers.py +87 -0
  556. opentrons/protocols/execution/types.py +7 -0
  557. opentrons/protocols/geometry/__init__.py +0 -0
  558. opentrons/protocols/geometry/planning.py +297 -0
  559. opentrons/protocols/labware.py +312 -0
  560. opentrons/protocols/models/__init__.py +0 -0
  561. opentrons/protocols/models/json_protocol.py +679 -0
  562. opentrons/protocols/parameters/__init__.py +0 -0
  563. opentrons/protocols/parameters/csv_parameter_definition.py +77 -0
  564. opentrons/protocols/parameters/csv_parameter_interface.py +96 -0
  565. opentrons/protocols/parameters/exceptions.py +34 -0
  566. opentrons/protocols/parameters/parameter_definition.py +272 -0
  567. opentrons/protocols/parameters/types.py +17 -0
  568. opentrons/protocols/parameters/validation.py +267 -0
  569. opentrons/protocols/parse.py +671 -0
  570. opentrons/protocols/types.py +159 -0
  571. opentrons/py.typed +0 -0
  572. opentrons/resources/scripts/lpc21isp +0 -0
  573. opentrons/resources/smoothie-edge-8414642.hex +23010 -0
  574. opentrons/simulate.py +1065 -0
  575. opentrons/system/__init__.py +6 -0
  576. opentrons/system/camera.py +51 -0
  577. opentrons/system/log_control.py +59 -0
  578. opentrons/system/nmcli.py +856 -0
  579. opentrons/system/resin.py +24 -0
  580. opentrons/system/smoothie_update.py +15 -0
  581. opentrons/system/wifi.py +204 -0
  582. opentrons/tools/__init__.py +0 -0
  583. opentrons/tools/args_handler.py +22 -0
  584. opentrons/tools/write_pipette_memory.py +157 -0
  585. opentrons/types.py +618 -0
  586. opentrons/util/__init__.py +1 -0
  587. opentrons/util/async_helpers.py +166 -0
  588. opentrons/util/broker.py +84 -0
  589. opentrons/util/change_notifier.py +47 -0
  590. opentrons/util/entrypoint_util.py +278 -0
  591. opentrons/util/get_union_elements.py +26 -0
  592. opentrons/util/helpers.py +6 -0
  593. opentrons/util/linal.py +178 -0
  594. opentrons/util/logging_config.py +265 -0
  595. opentrons/util/logging_queue_handler.py +61 -0
  596. opentrons/util/performance_helpers.py +157 -0
  597. opentrons-8.6.0.dist-info/METADATA +37 -0
  598. opentrons-8.6.0.dist-info/RECORD +601 -0
  599. opentrons-8.6.0.dist-info/WHEEL +4 -0
  600. opentrons-8.6.0.dist-info/entry_points.txt +3 -0
  601. opentrons-8.6.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,2408 @@
1
+ """Geometry state getters."""
2
+
3
+ from logging import getLogger
4
+ import enum
5
+ from numpy import array, dot, double as npdouble
6
+ from numpy.typing import NDArray
7
+ from typing import Optional, List, Tuple, Union, cast, TypeVar, Dict, Set
8
+ from dataclasses import dataclass
9
+ from functools import cached_property
10
+
11
+ from opentrons.types import (
12
+ Point,
13
+ DeckSlotName,
14
+ StagingSlotName,
15
+ MountType,
16
+ MeniscusTrackingTarget,
17
+ )
18
+
19
+ from opentrons_shared_data.errors.exceptions import (
20
+ InvalidStoredData,
21
+ PipetteLiquidNotFoundError,
22
+ )
23
+ from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN
24
+ from opentrons_shared_data.labware.labware_definition import (
25
+ LabwareDefinition,
26
+ LabwareDefinition2,
27
+ InnerWellGeometry,
28
+ )
29
+ from opentrons_shared_data.deck.types import CutoutFixture
30
+ from opentrons_shared_data.pipette import PIPETTE_X_SPAN
31
+ from opentrons_shared_data.pipette.types import ChannelCount, LabwareUri
32
+
33
+ from .. import errors
34
+ from ..errors import (
35
+ LabwareNotLoadedError,
36
+ LabwareNotLoadedOnLabwareError,
37
+ LabwareNotLoadedOnModuleError,
38
+ LabwareMovementNotAllowedError,
39
+ InvalidLabwarePositionError,
40
+ LabwareNotOnDeckError,
41
+ )
42
+ from ..errors.exceptions import (
43
+ InvalidLiquidHeightFound,
44
+ )
45
+ from ..resources import (
46
+ fixture_validation,
47
+ labware_validation,
48
+ deck_configuration_provider,
49
+ )
50
+ from ..types import (
51
+ OFF_DECK_LOCATION,
52
+ SYSTEM_LOCATION,
53
+ LoadedLabware,
54
+ LoadedModule,
55
+ WellLocation,
56
+ LiquidHandlingWellLocation,
57
+ DropTipWellLocation,
58
+ PickUpTipWellLocation,
59
+ WellOrigin,
60
+ DropTipWellOrigin,
61
+ WellOffset,
62
+ DeckSlotLocation,
63
+ ModuleLocation,
64
+ OnLabwareLocation,
65
+ LabwareLocation,
66
+ LabwareOffsetVector,
67
+ ModuleOffsetData,
68
+ CurrentWell,
69
+ CurrentPipetteLocation,
70
+ TipGeometry,
71
+ LabwareMovementOffsetData,
72
+ InStackerHopperLocation,
73
+ OnDeckLabwareLocation,
74
+ AddressableAreaLocation,
75
+ AddressableOffsetVector,
76
+ StagingSlotLocation,
77
+ LabwareOffsetLocationSequence,
78
+ OnModuleOffsetLocationSequenceComponent,
79
+ OnAddressableAreaOffsetLocationSequenceComponent,
80
+ OnLabwareOffsetLocationSequenceComponent,
81
+ OnLabwareLocationSequenceComponent,
82
+ ModuleModel,
83
+ PotentialCutoutFixture,
84
+ LabwareLocationSequence,
85
+ OnModuleLocationSequenceComponent,
86
+ OnAddressableAreaLocationSequenceComponent,
87
+ OnCutoutFixtureLocationSequenceComponent,
88
+ NotOnDeckLocationSequenceComponent,
89
+ AreaType,
90
+ labware_location_is_off_deck,
91
+ labware_location_is_system,
92
+ WellLocationType,
93
+ WellLocationFunction,
94
+ LabwareParentDefinition,
95
+ AddressableArea,
96
+ )
97
+ from ..types.liquid_level_detection import SimulatedProbeResult, LiquidTrackingType
98
+ from .config import Config
99
+ from .labware import LabwareView
100
+ from .wells import WellView
101
+ from .modules import ModuleView
102
+ from .pipettes import PipetteView
103
+ from .addressable_areas import AddressableAreaView
104
+ from .inner_well_math_utils import (
105
+ find_height_inner_well_geometry,
106
+ find_volume_inner_well_geometry,
107
+ find_height_user_defined_volumes,
108
+ find_volume_user_defined_volumes,
109
+ )
110
+ from ._well_math import wells_covered_by_pipette_configuration, nozzles_per_well
111
+ from ._labware_origin_math import get_parent_placement_origin_to_lw_origin
112
+
113
+
114
+ _LOG = getLogger(__name__)
115
+ SLOT_WIDTH = 128
116
+ _PIPETTE_HOMED_POSITION_Z = (
117
+ 248.0 # Height of the bottom of the nozzle without the tip attached when homed
118
+ )
119
+
120
+
121
+ class _TipDropSection(enum.Enum):
122
+ """Well sections to drop tips in."""
123
+
124
+ LEFT = "left"
125
+ RIGHT = "right"
126
+
127
+
128
+ class _GripperMoveType(enum.Enum):
129
+ """Types of gripper movement."""
130
+
131
+ PICK_UP_LABWARE = enum.auto()
132
+ DROP_LABWARE = enum.auto()
133
+
134
+
135
+ @dataclass
136
+ class _AbsoluteRobotExtents:
137
+ front_left: Dict[MountType, Point]
138
+ back_right: Dict[MountType, Point]
139
+ deck_extents: Point
140
+ padding_rear: float
141
+ padding_front: float
142
+ padding_left_side: float
143
+ padding_right_side: float
144
+
145
+
146
+ _LabwareLocation = TypeVar("_LabwareLocation", bound=LabwareLocation)
147
+
148
+
149
+ # TODO(mc, 2021-06-03): continue evaluation of which selectors should go here
150
+ # vs which selectors should be in LabwareView
151
+ class GeometryView:
152
+ """Geometry computed state getters."""
153
+
154
+ def __init__(
155
+ self,
156
+ config: Config,
157
+ labware_view: LabwareView,
158
+ well_view: WellView,
159
+ module_view: ModuleView,
160
+ pipette_view: PipetteView,
161
+ addressable_area_view: AddressableAreaView,
162
+ ) -> None:
163
+ """Initialize a GeometryView instance."""
164
+ self._config = config
165
+ self._labware = labware_view
166
+ self._wells = well_view
167
+ self._modules = module_view
168
+ self._pipettes = pipette_view
169
+ self._addressable_areas = addressable_area_view
170
+ self._last_drop_tip_location_spot: Dict[str, _TipDropSection] = {}
171
+
172
+ @cached_property
173
+ def absolute_deck_extents(self) -> _AbsoluteRobotExtents:
174
+ """The absolute deck extents for a given robot deck."""
175
+ left_offset = self._addressable_areas.mount_offsets["left"]
176
+ right_offset = self._addressable_areas.mount_offsets["right"]
177
+
178
+ front_left_abs = {
179
+ MountType.LEFT: Point(left_offset.x, -1 * left_offset.y, left_offset.z),
180
+ MountType.RIGHT: Point(right_offset.x, -1 * right_offset.y, right_offset.z),
181
+ }
182
+ back_right_abs = {
183
+ MountType.LEFT: self._addressable_areas.deck_extents + left_offset,
184
+ MountType.RIGHT: self._addressable_areas.deck_extents + right_offset,
185
+ }
186
+ return _AbsoluteRobotExtents(
187
+ front_left=front_left_abs,
188
+ back_right=back_right_abs,
189
+ deck_extents=self._addressable_areas.deck_extents,
190
+ padding_rear=self._addressable_areas.padding_offsets["rear"],
191
+ padding_front=self._addressable_areas.padding_offsets["front"],
192
+ padding_left_side=self._addressable_areas.padding_offsets["left_side"],
193
+ padding_right_side=self._addressable_areas.padding_offsets["right_side"],
194
+ )
195
+
196
+ def get_labware_highest_z(self, labware_id: str) -> float:
197
+ """Get the highest Z-point of a labware."""
198
+ labware_data = self._labware.get(labware_id)
199
+ return self._get_highest_z_from_labware_data(labware_data)
200
+
201
+ def _is_obstacle_labware(self, labware_id: str) -> bool:
202
+ """Check if the labware is a deck obstacle."""
203
+ for loc in self.get_location_sequence(labware_id):
204
+ if isinstance(loc, InStackerHopperLocation) or isinstance(
205
+ loc, NotOnDeckLocationSequenceComponent
206
+ ):
207
+ return False
208
+ return True
209
+
210
+ def _get_tallest_obstacle_labware(self) -> float:
211
+ """Get the highest Z-point of all labware on the deck."""
212
+ return max(
213
+ (
214
+ self._get_highest_z_from_labware_data(lw_data)
215
+ for lw_data in self._labware.get_all()
216
+ if self._is_obstacle_labware(lw_data.id)
217
+ ),
218
+ default=0.0,
219
+ )
220
+
221
+ def _get_tallest_obstacle_module(self) -> float:
222
+ """Get the highest Z-point of all modules on the deck."""
223
+ return max(
224
+ (
225
+ self._modules.get_module_highest_z(module.id, self._addressable_areas)
226
+ for module in self._modules.get_all()
227
+ ),
228
+ default=0.0,
229
+ )
230
+
231
+ def _get_tallest_obstacle_fixture(self) -> float:
232
+ """Get the highest Z-point of all fixtures on the deck."""
233
+ all_fixtures = self._addressable_areas.get_all_cutout_fixtures()
234
+ if all_fixtures is None:
235
+ # We're using a simulated deck config (see `Config.use_simulated_deck_config`).
236
+ # We only know the addressable areas referenced by the protocol, not the fixtures
237
+ # providing them. And there is more than one possible configuration of fixtures
238
+ # to provide them. So, we can't know what the highest fixture is. Default to 0.
239
+ #
240
+ # Defaulting to 0 may not be the right thing to do here.
241
+ # For example, suppose a protocol references an addressable area that implies a tall
242
+ # fixture must be on the deck, and then it uses long tips that wouldn't be able to
243
+ # clear the top of that fixture. We should perhaps raise an analysis error for that,
244
+ # but defaulting to 0 here means we won't.
245
+ return 0.0
246
+ return max(
247
+ (
248
+ self._addressable_areas.get_fixture_height(cutout_fixture_name)
249
+ for cutout_fixture_name in all_fixtures
250
+ ),
251
+ default=0.0,
252
+ )
253
+
254
+ def get_all_obstacle_highest_z(self) -> float:
255
+ """Get the highest Z-point across all obstacles that the instruments need to fly over."""
256
+ return max(
257
+ self._get_tallest_obstacle_labware(),
258
+ self._get_tallest_obstacle_module(),
259
+ self._get_tallest_obstacle_fixture(),
260
+ )
261
+
262
+ def get_highest_z_in_slot(
263
+ self, slot: Union[DeckSlotLocation, StagingSlotLocation]
264
+ ) -> float:
265
+ """Get the highest Z-point of all items stacked in the given deck slot.
266
+
267
+ This height includes the height of any module that occupies the given slot
268
+ even if it wasn't loaded in that slot (e.g., thermocycler).
269
+ """
270
+ slot_item = self.get_slot_item(slot.slotName)
271
+ if isinstance(slot_item, LoadedModule):
272
+ # get height of module + all labware on it
273
+ module_id = slot_item.id
274
+ try:
275
+ labware_id = self._labware.get_id_by_module(module_id=module_id)
276
+ except LabwareNotLoadedOnModuleError:
277
+ return self._modules.get_module_highest_z(
278
+ module_id=module_id,
279
+ addressable_areas=self._addressable_areas,
280
+ )
281
+ else:
282
+ return self.get_highest_z_of_labware_stack(labware_id)
283
+ elif isinstance(slot_item, LoadedLabware):
284
+ # get stacked heights of all labware in the slot
285
+ return self.get_highest_z_of_labware_stack(slot_item.id)
286
+ elif type(slot_item) is dict:
287
+ # TODO (cb, 2024-02-05): Eventually this logic should become the responsibility of bounding box
288
+ # conflict checking, as fixtures may not always be considered as items from slots.
289
+ return self._addressable_areas.get_fixture_height(slot_item["id"])
290
+ else:
291
+ return 0
292
+
293
+ def get_highest_z_of_labware_stack(self, labware_id: str) -> float:
294
+ """Get the highest Z-point of the topmost labware in the stack of labware on the given labware.
295
+
296
+ If there is no labware on the given labware, returns highest z of the given labware.
297
+ """
298
+ try:
299
+ stacked_labware_id = self._labware.get_id_by_labware(labware_id)
300
+ except LabwareNotLoadedOnLabwareError:
301
+ return self.get_labware_highest_z(labware_id)
302
+ return self.get_highest_z_of_labware_stack(stacked_labware_id)
303
+
304
+ def get_min_travel_z(
305
+ self,
306
+ pipette_id: str,
307
+ labware_id: str,
308
+ location: Optional[CurrentPipetteLocation],
309
+ minimum_z_height: Optional[float],
310
+ ) -> float:
311
+ """Get the minimum allowed travel height of an arc move."""
312
+ if (
313
+ isinstance(location, CurrentWell)
314
+ and pipette_id == location.pipette_id
315
+ and labware_id == location.labware_id
316
+ ):
317
+ min_travel_z = self.get_labware_highest_z(labware_id)
318
+ else:
319
+ min_travel_z = self.get_all_obstacle_highest_z()
320
+ if minimum_z_height:
321
+ min_travel_z = max(min_travel_z, minimum_z_height)
322
+ return min_travel_z
323
+
324
+ def _normalize_module_calibration_offset(
325
+ self,
326
+ module_location: DeckSlotLocation,
327
+ offset_data: Optional[ModuleOffsetData],
328
+ ) -> Point:
329
+ """Normalize the module calibration offset depending on the module location."""
330
+ if not offset_data:
331
+ return Point(x=0, y=0, z=0)
332
+ offset = Point.from_xyz_attrs(offset_data.moduleOffsetVector)
333
+ calibrated_slot = offset_data.location.slotName
334
+ calibrated_slot_column = self.get_slot_column(calibrated_slot)
335
+ current_slot_column = self.get_slot_column(module_location.slotName)
336
+ # make sure that we have valid colums since we cant have modules in the middle of the deck
337
+ assert set([calibrated_slot_column, current_slot_column]).issubset(
338
+ {1, 3}
339
+ ), f"Module calibration offset is an invalid slot {calibrated_slot}"
340
+
341
+ # Check if the module has moved from one side of the deck to the other
342
+ if calibrated_slot_column != current_slot_column:
343
+ # Since the module was rotated, the calibration offset vector needs to be rotated by 180 degrees along the z axis
344
+ saved_offset: NDArray[npdouble] = array([offset.x, offset.y, offset.z])
345
+ rotation_matrix: NDArray[npdouble] = array(
346
+ [[-1, 0, 0], [0, -1, 0], [0, 0, 1]]
347
+ )
348
+ new_offset = dot(saved_offset, rotation_matrix)
349
+ offset = Point(x=new_offset[0], y=new_offset[1], z=new_offset[2])
350
+ return offset
351
+
352
+ def _get_calibrated_module_offset(self, location: LabwareLocation) -> Point:
353
+ """Get a labware location's underlying calibrated module offset, if it is on a module."""
354
+ if isinstance(location, ModuleLocation):
355
+ module_id = location.moduleId
356
+ module_location = self._modules.get_location(module_id)
357
+ offset_data = self._modules.get_module_calibration_offset(module_id)
358
+ return self._normalize_module_calibration_offset(
359
+ module_location, offset_data
360
+ )
361
+ elif isinstance(location, (DeckSlotLocation, AddressableAreaLocation)):
362
+ # TODO we might want to do a check here to make sure addressable area location is a standard deck slot
363
+ # and raise if its not (or maybe we don't actually care since modules will never be loaded elsewhere)
364
+ return Point(x=0, y=0, z=0)
365
+ elif isinstance(location, OnLabwareLocation):
366
+ labware_data = self._labware.get(location.labwareId)
367
+ return self._get_calibrated_module_offset(labware_data.location)
368
+ elif (
369
+ location == OFF_DECK_LOCATION
370
+ or location == SYSTEM_LOCATION
371
+ or isinstance(location, InStackerHopperLocation)
372
+ ):
373
+ raise errors.LabwareNotOnDeckError(
374
+ "Labware does not have a slot or module associated with it"
375
+ " since it is no longer on the deck."
376
+ )
377
+
378
+ def get_labware_origin_position(self, labware_id: str) -> Point:
379
+ """Get the deck coordinates of a labware's origin.
380
+
381
+ This includes module calibration but excludes the calibration of the given labware.
382
+ """
383
+ location = self._labware.get(labware_id).location
384
+ definition = self._labware.get_definition(labware_id)
385
+
386
+ slot_front_left = self._get_labware_ancestor_position(labware_id)
387
+ stackup_origin_to_lw_origin = self._get_stackup_placement_origin_to_lw_origin(
388
+ location=location, definition=definition, is_topmost_labware=True
389
+ )
390
+ module_cal_offset = self._get_calibrated_module_offset(location)
391
+
392
+ return slot_front_left + stackup_origin_to_lw_origin + module_cal_offset
393
+
394
+ def _get_labware_ancestor_position(self, labware_id: str) -> Point:
395
+ """Get the position of the labware's underlying ancestor."""
396
+ slot_name = self._get_underlying_addressable_area_name(
397
+ self._labware.get(labware_id).location
398
+ )
399
+ parent_pos = self._addressable_areas.get_addressable_area_position(slot_name)
400
+
401
+ return parent_pos
402
+
403
+ def _get_stackup_placement_origin_to_lw_origin(
404
+ self,
405
+ location: LabwareLocation,
406
+ definition: LabwareDefinition,
407
+ is_topmost_labware: bool,
408
+ ) -> Point:
409
+ """Get the offset vector from the lowest entity in a stackup to the labware."""
410
+ if isinstance(
411
+ location, (AddressableAreaLocation, DeckSlotLocation, ModuleLocation)
412
+ ):
413
+ return self._get_parent_placement_origin_to_lw_origin(
414
+ labware_location=location,
415
+ labware_definition=definition,
416
+ is_topmost_labware=is_topmost_labware,
417
+ )
418
+ elif isinstance(location, OnLabwareLocation):
419
+ parent_id = location.labwareId
420
+ parent_location = self._labware.get(parent_id).location
421
+ parent_definition = self._labware.get_definition(parent_id)
422
+
423
+ parent_placement_origin_to_lw_origin = (
424
+ self._get_parent_placement_origin_to_lw_origin(
425
+ labware_location=location,
426
+ labware_definition=definition,
427
+ is_topmost_labware=is_topmost_labware,
428
+ )
429
+ )
430
+
431
+ return (
432
+ parent_placement_origin_to_lw_origin
433
+ + self._get_stackup_placement_origin_to_lw_origin(
434
+ location=parent_location,
435
+ definition=parent_definition,
436
+ is_topmost_labware=False,
437
+ )
438
+ )
439
+ else:
440
+ raise errors.LabwareNotOnDeckError(
441
+ "Cannot access labware since it is not on the deck. "
442
+ "Either it has been loaded off-deck or its been moved off-deck."
443
+ )
444
+
445
+ def _get_parent_placement_origin_to_lw_origin(
446
+ self,
447
+ labware_location: LabwareLocation,
448
+ labware_definition: LabwareDefinition,
449
+ is_topmost_labware: bool,
450
+ ) -> Point:
451
+ parent_deck_item = self._get_parent_definition(labware_location)
452
+
453
+ if isinstance(labware_location, ModuleLocation):
454
+ module_parent_to_child_offset = (
455
+ self._modules.get_nominal_offset_to_child_from_addressable_area(
456
+ module_id=labware_location.moduleId,
457
+ )
458
+ )
459
+ return get_parent_placement_origin_to_lw_origin(
460
+ child_labware=labware_definition,
461
+ parent_deck_item=parent_deck_item, # type: ignore[arg-type]
462
+ module_parent_to_child_offset=module_parent_to_child_offset,
463
+ deck_definition=self._addressable_areas.deck_definition,
464
+ is_topmost_labware=is_topmost_labware,
465
+ labware_location=labware_location,
466
+ )
467
+ elif isinstance(labware_location, OnLabwareLocation):
468
+ return get_parent_placement_origin_to_lw_origin(
469
+ child_labware=labware_definition,
470
+ parent_deck_item=parent_deck_item, # type: ignore[arg-type]
471
+ module_parent_to_child_offset=None,
472
+ deck_definition=self._addressable_areas.deck_definition,
473
+ is_topmost_labware=is_topmost_labware,
474
+ labware_location=labware_location,
475
+ )
476
+ elif isinstance(labware_location, (DeckSlotLocation, AddressableAreaLocation)):
477
+ return get_parent_placement_origin_to_lw_origin(
478
+ child_labware=labware_definition,
479
+ parent_deck_item=parent_deck_item, # type: ignore[arg-type]
480
+ module_parent_to_child_offset=None,
481
+ deck_definition=self._addressable_areas.deck_definition,
482
+ is_topmost_labware=is_topmost_labware,
483
+ labware_location=labware_location,
484
+ )
485
+ else:
486
+ raise ValueError(f"Invalid labware location: {labware_location}")
487
+
488
+ def _get_parent_definition(
489
+ self, location: LabwareLocation
490
+ ) -> LabwareParentDefinition:
491
+ """Get the parent's definition given the labware's location."""
492
+ if isinstance(location, DeckSlotLocation):
493
+ addressable_area_name = location.slotName.id
494
+ return self._addressable_areas.get_slot_definition(addressable_area_name)
495
+
496
+ elif isinstance(location, AddressableAreaLocation):
497
+ addressable_area_name = location.addressableAreaName
498
+ return self._addressable_areas.get_addressable_area(addressable_area_name)
499
+
500
+ elif isinstance(location, ModuleLocation):
501
+ module_id = location.moduleId
502
+ return self._modules.get_definition(module_id)
503
+
504
+ elif isinstance(location, OnLabwareLocation):
505
+ below_labware_id = location.labwareId
506
+ return self._labware.get_definition(below_labware_id)
507
+
508
+ elif location == OFF_DECK_LOCATION or location == SYSTEM_LOCATION:
509
+ raise errors.LabwareNotOnDeckError(
510
+ f"Labware location {location} does not have a slot associated with it"
511
+ f" since it is no longer on the deck."
512
+ )
513
+
514
+ elif isinstance(location, InStackerHopperLocation):
515
+ raise errors.LabwareNotOnDeckError(
516
+ "Labware does not have a slot or module associated with it"
517
+ " since it is no longer on the deck."
518
+ )
519
+
520
+ else:
521
+ raise errors.InvalidLabwarePositionError(
522
+ f"Cannot get ancestor from location {location}"
523
+ )
524
+
525
+ def _get_underlying_addressable_area_name(self, location: LabwareLocation) -> str:
526
+ if isinstance(location, DeckSlotLocation):
527
+ return location.slotName.id
528
+ elif isinstance(location, AddressableAreaLocation):
529
+ return location.addressableAreaName
530
+ elif isinstance(location, ModuleLocation):
531
+ return self._modules.get_provided_addressable_area(location.moduleId)
532
+ elif isinstance(location, OnLabwareLocation):
533
+ return self.get_ancestor_addressable_area_name(location.labwareId)
534
+ else:
535
+ raise errors.InvalidLabwarePositionError(
536
+ f"Cannot get ancestor slot of location {location}"
537
+ )
538
+
539
+ def get_labware_position(self, labware_id: str) -> Point:
540
+ """Get the calibrated origin of the labware."""
541
+ origin_pos = self.get_labware_origin_position(labware_id)
542
+ cal_offset = self._labware.get_labware_offset_vector(labware_id)
543
+ return Point(
544
+ x=origin_pos.x + cal_offset.x,
545
+ y=origin_pos.y + cal_offset.y,
546
+ z=origin_pos.z + cal_offset.z,
547
+ )
548
+
549
+ def _validate_well_position(
550
+ self,
551
+ target_height: LiquidTrackingType, # height in mm inside a well relative to the bottom
552
+ well_max_height: float,
553
+ pipette_id: str,
554
+ ) -> LiquidTrackingType:
555
+ """If well offset would be outside the bounds of a well, silently bring it back to the boundary."""
556
+ if isinstance(target_height, SimulatedProbeResult):
557
+ return target_height
558
+ lld_min_height = self._pipettes.get_current_tip_lld_settings(
559
+ pipette_id=pipette_id
560
+ )
561
+ if target_height < lld_min_height:
562
+ target_height = lld_min_height
563
+ elif target_height > well_max_height:
564
+ target_height = well_max_height
565
+ return target_height
566
+
567
+ def validate_probed_height(
568
+ self,
569
+ labware_id: str,
570
+ well_name: str,
571
+ pipette_id: str,
572
+ probed_height: LiquidTrackingType,
573
+ ) -> None:
574
+ """Raise an error if a probed liquid height is not within well bounds."""
575
+ if isinstance(probed_height, SimulatedProbeResult):
576
+ return
577
+ lld_min_height = self._pipettes.get_current_tip_lld_settings(
578
+ pipette_id=pipette_id
579
+ )
580
+ well_def = self._labware.get_well_definition(labware_id, well_name)
581
+ well_depth = well_def.depth
582
+ if probed_height < lld_min_height:
583
+ raise PipetteLiquidNotFoundError(
584
+ f"Liquid Height of {probed_height} mm is lower minumum allowed lld height {lld_min_height} mm."
585
+ )
586
+ if probed_height > well_depth:
587
+ raise PipetteLiquidNotFoundError(
588
+ f"Liquid Height of {probed_height} mm is greater than maximum well height {well_depth} mm."
589
+ )
590
+
591
+ def get_well_position(
592
+ self,
593
+ labware_id: str,
594
+ well_name: str,
595
+ well_location: Optional[WellLocationType] = None,
596
+ operation_volume: Optional[float] = None,
597
+ pipette_id: Optional[str] = None,
598
+ ) -> Point:
599
+ """Given relative well location in a labware, get absolute position."""
600
+ labware_pos = self.get_labware_position(labware_id)
601
+ well_def = self._labware.get_well_definition(labware_id, well_name)
602
+ well_depth = well_def.depth
603
+
604
+ offset = WellOffset(x=0, y=0, z=well_depth)
605
+ if well_location is not None:
606
+ offset = well_location.offset # location of the bottom of the well
607
+ offset_adjustment = self.get_well_offset_adjustment(
608
+ labware_id=labware_id,
609
+ well_name=well_name,
610
+ well_location=well_location,
611
+ well_depth=well_depth,
612
+ operation_volume=operation_volume,
613
+ pipette_id=pipette_id,
614
+ )
615
+ if not isinstance(offset_adjustment, SimulatedProbeResult):
616
+ offset = offset.model_copy(update={"z": offset.z + offset_adjustment})
617
+ return Point(
618
+ x=labware_pos.x + offset.x + well_def.x,
619
+ y=labware_pos.y + offset.y + well_def.y,
620
+ z=labware_pos.z + offset.z + well_def.z,
621
+ )
622
+
623
+ def _get_relative_liquid_handling_well_location(
624
+ self,
625
+ labware_id: str,
626
+ well_name: str,
627
+ absolute_point: Point,
628
+ delta: Point,
629
+ meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
630
+ ) -> Tuple[WellLocationType, bool]:
631
+ """Given absolute position, get relative location of a well in a labware."""
632
+ dynamic_liquid_tracking = False
633
+ if meniscus_tracking:
634
+ location = LiquidHandlingWellLocation(
635
+ origin=WellOrigin.MENISCUS,
636
+ offset=WellOffset(x=0, y=0, z=absolute_point.z),
637
+ )
638
+ # TODO(cm): handle operationVolume being a float other than 0
639
+ if meniscus_tracking == MeniscusTrackingTarget.END:
640
+ location.volumeOffset = "operationVolume"
641
+ elif meniscus_tracking == MeniscusTrackingTarget.DYNAMIC:
642
+ dynamic_liquid_tracking = True
643
+ else:
644
+ location = LiquidHandlingWellLocation(
645
+ offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)
646
+ )
647
+ return location, dynamic_liquid_tracking
648
+
649
+ def get_relative_well_location(
650
+ self,
651
+ labware_id: str,
652
+ well_name: str,
653
+ absolute_point: Point,
654
+ location_type: WellLocationFunction,
655
+ meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
656
+ ) -> Tuple[WellLocationType, bool]:
657
+ """Given absolute position, get relative location of a well in a labware."""
658
+ well_absolute_point = self.get_well_position(labware_id, well_name)
659
+ delta = absolute_point - well_absolute_point
660
+ match location_type:
661
+ case WellLocationFunction.BASE | WellLocationFunction.DROP_TIP:
662
+ return (
663
+ WellLocation(offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)),
664
+ False,
665
+ )
666
+ case WellLocationFunction.PICK_UP_TIP:
667
+ return (
668
+ PickUpTipWellLocation(
669
+ offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)
670
+ ),
671
+ False,
672
+ )
673
+ case WellLocationFunction.LIQUID_HANDLING:
674
+ return self._get_relative_liquid_handling_well_location(
675
+ labware_id=labware_id,
676
+ well_name=well_name,
677
+ absolute_point=absolute_point,
678
+ delta=delta,
679
+ meniscus_tracking=meniscus_tracking,
680
+ )
681
+
682
+ def get_well_height(
683
+ self,
684
+ labware_id: str,
685
+ well_name: str,
686
+ ) -> float:
687
+ """Get the height of a specified well for a labware."""
688
+ well_def = self._labware.get_well_definition(labware_id, well_name)
689
+ return well_def.depth
690
+
691
+ def _get_highest_z_from_labware_data(self, lw_data: LoadedLabware) -> float:
692
+ labware_pos = self.get_labware_position(lw_data.id)
693
+ z_dim = self._labware.get_dimensions(labware_id=lw_data.id).z
694
+ height_over_labware: float = 0
695
+ if isinstance(lw_data.location, ModuleLocation):
696
+ # Note: when calculating highest z of stacked labware, height-over-labware
697
+ # gets accounted for only if the top labware is directly on the module.
698
+ # So if there's a labware on an adapter on a module, then this
699
+ # over-module-height gets ignored. We currently do not have any modules
700
+ # that use an adapter and has height over labware so this doesn't cause
701
+ # any issues yet. But if we add one in the future then this calculation
702
+ # should be updated.
703
+ module_id = lw_data.location.moduleId
704
+ height_over_labware = self._modules.get_height_over_labware(module_id)
705
+ # todo(mm, 2025-07-31): This math needs updating for schema 2:
706
+ # labware_pos.z is not necessarily the bottom of the labware.
707
+ return labware_pos.z + z_dim + height_over_labware
708
+
709
+ def get_nominal_effective_tip_length(
710
+ self,
711
+ pipette_id: str,
712
+ labware_id: str,
713
+ ) -> float:
714
+ """Given a labware and a pipette's config, get the nominal effective tip length.
715
+
716
+ Effective tip length is the nominal tip length less the distance the
717
+ tip overlaps with the pipette nozzle. This does not take calibrated
718
+ tip lengths into account.
719
+ """
720
+ labware_uri = self._labware.get_definition_uri(labware_id)
721
+ nominal_overlap = self._pipettes.get_nominal_tip_overlap(
722
+ pipette_id=pipette_id, labware_uri=labware_uri
723
+ )
724
+
725
+ return self._labware.get_tip_length(
726
+ labware_id=labware_id, overlap=nominal_overlap
727
+ )
728
+
729
+ def get_nominal_tip_geometry(
730
+ self,
731
+ pipette_id: str,
732
+ labware_id: str,
733
+ well_name: Optional[str],
734
+ ) -> TipGeometry:
735
+ """Given a labware, well, and hardware pipette config, get the tip geometry.
736
+
737
+ Tip geometry includes effective tip length, tip diameter, and tip volume,
738
+ which is all data required by the hardware controller for proper tip handling.
739
+
740
+ This geometry data is based solely on labware and pipette definitions and
741
+ does not take calibrated tip lengths into account.
742
+ """
743
+ effective_length = self.get_nominal_effective_tip_length(
744
+ pipette_id=pipette_id,
745
+ labware_id=labware_id,
746
+ )
747
+ well_def = self._labware.get_well_definition(labware_id, well_name)
748
+
749
+ if well_def.shape != "circular":
750
+ raise errors.LabwareIsNotTipRackError(
751
+ f"Well {well_name} in labware {labware_id} is not circular."
752
+ )
753
+
754
+ return TipGeometry(
755
+ length=effective_length,
756
+ diameter=well_def.diameter,
757
+ # TODO(mc, 2020-11-12): WellDefinition type says totalLiquidVolume
758
+ # is a float, but hardware controller expects an int
759
+ volume=int(well_def.totalLiquidVolume),
760
+ )
761
+
762
+ def get_checked_tip_drop_location(
763
+ self,
764
+ pipette_id: str,
765
+ labware_id: str,
766
+ well_location: DropTipWellLocation,
767
+ partially_configured: bool = False,
768
+ override_default_offset: float | None = None,
769
+ ) -> WellLocation:
770
+ """Get tip drop location given labware and hardware pipette.
771
+
772
+ This makes sure that the well location has an appropriate origin & offset
773
+ if one is not already set previously.
774
+ """
775
+ if (
776
+ self._labware.get_definition(labware_id).parameters.isTiprack
777
+ and partially_configured
778
+ ):
779
+ raise errors.UnexpectedProtocolError(
780
+ "Cannot return tip to a tiprack while the pipette is configured for partial tip."
781
+ )
782
+ if well_location.origin != DropTipWellOrigin.DEFAULT:
783
+ return WellLocation(
784
+ origin=WellOrigin(well_location.origin.value),
785
+ offset=well_location.offset,
786
+ )
787
+ if override_default_offset is not None:
788
+ z_offset = override_default_offset
789
+ elif self._labware.get_definition(labware_id).parameters.isTiprack:
790
+ z_offset = self._labware.get_tip_drop_z_offset(
791
+ labware_id=labware_id,
792
+ length_scale=self._pipettes.get_return_tip_scale(pipette_id),
793
+ additional_offset=well_location.offset.z,
794
+ )
795
+ else:
796
+ # return to top if labware is not tip rack
797
+ z_offset = well_location.offset.z
798
+
799
+ return WellLocation(
800
+ origin=WellOrigin.TOP,
801
+ offset=WellOffset(
802
+ x=well_location.offset.x,
803
+ y=well_location.offset.y,
804
+ z=z_offset,
805
+ ),
806
+ )
807
+
808
+ def convert_pick_up_tip_well_location(
809
+ self, well_location: PickUpTipWellLocation
810
+ ) -> WellLocation:
811
+ """Convert PickUpTipWellLocation to WellLocation."""
812
+ return WellLocation(
813
+ origin=WellOrigin(well_location.origin.value), offset=well_location.offset
814
+ )
815
+
816
+ def get_ancestor_slot_name(
817
+ self, labware_id: str
818
+ ) -> Union[DeckSlotName, StagingSlotName]:
819
+ """Get the slot name of the labware or the module that the labware is on."""
820
+ labware = self._labware.get(labware_id)
821
+ slot_name: Union[DeckSlotName, StagingSlotName]
822
+ if isinstance(labware.location, DeckSlotLocation):
823
+ slot_name = labware.location.slotName
824
+ elif isinstance(labware.location, ModuleLocation):
825
+ module_id = labware.location.moduleId
826
+ slot_name = self._modules.get_location(module_id).slotName
827
+ elif isinstance(labware.location, OnLabwareLocation):
828
+ below_labware_id = labware.location.labwareId
829
+ slot_name = self.get_ancestor_slot_name(below_labware_id)
830
+ elif isinstance(labware.location, AddressableAreaLocation):
831
+ area_name = labware.location.addressableAreaName
832
+ if self._labware.is_absorbance_reader_lid(labware_id):
833
+ raise errors.LocationIsLidDockSlotError(
834
+ "Cannot get ancestor slot name for labware on lid dock slot."
835
+ )
836
+ elif fixture_validation.is_staging_slot(area_name):
837
+ slot_name = StagingSlotName.from_primitive(area_name)
838
+ else:
839
+ slot_name = DeckSlotName.from_primitive(area_name)
840
+ elif labware.location == OFF_DECK_LOCATION:
841
+ raise errors.LabwareNotOnDeckError(
842
+ f"Labware {labware_id} does not have a slot associated with it"
843
+ f" since it is no longer on the deck."
844
+ )
845
+ else:
846
+ _LOG.error(
847
+ f"Unhandled location type in get_ancestor_slot_name: {labware.location}"
848
+ )
849
+ raise errors.InvalidLabwarePositionError(
850
+ f"Cannot get ancestor slot of {self._labware.get_display_name(labware_id)} with location {labware.location}"
851
+ )
852
+
853
+ return slot_name
854
+
855
+ def get_ancestor_addressable_area_name(self, labware_id: str) -> str:
856
+ """Get the name of the addressable area the labware is eventually on."""
857
+ labware = self._labware.get(labware_id)
858
+ original_display_name = self._labware.get_display_name(labware_id)
859
+ seen: Set[str] = set((labware_id,))
860
+ while isinstance(labware.location, OnLabwareLocation):
861
+ labware = self._labware.get(labware.location.labwareId)
862
+ if labware.id in seen:
863
+ raise InvalidLabwarePositionError(
864
+ f"Cycle detected in labware positioning for {original_display_name}"
865
+ )
866
+ seen.add(labware.id)
867
+ if isinstance(labware.location, DeckSlotLocation):
868
+ return labware.location.slotName.id
869
+ elif isinstance(labware.location, AddressableAreaLocation):
870
+ return labware.location.addressableAreaName
871
+ elif isinstance(labware.location, ModuleLocation):
872
+ return self._modules.get_provided_addressable_area(
873
+ labware.location.moduleId
874
+ )
875
+ else:
876
+ raise LabwareNotOnDeckError(
877
+ f"Labware {original_display_name} is not loaded on deck",
878
+ details={"eventual-location": repr(labware.location)},
879
+ )
880
+
881
+ def ensure_location_not_occupied(
882
+ self,
883
+ location: _LabwareLocation,
884
+ desired_addressable_area: Optional[str] = None,
885
+ ) -> _LabwareLocation:
886
+ """Ensure that the location does not already have either Labware or a Module in it."""
887
+ # Collect set of existing fixtures, if any
888
+ existing_fixtures = self._get_potential_fixtures_for_location_occupation(
889
+ location
890
+ )
891
+ potential_fixtures = (
892
+ self._get_potential_fixtures_for_location_occupation(
893
+ AddressableAreaLocation(addressableAreaName=desired_addressable_area)
894
+ )
895
+ if desired_addressable_area is not None
896
+ else None
897
+ )
898
+
899
+ # Handle the checking conflict on an incoming fixture
900
+ if potential_fixtures is not None and isinstance(location, DeckSlotLocation):
901
+ if (
902
+ existing_fixtures is not None
903
+ and not any(
904
+ location.slotName.id in fixture.provided_addressable_areas
905
+ for fixture in potential_fixtures[1].intersection(
906
+ existing_fixtures[1]
907
+ )
908
+ )
909
+ ) or (
910
+ self._labware.get_by_slot(location.slotName) is not None
911
+ and not any(
912
+ location.slotName.id in fixture.provided_addressable_areas
913
+ for fixture in potential_fixtures[1]
914
+ )
915
+ ):
916
+ self._labware.raise_if_labware_in_location(location)
917
+
918
+ else:
919
+ self._modules.raise_if_module_in_location(location)
920
+
921
+ # Otherwise handle standard conflict checking
922
+ else:
923
+ if isinstance(
924
+ location,
925
+ (
926
+ DeckSlotLocation,
927
+ ModuleLocation,
928
+ OnLabwareLocation,
929
+ AddressableAreaLocation,
930
+ ),
931
+ ):
932
+ self._labware.raise_if_labware_in_location(location)
933
+
934
+ area = (
935
+ location.slotName.id
936
+ if isinstance(location, DeckSlotLocation)
937
+ else (
938
+ location.addressableAreaName
939
+ if isinstance(location, AddressableAreaLocation)
940
+ else None
941
+ )
942
+ )
943
+ if area is not None and (
944
+ existing_fixtures is None
945
+ or not any(
946
+ area in fixture.provided_addressable_areas
947
+ for fixture in existing_fixtures[1]
948
+ )
949
+ ):
950
+ if isinstance(location, DeckSlotLocation):
951
+ self._modules.raise_if_module_in_location(location)
952
+ elif isinstance(location, AddressableAreaLocation):
953
+ self._modules.raise_if_module_in_location(
954
+ DeckSlotLocation(
955
+ slotName=self._addressable_areas.get_addressable_area_base_slot(
956
+ location.addressableAreaName
957
+ )
958
+ )
959
+ )
960
+
961
+ return location
962
+
963
+ def _get_potential_fixtures_for_location_occupation(
964
+ self, location: _LabwareLocation
965
+ ) -> Tuple[str, Set[PotentialCutoutFixture]] | None:
966
+ loc: DeckSlotLocation | AddressableAreaLocation | None = None
967
+ if isinstance(location, AddressableAreaLocation):
968
+ # Convert the addressable area into a staging slot if applicable
969
+ slots = StagingSlotName._value2member_map_
970
+ for slot in slots:
971
+ if location.addressableAreaName == slot:
972
+ loc = DeckSlotLocation(
973
+ slotName=DeckSlotName(location.addressableAreaName[0] + "3")
974
+ )
975
+ if loc is None:
976
+ loc = location
977
+ elif isinstance(location, DeckSlotLocation):
978
+ loc = location
979
+
980
+ if isinstance(loc, DeckSlotLocation):
981
+ module = self._modules.get_by_slot(loc.slotName)
982
+ if module is not None and self._config.robot_type != "OT-2 Standard":
983
+ fixtures = deck_configuration_provider.get_potential_cutout_fixtures(
984
+ addressable_area_name=self._modules.ensure_and_convert_module_fixture_location(
985
+ deck_slot=loc.slotName,
986
+ model=module.model,
987
+ ),
988
+ deck_definition=self._addressable_areas.deck_definition,
989
+ )
990
+ else:
991
+ fixtures = None
992
+ elif isinstance(loc, AddressableAreaLocation):
993
+ fixtures = deck_configuration_provider.get_potential_cutout_fixtures(
994
+ addressable_area_name=loc.addressableAreaName,
995
+ deck_definition=self._addressable_areas.deck_definition,
996
+ )
997
+ else:
998
+ fixtures = None
999
+ return fixtures
1000
+
1001
+ def _get_potential_disposal_location_cutout_fixtures(
1002
+ self, slot_name: DeckSlotName
1003
+ ) -> CutoutFixture | None:
1004
+ for area in self._addressable_areas.get_all():
1005
+ if (
1006
+ self._addressable_areas.get_addressable_area(area).area_type
1007
+ == AreaType.WASTE_CHUTE
1008
+ or self._addressable_areas.get_addressable_area(area).area_type
1009
+ == AreaType.MOVABLE_TRASH
1010
+ ) and slot_name == self._addressable_areas.get_addressable_area_base_slot(
1011
+ area
1012
+ ):
1013
+ # Given we only have one Waste Chute fixture and one type of Trash bin fixture it's
1014
+ # fine to return the first result of our potential fixtures here. This will need to
1015
+ # change in the future if there multiple trash fixtures that share the same area type.
1016
+ potential_fixture = (
1017
+ deck_configuration_provider.get_potential_cutout_fixtures(
1018
+ area, self._addressable_areas.deck_definition
1019
+ )[1].pop()
1020
+ )
1021
+ return deck_configuration_provider.get_cutout_fixture(
1022
+ potential_fixture.cutout_fixture_id,
1023
+ self._addressable_areas.deck_definition,
1024
+ )
1025
+ return None
1026
+
1027
+ def get_labware_grip_point(
1028
+ self,
1029
+ labware_definition: LabwareDefinition,
1030
+ location: Union[
1031
+ DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
1032
+ ],
1033
+ ) -> Point:
1034
+ """Get the grip point of the labware as placed on the given location.
1035
+
1036
+ Returns the absolute position of the labware's gripping point as if
1037
+ it were placed on the specified location. Labware offset (LPC offset) not included.
1038
+
1039
+ Grip point is the location where critical point of the gripper should move to
1040
+ in order to pick/drop the given labware in the specified location.
1041
+ It is calculated as the xy center of the slot with z as the point indicated by
1042
+ z-position of labware bottom + grip height from labware bottom.
1043
+ """
1044
+ grip_z_from_lw_origin = self._labware.get_grip_z(labware_definition)
1045
+ aa_name = self._get_underlying_addressable_area_name(location)
1046
+ parent_to_lw_offset = self._get_stackup_placement_origin_to_lw_origin(
1047
+ location=location,
1048
+ definition=labware_definition,
1049
+ is_topmost_labware=True, # We aren't concerned with entities above the gripped labware.
1050
+ )
1051
+ addressable_area = self._addressable_areas.get_addressable_area(aa_name)
1052
+ lw_origin_to_parent = self._get_lw_origin_to_parent(
1053
+ labware_definition=labware_definition, addressable_area=addressable_area
1054
+ )
1055
+ mod_cal_offset = self._get_calibrated_module_offset(location)
1056
+ location_center = self._addressable_areas.get_addressable_area_center(aa_name)
1057
+
1058
+ return (
1059
+ location_center
1060
+ + parent_to_lw_offset
1061
+ + lw_origin_to_parent
1062
+ + mod_cal_offset
1063
+ + Point(0, 0, grip_z_from_lw_origin)
1064
+ )
1065
+
1066
+ def _get_lw_origin_to_parent(
1067
+ self, labware_definition: LabwareDefinition, addressable_area: AddressableArea
1068
+ ) -> Point:
1069
+ if isinstance(labware_definition, LabwareDefinition2):
1070
+ return Point(0, 0, 0)
1071
+ else:
1072
+ bb_y = addressable_area.bounding_box.y
1073
+ bb_z = addressable_area.bounding_box.z
1074
+ return (
1075
+ Point(
1076
+ x=0,
1077
+ y=bb_y,
1078
+ z=bb_z,
1079
+ )
1080
+ * -1
1081
+ )
1082
+
1083
+ def get_extra_waypoints(
1084
+ self,
1085
+ location: Optional[CurrentPipetteLocation],
1086
+ to_slot: Union[DeckSlotName, StagingSlotName],
1087
+ ) -> List[Tuple[float, float]]:
1088
+ """Get extra waypoints for movement if thermocycler needs to be dodged."""
1089
+ if location is not None:
1090
+ if isinstance(location, CurrentWell):
1091
+ from_slot = self.get_ancestor_slot_name(location.labware_id)
1092
+ else:
1093
+ from_slot = self._addressable_areas.get_addressable_area_base_slot(
1094
+ location.addressable_area_name
1095
+ )
1096
+ if self._modules.should_dodge_thermocycler(
1097
+ from_slot=from_slot, to_slot=to_slot
1098
+ ):
1099
+
1100
+ middle_slot_fixture = (
1101
+ self._addressable_areas.get_fixture_by_deck_slot_name(
1102
+ DeckSlotName.SLOT_C2.to_equivalent_for_robot_type(
1103
+ self._config.robot_type
1104
+ )
1105
+ )
1106
+ )
1107
+ if middle_slot_fixture is None:
1108
+ middle_slot = DeckSlotName.SLOT_5.to_equivalent_for_robot_type(
1109
+ self._config.robot_type
1110
+ )
1111
+ middle_slot_center = (
1112
+ self._addressable_areas.get_addressable_area_center(
1113
+ addressable_area_name=middle_slot.id,
1114
+ )
1115
+ )
1116
+ else:
1117
+ # todo(chb, 2025-07-30): For now we're defaulting to the first addressable area for these center slot fixtures, but
1118
+ # if we ever introduce a fixture in the center slot with many addressable areas that aren't "centered" over the deck
1119
+ # slot we will enter up generating a pretty whacky movement path (potentially dangerous).
1120
+ middle_slot_center = self._addressable_areas.get_addressable_area_center(
1121
+ addressable_area_name=middle_slot_fixture[
1122
+ "providesAddressableAreas"
1123
+ ][
1124
+ deck_configuration_provider.get_cutout_id_by_deck_slot_name(
1125
+ DeckSlotName.SLOT_C2.to_equivalent_for_robot_type(
1126
+ self._config.robot_type
1127
+ )
1128
+ )
1129
+ ][
1130
+ 0
1131
+ ],
1132
+ )
1133
+ return [(middle_slot_center.x, middle_slot_center.y)]
1134
+ return []
1135
+
1136
+ def get_slot_item(
1137
+ self, slot_name: Union[DeckSlotName, StagingSlotName]
1138
+ ) -> Union[LoadedLabware, LoadedModule, CutoutFixture, None]:
1139
+ """Get the top-most item present in a deck slot, if any.
1140
+
1141
+ This includes any module that occupies the given slot even if it wasn't loaded
1142
+ in that slot (e.g., thermocycler).
1143
+ """
1144
+ maybe_labware = self._labware.get_by_slot(
1145
+ slot_name=slot_name,
1146
+ )
1147
+
1148
+ if isinstance(slot_name, DeckSlotName):
1149
+ maybe_fixture = self._addressable_areas.get_fixture_by_deck_slot_name(
1150
+ slot_name
1151
+ )
1152
+
1153
+ # Ignore generic single slot fixtures
1154
+ if maybe_fixture and maybe_fixture["id"] in {
1155
+ "singleLeftSlot",
1156
+ "singleCenterSlot",
1157
+ "singleRightSlot",
1158
+ }:
1159
+ maybe_fixture = None
1160
+
1161
+ maybe_module = self._modules.get_by_slot(
1162
+ slot_name=slot_name,
1163
+ ) or self._modules.get_overflowed_module_in_slot(slot_name=slot_name)
1164
+
1165
+ # For situations in which the deck config is none
1166
+ if maybe_fixture is None and maybe_labware is None and maybe_module is None:
1167
+ # todo(chb 2025-03-19): This can go away once we solve the problem of no deck config in analysis
1168
+ maybe_fixture = self._get_potential_disposal_location_cutout_fixtures(
1169
+ slot_name
1170
+ )
1171
+ else:
1172
+ # Modules and fixtures can't be loaded on staging slots
1173
+ maybe_fixture = None
1174
+ maybe_module = None
1175
+
1176
+ return maybe_labware or maybe_module or maybe_fixture or None
1177
+
1178
+ @staticmethod
1179
+ def get_slot_column(slot_name: Union[DeckSlotName, StagingSlotName]) -> int:
1180
+ """Get the column number for the specified slot."""
1181
+ if isinstance(slot_name, StagingSlotName):
1182
+ return 4
1183
+ row_col_name = slot_name.to_ot3_equivalent()
1184
+ slot_name_match = WELL_NAME_PATTERN.match(row_col_name.value)
1185
+ assert (
1186
+ slot_name_match is not None
1187
+ ), f"Slot name {row_col_name} did not match required pattern; please check labware location."
1188
+
1189
+ row_name, column_name = slot_name_match.group(1, 2)
1190
+ return int(column_name)
1191
+
1192
+ def get_next_tip_drop_location(
1193
+ self, labware_id: str, well_name: str, pipette_id: str
1194
+ ) -> DropTipWellLocation:
1195
+ """Get the next location within the specified well to drop the tip into.
1196
+
1197
+ In order to prevent tip stacking, we will alternate between two tip drop locations:
1198
+ 1. location in left section: a safe distance from left edge of the well
1199
+ 2. location in right section: a safe distance from right edge of the well
1200
+
1201
+ This safe distance for most cases would be a location where all tips drop
1202
+ reliably inside the labware's well. This can be calculated based off of the
1203
+ span of a pipette, including all its tips, in the x-direction.
1204
+
1205
+ But we also need to account for the not-so-uncommon case of a left pipette
1206
+ trying to drop tips in a labware in the rightmost deck column and vice versa.
1207
+ If this labware extends beyond a regular deck slot, like the Flex's default trash,
1208
+ then even after keeping a margin for x-span of a pipette, we will get
1209
+ a location that's unreachable for the pipette. In such cases, we try to drop tips
1210
+ at the rightmost location that a left pipette is able to reach,
1211
+ and leftmost location that a right pipette is able to reach respectively.
1212
+
1213
+ In these calculations we assume that the critical point of a pipette
1214
+ is considered to be the midpoint of the pipette's tip for single channel,
1215
+ and the midpoint of the entire tip assembly for multi-channel pipettes.
1216
+ We also assume that the pipette_x_span includes any safety margins required.
1217
+ """
1218
+ if not self._labware.is_fixed_trash(labware_id=labware_id):
1219
+ # In order to avoid the complexity of finding tip drop locations for
1220
+ # variety of labware with different well configs, we will allow
1221
+ # location cycling only for fixed trash labware right now.
1222
+ # TODO (spp, 2023-09-12): update this to possibly a labware-width based check,
1223
+ # or a 'trash' quirk check, once movable trash is implemented.
1224
+ return DropTipWellLocation(
1225
+ origin=DropTipWellOrigin.DEFAULT,
1226
+ offset=WellOffset(x=0, y=0, z=0),
1227
+ )
1228
+
1229
+ well_x_dim = self._labware.get_well_size(
1230
+ labware_id=labware_id, well_name=well_name
1231
+ )[0]
1232
+ pipette_channels = self._pipettes.get_config(pipette_id).channels
1233
+ pipette_mount = self._pipettes.get_mount(pipette_id)
1234
+
1235
+ labware_slot_column = self.get_slot_column(
1236
+ slot_name=self.get_ancestor_slot_name(labware_id)
1237
+ )
1238
+
1239
+ if self._last_drop_tip_location_spot.get(labware_id) == _TipDropSection.RIGHT:
1240
+ # Drop tip in LEFT section
1241
+ x_offset = self._get_drop_tip_well_x_offset(
1242
+ tip_drop_section=_TipDropSection.LEFT,
1243
+ well_x_dim=well_x_dim,
1244
+ pipette_channels=pipette_channels,
1245
+ pipette_mount=pipette_mount,
1246
+ labware_slot_column=labware_slot_column,
1247
+ )
1248
+ self._last_drop_tip_location_spot[labware_id] = _TipDropSection.LEFT
1249
+ else:
1250
+ # Drop tip in RIGHT section
1251
+ x_offset = self._get_drop_tip_well_x_offset(
1252
+ tip_drop_section=_TipDropSection.RIGHT,
1253
+ well_x_dim=well_x_dim,
1254
+ pipette_channels=pipette_channels,
1255
+ pipette_mount=pipette_mount,
1256
+ labware_slot_column=labware_slot_column,
1257
+ )
1258
+ self._last_drop_tip_location_spot[labware_id] = _TipDropSection.RIGHT
1259
+
1260
+ return DropTipWellLocation(
1261
+ origin=DropTipWellOrigin.TOP,
1262
+ offset=WellOffset(
1263
+ x=x_offset,
1264
+ y=0,
1265
+ z=0,
1266
+ ),
1267
+ )
1268
+
1269
+ # TODO find way to combine this with above
1270
+ def get_next_tip_drop_location_for_addressable_area(
1271
+ self,
1272
+ addressable_area_name: str,
1273
+ pipette_id: str,
1274
+ ) -> AddressableOffsetVector:
1275
+ """Get the next location within the specified well to drop the tip into.
1276
+
1277
+ See the doc-string for `get_next_tip_drop_location` for more info on execution.
1278
+ """
1279
+ area_x_dim = self._addressable_areas.get_addressable_area(
1280
+ addressable_area_name
1281
+ ).bounding_box.x
1282
+
1283
+ pipette_channels = self._pipettes.get_config(pipette_id).channels
1284
+ pipette_mount = self._pipettes.get_mount(pipette_id)
1285
+
1286
+ labware_slot_column = self.get_slot_column(
1287
+ slot_name=self._addressable_areas.get_addressable_area_base_slot(
1288
+ addressable_area_name
1289
+ )
1290
+ )
1291
+
1292
+ if (
1293
+ self._last_drop_tip_location_spot.get(addressable_area_name)
1294
+ == _TipDropSection.RIGHT
1295
+ ):
1296
+ # Drop tip in LEFT section
1297
+ x_offset = self._get_drop_tip_well_x_offset(
1298
+ tip_drop_section=_TipDropSection.LEFT,
1299
+ well_x_dim=area_x_dim,
1300
+ pipette_channels=pipette_channels,
1301
+ pipette_mount=pipette_mount,
1302
+ labware_slot_column=labware_slot_column,
1303
+ )
1304
+ self._last_drop_tip_location_spot[
1305
+ addressable_area_name
1306
+ ] = _TipDropSection.LEFT
1307
+ else:
1308
+ # Drop tip in RIGHT section
1309
+ x_offset = self._get_drop_tip_well_x_offset(
1310
+ tip_drop_section=_TipDropSection.RIGHT,
1311
+ well_x_dim=area_x_dim,
1312
+ pipette_channels=pipette_channels,
1313
+ pipette_mount=pipette_mount,
1314
+ labware_slot_column=labware_slot_column,
1315
+ )
1316
+ self._last_drop_tip_location_spot[
1317
+ addressable_area_name
1318
+ ] = _TipDropSection.RIGHT
1319
+
1320
+ return AddressableOffsetVector(x=x_offset, y=0, z=0)
1321
+
1322
+ @staticmethod
1323
+ def _get_drop_tip_well_x_offset(
1324
+ tip_drop_section: _TipDropSection,
1325
+ well_x_dim: float,
1326
+ pipette_channels: int,
1327
+ pipette_mount: MountType,
1328
+ labware_slot_column: int,
1329
+ ) -> float:
1330
+ """Get the well x offset for DropTipWellLocation."""
1331
+ drop_location_margin_from_labware_edge = (
1332
+ PIPETTE_X_SPAN[cast(ChannelCount, pipette_channels)] / 2
1333
+ )
1334
+ if tip_drop_section == _TipDropSection.LEFT:
1335
+ if (
1336
+ well_x_dim > SLOT_WIDTH
1337
+ and pipette_channels != 96
1338
+ and pipette_mount == MountType.RIGHT
1339
+ and labware_slot_column == 1
1340
+ ):
1341
+ # Pipette might not reach the default left spot so use a different left spot
1342
+ x_well_offset = (
1343
+ -well_x_dim / 2 + drop_location_margin_from_labware_edge * 2
1344
+ )
1345
+ else:
1346
+ x_well_offset = -well_x_dim / 2 + drop_location_margin_from_labware_edge
1347
+ if x_well_offset > 0:
1348
+ x_well_offset = 0
1349
+ else:
1350
+ if (
1351
+ well_x_dim > SLOT_WIDTH
1352
+ and pipette_channels != 96
1353
+ and pipette_mount == MountType.LEFT
1354
+ and labware_slot_column == 3
1355
+ ):
1356
+ # Pipette might not reach the default right spot so use a different right spot
1357
+ x_well_offset = (
1358
+ -well_x_dim / 2
1359
+ + SLOT_WIDTH
1360
+ - drop_location_margin_from_labware_edge
1361
+ )
1362
+ else:
1363
+ x_well_offset = well_x_dim / 2 - drop_location_margin_from_labware_edge
1364
+ if x_well_offset < 0:
1365
+ x_well_offset = 0
1366
+ return x_well_offset
1367
+
1368
+ def get_final_labware_movement_offset_vectors(
1369
+ self,
1370
+ from_location: OnDeckLabwareLocation,
1371
+ to_location: OnDeckLabwareLocation,
1372
+ additional_pick_up_offset: Point,
1373
+ additional_drop_offset: Point,
1374
+ current_labware: LabwareDefinition,
1375
+ ) -> LabwareMovementOffsetData:
1376
+ """Calculate the final labware offset vector to use in labware movement."""
1377
+ pick_up_offset = (
1378
+ self.get_total_nominal_gripper_offset_for_move_type(
1379
+ location=from_location,
1380
+ move_type=_GripperMoveType.PICK_UP_LABWARE,
1381
+ current_labware=current_labware,
1382
+ )
1383
+ + additional_pick_up_offset
1384
+ )
1385
+ drop_offset = (
1386
+ self.get_total_nominal_gripper_offset_for_move_type(
1387
+ location=to_location,
1388
+ move_type=_GripperMoveType.DROP_LABWARE,
1389
+ current_labware=current_labware,
1390
+ )
1391
+ + additional_drop_offset
1392
+ )
1393
+
1394
+ return LabwareMovementOffsetData(
1395
+ pickUpOffset=LabwareOffsetVector(
1396
+ x=pick_up_offset.x, y=pick_up_offset.y, z=pick_up_offset.z
1397
+ ),
1398
+ dropOffset=LabwareOffsetVector(
1399
+ x=drop_offset.x, y=drop_offset.y, z=drop_offset.z
1400
+ ),
1401
+ )
1402
+
1403
+ @staticmethod
1404
+ def ensure_valid_gripper_location(
1405
+ location: LabwareLocation,
1406
+ ) -> Union[
1407
+ DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation
1408
+ ]:
1409
+ """Ensure valid on-deck location for gripper, otherwise raise error."""
1410
+ if not isinstance(
1411
+ location,
1412
+ (
1413
+ DeckSlotLocation,
1414
+ ModuleLocation,
1415
+ OnLabwareLocation,
1416
+ AddressableAreaLocation,
1417
+ ),
1418
+ ):
1419
+ raise errors.LabwareMovementNotAllowedError(
1420
+ "Off-deck labware movements are not supported using the gripper."
1421
+ )
1422
+ return location
1423
+
1424
+ def get_total_nominal_gripper_offset_for_move_type(
1425
+ self,
1426
+ location: OnDeckLabwareLocation,
1427
+ move_type: _GripperMoveType,
1428
+ current_labware: LabwareDefinition,
1429
+ ) -> Point:
1430
+ """Get the total of the offsets to be used to pick up labware in its current location."""
1431
+ if move_type == _GripperMoveType.PICK_UP_LABWARE:
1432
+ if isinstance(
1433
+ location, (ModuleLocation, DeckSlotLocation, AddressableAreaLocation)
1434
+ ):
1435
+ return Point.from_xyz_attrs(
1436
+ self._nominal_gripper_offsets_for_location(location).pickUpOffset
1437
+ )
1438
+ else:
1439
+ # If it's a labware on a labware (most likely an adapter),
1440
+ # we calculate the offset as sum of offsets for the direct parent labware
1441
+ # and the underlying non-labware parent location.
1442
+ direct_parent_offset = self._nominal_gripper_offsets_for_location(
1443
+ location
1444
+ )
1445
+ ancestor = self._labware.get_parent_location(location.labwareId)
1446
+ extra_offset = Point(x=0, y=0, z=0)
1447
+ if (
1448
+ isinstance(ancestor, ModuleLocation)
1449
+ # todo(mm, 2025-06-20): Avoid this private attribute access.
1450
+ and self._modules._state.requested_model_by_id[ancestor.moduleId]
1451
+ == ModuleModel.THERMOCYCLER_MODULE_V2
1452
+ and labware_validation.validate_definition_is_lid(current_labware)
1453
+ ):
1454
+ if "lidOffsets" in current_labware.gripperOffsets.keys():
1455
+ extra_offset = Point(
1456
+ x=current_labware.gripperOffsets[
1457
+ "lidOffsets"
1458
+ ].pickUpOffset.x,
1459
+ y=current_labware.gripperOffsets[
1460
+ "lidOffsets"
1461
+ ].pickUpOffset.y,
1462
+ z=current_labware.gripperOffsets[
1463
+ "lidOffsets"
1464
+ ].pickUpOffset.z,
1465
+ )
1466
+ else:
1467
+ raise errors.LabwareOffsetDoesNotExistError(
1468
+ f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
1469
+ )
1470
+
1471
+ assert isinstance(
1472
+ ancestor,
1473
+ (
1474
+ DeckSlotLocation,
1475
+ ModuleLocation,
1476
+ OnLabwareLocation,
1477
+ AddressableAreaLocation,
1478
+ ),
1479
+ ), "No gripper offsets for off-deck labware"
1480
+ return (
1481
+ Point.from_xyz_attrs(direct_parent_offset.pickUpOffset)
1482
+ + Point.from_xyz_attrs(
1483
+ self._nominal_gripper_offsets_for_location(
1484
+ location=ancestor
1485
+ ).pickUpOffset
1486
+ )
1487
+ + extra_offset
1488
+ )
1489
+ else:
1490
+ if isinstance(
1491
+ location, (ModuleLocation, DeckSlotLocation, AddressableAreaLocation)
1492
+ ):
1493
+ return Point.from_xyz_attrs(
1494
+ self._nominal_gripper_offsets_for_location(location).dropOffset
1495
+ )
1496
+ else:
1497
+ # If it's a labware on a labware (most likely an adapter),
1498
+ # we calculate the offset as sum of offsets for the direct parent labware
1499
+ # and the underlying non-labware parent location.
1500
+ direct_parent_offset = self._nominal_gripper_offsets_for_location(
1501
+ location
1502
+ )
1503
+ ancestor = self._labware.get_parent_location(location.labwareId)
1504
+ extra_offset = Point(x=0, y=0, z=0)
1505
+ if (
1506
+ isinstance(ancestor, ModuleLocation)
1507
+ # todo(mm, 2024-11-06): Do not access private module state; only use public ModuleView methods.
1508
+ and self._modules._state.requested_model_by_id[ancestor.moduleId]
1509
+ == ModuleModel.THERMOCYCLER_MODULE_V2
1510
+ and labware_validation.validate_definition_is_lid(current_labware)
1511
+ ):
1512
+ if "lidOffsets" in current_labware.gripperOffsets.keys():
1513
+ extra_offset = Point(
1514
+ x=current_labware.gripperOffsets[
1515
+ "lidOffsets"
1516
+ ].pickUpOffset.x,
1517
+ y=current_labware.gripperOffsets[
1518
+ "lidOffsets"
1519
+ ].pickUpOffset.y,
1520
+ z=current_labware.gripperOffsets[
1521
+ "lidOffsets"
1522
+ ].pickUpOffset.z,
1523
+ )
1524
+ else:
1525
+ raise errors.LabwareOffsetDoesNotExistError(
1526
+ f"Labware Definition {current_labware.parameters.loadName} does not contain required field 'lidOffsets' of 'gripperOffsets'."
1527
+ )
1528
+
1529
+ assert isinstance(
1530
+ ancestor,
1531
+ (
1532
+ DeckSlotLocation,
1533
+ ModuleLocation,
1534
+ OnLabwareLocation,
1535
+ AddressableAreaLocation,
1536
+ ),
1537
+ ), "No gripper offsets for off-deck labware"
1538
+ return (
1539
+ Point.from_xyz_attrs(direct_parent_offset.dropOffset)
1540
+ + Point.from_xyz_attrs(
1541
+ self._nominal_gripper_offsets_for_location(
1542
+ location=ancestor
1543
+ ).dropOffset
1544
+ )
1545
+ + extra_offset
1546
+ )
1547
+
1548
+ # todo(mm, 2024-11-05): This may be incorrect because it does not take the following
1549
+ # offsets into account, which *are* taken into account for the actual gripper movement:
1550
+ #
1551
+ # * The pickup offset in the definition of the parent of the gripped labware.
1552
+ # * The "additional offset" or "user offset", e.g. the `pickUpOffset` and `dropOffset`
1553
+ # params in the `moveLabware` command.
1554
+ #
1555
+ # For robustness, we should combine this with `get_gripper_labware_movement_waypoints()`.
1556
+ #
1557
+ # We should also be more explicit about which offsets act to move the gripper paddles
1558
+ # relative to the gripped labware, and which offsets act to change how the gripped
1559
+ # labware sits atop its parent. Those have different effects on how far the gripped
1560
+ # labware juts beyond the paddles while it's in transit.
1561
+ def check_gripper_labware_tip_collision(
1562
+ self,
1563
+ gripper_homed_position_z: float,
1564
+ labware_id: str,
1565
+ # todo(mm, 2025-07-31): arg unused, investigate or remove.
1566
+ current_location: OnDeckLabwareLocation,
1567
+ ) -> None:
1568
+ """Check for potential collision of tips against labware to be lifted."""
1569
+ labware_definition = self._labware.get_definition(labware_id)
1570
+ pipettes = self._pipettes.get_all()
1571
+ for pipette in pipettes:
1572
+ # TODO(cb, 2024-01-22): Remove the 1 and 8 channel special case once we are doing X axis validation
1573
+ if self._pipettes.get_channels(pipette.id) in [1, 8]:
1574
+ return
1575
+
1576
+ tip = self._pipettes.get_attached_tip(pipette.id)
1577
+ if not tip:
1578
+ continue
1579
+
1580
+ labware_origin_to_grip_point = self._labware.get_grip_z(labware_definition)
1581
+ grip_point_to_labware_origin = -labware_origin_to_grip_point
1582
+ height_above_labware_origin = self._labware.get_extents_around_lw_origin(
1583
+ labware_definition
1584
+ ).max_z
1585
+ labware_top_z_when_gripped = (
1586
+ gripper_homed_position_z
1587
+ + grip_point_to_labware_origin
1588
+ + height_above_labware_origin
1589
+ )
1590
+
1591
+ # TODO(cb, 2024-01-18): Utilizing the nozzle map and labware X coordinates,
1592
+ # verify if collisions will occur on the X axis (analysis will use hard coded data
1593
+ # to measure from the gripper critical point to the pipette mount)
1594
+ if (_PIPETTE_HOMED_POSITION_Z - tip.length) < labware_top_z_when_gripped:
1595
+ raise LabwareMovementNotAllowedError(
1596
+ f"Cannot move labware '{labware_definition.parameters.loadName}' when {int(tip.volume)} µL tips are attached."
1597
+ )
1598
+
1599
+ def _nominal_gripper_offsets_for_location(
1600
+ self, location: OnDeckLabwareLocation
1601
+ ) -> LabwareMovementOffsetData:
1602
+ """Provide the default gripper offset data for the given location type."""
1603
+ if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)):
1604
+ # TODO we might need a separate type of gripper offset for addressable areas but that also might just
1605
+ # be covered by the drop labware offset/location
1606
+ offsets = self._labware.get_deck_default_gripper_offsets()
1607
+ elif isinstance(location, ModuleLocation):
1608
+ offsets = self._modules.get_default_gripper_offsets(location.moduleId)
1609
+ else:
1610
+ # Labware is on a labware/adapter
1611
+ offsets = self._labware_gripper_offsets(location.labwareId)
1612
+ return offsets or LabwareMovementOffsetData(
1613
+ pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0),
1614
+ dropOffset=LabwareOffsetVector(x=0, y=0, z=0),
1615
+ )
1616
+
1617
+ def _labware_gripper_offsets(
1618
+ self, labware_id: str
1619
+ ) -> Optional[LabwareMovementOffsetData]:
1620
+ """Provide the most appropriate gripper offset data for the specified labware.
1621
+
1622
+ We check the types of gripper offsets available for the labware ("default" or slot-based)
1623
+ and return the most appropriate one for the overall location of the labware.
1624
+ Currently, only module adapters (specifically, the H/S universal flat adapter)
1625
+ have non-default offsets that are specific to location of the module on deck,
1626
+ so, this code only checks for the presence of those known offsets.
1627
+ """
1628
+ parent_location = self._labware.get_parent_location(labware_id)
1629
+ assert isinstance(
1630
+ parent_location,
1631
+ (
1632
+ DeckSlotLocation,
1633
+ ModuleLocation,
1634
+ AddressableAreaLocation,
1635
+ OnLabwareLocation,
1636
+ ),
1637
+ ), "No gripper offsets for off-deck labware"
1638
+
1639
+ if isinstance(parent_location, DeckSlotLocation):
1640
+ slot_name = parent_location.slotName
1641
+ elif isinstance(parent_location, AddressableAreaLocation):
1642
+ slot_name = self._addressable_areas.get_addressable_area_base_slot(
1643
+ parent_location.addressableAreaName
1644
+ )
1645
+ else:
1646
+ module_loc = self._modules.get_location(parent_location.moduleId)
1647
+ slot_name = module_loc.slotName
1648
+
1649
+ slot_based_offset = self._labware.get_child_gripper_offsets(
1650
+ labware_id=labware_id, slot_name=slot_name.to_ot3_equivalent()
1651
+ )
1652
+
1653
+ return slot_based_offset or self._labware.get_child_gripper_offsets(
1654
+ labware_id=labware_id, slot_name=None
1655
+ )
1656
+
1657
+ def get_location_sequence(self, labware_id: str) -> LabwareLocationSequence:
1658
+ """Provide the LocationSequence specifying the current position of the labware.
1659
+
1660
+ Elements in this sequence contain instance IDs of things. The chain is valid only until the
1661
+ labware is moved.
1662
+ """
1663
+ return self.get_predicted_location_sequence(
1664
+ self._labware.get_location(labware_id)
1665
+ )
1666
+
1667
+ def get_predicted_location_sequence(
1668
+ self,
1669
+ labware_location: LabwareLocation,
1670
+ labware_pending_load: dict[str, LoadedLabware] | None = None,
1671
+ ) -> LabwareLocationSequence:
1672
+ """Get the location sequence for this location. Useful for a labware that hasn't been loaded."""
1673
+ return self._recurse_labware_location(
1674
+ labware_location, [], labware_pending_load or {}
1675
+ )
1676
+
1677
+ def _cutout_fixture_location_sequence_from_addressable_area(
1678
+ self, addressable_area_name: str
1679
+ ) -> OnCutoutFixtureLocationSequenceComponent:
1680
+ (
1681
+ cutout_id,
1682
+ potential_fixtures,
1683
+ ) = self._addressable_areas.get_current_potential_cutout_fixtures_for_addressable_area(
1684
+ addressable_area_name
1685
+ )
1686
+ return OnCutoutFixtureLocationSequenceComponent(
1687
+ possibleCutoutFixtureIds=sorted(
1688
+ [fixture.cutout_fixture_id for fixture in potential_fixtures]
1689
+ ),
1690
+ cutoutId=cutout_id,
1691
+ )
1692
+
1693
+ def _recurse_labware_location_from_aa_component(
1694
+ self,
1695
+ labware_location: AddressableAreaLocation,
1696
+ building: LabwareLocationSequence,
1697
+ ) -> LabwareLocationSequence:
1698
+ cutout_location = self._cutout_fixture_location_sequence_from_addressable_area(
1699
+ labware_location.addressableAreaName
1700
+ )
1701
+ # If the labware is loaded on an AA that is a module, we want to respect the convention
1702
+ # of giving it an OnModuleLocation.
1703
+ possible_module = self._modules.get_by_addressable_area(
1704
+ labware_location.addressableAreaName
1705
+ )
1706
+ if possible_module is not None:
1707
+ return building + [
1708
+ OnAddressableAreaLocationSequenceComponent(
1709
+ addressableAreaName=labware_location.addressableAreaName
1710
+ ),
1711
+ OnModuleLocationSequenceComponent(moduleId=possible_module.id),
1712
+ cutout_location,
1713
+ ]
1714
+ else:
1715
+ return building + [
1716
+ OnAddressableAreaLocationSequenceComponent(
1717
+ addressableAreaName=labware_location.addressableAreaName,
1718
+ ),
1719
+ cutout_location,
1720
+ ]
1721
+
1722
+ def _recurse_labware_location_from_module_component(
1723
+ self, labware_location: ModuleLocation, building: LabwareLocationSequence
1724
+ ) -> LabwareLocationSequence:
1725
+ module_id = labware_location.moduleId
1726
+ module_aa = self._modules.get_provided_addressable_area(module_id)
1727
+ base_location: (
1728
+ OnCutoutFixtureLocationSequenceComponent
1729
+ | NotOnDeckLocationSequenceComponent
1730
+ ) = self._cutout_fixture_location_sequence_from_addressable_area(module_aa)
1731
+
1732
+ if self._modules.get_deck_supports_module_fixtures():
1733
+ # On a deck with modules as cutout fixtures, we want, in order,
1734
+ # - the addressable area of the module
1735
+ # - the module with its module id, which is what clients want
1736
+ # - the cutout
1737
+ loc = self._modules.get_location(module_id)
1738
+ model = self._modules.get_connected_model(module_id)
1739
+ module_aa = self._modules.ensure_and_convert_module_fixture_location(
1740
+ loc.slotName, model
1741
+ )
1742
+ return building + [
1743
+ OnAddressableAreaLocationSequenceComponent(
1744
+ addressableAreaName=module_aa
1745
+ ),
1746
+ OnModuleLocationSequenceComponent(moduleId=module_id),
1747
+ base_location,
1748
+ ]
1749
+ else:
1750
+ # If the module isn't a cutout fixture, then we want
1751
+ # - the module
1752
+ # - the addressable area the module is loaded on
1753
+ # - the cutout
1754
+ location = self._modules.get_location(module_id)
1755
+ return building + [
1756
+ OnModuleLocationSequenceComponent(moduleId=module_id),
1757
+ OnAddressableAreaLocationSequenceComponent(
1758
+ addressableAreaName=location.slotName.value
1759
+ ),
1760
+ base_location,
1761
+ ]
1762
+
1763
+ def _recurse_labware_location_from_stacker_hopper(
1764
+ self,
1765
+ labware_location: InStackerHopperLocation,
1766
+ building: LabwareLocationSequence,
1767
+ ) -> LabwareLocationSequence:
1768
+ loc = self._modules.get_location(labware_location.moduleId)
1769
+ model = self._modules.get_connected_model(labware_location.moduleId)
1770
+ module_aa = self._modules.ensure_and_convert_module_fixture_location(
1771
+ loc.slotName, model
1772
+ )
1773
+ cutout_base = self._cutout_fixture_location_sequence_from_addressable_area(
1774
+ module_aa
1775
+ )
1776
+ return building + [labware_location, cutout_base]
1777
+
1778
+ def _recurse_labware_location(
1779
+ self,
1780
+ labware_location: LabwareLocation,
1781
+ building: LabwareLocationSequence,
1782
+ labware_pending_load: dict[str, LoadedLabware],
1783
+ ) -> LabwareLocationSequence:
1784
+ if isinstance(labware_location, AddressableAreaLocation):
1785
+ return self._recurse_labware_location_from_aa_component(
1786
+ labware_location, building
1787
+ )
1788
+ elif labware_location_is_off_deck(
1789
+ labware_location
1790
+ ) or labware_location_is_system(labware_location):
1791
+ return building + [
1792
+ NotOnDeckLocationSequenceComponent(logicalLocationName=labware_location)
1793
+ ]
1794
+
1795
+ elif isinstance(labware_location, OnLabwareLocation):
1796
+ labware = self._get_or_default_labware(
1797
+ labware_location.labwareId, labware_pending_load
1798
+ )
1799
+ return self._recurse_labware_location(
1800
+ labware.location,
1801
+ building
1802
+ + [
1803
+ OnLabwareLocationSequenceComponent(
1804
+ labwareId=labware_location.labwareId, lidId=labware.lid_id
1805
+ )
1806
+ ],
1807
+ labware_pending_load,
1808
+ )
1809
+ elif isinstance(labware_location, ModuleLocation):
1810
+ return self._recurse_labware_location_from_module_component(
1811
+ labware_location, building
1812
+ )
1813
+ elif isinstance(labware_location, DeckSlotLocation):
1814
+ return building + [
1815
+ OnAddressableAreaLocationSequenceComponent(
1816
+ addressableAreaName=labware_location.slotName.value,
1817
+ ),
1818
+ self._cutout_fixture_location_sequence_from_addressable_area(
1819
+ labware_location.slotName.value
1820
+ ),
1821
+ ]
1822
+ elif isinstance(labware_location, InStackerHopperLocation):
1823
+ return self._recurse_labware_location_from_stacker_hopper(
1824
+ labware_location, building
1825
+ )
1826
+ else:
1827
+ _LOG.warn(f"Unhandled labware location kind: {labware_location}")
1828
+ return building
1829
+
1830
+ def get_offset_location(
1831
+ self, labware_id: str
1832
+ ) -> Optional[LabwareOffsetLocationSequence]:
1833
+ """Provide the LegacyLabwareOffsetLocation specifying the current position of the labware.
1834
+
1835
+ If the labware is in a location that cannot be specified by a LabwareOffsetLocationSequence
1836
+ (for instance, OFF_DECK) then return None.
1837
+ """
1838
+ parent_location = self._labware.get_location(labware_id)
1839
+ return self.get_projected_offset_location(parent_location)
1840
+
1841
+ def get_projected_offset_location(
1842
+ self,
1843
+ labware_location: LabwareLocation,
1844
+ labware_pending_load: dict[str, LoadedLabware] | None = None,
1845
+ ) -> Optional[LabwareOffsetLocationSequence]:
1846
+ """Get the offset location that a labware loaded into this location would match.
1847
+
1848
+ `None` indicates that the very concept of a labware offset would not make sense
1849
+ for the given location, such as if it's some kind of off-deck location. This
1850
+ is a difference from `get_predicted_location_sequence()`, where off-deck
1851
+ locations are still represented as lists, but with special final elements.
1852
+ """
1853
+ return self._recurse_labware_offset_location(
1854
+ labware_location, [], labware_pending_load or {}
1855
+ )
1856
+
1857
+ def _recurse_labware_offset_location(
1858
+ self,
1859
+ labware_location: LabwareLocation,
1860
+ building: LabwareOffsetLocationSequence,
1861
+ labware_pending_load: dict[str, LoadedLabware],
1862
+ ) -> LabwareOffsetLocationSequence | None:
1863
+ if isinstance(labware_location, DeckSlotLocation):
1864
+ return building + [
1865
+ OnAddressableAreaOffsetLocationSequenceComponent(
1866
+ addressableAreaName=labware_location.slotName.value
1867
+ )
1868
+ ]
1869
+
1870
+ elif isinstance(labware_location, ModuleLocation):
1871
+ module_id = labware_location.moduleId
1872
+ # Allow ModuleNotLoadedError to propagate.
1873
+ # Note also that we match based on the module's requested model, not its
1874
+ # actual model, to implement robot-server's documented HTTP API semantics.
1875
+ module_model = self._modules.get_requested_model(module_id=module_id)
1876
+
1877
+ # If `module_model is None`, it probably means that this module was added by
1878
+ # `ProtocolEngine.use_attached_modules()`, instead of an explicit
1879
+ # `loadModule` command.
1880
+ #
1881
+ # This assert should never raise in practice because:
1882
+ # 1. `ProtocolEngine.use_attached_modules()` is only used by
1883
+ # robot-server's "stateless command" endpoints, under `/commands`.
1884
+ # 2. Those endpoints don't support loading labware, so this code will
1885
+ # never run.
1886
+ #
1887
+ # Nevertheless, if it does happen somehow, we do NOT want to pass the
1888
+ # `None` value along to `LabwareView.find_applicable_labware_offset()`.
1889
+ # `None` means something different there, which will cause us to return
1890
+ # wrong results.
1891
+ assert module_model is not None, (
1892
+ "Can't find offsets for labware"
1893
+ " that are loaded on modules"
1894
+ " that were loaded with ProtocolEngine.use_attached_modules()."
1895
+ )
1896
+
1897
+ module_location = self._modules.get_location(module_id=module_id)
1898
+ if self._modules.get_deck_supports_module_fixtures():
1899
+ module_aa = self._modules.ensure_and_convert_module_fixture_location(
1900
+ module_location.slotName, module_model
1901
+ )
1902
+ else:
1903
+ module_aa = module_location.slotName.value
1904
+ return building + [
1905
+ OnModuleOffsetLocationSequenceComponent(moduleModel=module_model),
1906
+ OnAddressableAreaOffsetLocationSequenceComponent(
1907
+ addressableAreaName=module_aa
1908
+ ),
1909
+ ]
1910
+
1911
+ elif isinstance(labware_location, OnLabwareLocation):
1912
+ parent_labware_id = labware_location.labwareId
1913
+ parent_labware = self._get_or_default_labware(
1914
+ parent_labware_id, labware_pending_load
1915
+ )
1916
+ parent_labware_uri = LabwareUri(parent_labware.definitionUri)
1917
+ base_location = parent_labware.location
1918
+ return self._recurse_labware_offset_location(
1919
+ base_location,
1920
+ building
1921
+ + [
1922
+ OnLabwareOffsetLocationSequenceComponent(
1923
+ labwareUri=parent_labware_uri
1924
+ )
1925
+ ],
1926
+ labware_pending_load,
1927
+ )
1928
+
1929
+ else: # Off deck
1930
+ return None
1931
+
1932
+ def get_liquid_handling_z_change(
1933
+ self,
1934
+ labware_id: str,
1935
+ well_name: str,
1936
+ pipette_id: str,
1937
+ operation_volume: float,
1938
+ ) -> float:
1939
+ """Get the change in height from a liquid handling operation."""
1940
+ initial_handling_height = self.get_meniscus_height(
1941
+ labware_id=labware_id, well_name=well_name
1942
+ )
1943
+ final_height = self.get_well_height_after_liquid_handling(
1944
+ labware_id=labware_id,
1945
+ well_name=well_name,
1946
+ pipette_id=pipette_id,
1947
+ initial_height=initial_handling_height,
1948
+ volume=operation_volume,
1949
+ )
1950
+ # this function is only called by
1951
+ # HardwarePipetteHandler::aspirate/dispense while_tracking, and shouldn't
1952
+ # be reached in the case of a simulated liquid_probe
1953
+ assert not isinstance(
1954
+ initial_handling_height, SimulatedProbeResult
1955
+ ), "Initial handling height got SimulatedProbeResult"
1956
+ assert not isinstance(
1957
+ final_height, SimulatedProbeResult
1958
+ ), "final height is SimulatedProbeResult"
1959
+ return final_height - initial_handling_height
1960
+
1961
+ def get_well_offset_adjustment(
1962
+ self,
1963
+ labware_id: str,
1964
+ well_name: str,
1965
+ well_location: WellLocationType,
1966
+ well_depth: float,
1967
+ pipette_id: Optional[str] = None,
1968
+ operation_volume: Optional[float] = None,
1969
+ ) -> LiquidTrackingType:
1970
+ """Return a z-axis distance that accounts for well handling height and operation volume.
1971
+
1972
+ Distance is with reference to the well bottom.
1973
+ """
1974
+ # TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
1975
+
1976
+ initial_handling_height = self.get_well_handling_height(
1977
+ labware_id=labware_id,
1978
+ well_name=well_name,
1979
+ well_location=well_location,
1980
+ well_depth=well_depth,
1981
+ )
1982
+ # if we're tracking a MENISCUS origin, and targeting either the beginning
1983
+ # position of the liquid or doing dynamic tracking, return the initial height
1984
+ if (
1985
+ well_location.origin == WellOrigin.MENISCUS
1986
+ and not well_location.volumeOffset
1987
+ ):
1988
+ return initial_handling_height
1989
+ volume: Optional[float] = None
1990
+ if isinstance(well_location, PickUpTipWellLocation):
1991
+ volume = 0.0
1992
+ elif isinstance(well_location, LiquidHandlingWellLocation):
1993
+ if well_location.volumeOffset == "operationVolume":
1994
+ volume = operation_volume or 0.0
1995
+ else:
1996
+ if not isinstance(well_location.volumeOffset, float):
1997
+ raise ValueError("Invalid volume offset.")
1998
+ volume = well_location.volumeOffset
1999
+
2000
+ if volume:
2001
+ if pipette_id is None:
2002
+ raise ValueError(
2003
+ "cannot get liquid handling offset without pipette id."
2004
+ )
2005
+ liquid_height_after = self.get_well_height_after_liquid_handling(
2006
+ labware_id=labware_id,
2007
+ well_name=well_name,
2008
+ pipette_id=pipette_id,
2009
+ initial_height=initial_handling_height,
2010
+ volume=volume,
2011
+ )
2012
+ return liquid_height_after
2013
+ else:
2014
+ return initial_handling_height
2015
+
2016
+ def get_current_well_volume(
2017
+ self,
2018
+ labware_id: str,
2019
+ well_name: str,
2020
+ ) -> LiquidTrackingType:
2021
+ """Returns most recently updated volume in specified well."""
2022
+ last_updated = self._wells.get_last_liquid_update(labware_id, well_name)
2023
+ if last_updated is None:
2024
+ raise errors.LiquidHeightUnknownError(
2025
+ "Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS."
2026
+ )
2027
+
2028
+ well_liquid = self._wells.get_well_liquid_info(
2029
+ labware_id=labware_id, well_name=well_name
2030
+ )
2031
+ if (
2032
+ well_liquid.probed_height is not None
2033
+ and well_liquid.probed_height.height is not None
2034
+ and well_liquid.probed_height.last_probed == last_updated
2035
+ ):
2036
+ volume = self.get_well_volume_at_height(
2037
+ labware_id=labware_id,
2038
+ well_name=well_name,
2039
+ height=well_liquid.probed_height.height,
2040
+ )
2041
+ return volume
2042
+ elif (
2043
+ well_liquid.loaded_volume is not None
2044
+ and well_liquid.loaded_volume.volume is not None
2045
+ and well_liquid.loaded_volume.last_loaded == last_updated
2046
+ ):
2047
+ return well_liquid.loaded_volume.volume
2048
+ elif (
2049
+ well_liquid.probed_volume is not None
2050
+ and well_liquid.probed_volume.volume is not None
2051
+ and well_liquid.probed_volume.last_probed == last_updated
2052
+ ):
2053
+ return well_liquid.probed_volume.volume
2054
+ else:
2055
+ # This should not happen if there was an update but who knows
2056
+ raise errors.LiquidVolumeUnknownError(
2057
+ f"Unable to find liquid volume despite an update at {last_updated}."
2058
+ )
2059
+
2060
+ def get_meniscus_height(
2061
+ self,
2062
+ labware_id: str,
2063
+ well_name: str,
2064
+ ) -> LiquidTrackingType:
2065
+ """Returns stored meniscus height in specified well."""
2066
+ last_updated = self._wells.get_last_liquid_update(labware_id, well_name)
2067
+ if last_updated is None:
2068
+ raise errors.LiquidHeightUnknownError(
2069
+ "Must LiquidProbe or LoadLiquid before specifying WellOrigin.MENISCUS."
2070
+ )
2071
+
2072
+ well_liquid = self._wells.get_well_liquid_info(
2073
+ labware_id=labware_id, well_name=well_name
2074
+ )
2075
+ if (
2076
+ well_liquid.probed_height is not None
2077
+ and well_liquid.probed_height.height is not None
2078
+ and well_liquid.probed_height.last_probed == last_updated
2079
+ ):
2080
+ return well_liquid.probed_height.height
2081
+ elif (
2082
+ well_liquid.loaded_volume is not None
2083
+ and well_liquid.loaded_volume.volume is not None
2084
+ and well_liquid.loaded_volume.last_loaded == last_updated
2085
+ ):
2086
+ return self.get_well_height_at_volume(
2087
+ labware_id=labware_id,
2088
+ well_name=well_name,
2089
+ volume=well_liquid.loaded_volume.volume,
2090
+ )
2091
+ elif (
2092
+ well_liquid.probed_volume is not None
2093
+ and well_liquid.probed_volume.volume is not None
2094
+ and well_liquid.probed_volume.last_probed == last_updated
2095
+ ):
2096
+ return self.get_well_height_at_volume(
2097
+ labware_id=labware_id,
2098
+ well_name=well_name,
2099
+ volume=well_liquid.probed_volume.volume,
2100
+ )
2101
+ else:
2102
+ # This should not happen if there was an update but who knows
2103
+ raise errors.LiquidHeightUnknownError(
2104
+ f"Unable to find liquid height despite an update at {last_updated}."
2105
+ )
2106
+
2107
+ def get_well_handling_height(
2108
+ self,
2109
+ labware_id: str,
2110
+ well_name: str,
2111
+ well_location: WellLocationType,
2112
+ well_depth: float,
2113
+ ) -> LiquidTrackingType:
2114
+ """Return the handling height for a labware well (with reference to the well bottom)."""
2115
+ handling_height: LiquidTrackingType = 0.0
2116
+ if well_location.origin == WellOrigin.TOP:
2117
+ handling_height = float(well_depth)
2118
+ elif well_location.origin == WellOrigin.CENTER:
2119
+ handling_height = float(well_depth / 2.0)
2120
+ elif well_location.origin == WellOrigin.MENISCUS:
2121
+ handling_height = self.get_meniscus_height(
2122
+ labware_id=labware_id, well_name=well_name
2123
+ )
2124
+ return handling_height
2125
+
2126
+ def find_volume_at_well_height(
2127
+ self,
2128
+ labware_id: str,
2129
+ well_name: str,
2130
+ target_height: LiquidTrackingType,
2131
+ ) -> LiquidTrackingType:
2132
+ """Call the correct volume from height function based on well geoemtry type."""
2133
+ well_geometry = self._labware.get_well_geometry(
2134
+ labware_id=labware_id, well_name=well_name
2135
+ )
2136
+ if isinstance(well_geometry, InnerWellGeometry):
2137
+ return find_volume_inner_well_geometry(
2138
+ target_height=target_height, well_geometry=well_geometry
2139
+ )
2140
+ else:
2141
+ return find_volume_user_defined_volumes(
2142
+ target_height=target_height, well_geometry=well_geometry
2143
+ )
2144
+
2145
+ def find_height_at_well_volume(
2146
+ self,
2147
+ labware_id: str,
2148
+ well_name: str,
2149
+ target_volume: LiquidTrackingType,
2150
+ ) -> LiquidTrackingType:
2151
+ """Call the correct height from volume function based on well geometry type."""
2152
+ well_geometry = self._labware.get_well_geometry(
2153
+ labware_id=labware_id, well_name=well_name
2154
+ )
2155
+ if isinstance(well_geometry, InnerWellGeometry):
2156
+ return find_height_inner_well_geometry(
2157
+ target_volume=target_volume, well_geometry=well_geometry
2158
+ )
2159
+ else:
2160
+ return find_height_user_defined_volumes(
2161
+ target_volume=target_volume, well_geometry=well_geometry
2162
+ )
2163
+
2164
+ def get_well_height_after_liquid_handling(
2165
+ self,
2166
+ labware_id: str,
2167
+ well_name: str,
2168
+ pipette_id: str,
2169
+ initial_height: LiquidTrackingType,
2170
+ volume: float,
2171
+ ) -> LiquidTrackingType:
2172
+ """Return the height of liquid in a labware well after a given volume has been handled.
2173
+
2174
+ This is given an initial handling height, with reference to the well bottom.
2175
+ """
2176
+ well_def = self._labware.get_well_definition(labware_id, well_name)
2177
+ well_depth = well_def.depth
2178
+
2179
+ try:
2180
+ initial_volume = self.find_volume_at_well_height(
2181
+ labware_id=labware_id, well_name=well_name, target_height=initial_height
2182
+ )
2183
+ final_volume = initial_volume + (
2184
+ volume
2185
+ * self.get_nozzles_per_well(
2186
+ labware_id=labware_id,
2187
+ target_well_name=well_name,
2188
+ pipette_id=pipette_id,
2189
+ )
2190
+ )
2191
+ # NOTE(cm): if final_volume is outside the bounds of the well, it will get
2192
+ # adjusted inside find_height_at_well_volume to accomodate well the height
2193
+ # calculation.
2194
+ height_inside_well = self.find_height_at_well_volume(
2195
+ labware_id=labware_id, well_name=well_name, target_volume=final_volume
2196
+ )
2197
+ return self._validate_well_position(
2198
+ target_height=height_inside_well,
2199
+ well_max_height=well_depth,
2200
+ pipette_id=pipette_id,
2201
+ )
2202
+ except InvalidLiquidHeightFound as _exception:
2203
+ raise InvalidLiquidHeightFound(
2204
+ message=_exception.message
2205
+ + f"for well {well_name} of {self._labware.get_display_name(labware_id)} on slot {self.get_ancestor_slot_name(labware_id)}"
2206
+ )
2207
+
2208
+ def get_well_height_at_volume(
2209
+ self, labware_id: str, well_name: str, volume: LiquidTrackingType
2210
+ ) -> LiquidTrackingType:
2211
+ """Convert well volume to height."""
2212
+ try:
2213
+ return self.find_height_at_well_volume(
2214
+ labware_id=labware_id, well_name=well_name, target_volume=volume
2215
+ )
2216
+ except InvalidLiquidHeightFound as _exception:
2217
+ raise InvalidLiquidHeightFound(
2218
+ message=_exception.message
2219
+ + f"for well {well_name} of {self._labware.get_display_name(labware_id)} on slot {self.get_ancestor_slot_name(labware_id)}"
2220
+ )
2221
+
2222
+ def get_well_volume_at_height(
2223
+ self,
2224
+ labware_id: str,
2225
+ well_name: str,
2226
+ height: LiquidTrackingType,
2227
+ ) -> LiquidTrackingType:
2228
+ """Convert well height to volume."""
2229
+ try:
2230
+ return self.find_volume_at_well_height(
2231
+ labware_id=labware_id, well_name=well_name, target_height=height
2232
+ )
2233
+ except InvalidLiquidHeightFound as _exception:
2234
+ raise InvalidLiquidHeightFound(
2235
+ message=_exception.message
2236
+ + f"for well {well_name} of {self._labware.get_display_name(labware_id)} on slot {self.get_ancestor_slot_name(labware_id)}"
2237
+ )
2238
+
2239
+ def validate_dispense_volume_into_well(
2240
+ self,
2241
+ labware_id: str,
2242
+ well_name: str,
2243
+ well_location: WellLocationType,
2244
+ volume: float,
2245
+ ) -> None:
2246
+ """Raise InvalidDispenseVolumeError if planned dispense volume will overflow well."""
2247
+ well_def = self._labware.get_well_definition(labware_id, well_name)
2248
+ well_volumetric_capacity = float(well_def.totalLiquidVolume)
2249
+ if well_location.origin == WellOrigin.MENISCUS:
2250
+ # TODO(pbm, 10-23-24): refactor to smartly reduce height/volume conversions
2251
+ meniscus_height = self.get_meniscus_height(
2252
+ labware_id=labware_id, well_name=well_name
2253
+ )
2254
+ try:
2255
+ meniscus_volume = self.find_volume_at_well_height(
2256
+ labware_id=labware_id,
2257
+ well_name=well_name,
2258
+ target_height=meniscus_height,
2259
+ )
2260
+ except InvalidLiquidHeightFound as _exception:
2261
+ raise InvalidLiquidHeightFound(
2262
+ message=_exception.message
2263
+ + f"for well {well_name} of {self._labware.get_display_name(labware_id)} on slot {self.get_ancestor_slot_name(labware_id)}"
2264
+ )
2265
+ # if meniscus volume is a simulated value, comparisons aren't meaningful
2266
+ if isinstance(meniscus_volume, SimulatedProbeResult):
2267
+ return
2268
+ remaining_volume = well_volumetric_capacity - meniscus_volume
2269
+ if volume > remaining_volume:
2270
+ raise errors.InvalidDispenseVolumeError(
2271
+ f"Attempting to dispense {volume}µL of liquid into a well that can currently only hold {remaining_volume}µL (well {well_name} in labware_id: {labware_id})"
2272
+ )
2273
+ else:
2274
+ # TODO(pbm, 10-08-24): factor in well (LabwareStore) state volume
2275
+ if volume > well_volumetric_capacity:
2276
+ raise errors.InvalidDispenseVolumeError(
2277
+ f"Attempting to dispense {volume}µL of liquid into a well that can only hold {well_volumetric_capacity}µL (well {well_name} in labware_id: {labware_id})"
2278
+ )
2279
+
2280
+ def get_wells_covered_by_pipette_with_active_well(
2281
+ self, labware_id: str, target_well_name: str, pipette_id: str
2282
+ ) -> list[str]:
2283
+ """Get a flat list of wells that are covered by a pipette when moved to a specified well.
2284
+
2285
+ When you move a pipette in a multichannel configuration to a specific well - the target well -
2286
+ the pipette will operate on other wells as well.
2287
+
2288
+ For instance, a pipette with a COLUMN configuration with well A1 of an SBS standard labware target
2289
+ will also "cover", under this definition, wells B1-H1. That same pipette, when C5 is the target well, will "cover"
2290
+ wells C5-H5.
2291
+
2292
+ This math only works, and may only be applied, if one of the following is true:
2293
+ - The pipette is in a SINGLE configuration
2294
+ - The pipette is in a non-SINGLE configuration, and the labware is an SBS-format 96 or 384 well plate (and is so
2295
+ marked in its definition's parameters.format key, as 96Standard or 384Standard)
2296
+
2297
+ If all of the following do not apply, regardless of the nozzle configuration of the pipette this function will
2298
+ return only the labware covered by the primary well.
2299
+ """
2300
+ pipette_nozzle_map = self._pipettes.get_nozzle_configuration(pipette_id)
2301
+ labware_columns = [
2302
+ column for column in self._labware.get_definition(labware_id).ordering
2303
+ ]
2304
+ try:
2305
+ return list(
2306
+ wells_covered_by_pipette_configuration(
2307
+ pipette_nozzle_map, target_well_name, labware_columns
2308
+ )
2309
+ )
2310
+ except InvalidStoredData:
2311
+ return [target_well_name]
2312
+
2313
+ def get_nozzles_per_well(
2314
+ self, labware_id: str, target_well_name: str, pipette_id: str
2315
+ ) -> int:
2316
+ """Get the number of nozzles that will interact with each well."""
2317
+ return nozzles_per_well(
2318
+ self._pipettes.get_nozzle_configuration(pipette_id),
2319
+ target_well_name,
2320
+ self._labware.get_definition(labware_id).ordering,
2321
+ )
2322
+
2323
+ def get_height_of_labware_stack(
2324
+ self, definitions: list[LabwareDefinition]
2325
+ ) -> float:
2326
+ """Get the overall height of a stack of labware listed by definition in top-first order."""
2327
+ if len(definitions) == 0:
2328
+ return 0
2329
+ if len(definitions) == 1:
2330
+ return self._labware.get_dimensions(labware_definition=definitions[0]).z
2331
+ total_height = 0.0
2332
+ upper_def: LabwareDefinition = definitions[0]
2333
+ for lower_def in definitions[1:]:
2334
+ overlap = self._labware.get_labware_overlap_offsets(
2335
+ upper_def, lower_def.parameters.loadName
2336
+ ).z
2337
+ total_height += (
2338
+ self._labware.get_dimensions(labware_definition=upper_def).z - overlap
2339
+ )
2340
+ upper_def = lower_def
2341
+ return (
2342
+ total_height + self._labware.get_dimensions(labware_definition=upper_def).z
2343
+ )
2344
+ return total_height + upper_def.dimensions.zDimension
2345
+
2346
+ def get_height_of_stacker_labware_pool(self, module_id: str) -> float:
2347
+ """Get the overall height of a stack of labware in a Stacker module."""
2348
+ stacker = self._modules.get_flex_stacker_substate(module_id)
2349
+ pool_list = stacker.get_pool_definition_ordered_list()
2350
+ if not pool_list:
2351
+ return 0.0
2352
+ return self.get_height_of_labware_stack(pool_list)
2353
+
2354
+ def _get_or_default_labware(
2355
+ self, labware_id: str, pending_labware: dict[str, LoadedLabware]
2356
+ ) -> LoadedLabware:
2357
+ try:
2358
+ return self._labware.get(labware_id)
2359
+ except LabwareNotLoadedError as lnle:
2360
+ try:
2361
+ return pending_labware[labware_id]
2362
+ except KeyError as ke:
2363
+ raise lnle from ke
2364
+
2365
+ def raise_if_labware_inaccessible_by_pipette( # noqa: C901
2366
+ self, labware_id: str
2367
+ ) -> None:
2368
+ """Raise an error if the specified location cannot be reached via a pipette."""
2369
+ labware = self._labware.get(labware_id)
2370
+ labware_location = labware.location
2371
+ if isinstance(labware_location, OnLabwareLocation):
2372
+ return self.raise_if_labware_inaccessible_by_pipette(
2373
+ labware_location.labwareId
2374
+ )
2375
+ elif labware.lid_id is not None:
2376
+ raise errors.LocationNotAccessibleByPipetteError(
2377
+ f"Cannot move pipette to {labware.loadName} "
2378
+ "because labware is currently covered by a lid."
2379
+ )
2380
+ elif isinstance(labware_location, AddressableAreaLocation):
2381
+ if fixture_validation.is_staging_slot(labware_location.addressableAreaName):
2382
+ raise errors.LocationNotAccessibleByPipetteError(
2383
+ f"Cannot move pipette to {labware.loadName},"
2384
+ f" labware is on staging slot {labware_location.addressableAreaName}"
2385
+ )
2386
+ elif fixture_validation.is_stacker_shuttle(
2387
+ labware_location.addressableAreaName
2388
+ ):
2389
+ raise errors.LocationNotAccessibleByPipetteError(
2390
+ f"Cannot move pipette to {labware.loadName} because it is on a stacker shuttle"
2391
+ )
2392
+ elif (
2393
+ labware_location == OFF_DECK_LOCATION or labware_location == SYSTEM_LOCATION
2394
+ ):
2395
+ raise errors.LocationNotAccessibleByPipetteError(
2396
+ f"Cannot move pipette to {labware.loadName}, labware is off-deck."
2397
+ )
2398
+ elif isinstance(labware_location, ModuleLocation):
2399
+ module = self._modules.get(labware_location.moduleId)
2400
+ if ModuleModel.is_flex_stacker(module.model):
2401
+ raise errors.LocationNotAccessibleByPipetteError(
2402
+ f"Cannot move pipette to {labware.loadName}, labware is on a stacker shuttle"
2403
+ )
2404
+
2405
+ elif isinstance(labware_location, InStackerHopperLocation):
2406
+ raise errors.LocationNotAccessibleByPipetteError(
2407
+ f"Cannot move pipette to {labware.loadName}, labware is in a stacker hopper"
2408
+ )