opentrons 8.3.1a1__py2.py3-none-any.whl → 8.4.0a0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of opentrons might be problematic. Click here for more details.
- opentrons/calibration_storage/ot2/mark_bad_calibration.py +2 -0
- opentrons/calibration_storage/ot2/tip_length.py +6 -6
- opentrons/config/advanced_settings.py +9 -11
- opentrons/config/feature_flags.py +0 -4
- opentrons/config/reset.py +7 -2
- opentrons/drivers/asyncio/communication/__init__.py +2 -0
- opentrons/drivers/asyncio/communication/async_serial.py +4 -0
- opentrons/drivers/asyncio/communication/errors.py +41 -8
- opentrons/drivers/asyncio/communication/serial_connection.py +36 -10
- opentrons/drivers/flex_stacker/__init__.py +9 -3
- opentrons/drivers/flex_stacker/abstract.py +140 -15
- opentrons/drivers/flex_stacker/driver.py +593 -47
- opentrons/drivers/flex_stacker/errors.py +64 -0
- opentrons/drivers/flex_stacker/simulator.py +222 -24
- opentrons/drivers/flex_stacker/types.py +211 -15
- opentrons/drivers/flex_stacker/utils.py +19 -0
- opentrons/execute.py +4 -2
- opentrons/hardware_control/api.py +5 -0
- opentrons/hardware_control/backends/flex_protocol.py +4 -0
- opentrons/hardware_control/backends/ot3controller.py +12 -1
- opentrons/hardware_control/backends/ot3simulator.py +3 -0
- opentrons/hardware_control/backends/subsystem_manager.py +7 -2
- opentrons/hardware_control/instruments/ot2/instrument_calibration.py +10 -6
- opentrons/hardware_control/instruments/ot3/pipette_handler.py +59 -6
- opentrons/hardware_control/modules/__init__.py +12 -1
- opentrons/hardware_control/modules/absorbance_reader.py +11 -9
- opentrons/hardware_control/modules/flex_stacker.py +498 -0
- opentrons/hardware_control/modules/heater_shaker.py +12 -10
- opentrons/hardware_control/modules/magdeck.py +5 -1
- opentrons/hardware_control/modules/tempdeck.py +5 -1
- opentrons/hardware_control/modules/thermocycler.py +15 -14
- opentrons/hardware_control/modules/types.py +191 -1
- opentrons/hardware_control/modules/utils.py +3 -0
- opentrons/hardware_control/motion_utilities.py +20 -0
- opentrons/hardware_control/ot3api.py +145 -15
- opentrons/hardware_control/protocols/liquid_handler.py +47 -1
- opentrons/hardware_control/types.py +6 -0
- opentrons/legacy_commands/commands.py +19 -3
- opentrons/legacy_commands/helpers.py +15 -0
- opentrons/legacy_commands/types.py +3 -2
- opentrons/protocol_api/__init__.py +2 -0
- opentrons/protocol_api/_liquid.py +39 -8
- opentrons/protocol_api/_liquid_properties.py +20 -19
- opentrons/protocol_api/_transfer_liquid_validation.py +91 -0
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/deck_conflict.py +11 -1
- opentrons/protocol_api/core/engine/instrument.py +1233 -65
- opentrons/protocol_api/core/engine/labware.py +8 -4
- opentrons/protocol_api/core/engine/load_labware_params.py +68 -10
- opentrons/protocol_api/core/engine/module_core.py +118 -2
- opentrons/protocol_api/core/engine/protocol.py +253 -11
- opentrons/protocol_api/core/engine/stringify.py +19 -8
- opentrons/protocol_api/core/engine/transfer_components_executor.py +853 -0
- opentrons/protocol_api/core/engine/well.py +60 -5
- opentrons/protocol_api/core/instrument.py +65 -19
- opentrons/protocol_api/core/labware.py +6 -2
- opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +69 -21
- opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
- opentrons/protocol_api/core/legacy/legacy_well_core.py +25 -1
- opentrons/protocol_api/core/legacy/load_info.py +4 -12
- opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
- opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +67 -21
- opentrons/protocol_api/core/module.py +43 -0
- opentrons/protocol_api/core/protocol.py +33 -0
- opentrons/protocol_api/core/well.py +21 -1
- opentrons/protocol_api/instrument_context.py +245 -123
- opentrons/protocol_api/labware.py +75 -11
- opentrons/protocol_api/module_contexts.py +140 -0
- opentrons/protocol_api/protocol_context.py +156 -16
- opentrons/protocol_api/validation.py +51 -41
- opentrons/protocol_engine/__init__.py +21 -2
- opentrons/protocol_engine/actions/actions.py +5 -5
- opentrons/protocol_engine/clients/sync_client.py +6 -0
- opentrons/protocol_engine/commands/__init__.py +30 -0
- opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
- opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
- opentrons/protocol_engine/commands/aspirate.py +6 -2
- opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +237 -0
- opentrons/protocol_engine/commands/blow_out.py +2 -0
- opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
- opentrons/protocol_engine/commands/command_unions.py +69 -0
- opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
- opentrons/protocol_engine/commands/dispense.py +3 -1
- opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +240 -0
- opentrons/protocol_engine/commands/drop_tip.py +23 -1
- opentrons/protocol_engine/commands/evotip_dispense.py +6 -7
- opentrons/protocol_engine/commands/evotip_seal_pipette.py +2 -9
- opentrons/protocol_engine/commands/evotip_unseal_pipette.py +1 -7
- opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
- opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
- opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
- opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
- opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
- opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
- opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
- opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
- opentrons/protocol_engine/commands/flex_stacker/store.py +288 -0
- opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
- opentrons/protocol_engine/commands/labware_handling_common.py +24 -0
- opentrons/protocol_engine/commands/liquid_probe.py +21 -12
- opentrons/protocol_engine/commands/load_labware.py +42 -39
- opentrons/protocol_engine/commands/load_lid.py +21 -13
- opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
- opentrons/protocol_engine/commands/load_module.py +18 -17
- opentrons/protocol_engine/commands/load_pipette.py +3 -0
- opentrons/protocol_engine/commands/move_labware.py +139 -20
- opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
- opentrons/protocol_engine/commands/pipetting_common.py +154 -7
- opentrons/protocol_engine/commands/prepare_to_aspirate.py +3 -1
- opentrons/protocol_engine/commands/reload_labware.py +6 -19
- opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
- opentrons/protocol_engine/errors/__init__.py +8 -0
- opentrons/protocol_engine/errors/exceptions.py +50 -0
- opentrons/protocol_engine/execution/equipment.py +123 -106
- opentrons/protocol_engine/execution/labware_movement.py +8 -6
- opentrons/protocol_engine/execution/pipetting.py +233 -26
- opentrons/protocol_engine/execution/tip_handler.py +14 -5
- opentrons/protocol_engine/labware_offset_standardization.py +173 -0
- opentrons/protocol_engine/protocol_engine.py +22 -13
- opentrons/protocol_engine/resources/deck_configuration_provider.py +94 -2
- opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
- opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
- opentrons/protocol_engine/resources/labware_validation.py +7 -5
- opentrons/protocol_engine/slot_standardization.py +11 -23
- opentrons/protocol_engine/state/addressable_areas.py +84 -46
- opentrons/protocol_engine/state/frustum_helpers.py +26 -10
- opentrons/protocol_engine/state/geometry.py +683 -100
- opentrons/protocol_engine/state/labware.py +252 -55
- opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
- opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
- opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
- opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
- opentrons/protocol_engine/state/modules.py +178 -52
- opentrons/protocol_engine/state/pipettes.py +54 -0
- opentrons/protocol_engine/state/state.py +1 -1
- opentrons/protocol_engine/state/tips.py +14 -0
- opentrons/protocol_engine/state/update_types.py +180 -25
- opentrons/protocol_engine/state/wells.py +54 -8
- opentrons/protocol_engine/types/__init__.py +292 -0
- opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
- opentrons/protocol_engine/types/command_annotations.py +53 -0
- opentrons/protocol_engine/types/deck_configuration.py +72 -0
- opentrons/protocol_engine/types/execution.py +96 -0
- opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
- opentrons/protocol_engine/types/instrument.py +47 -0
- opentrons/protocol_engine/types/instrument_sensors.py +47 -0
- opentrons/protocol_engine/types/labware.py +110 -0
- opentrons/protocol_engine/types/labware_movement.py +22 -0
- opentrons/protocol_engine/types/labware_offset_location.py +108 -0
- opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
- opentrons/protocol_engine/types/liquid.py +40 -0
- opentrons/protocol_engine/types/liquid_class.py +59 -0
- opentrons/protocol_engine/types/liquid_handling.py +13 -0
- opentrons/protocol_engine/types/liquid_level_detection.py +137 -0
- opentrons/protocol_engine/types/location.py +193 -0
- opentrons/protocol_engine/types/module.py +269 -0
- opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
- opentrons/protocol_engine/types/run_time_parameters.py +133 -0
- opentrons/protocol_engine/types/tip.py +18 -0
- opentrons/protocol_engine/types/util.py +21 -0
- opentrons/protocol_engine/types/well_position.py +107 -0
- opentrons/protocol_reader/extract_labware_definitions.py +7 -3
- opentrons/protocol_reader/file_format_validator.py +5 -3
- opentrons/protocol_runner/json_translator.py +4 -2
- opentrons/protocol_runner/legacy_command_mapper.py +6 -2
- opentrons/protocol_runner/run_orchestrator.py +4 -1
- opentrons/protocols/advanced_control/transfers/common.py +48 -1
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/instrument.py +16 -3
- opentrons/protocols/labware.py +5 -6
- opentrons/protocols/models/__init__.py +0 -21
- opentrons/simulate.py +4 -2
- opentrons/types.py +15 -6
- {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/METADATA +4 -4
- {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/RECORD +187 -147
- opentrons/calibration_storage/ot2/models/defaults.py +0 -0
- opentrons/calibration_storage/ot3/models/defaults.py +0 -0
- opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
- opentrons/protocol_engine/types.py +0 -1311
- {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/LICENSE +0 -0
- {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/WHEEL +0 -0
- {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/entry_points.txt +0 -0
- {opentrons-8.3.1a1.dist-info → opentrons-8.4.0a0.dist-info}/top_level.txt +0 -0
|
@@ -19,6 +19,7 @@ from opentrons.drivers.thermocycler.driver import (
|
|
|
19
19
|
LID_TARGET_MIN,
|
|
20
20
|
LID_TARGET_MAX,
|
|
21
21
|
)
|
|
22
|
+
from opentrons.hardware_control.modules import ModuleData, ModuleDataValidator
|
|
22
23
|
|
|
23
24
|
ThermocyclerModuleId = NewType("ThermocyclerModuleId", str)
|
|
24
25
|
|
|
@@ -141,3 +142,22 @@ class ThermocyclerModuleSubState:
|
|
|
141
142
|
f"Module {self.module_id} does not have a target block temperature set."
|
|
142
143
|
)
|
|
143
144
|
return target
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def from_live_data(
|
|
148
|
+
cls, module_id: ThermocyclerModuleId, data: ModuleData | None
|
|
149
|
+
) -> "ThermocyclerModuleSubState":
|
|
150
|
+
"""Create a ThermocyclerModuleSubState from live data."""
|
|
151
|
+
if ModuleDataValidator.is_thermocycler_data(data):
|
|
152
|
+
return cls(
|
|
153
|
+
module_id=module_id,
|
|
154
|
+
is_lid_open=data["lid"] == "open",
|
|
155
|
+
target_block_temperature=data["targetTemp"],
|
|
156
|
+
target_lid_temperature=data["lidTarget"],
|
|
157
|
+
)
|
|
158
|
+
return cls(
|
|
159
|
+
module_id=module_id,
|
|
160
|
+
is_lid_open=False,
|
|
161
|
+
target_block_temperature=None,
|
|
162
|
+
target_lid_temperature=None,
|
|
163
|
+
)
|
|
@@ -36,8 +36,9 @@ from opentrons.protocol_engine.state.module_substates.absorbance_reader_substate
|
|
|
36
36
|
AbsorbanceReaderMeasureMode,
|
|
37
37
|
)
|
|
38
38
|
from opentrons.types import DeckSlotName, MountType, StagingSlotName
|
|
39
|
-
from .update_types import AbsorbanceReaderStateUpdate
|
|
40
|
-
from ..errors import ModuleNotConnectedError
|
|
39
|
+
from .update_types import AbsorbanceReaderStateUpdate, FlexStackerStateUpdate
|
|
40
|
+
from ..errors import ModuleNotConnectedError, AreaNotInDeckConfigurationError
|
|
41
|
+
from ..resources import deck_configuration_provider
|
|
41
42
|
|
|
42
43
|
from ..types import (
|
|
43
44
|
LoadedModule,
|
|
@@ -78,11 +79,13 @@ from .module_substates import (
|
|
|
78
79
|
TemperatureModuleSubState,
|
|
79
80
|
ThermocyclerModuleSubState,
|
|
80
81
|
AbsorbanceReaderSubState,
|
|
82
|
+
FlexStackerSubState,
|
|
81
83
|
MagneticModuleId,
|
|
82
84
|
HeaterShakerModuleId,
|
|
83
85
|
TemperatureModuleId,
|
|
84
86
|
ThermocyclerModuleId,
|
|
85
87
|
AbsorbanceReaderId,
|
|
88
|
+
FlexStackerId,
|
|
86
89
|
MagneticBlockSubState,
|
|
87
90
|
MagneticBlockId,
|
|
88
91
|
ModuleSubStateType,
|
|
@@ -147,11 +150,14 @@ class HardwareModule:
|
|
|
147
150
|
class ModuleState:
|
|
148
151
|
"""The internal data to keep track of loaded modules."""
|
|
149
152
|
|
|
150
|
-
|
|
151
|
-
"""The
|
|
153
|
+
load_location_by_module_id: Dict[str, Optional[str]]
|
|
154
|
+
"""The Cutout ID of the cutout (Flex) or slot (OT-2) that each module has been loaded.
|
|
152
155
|
|
|
153
156
|
This will be None when the module was added via
|
|
154
157
|
ProtocolEngine.use_attached_modules() instead of an explicit loadModule command.
|
|
158
|
+
AddressableAreaLocation is used to represent a literal Deck Slot for OT-2 locations.
|
|
159
|
+
The CutoutID string for a given Cutout that a Module Fixture is loaded into is used
|
|
160
|
+
for Flex. The type distinction is in place for implementation seperation between the two.
|
|
155
161
|
"""
|
|
156
162
|
|
|
157
163
|
additional_slots_occupied_by_module_id: Dict[str, List[DeckSlotName]]
|
|
@@ -211,7 +217,7 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
211
217
|
) -> None:
|
|
212
218
|
"""Initialize a ModuleStore and its state."""
|
|
213
219
|
self._state = ModuleState(
|
|
214
|
-
|
|
220
|
+
load_location_by_module_id={},
|
|
215
221
|
additional_slots_occupied_by_module_id={},
|
|
216
222
|
requested_model_by_id={},
|
|
217
223
|
hardware_by_module_id={},
|
|
@@ -302,6 +308,8 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
302
308
|
self._handle_absorbance_reader_commands(
|
|
303
309
|
state_update.absorbance_reader_state_update
|
|
304
310
|
)
|
|
311
|
+
if state_update.flex_stacker_state_update != update_types.NO_CHANGE:
|
|
312
|
+
self._handle_flex_stacker_commands(state_update.flex_stacker_state_update)
|
|
305
313
|
|
|
306
314
|
def _add_module_substate(
|
|
307
315
|
self,
|
|
@@ -312,11 +320,19 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
312
320
|
requested_model: Optional[ModuleModel],
|
|
313
321
|
module_live_data: Optional[LiveData],
|
|
314
322
|
) -> None:
|
|
323
|
+
# Loading slot name to Cutout ID (Flex)(OT-2) resolution
|
|
324
|
+
load_location: Optional[str]
|
|
325
|
+
if slot_name is not None:
|
|
326
|
+
load_location = deck_configuration_provider.get_cutout_id_by_deck_slot_name(
|
|
327
|
+
slot_name
|
|
328
|
+
)
|
|
329
|
+
else:
|
|
330
|
+
load_location = slot_name
|
|
331
|
+
|
|
315
332
|
actual_model = definition.model
|
|
316
333
|
live_data = module_live_data["data"] if module_live_data else None
|
|
317
|
-
|
|
318
334
|
self._state.requested_model_by_id[module_id] = requested_model
|
|
319
|
-
self._state.
|
|
335
|
+
self._state.load_location_by_module_id[module_id] = load_location
|
|
320
336
|
self._state.hardware_by_module_id[module_id] = HardwareModule(
|
|
321
337
|
serial_number=serial_number,
|
|
322
338
|
definition=definition,
|
|
@@ -328,31 +344,24 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
328
344
|
model=actual_model,
|
|
329
345
|
)
|
|
330
346
|
elif ModuleModel.is_heater_shaker_module_model(actual_model):
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
labware_latch_status = HeaterShakerLatchStatus.CLOSED
|
|
335
|
-
else:
|
|
336
|
-
labware_latch_status = HeaterShakerLatchStatus.OPEN
|
|
337
|
-
self._state.substate_by_module_id[module_id] = HeaterShakerModuleSubState(
|
|
347
|
+
self._state.substate_by_module_id[
|
|
348
|
+
module_id
|
|
349
|
+
] = HeaterShakerModuleSubState.from_live_data(
|
|
338
350
|
module_id=HeaterShakerModuleId(module_id),
|
|
339
|
-
|
|
340
|
-
is_plate_shaking=(
|
|
341
|
-
live_data is not None and live_data["targetSpeed"] is not None
|
|
342
|
-
),
|
|
343
|
-
plate_target_temperature=live_data["targetTemp"] if live_data else None, # type: ignore[arg-type]
|
|
351
|
+
data=live_data,
|
|
344
352
|
)
|
|
345
353
|
elif ModuleModel.is_temperature_module_model(actual_model):
|
|
346
|
-
self._state.substate_by_module_id[
|
|
354
|
+
self._state.substate_by_module_id[
|
|
355
|
+
module_id
|
|
356
|
+
] = TemperatureModuleSubState.from_live_data(
|
|
347
357
|
module_id=TemperatureModuleId(module_id),
|
|
348
|
-
|
|
358
|
+
data=live_data,
|
|
349
359
|
)
|
|
350
360
|
elif ModuleModel.is_thermocycler_module_model(actual_model):
|
|
351
|
-
self._state.substate_by_module_id[
|
|
352
|
-
module_id
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
target_lid_temperature=live_data["lidTarget"] if live_data else None, # type: ignore[arg-type]
|
|
361
|
+
self._state.substate_by_module_id[
|
|
362
|
+
module_id
|
|
363
|
+
] = ThermocyclerModuleSubState.from_live_data(
|
|
364
|
+
module_id=ThermocyclerModuleId(module_id), data=live_data
|
|
356
365
|
)
|
|
357
366
|
self._update_additional_slots_occupied_by_thermocycler(
|
|
358
367
|
module_id=module_id, slot_name=slot_name
|
|
@@ -372,6 +381,15 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
372
381
|
configured_wavelengths=None,
|
|
373
382
|
reference_wavelength=None,
|
|
374
383
|
)
|
|
384
|
+
elif ModuleModel.is_flex_stacker(actual_model):
|
|
385
|
+
self._state.substate_by_module_id[module_id] = FlexStackerSubState(
|
|
386
|
+
module_id=FlexStackerId(module_id),
|
|
387
|
+
pool_primary_definition=None,
|
|
388
|
+
pool_adapter_definition=None,
|
|
389
|
+
pool_lid_definition=None,
|
|
390
|
+
pool_count=0,
|
|
391
|
+
max_pool_count=0,
|
|
392
|
+
)
|
|
375
393
|
|
|
376
394
|
def _update_additional_slots_occupied_by_thermocycler(
|
|
377
395
|
self,
|
|
@@ -613,6 +631,20 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
613
631
|
data=data,
|
|
614
632
|
)
|
|
615
633
|
|
|
634
|
+
def _handle_flex_stacker_commands(
|
|
635
|
+
self, state_update: FlexStackerStateUpdate
|
|
636
|
+
) -> None:
|
|
637
|
+
"""Handle Flex Stacker state updates."""
|
|
638
|
+
module_id = state_update.module_id
|
|
639
|
+
prev_substate = self._state.substate_by_module_id[module_id]
|
|
640
|
+
assert isinstance(
|
|
641
|
+
prev_substate, FlexStackerSubState
|
|
642
|
+
), f"{module_id} is not a Flex Stacker."
|
|
643
|
+
|
|
644
|
+
self._state.substate_by_module_id[
|
|
645
|
+
module_id
|
|
646
|
+
] = prev_substate.new_from_state_change(state_update)
|
|
647
|
+
|
|
616
648
|
|
|
617
649
|
class ModuleView:
|
|
618
650
|
"""Read-only view of computed module state."""
|
|
@@ -626,12 +658,17 @@ class ModuleView:
|
|
|
626
658
|
def get(self, module_id: str) -> LoadedModule:
|
|
627
659
|
"""Get module data by the module's unique identifier."""
|
|
628
660
|
try:
|
|
629
|
-
|
|
661
|
+
load_location = self._state.load_location_by_module_id[module_id]
|
|
630
662
|
attached_module = self._state.hardware_by_module_id[module_id]
|
|
631
663
|
|
|
632
664
|
except KeyError as e:
|
|
633
665
|
raise errors.ModuleNotLoadedError(module_id=module_id) from e
|
|
634
666
|
|
|
667
|
+
slot_name = None
|
|
668
|
+
if isinstance(load_location, str):
|
|
669
|
+
slot_name = deck_configuration_provider.get_deck_slot_for_cutout_id(
|
|
670
|
+
load_location
|
|
671
|
+
)
|
|
635
672
|
location = (
|
|
636
673
|
DeckSlotLocation(slotName=slot_name) if slot_name is not None else None
|
|
637
674
|
)
|
|
@@ -645,21 +682,39 @@ class ModuleView:
|
|
|
645
682
|
|
|
646
683
|
def get_all(self) -> List[LoadedModule]:
|
|
647
684
|
"""Get a list of all module entries in state."""
|
|
648
|
-
return [
|
|
685
|
+
return [
|
|
686
|
+
self.get(mod_id) for mod_id in self._state.load_location_by_module_id.keys()
|
|
687
|
+
]
|
|
649
688
|
|
|
650
689
|
def get_by_slot(
|
|
651
690
|
self,
|
|
652
691
|
slot_name: DeckSlotName,
|
|
653
692
|
) -> Optional[LoadedModule]:
|
|
654
693
|
"""Get the module located in a given slot, if any."""
|
|
655
|
-
|
|
694
|
+
locations_by_id = reversed(list(self._state.load_location_by_module_id.items()))
|
|
656
695
|
|
|
657
|
-
for module_id,
|
|
696
|
+
for module_id, load_location in locations_by_id:
|
|
697
|
+
module_slot: Optional[DeckSlotName]
|
|
698
|
+
if isinstance(load_location, str):
|
|
699
|
+
module_slot = deck_configuration_provider.get_deck_slot_for_cutout_id(
|
|
700
|
+
load_location
|
|
701
|
+
)
|
|
702
|
+
else:
|
|
703
|
+
module_slot = load_location
|
|
658
704
|
if module_slot == slot_name:
|
|
659
705
|
return self.get(module_id)
|
|
660
706
|
|
|
661
707
|
return None
|
|
662
708
|
|
|
709
|
+
def get_by_addressable_area(
|
|
710
|
+
self, addressable_area_name: str
|
|
711
|
+
) -> Optional[LoadedModule]:
|
|
712
|
+
"""Get the module associated with this addressable area, if any."""
|
|
713
|
+
for module_id in self._state.load_location_by_module_id.keys():
|
|
714
|
+
if addressable_area_name == self.get_provided_addressable_area(module_id):
|
|
715
|
+
return self.get(module_id)
|
|
716
|
+
return None
|
|
717
|
+
|
|
663
718
|
def _get_module_substate(
|
|
664
719
|
self, module_id: str, expected_type: Type[ModuleSubStateT], expected_name: str
|
|
665
720
|
) -> ModuleSubStateT:
|
|
@@ -764,6 +819,20 @@ class ModuleView:
|
|
|
764
819
|
expected_name="Absorbance Reader",
|
|
765
820
|
)
|
|
766
821
|
|
|
822
|
+
def get_flex_stacker_substate(self, module_id: str) -> FlexStackerSubState:
|
|
823
|
+
"""Return a `FlexStackerSubState` for the given Flex Stacker.
|
|
824
|
+
|
|
825
|
+
Raises:
|
|
826
|
+
ModuleNotLoadedError: If module_id has not been loaded.
|
|
827
|
+
WrongModuleTypeError: If module_id has been loaded,
|
|
828
|
+
but it's not a Flex Stacker.
|
|
829
|
+
"""
|
|
830
|
+
return self._get_module_substate(
|
|
831
|
+
module_id=module_id,
|
|
832
|
+
expected_type=FlexStackerSubState,
|
|
833
|
+
expected_name="Flex Stacker",
|
|
834
|
+
)
|
|
835
|
+
|
|
767
836
|
def get_location(self, module_id: str) -> DeckSlotLocation:
|
|
768
837
|
"""Get the slot location of the given module."""
|
|
769
838
|
location = self.get(module_id).location
|
|
@@ -773,6 +842,26 @@ class ModuleView:
|
|
|
773
842
|
)
|
|
774
843
|
return location
|
|
775
844
|
|
|
845
|
+
def get_provided_addressable_area(self, module_id: str) -> str:
|
|
846
|
+
"""Get the addressable area provided by this module.
|
|
847
|
+
|
|
848
|
+
If the current deck does not allow modules to provide locations (i.e., is an OT-2 deck)
|
|
849
|
+
then return the addressable area underneath the module.
|
|
850
|
+
"""
|
|
851
|
+
module = self.get(module_id)
|
|
852
|
+
|
|
853
|
+
if isinstance(module.location, DeckSlotLocation):
|
|
854
|
+
location = module.location.slotName
|
|
855
|
+
elif module.model == ModuleModel.THERMOCYCLER_MODULE_V2:
|
|
856
|
+
location = DeckSlotName.SLOT_B1
|
|
857
|
+
else:
|
|
858
|
+
raise ValueError(
|
|
859
|
+
"Module location invalid for nominal module offset calculation."
|
|
860
|
+
)
|
|
861
|
+
if not self.get_deck_supports_module_fixtures():
|
|
862
|
+
return location.value
|
|
863
|
+
return self.ensure_and_convert_module_fixture_location(location, module.model)
|
|
864
|
+
|
|
776
865
|
def get_requested_model(self, module_id: str) -> Optional[ModuleModel]:
|
|
777
866
|
"""Return the model by which this module was requested.
|
|
778
867
|
|
|
@@ -880,18 +969,7 @@ class ModuleView:
|
|
|
880
969
|
z=xformed[2],
|
|
881
970
|
)
|
|
882
971
|
else:
|
|
883
|
-
|
|
884
|
-
if isinstance(module.location, DeckSlotLocation):
|
|
885
|
-
location = module.location.slotName
|
|
886
|
-
elif module.model == ModuleModel.THERMOCYCLER_MODULE_V2:
|
|
887
|
-
location = DeckSlotName.SLOT_B1
|
|
888
|
-
else:
|
|
889
|
-
raise ValueError(
|
|
890
|
-
"Module location invalid for nominal module offset calculation."
|
|
891
|
-
)
|
|
892
|
-
module_addressable_area = self.ensure_and_convert_module_fixture_location(
|
|
893
|
-
location, module.model
|
|
894
|
-
)
|
|
972
|
+
module_addressable_area = self.get_provided_addressable_area(module_id)
|
|
895
973
|
module_addressable_area_position = (
|
|
896
974
|
addressable_areas.get_addressable_area_offsets_from_cutout(
|
|
897
975
|
module_addressable_area
|
|
@@ -1109,12 +1187,21 @@ class ModuleView:
|
|
|
1109
1187
|
else:
|
|
1110
1188
|
neighbor_slot = DeckSlotName.from_primitive(neighbor_int)
|
|
1111
1189
|
|
|
1112
|
-
|
|
1190
|
+
# Convert the load location list from addressable areas and cutout IDs to a slot name list
|
|
1191
|
+
load_locations = self._state.load_location_by_module_id.values()
|
|
1192
|
+
module_slots = []
|
|
1193
|
+
for location in load_locations:
|
|
1194
|
+
if isinstance(location, str):
|
|
1195
|
+
module_slots.append(
|
|
1196
|
+
deck_configuration_provider.get_deck_slot_for_cutout_id(location)
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
return neighbor_slot in module_slots
|
|
1113
1200
|
|
|
1114
1201
|
def select_hardware_module_to_load( # noqa: C901
|
|
1115
1202
|
self,
|
|
1116
1203
|
model: ModuleModel,
|
|
1117
|
-
location:
|
|
1204
|
+
location: str,
|
|
1118
1205
|
attached_modules: Sequence[HardwareModule],
|
|
1119
1206
|
expected_serial_number: Optional[str] = None,
|
|
1120
1207
|
) -> HardwareModule:
|
|
@@ -1143,10 +1230,13 @@ class ModuleView:
|
|
|
1143
1230
|
"""
|
|
1144
1231
|
existing_mod_in_slot = None
|
|
1145
1232
|
|
|
1146
|
-
for
|
|
1147
|
-
|
|
1233
|
+
for (
|
|
1234
|
+
mod_id,
|
|
1235
|
+
load_location,
|
|
1236
|
+
) in self._state.load_location_by_module_id.items():
|
|
1237
|
+
if isinstance(load_location, str) and location == load_location:
|
|
1148
1238
|
existing_mod_in_slot = self._state.hardware_by_module_id.get(mod_id)
|
|
1149
|
-
|
|
1239
|
+
|
|
1150
1240
|
if existing_mod_in_slot:
|
|
1151
1241
|
existing_def = existing_mod_in_slot.definition
|
|
1152
1242
|
|
|
@@ -1154,9 +1244,9 @@ class ModuleView:
|
|
|
1154
1244
|
return existing_mod_in_slot
|
|
1155
1245
|
|
|
1156
1246
|
else:
|
|
1247
|
+
_err = f" present in {location}"
|
|
1157
1248
|
raise errors.ModuleAlreadyPresentError(
|
|
1158
|
-
f"A {existing_def.model.value} is already"
|
|
1159
|
-
f" present in {location.slotName.value}"
|
|
1249
|
+
f"A {existing_def.model.value} is already" + _err
|
|
1160
1250
|
)
|
|
1161
1251
|
|
|
1162
1252
|
for m in attached_modules:
|
|
@@ -1168,7 +1258,10 @@ class ModuleView:
|
|
|
1168
1258
|
else:
|
|
1169
1259
|
return m
|
|
1170
1260
|
|
|
1171
|
-
raise errors.ModuleNotAttachedError(
|
|
1261
|
+
raise errors.ModuleNotAttachedError(
|
|
1262
|
+
f"No available {model.value} with {expected_serial_number or 'any'}"
|
|
1263
|
+
" serial found."
|
|
1264
|
+
)
|
|
1172
1265
|
|
|
1173
1266
|
def get_heater_shaker_movement_restrictors(
|
|
1174
1267
|
self,
|
|
@@ -1258,6 +1351,11 @@ class ModuleView:
|
|
|
1258
1351
|
"Only readings of 96 Well labware are supported for conversion to map of values by well."
|
|
1259
1352
|
)
|
|
1260
1353
|
|
|
1354
|
+
def get_deck_supports_module_fixtures(self) -> bool:
|
|
1355
|
+
"""Check if the loaded deck supports modules as fixtures."""
|
|
1356
|
+
deck_type = self._state.deck_type
|
|
1357
|
+
return deck_type not in [DeckType.OT2_STANDARD, DeckType.OT2_SHORT_TRASH]
|
|
1358
|
+
|
|
1261
1359
|
def ensure_and_convert_module_fixture_location(
|
|
1262
1360
|
self,
|
|
1263
1361
|
deck_slot: DeckSlotName,
|
|
@@ -1269,8 +1367,8 @@ class ModuleView:
|
|
|
1269
1367
|
"""
|
|
1270
1368
|
deck_type = self._state.deck_type
|
|
1271
1369
|
|
|
1272
|
-
if
|
|
1273
|
-
raise
|
|
1370
|
+
if not self.get_deck_supports_module_fixtures():
|
|
1371
|
+
raise AreaNotInDeckConfigurationError(
|
|
1274
1372
|
f"Invalid Deck Type: {deck_type.name} - Does not support modules as fixtures."
|
|
1275
1373
|
)
|
|
1276
1374
|
|
|
@@ -1296,6 +1394,11 @@ class ModuleView:
|
|
|
1296
1394
|
assert deck_slot.value[-1] == "3"
|
|
1297
1395
|
return f"absorbanceReaderV1{deck_slot.value}"
|
|
1298
1396
|
|
|
1397
|
+
elif model == ModuleModel.FLEX_STACKER_MODULE_V1:
|
|
1398
|
+
# loaded to column 3 but the addressable area is in column 4
|
|
1399
|
+
assert deck_slot.value[-1] == "3"
|
|
1400
|
+
return f"flexStackerModuleV1{deck_slot.value[0]}4"
|
|
1401
|
+
|
|
1299
1402
|
raise ValueError(
|
|
1300
1403
|
f"Unknown module {model.name} has no addressable areas to provide."
|
|
1301
1404
|
)
|
|
@@ -1311,3 +1414,26 @@ class ModuleView:
|
|
|
1311
1414
|
addressableAreaName="absorbanceReaderV1LidDock" + lid_doc_slot.value
|
|
1312
1415
|
)
|
|
1313
1416
|
return lid_dock_area
|
|
1417
|
+
|
|
1418
|
+
def get_stacker_max_fill_height(self, module_id: str) -> float:
|
|
1419
|
+
"""Get the maximum fill height for the Flex Stacker."""
|
|
1420
|
+
definition = self.get_definition(module_id)
|
|
1421
|
+
|
|
1422
|
+
if (
|
|
1423
|
+
definition.moduleType == ModuleType.FLEX_STACKER
|
|
1424
|
+
and hasattr(definition.dimensions, "maxStackerFillHeight")
|
|
1425
|
+
and definition.dimensions.maxStackerFillHeight is not None
|
|
1426
|
+
):
|
|
1427
|
+
return definition.dimensions.maxStackerFillHeight
|
|
1428
|
+
else:
|
|
1429
|
+
raise errors.WrongModuleTypeError(
|
|
1430
|
+
f"Cannot get max fill height of {definition.moduleType}"
|
|
1431
|
+
)
|
|
1432
|
+
|
|
1433
|
+
def stacker_max_pool_count_by_height(
|
|
1434
|
+
self, module_id: str, pool_height: float
|
|
1435
|
+
) -> int:
|
|
1436
|
+
"""Get the maximum stack count for the Flex Stacker by stack height."""
|
|
1437
|
+
max_fill_height = self.get_stacker_max_fill_height(module_id)
|
|
1438
|
+
assert max_fill_height > 0
|
|
1439
|
+
return math.floor(max_fill_height / pool_height)
|
|
@@ -125,6 +125,8 @@ class PipetteState:
|
|
|
125
125
|
flow_rates_by_id: Dict[str, FlowRates]
|
|
126
126
|
nozzle_configuration_by_id: Dict[str, NozzleMap]
|
|
127
127
|
liquid_presence_detection_by_id: Dict[str, bool]
|
|
128
|
+
ready_to_aspirate_by_id: Dict[str, bool]
|
|
129
|
+
has_clean_tips_by_id: Dict[str, bool]
|
|
128
130
|
|
|
129
131
|
|
|
130
132
|
class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
@@ -145,6 +147,8 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
|
145
147
|
flow_rates_by_id={},
|
|
146
148
|
nozzle_configuration_by_id={},
|
|
147
149
|
liquid_presence_detection_by_id={},
|
|
150
|
+
ready_to_aspirate_by_id={},
|
|
151
|
+
has_clean_tips_by_id={},
|
|
148
152
|
)
|
|
149
153
|
|
|
150
154
|
def handle_action(self, action: Action) -> None:
|
|
@@ -156,6 +160,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
|
156
160
|
self._update_pipette_nozzle_map(state_update)
|
|
157
161
|
self._update_tip_state(state_update)
|
|
158
162
|
self._update_volumes(state_update)
|
|
163
|
+
self._update_ready_for_aspirate(state_update)
|
|
159
164
|
|
|
160
165
|
if isinstance(action, SetPipetteMovementSpeedAction):
|
|
161
166
|
self._state.movement_speed_by_id[action.pipette_id] = action.speed
|
|
@@ -174,6 +179,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
|
174
179
|
)
|
|
175
180
|
self._state.movement_speed_by_id[pipette_id] = None
|
|
176
181
|
self._state.attached_tip_by_id[pipette_id] = None
|
|
182
|
+
self._state.ready_to_aspirate_by_id[pipette_id] = False
|
|
177
183
|
|
|
178
184
|
def _update_tip_state(self, state_update: update_types.StateUpdate) -> None:
|
|
179
185
|
if state_update.pipette_tip_state != update_types.NO_CHANGE:
|
|
@@ -209,6 +215,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
|
209
215
|
else:
|
|
210
216
|
pipette_id = state_update.pipette_tip_state.pipette_id
|
|
211
217
|
self._state.attached_tip_by_id[pipette_id] = None
|
|
218
|
+
self._state.has_clean_tips_by_id[pipette_id] = False
|
|
212
219
|
|
|
213
220
|
static_config = self._state.static_config_by_id.get(pipette_id)
|
|
214
221
|
if static_config:
|
|
@@ -314,9 +321,23 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
|
314
321
|
state_update.pipette_nozzle_map.pipette_id
|
|
315
322
|
] = state_update.pipette_nozzle_map.nozzle_map
|
|
316
323
|
|
|
324
|
+
def _update_ready_for_aspirate(
|
|
325
|
+
self, state_update: update_types.StateUpdate
|
|
326
|
+
) -> None:
|
|
327
|
+
if state_update.ready_to_aspirate != update_types.NO_CHANGE:
|
|
328
|
+
self._state.ready_to_aspirate_by_id[
|
|
329
|
+
state_update.ready_to_aspirate.pipette_id
|
|
330
|
+
] = state_update.ready_to_aspirate.ready_to_aspirate
|
|
331
|
+
|
|
317
332
|
def _update_volumes(self, state_update: update_types.StateUpdate) -> None:
|
|
318
333
|
if state_update.pipette_aspirated_fluid == update_types.NO_CHANGE:
|
|
319
334
|
return
|
|
335
|
+
# set the tip state to unclean, if an "empty" update has a clean_tip flag
|
|
336
|
+
# it will set it to true
|
|
337
|
+
self._state.has_clean_tips_by_id[
|
|
338
|
+
state_update.pipette_aspirated_fluid.pipette_id
|
|
339
|
+
] = False
|
|
340
|
+
|
|
320
341
|
if state_update.pipette_aspirated_fluid.type == "aspirated":
|
|
321
342
|
self._update_aspirated(state_update.pipette_aspirated_fluid)
|
|
322
343
|
elif state_update.pipette_aspirated_fluid.type == "ejected":
|
|
@@ -343,6 +364,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
|
343
364
|
|
|
344
365
|
def _update_empty(self, update: update_types.PipetteEmptyFluidUpdate) -> None:
|
|
345
366
|
self._state.pipette_contents_by_id[update.pipette_id] = fluid_stack.FluidStack()
|
|
367
|
+
self._state.has_clean_tips_by_id[update.pipette_id] = update.clean_tip
|
|
346
368
|
|
|
347
369
|
def _update_unknown(self, update: update_types.PipetteUnknownFluidUpdate) -> None:
|
|
348
370
|
self._state.pipette_contents_by_id[update.pipette_id] = None
|
|
@@ -482,6 +504,29 @@ class PipetteView:
|
|
|
482
504
|
f"Pipette {pipette_id} not found; unable to get current volume."
|
|
483
505
|
) from e
|
|
484
506
|
|
|
507
|
+
def get_has_clean_tip(self, pipette_id: str) -> bool:
|
|
508
|
+
"""Get if the tip of a pipette by ID is clean.
|
|
509
|
+
|
|
510
|
+
This is only true directly after a pick up tip, once any kind of aspirate happens
|
|
511
|
+
it is no longer clean
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
True if the tip is clean
|
|
515
|
+
False if it is unclean
|
|
516
|
+
|
|
517
|
+
Raises:
|
|
518
|
+
PipetteNotLoadedError: pipette ID does not exist.
|
|
519
|
+
TipNotAttachedError: if no tip is attached to the pipette.
|
|
520
|
+
"""
|
|
521
|
+
self.validate_tip_state(pipette_id, True)
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
return self._state.has_clean_tips_by_id[pipette_id]
|
|
525
|
+
except KeyError as e:
|
|
526
|
+
raise errors.PipetteNotLoadedError(
|
|
527
|
+
f"Pipette {pipette_id} not found; unable to get current volume."
|
|
528
|
+
) from e
|
|
529
|
+
|
|
485
530
|
def get_liquid_dispensed_by_ejecting_volume(
|
|
486
531
|
self, pipette_id: str, volume: float
|
|
487
532
|
) -> Optional[float]:
|
|
@@ -822,3 +867,12 @@ class PipetteView:
|
|
|
822
867
|
) -> float:
|
|
823
868
|
"""Get the plunger position provided for the given pipette id."""
|
|
824
869
|
return self.get_config(pipette_id).plunger_positions[position_name]
|
|
870
|
+
|
|
871
|
+
def get_ready_to_aspirate(self, pipette_id: str) -> bool:
|
|
872
|
+
"""Get if the provided pipette is ready to aspirate for the given pipette id."""
|
|
873
|
+
try:
|
|
874
|
+
return self._state.ready_to_aspirate_by_id[pipette_id]
|
|
875
|
+
except KeyError as e:
|
|
876
|
+
raise errors.PipetteNotLoadedError(
|
|
877
|
+
f"Pipette {pipette_id} not found; unable to determine if pipette ready to aspirate."
|
|
878
|
+
) from e
|
|
@@ -374,7 +374,7 @@ class StateStore(StateView, ActionHandler):
|
|
|
374
374
|
self._addressable_areas = AddressableAreaView(state.addressable_areas)
|
|
375
375
|
self._labware = LabwareView(state.labware)
|
|
376
376
|
self._pipettes = PipetteView(state.pipettes)
|
|
377
|
-
self._modules = ModuleView(state.modules)
|
|
377
|
+
self._modules = ModuleView(state=state.modules)
|
|
378
378
|
self._liquid = LiquidView(state.liquids)
|
|
379
379
|
self._liquid_classes = LiquidClassView(state.liquid_classes)
|
|
380
380
|
self._tips = TipView(state.tips)
|
|
@@ -110,6 +110,20 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
110
110
|
self._state.column_by_labware_id[labware_id] = [
|
|
111
111
|
column for column in definition.ordering
|
|
112
112
|
]
|
|
113
|
+
if state_update.batch_loaded_labware != update_types.NO_CHANGE:
|
|
114
|
+
for labware_id in state_update.batch_loaded_labware.new_locations_by_id:
|
|
115
|
+
definition = state_update.batch_loaded_labware.definitions_by_id[
|
|
116
|
+
labware_id
|
|
117
|
+
]
|
|
118
|
+
if definition.parameters.isTiprack:
|
|
119
|
+
self._state.tips_by_labware_id[labware_id] = {
|
|
120
|
+
well_name: TipRackWellState.CLEAN
|
|
121
|
+
for column in definition.ordering
|
|
122
|
+
for well_name in column
|
|
123
|
+
}
|
|
124
|
+
self._state.column_by_labware_id[labware_id] = [
|
|
125
|
+
column for column in definition.ordering
|
|
126
|
+
]
|
|
113
127
|
|
|
114
128
|
def _set_used_tips(self, pipette_id: str, well_name: str, labware_id: str) -> None:
|
|
115
129
|
columns = self._state.column_by_labware_id.get(labware_id, [])
|