opentrons 8.7.0a5__py3-none-any.whl → 8.7.0a7__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/_version.py +2 -2
- opentrons/drivers/asyncio/communication/serial_connection.py +129 -52
- opentrons/drivers/heater_shaker/abstract.py +5 -0
- opentrons/drivers/heater_shaker/driver.py +10 -0
- opentrons/drivers/heater_shaker/simulator.py +4 -0
- opentrons/drivers/thermocycler/abstract.py +6 -0
- opentrons/drivers/thermocycler/driver.py +61 -10
- opentrons/drivers/thermocycler/simulator.py +6 -0
- opentrons/hardware_control/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/ot3controller.py +3 -0
- opentrons/hardware_control/backends/ot3simulator.py +2 -1
- opentrons/hardware_control/backends/simulator.py +2 -1
- opentrons/hardware_control/backends/subsystem_manager.py +5 -2
- opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
- opentrons/hardware_control/emulation/connection_handler.py +8 -5
- opentrons/hardware_control/emulation/heater_shaker.py +12 -3
- opentrons/hardware_control/emulation/settings.py +1 -1
- opentrons/hardware_control/emulation/thermocycler.py +67 -15
- opentrons/hardware_control/module_control.py +82 -8
- opentrons/hardware_control/modules/__init__.py +3 -0
- opentrons/hardware_control/modules/absorbance_reader.py +11 -4
- opentrons/hardware_control/modules/flex_stacker.py +38 -9
- opentrons/hardware_control/modules/heater_shaker.py +42 -5
- opentrons/hardware_control/modules/magdeck.py +8 -4
- opentrons/hardware_control/modules/mod_abc.py +13 -5
- opentrons/hardware_control/modules/tempdeck.py +25 -5
- opentrons/hardware_control/modules/thermocycler.py +68 -11
- opentrons/hardware_control/modules/types.py +20 -1
- opentrons/hardware_control/modules/utils.py +11 -4
- opentrons/hardware_control/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +26 -5
- opentrons/hardware_control/poller.py +22 -8
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +31 -2
- opentrons/legacy_commands/module_commands.py +23 -0
- opentrons/legacy_commands/protocol_commands.py +20 -0
- opentrons/legacy_commands/types.py +80 -0
- opentrons/motion_planning/deck_conflict.py +17 -12
- opentrons/motion_planning/waypoints.py +15 -29
- opentrons/protocol_api/__init__.py +5 -1
- opentrons/protocol_api/_types.py +6 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +32 -11
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +75 -8
- opentrons/protocol_api/core/engine/protocol.py +18 -1
- opentrons/protocol_api/core/engine/tasks.py +48 -0
- opentrons/protocol_api/core/engine/well.py +8 -0
- opentrons/protocol_api/core/legacy/legacy_module_core.py +24 -4
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -1
- opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
- opentrons/protocol_api/core/legacy/tasks.py +19 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
- opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
- opentrons/protocol_api/core/module.py +37 -4
- opentrons/protocol_api/core/protocol.py +11 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/core/well.py +4 -0
- opentrons/protocol_api/labware.py +5 -0
- opentrons/protocol_api/module_contexts.py +117 -11
- opentrons/protocol_api/protocol_context.py +26 -4
- opentrons/protocol_api/robot_context.py +38 -21
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +6 -1
- opentrons/protocol_engine/actions/__init__.py +4 -2
- opentrons/protocol_engine/actions/actions.py +22 -9
- opentrons/protocol_engine/clients/sync_client.py +42 -7
- opentrons/protocol_engine/commands/__init__.py +42 -0
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
- opentrons/protocol_engine/commands/aspirate.py +1 -0
- opentrons/protocol_engine/commands/command.py +1 -0
- opentrons/protocol_engine/commands/command_unions.py +49 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
- opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
- opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
- opentrons/protocol_engine/commands/movement_common.py +2 -0
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- opentrons/protocol_engine/commands/set_tip_state.py +97 -0
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
- opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +40 -6
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +29 -5
- opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
- opentrons/protocol_engine/commands/touch_tip.py +1 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
- opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
- opentrons/protocol_engine/errors/__init__.py +4 -0
- opentrons/protocol_engine/errors/exceptions.py +55 -0
- opentrons/protocol_engine/execution/__init__.py +2 -0
- opentrons/protocol_engine/execution/command_executor.py +8 -0
- opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
- opentrons/protocol_engine/execution/labware_movement.py +9 -12
- opentrons/protocol_engine/execution/movement.py +2 -0
- opentrons/protocol_engine/execution/queue_worker.py +4 -0
- opentrons/protocol_engine/execution/run_control.py +8 -0
- opentrons/protocol_engine/execution/task_handler.py +157 -0
- opentrons/protocol_engine/protocol_engine.py +75 -34
- opentrons/protocol_engine/resources/__init__.py +2 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/commands.py +14 -11
- opentrons/protocol_engine/state/geometry.py +213 -374
- opentrons/protocol_engine/state/labware.py +52 -102
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1331 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +21 -8
- opentrons/protocol_engine/state/motion.py +44 -0
- opentrons/protocol_engine/state/state.py +14 -0
- opentrons/protocol_engine/state/state_summary.py +2 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +16 -9
- opentrons/protocol_engine/types/__init__.py +9 -3
- opentrons/protocol_engine/types/deck_configuration.py +5 -1
- opentrons/protocol_engine/types/instrument.py +8 -1
- opentrons/protocol_engine/types/labware.py +1 -13
- opentrons/protocol_engine/types/module.py +10 -0
- opentrons/protocol_engine/types/tasks.py +38 -0
- opentrons/protocol_engine/types/tip.py +9 -0
- opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
- opentrons/protocol_runner/run_orchestrator.py +18 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +48 -15
- opentrons/system/camera.py +1 -1
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/RECORD +143 -127
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/licenses/LICENSE +0 -0
|
@@ -51,6 +51,7 @@ from ..module import (
|
|
|
51
51
|
from .exceptions import InvalidMagnetEngageHeightError
|
|
52
52
|
|
|
53
53
|
from .labware import LabwareCore
|
|
54
|
+
from .tasks import EngineTaskCore
|
|
54
55
|
from . import load_labware_params
|
|
55
56
|
|
|
56
57
|
if TYPE_CHECKING:
|
|
@@ -175,13 +176,17 @@ class TemperatureModuleCore(ModuleCore, AbstractTemperatureModuleCore[LabwareCor
|
|
|
175
176
|
|
|
176
177
|
_sync_module_hardware: SynchronousAdapter[hw_modules.TempDeck]
|
|
177
178
|
|
|
178
|
-
def set_target_temperature(self, celsius: float) ->
|
|
179
|
+
def set_target_temperature(self, celsius: float) -> EngineTaskCore:
|
|
179
180
|
"""Set the Temperature Module's target temperature in °C."""
|
|
180
|
-
self._engine_client.
|
|
181
|
+
result = self._engine_client.execute_command_without_recovery(
|
|
181
182
|
cmd.temperature_module.SetTargetTemperatureParams(
|
|
182
183
|
moduleId=self.module_id, celsius=celsius
|
|
183
184
|
)
|
|
184
185
|
)
|
|
186
|
+
temperature_task = EngineTaskCore(
|
|
187
|
+
engine_client=self._engine_client, task_id=result.taskId
|
|
188
|
+
)
|
|
189
|
+
return temperature_task
|
|
185
190
|
|
|
186
191
|
def wait_for_target_temperature(self, celsius: Optional[float] = None) -> None:
|
|
187
192
|
"""Wait until the module's target temperature is reached.
|
|
@@ -317,18 +322,24 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore[LabwareCore]):
|
|
|
317
322
|
def set_target_block_temperature(
|
|
318
323
|
self,
|
|
319
324
|
celsius: float,
|
|
325
|
+
ramp_rate: Optional[float],
|
|
320
326
|
hold_time_seconds: Optional[float] = None,
|
|
321
327
|
block_max_volume: Optional[float] = None,
|
|
322
|
-
) ->
|
|
328
|
+
) -> EngineTaskCore:
|
|
323
329
|
"""Set the target temperature for the well block, in °C."""
|
|
324
|
-
self._engine_client.
|
|
330
|
+
result = self._engine_client.execute_command_without_recovery(
|
|
325
331
|
cmd.thermocycler.SetTargetBlockTemperatureParams(
|
|
326
332
|
moduleId=self.module_id,
|
|
327
333
|
celsius=celsius,
|
|
328
334
|
blockMaxVolumeUl=block_max_volume,
|
|
329
335
|
holdTimeSeconds=hold_time_seconds,
|
|
336
|
+
ramp_rate=ramp_rate,
|
|
330
337
|
)
|
|
331
338
|
)
|
|
339
|
+
block_temperature_task = EngineTaskCore(
|
|
340
|
+
engine_client=self._engine_client, task_id=result.taskId
|
|
341
|
+
)
|
|
342
|
+
return block_temperature_task
|
|
332
343
|
|
|
333
344
|
def wait_for_block_temperature(self) -> None:
|
|
334
345
|
"""Wait for target block temperature to be reached."""
|
|
@@ -336,13 +347,17 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore[LabwareCore]):
|
|
|
336
347
|
cmd.thermocycler.WaitForBlockTemperatureParams(moduleId=self.module_id)
|
|
337
348
|
)
|
|
338
349
|
|
|
339
|
-
def set_target_lid_temperature(self, celsius: float) ->
|
|
350
|
+
def set_target_lid_temperature(self, celsius: float) -> EngineTaskCore:
|
|
340
351
|
"""Set the target temperature for the heated lid, in °C."""
|
|
341
|
-
self._engine_client.
|
|
352
|
+
result = self._engine_client.execute_command_without_recovery(
|
|
342
353
|
cmd.thermocycler.SetTargetLidTemperatureParams(
|
|
343
354
|
moduleId=self.module_id, celsius=celsius
|
|
344
355
|
)
|
|
345
356
|
)
|
|
357
|
+
lid_temperature_task = EngineTaskCore(
|
|
358
|
+
engine_client=self._engine_client, task_id=result.taskId
|
|
359
|
+
)
|
|
360
|
+
return lid_temperature_task
|
|
346
361
|
|
|
347
362
|
def wait_for_lid_temperature(self) -> None:
|
|
348
363
|
"""Wait for target lid temperature to be reached."""
|
|
@@ -361,6 +376,7 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore[LabwareCore]):
|
|
|
361
376
|
cmd.thermocycler.RunProfileStepParams(
|
|
362
377
|
celsius=step["temperature"],
|
|
363
378
|
holdSeconds=step["hold_time_seconds"],
|
|
379
|
+
rampRate=step["ramp_rate"],
|
|
364
380
|
)
|
|
365
381
|
for step in steps
|
|
366
382
|
]
|
|
@@ -389,6 +405,7 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore[LabwareCore]):
|
|
|
389
405
|
cmd.thermocycler.ProfileStep(
|
|
390
406
|
celsius=step["temperature"],
|
|
391
407
|
holdSeconds=step["hold_time_seconds"],
|
|
408
|
+
rampRate=step["ramp_rate"],
|
|
392
409
|
)
|
|
393
410
|
for step in steps
|
|
394
411
|
],
|
|
@@ -416,6 +433,42 @@ class ThermocyclerModuleCore(ModuleCore, AbstractThermocyclerCore[LabwareCore]):
|
|
|
416
433
|
else:
|
|
417
434
|
return self._execute_profile_pre_221(steps, repetitions, block_max_volume)
|
|
418
435
|
|
|
436
|
+
def start_execute_profile(
|
|
437
|
+
self,
|
|
438
|
+
steps: List[ThermocyclerStep],
|
|
439
|
+
repetitions: int,
|
|
440
|
+
block_max_volume: Optional[float] = None,
|
|
441
|
+
) -> EngineTaskCore:
|
|
442
|
+
"""Start the execution of a hermocycler profile and return a task."""
|
|
443
|
+
self._repetitions = repetitions
|
|
444
|
+
self._step_count = len(steps)
|
|
445
|
+
engine_steps: List[
|
|
446
|
+
Union[cmd.thermocycler.ProfileStep, cmd.thermocycler.ProfileCycle]
|
|
447
|
+
] = [
|
|
448
|
+
cmd.thermocycler.ProfileCycle(
|
|
449
|
+
repetitions=repetitions,
|
|
450
|
+
steps=[
|
|
451
|
+
cmd.thermocycler.ProfileStep(
|
|
452
|
+
celsius=step["temperature"],
|
|
453
|
+
holdSeconds=step["hold_time_seconds"],
|
|
454
|
+
rampRate=step["ramp_rate"],
|
|
455
|
+
)
|
|
456
|
+
for step in steps
|
|
457
|
+
],
|
|
458
|
+
)
|
|
459
|
+
]
|
|
460
|
+
result = self._engine_client.execute_command_without_recovery(
|
|
461
|
+
cmd.thermocycler.StartRunExtendedProfileParams(
|
|
462
|
+
moduleId=self.module_id,
|
|
463
|
+
profileElements=engine_steps,
|
|
464
|
+
blockMaxVolumeUl=block_max_volume,
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
start_execute_profile_result = EngineTaskCore(
|
|
468
|
+
engine_client=self._engine_client, task_id=result.taskId
|
|
469
|
+
)
|
|
470
|
+
return start_execute_profile_result
|
|
471
|
+
|
|
419
472
|
def deactivate_lid(self) -> None:
|
|
420
473
|
"""Turn off the heated lid."""
|
|
421
474
|
self._engine_client.execute_command(
|
|
@@ -507,13 +560,17 @@ class HeaterShakerModuleCore(ModuleCore, AbstractHeaterShakerCore[LabwareCore]):
|
|
|
507
560
|
|
|
508
561
|
_sync_module_hardware: SynchronousAdapter[hw_modules.HeaterShaker]
|
|
509
562
|
|
|
510
|
-
def set_target_temperature(self, celsius: float) ->
|
|
563
|
+
def set_target_temperature(self, celsius: float) -> EngineTaskCore:
|
|
511
564
|
"""Set the labware plate's target temperature in °C."""
|
|
512
|
-
self._engine_client.
|
|
565
|
+
result = self._engine_client.execute_command_without_recovery(
|
|
513
566
|
cmd.heater_shaker.SetTargetTemperatureParams(
|
|
514
567
|
moduleId=self.module_id, celsius=celsius
|
|
515
568
|
)
|
|
516
569
|
)
|
|
570
|
+
temperature_task = EngineTaskCore(
|
|
571
|
+
engine_client=self._engine_client, task_id=result.taskId
|
|
572
|
+
)
|
|
573
|
+
return temperature_task
|
|
517
574
|
|
|
518
575
|
def wait_for_target_temperature(self) -> None:
|
|
519
576
|
"""Wait for the labware plate's target temperature to be reached."""
|
|
@@ -529,6 +586,16 @@ class HeaterShakerModuleCore(ModuleCore, AbstractHeaterShakerCore[LabwareCore]):
|
|
|
529
586
|
)
|
|
530
587
|
)
|
|
531
588
|
|
|
589
|
+
def set_shake_speed(self, rpm: int) -> EngineTaskCore:
|
|
590
|
+
"""Set the shaker's target shake speed and wait for it to spin up."""
|
|
591
|
+
result = self._engine_client.execute_command_without_recovery(
|
|
592
|
+
cmd.heater_shaker.SetShakeSpeedParams(moduleId=self.module_id, rpm=rpm)
|
|
593
|
+
)
|
|
594
|
+
shake_task = EngineTaskCore(
|
|
595
|
+
engine_client=self._engine_client, task_id=result.taskId
|
|
596
|
+
)
|
|
597
|
+
return shake_task
|
|
598
|
+
|
|
532
599
|
def open_labware_latch(self) -> None:
|
|
533
600
|
"""Open the labware latch."""
|
|
534
601
|
self._engine_client.execute_command(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""ProtocolEngine-based Protocol API core implementation."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from typing import Dict, Optional, Type, Union, List, Tuple, TYPE_CHECKING
|
|
4
|
+
from typing import Dict, Optional, Type, Union, List, Tuple, TYPE_CHECKING, Sequence
|
|
5
5
|
|
|
6
6
|
from opentrons_shared_data.liquid_classes import LiquidClassDefinitionDoesNotExist
|
|
7
7
|
from opentrons_shared_data.deck.types import DeckDefinitionV5, SlotDefV3
|
|
@@ -64,6 +64,7 @@ from ...disposal_locations import TrashBin, WasteChute
|
|
|
64
64
|
from ..protocol import AbstractProtocol
|
|
65
65
|
from ..labware import LabwareLoadParams
|
|
66
66
|
from .labware import LabwareCore
|
|
67
|
+
from .tasks import EngineTaskCore
|
|
67
68
|
from .instrument import InstrumentCore
|
|
68
69
|
from .robot import RobotCore
|
|
69
70
|
from .module_core import (
|
|
@@ -95,6 +96,7 @@ class ProtocolCore(
|
|
|
95
96
|
InstrumentCore,
|
|
96
97
|
LabwareCore,
|
|
97
98
|
Union[ModuleCore, NonConnectedModuleCore],
|
|
99
|
+
EngineTaskCore,
|
|
98
100
|
]
|
|
99
101
|
):
|
|
100
102
|
"""Protocol API core using a ProtocolEngine.
|
|
@@ -912,6 +914,21 @@ class ProtocolCore(
|
|
|
912
914
|
cmd.WaitForDurationParams(seconds=seconds, message=msg)
|
|
913
915
|
)
|
|
914
916
|
|
|
917
|
+
def wait_for_tasks(self, task_cores: Sequence[EngineTaskCore]) -> None:
|
|
918
|
+
"""Wait for specified tasks to complete."""
|
|
919
|
+
task_ids = task_ids = [task._id for task in task_cores if task._id is not None]
|
|
920
|
+
self._engine_client.execute_command(cmd.WaitForTasksParams(task_ids=task_ids))
|
|
921
|
+
|
|
922
|
+
def create_timer(self, seconds: float) -> EngineTaskCore:
|
|
923
|
+
"""Create a timer task that runs in the background."""
|
|
924
|
+
result = self._engine_client.execute_command_without_recovery(
|
|
925
|
+
cmd.CreateTimerParams(time=seconds)
|
|
926
|
+
)
|
|
927
|
+
timer_task = EngineTaskCore(
|
|
928
|
+
engine_client=self._engine_client, task_id=result.task_id
|
|
929
|
+
)
|
|
930
|
+
return timer_task
|
|
931
|
+
|
|
915
932
|
def home(self) -> None:
|
|
916
933
|
"""Move all axes to their home positions."""
|
|
917
934
|
self._engine_client.execute_command(cmd.HomeParams(axes=None))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from ..tasks import AbstractTaskCore
|
|
3
|
+
from opentrons.protocol_engine.clients import SyncClient as ProtocolEngineClient
|
|
4
|
+
from opentrons.protocol_engine.errors.exceptions import NoTaskFoundError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EngineTaskCore(AbstractTaskCore):
|
|
8
|
+
def __init__(
|
|
9
|
+
self, task_id: str | None, engine_client: ProtocolEngineClient
|
|
10
|
+
) -> None:
|
|
11
|
+
self._id = task_id
|
|
12
|
+
self._engine_client = engine_client
|
|
13
|
+
|
|
14
|
+
def get_created_at_timestamp(self) -> datetime:
|
|
15
|
+
if self._id is None:
|
|
16
|
+
raise NoTaskFoundError
|
|
17
|
+
try:
|
|
18
|
+
task = self._engine_client.state.tasks.get(self._id)
|
|
19
|
+
return task.createdAt
|
|
20
|
+
except NoTaskFoundError:
|
|
21
|
+
raise NoTaskFoundError
|
|
22
|
+
|
|
23
|
+
def is_done(self) -> bool:
|
|
24
|
+
if self._id is None:
|
|
25
|
+
raise NoTaskFoundError
|
|
26
|
+
try:
|
|
27
|
+
self._engine_client.state.tasks.get_finished(self._id)
|
|
28
|
+
return True
|
|
29
|
+
except NoTaskFoundError:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
def is_started(self) -> bool:
|
|
33
|
+
if self._id is None:
|
|
34
|
+
raise NoTaskFoundError
|
|
35
|
+
try:
|
|
36
|
+
self._engine_client.state.tasks.get_current(self._id)
|
|
37
|
+
return True
|
|
38
|
+
except NoTaskFoundError:
|
|
39
|
+
return self.is_done()
|
|
40
|
+
|
|
41
|
+
def get_finished_at_timestamp(self) -> datetime | None:
|
|
42
|
+
if self._id is None:
|
|
43
|
+
raise NoTaskFoundError
|
|
44
|
+
try:
|
|
45
|
+
finished_task = self._engine_client.state.tasks.get_finished(self._id)
|
|
46
|
+
return finished_task.finishedAt
|
|
47
|
+
except NoTaskFoundError:
|
|
48
|
+
return None
|
|
@@ -216,6 +216,14 @@ class WellCore(AbstractWellCore):
|
|
|
216
216
|
labware_id=labware_id, well_name=well_name
|
|
217
217
|
)
|
|
218
218
|
|
|
219
|
+
def has_tracked_liquid(self) -> bool:
|
|
220
|
+
"""Return true if liquid has been loaded or probed."""
|
|
221
|
+
labware_id = self.labware_id
|
|
222
|
+
well_name = self._name
|
|
223
|
+
return self._engine_client.state.geometry.well_has_tracked_liquid(
|
|
224
|
+
labware_id=labware_id, well_name=well_name
|
|
225
|
+
)
|
|
226
|
+
|
|
219
227
|
def get_liquid_volume(self) -> LiquidTrackingType:
|
|
220
228
|
"""Return the current volume in a well."""
|
|
221
229
|
labware_id = self.labware_id
|
|
@@ -31,6 +31,7 @@ from ..module import (
|
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
from .legacy_labware_core import LegacyLabwareCore
|
|
34
|
+
from .tasks import LegacyTaskCore
|
|
34
35
|
from .module_geometry import ModuleGeometry, ThermocyclerGeometry, HeaterShakerGeometry
|
|
35
36
|
from ...labware import Labware
|
|
36
37
|
|
|
@@ -116,9 +117,10 @@ class LegacyTemperatureModuleCore(
|
|
|
116
117
|
|
|
117
118
|
_sync_module_hardware: SynchronousAdapter[hw_modules.TempDeck]
|
|
118
119
|
|
|
119
|
-
def set_target_temperature(self, celsius: float) ->
|
|
120
|
+
def set_target_temperature(self, celsius: float) -> LegacyTaskCore:
|
|
120
121
|
"""Set the Temperature Module's target temperature in °C."""
|
|
121
122
|
self._sync_module_hardware.start_set_temperature(celsius)
|
|
123
|
+
return LegacyTaskCore()
|
|
122
124
|
|
|
123
125
|
def wait_for_target_temperature(self, celsius: Optional[float] = None) -> None:
|
|
124
126
|
"""Wait until the module's target temperature is reached.
|
|
@@ -263,23 +265,27 @@ class LegacyThermocyclerCore(
|
|
|
263
265
|
def set_target_block_temperature(
|
|
264
266
|
self,
|
|
265
267
|
celsius: float,
|
|
268
|
+
ramp_rate: Optional[float],
|
|
266
269
|
hold_time_seconds: Optional[float] = None,
|
|
267
270
|
block_max_volume: Optional[float] = None,
|
|
268
|
-
) ->
|
|
271
|
+
) -> LegacyTaskCore:
|
|
269
272
|
"""Set the target temperature for the well block, in °C."""
|
|
270
273
|
self._sync_module_hardware.set_target_block_temperature(
|
|
271
274
|
celsius=celsius,
|
|
272
275
|
hold_time_seconds=hold_time_seconds,
|
|
273
276
|
volume=block_max_volume,
|
|
277
|
+
ramp_rate=ramp_rate,
|
|
274
278
|
)
|
|
279
|
+
return LegacyTaskCore()
|
|
275
280
|
|
|
276
281
|
def wait_for_block_temperature(self) -> None:
|
|
277
282
|
"""Wait for target block temperature to be reached."""
|
|
278
283
|
self._sync_module_hardware.wait_for_block_target()
|
|
279
284
|
|
|
280
|
-
def set_target_lid_temperature(self, celsius: float) ->
|
|
285
|
+
def set_target_lid_temperature(self, celsius: float) -> LegacyTaskCore:
|
|
281
286
|
"""Set the target temperature for the heated lid, in °C."""
|
|
282
287
|
self._sync_module_hardware.set_target_lid_temperature(celsius=celsius)
|
|
288
|
+
return LegacyTaskCore()
|
|
283
289
|
|
|
284
290
|
def wait_for_lid_temperature(self) -> None:
|
|
285
291
|
"""Wait for target lid temperature to be reached."""
|
|
@@ -296,6 +302,15 @@ class LegacyThermocyclerCore(
|
|
|
296
302
|
steps=steps, repetitions=repetitions, volume=block_max_volume
|
|
297
303
|
)
|
|
298
304
|
|
|
305
|
+
def start_execute_profile(
|
|
306
|
+
self,
|
|
307
|
+
steps: List[ThermocyclerStep],
|
|
308
|
+
repetitions: int,
|
|
309
|
+
block_max_volume: Optional[float] = None,
|
|
310
|
+
) -> LegacyTaskCore:
|
|
311
|
+
"""Start a Thermocycler Profile."""
|
|
312
|
+
assert False, "start_execute_profile only supported on engine core"
|
|
313
|
+
|
|
299
314
|
def deactivate_lid(self) -> None:
|
|
300
315
|
"""Turn off the heated lid."""
|
|
301
316
|
self._sync_module_hardware.deactivate_lid()
|
|
@@ -413,9 +428,10 @@ class LegacyHeaterShakerCore(
|
|
|
413
428
|
_sync_module_hardware: SynchronousAdapter[hw_modules.HeaterShaker]
|
|
414
429
|
_geometry: HeaterShakerGeometry
|
|
415
430
|
|
|
416
|
-
def set_target_temperature(self, celsius: float) ->
|
|
431
|
+
def set_target_temperature(self, celsius: float) -> LegacyTaskCore:
|
|
417
432
|
"""Set the labware plate's target temperature in °C."""
|
|
418
433
|
self._sync_module_hardware.start_set_temperature(celsius)
|
|
434
|
+
return LegacyTaskCore()
|
|
419
435
|
|
|
420
436
|
def wait_for_target_temperature(self) -> None:
|
|
421
437
|
"""Wait for the labware plate's target temperature to be reached."""
|
|
@@ -442,6 +458,10 @@ class LegacyHeaterShakerCore(
|
|
|
442
458
|
"Cannot start shaking unless labware latch is closed."
|
|
443
459
|
)
|
|
444
460
|
|
|
461
|
+
def set_shake_speed(self, rpm: int) -> LegacyTaskCore:
|
|
462
|
+
"""Set heatershaker speed."""
|
|
463
|
+
assert False, "set_shake_speed only supported on engine core."
|
|
464
|
+
|
|
445
465
|
def open_labware_latch(self) -> None:
|
|
446
466
|
"""Open the labware latch."""
|
|
447
467
|
if self.get_speed_status() != SpeedStatus.IDLE:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Dict, List, Optional, Set, Union, cast, Tuple
|
|
2
|
+
from typing import Dict, List, Optional, Set, Union, cast, Tuple, Sequence
|
|
3
3
|
|
|
4
4
|
from opentrons_shared_data.deck.types import DeckDefinitionV5, SlotDefV3
|
|
5
5
|
from opentrons_shared_data.labware.types import LabwareDefinition
|
|
@@ -34,6 +34,7 @@ from .legacy_instrument_core import LegacyInstrumentCore
|
|
|
34
34
|
from .labware_offset_provider import AbstractLabwareOffsetProvider
|
|
35
35
|
from .legacy_labware_core import LegacyLabwareCore
|
|
36
36
|
from .load_info import LoadInfo, InstrumentLoadInfo, LabwareLoadInfo, ModuleLoadInfo
|
|
37
|
+
from .tasks import LegacyTaskCore
|
|
37
38
|
|
|
38
39
|
logger = logging.getLogger(__name__)
|
|
39
40
|
|
|
@@ -43,6 +44,7 @@ class LegacyProtocolCore(
|
|
|
43
44
|
LegacyInstrumentCore,
|
|
44
45
|
LegacyLabwareCore,
|
|
45
46
|
legacy_module_core.LegacyModuleCore,
|
|
47
|
+
LegacyTaskCore,
|
|
46
48
|
]
|
|
47
49
|
):
|
|
48
50
|
def __init__(
|
|
@@ -610,3 +612,11 @@ class LegacyProtocolCore(
|
|
|
610
612
|
]:
|
|
611
613
|
"""Get labware parent location."""
|
|
612
614
|
assert False, "get_labware_location only supported on engine core"
|
|
615
|
+
|
|
616
|
+
def wait_for_tasks(self, task: Sequence[LegacyTaskCore]) -> None:
|
|
617
|
+
"""Wait for list of tasks to complete before executing subsequent commands."""
|
|
618
|
+
assert False, "wait_for_tasks only supported on engine core"
|
|
619
|
+
|
|
620
|
+
def create_timer(self, seconds: float) -> LegacyTaskCore:
|
|
621
|
+
"""Create a timer task that runs in the background."""
|
|
622
|
+
assert False, "create_timer only supported on engine core"
|
|
@@ -151,6 +151,10 @@ class LegacyWellCore(AbstractWellCore):
|
|
|
151
151
|
"""Return the volume contained in a well at any height."""
|
|
152
152
|
return 0.0
|
|
153
153
|
|
|
154
|
+
def has_tracked_liquid(self) -> bool:
|
|
155
|
+
"""Return true if liquid has been loaded or probed."""
|
|
156
|
+
return False
|
|
157
|
+
|
|
154
158
|
# TODO(mc, 2022-10-28): is this used and/or necessary?
|
|
155
159
|
def __repr__(self) -> str:
|
|
156
160
|
"""Use the well's display name as its repr."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ..tasks import AbstractTaskCore
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LegacyTaskCore(AbstractTaskCore):
|
|
6
|
+
def __init__(self) -> None:
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
def get_created_at_timestamp(self) -> datetime:
|
|
10
|
+
raise NotImplementedError("Legacy protocols do not implement tasks.")
|
|
11
|
+
|
|
12
|
+
def is_done(self) -> bool:
|
|
13
|
+
raise NotImplementedError("Legacy protocols do not implement tasks.")
|
|
14
|
+
|
|
15
|
+
def is_started(self) -> bool:
|
|
16
|
+
raise NotImplementedError("Legacy protocols do not implement tasks.")
|
|
17
|
+
|
|
18
|
+
def get_finished_at_timestamp(self) -> datetime | None:
|
|
19
|
+
raise NotImplementedError("Legacy protocols do not implement tasks.")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Dict, Optional
|
|
2
|
+
from typing import Dict, Optional, Sequence
|
|
3
3
|
|
|
4
4
|
from opentrons_shared_data.pipette.types import PipetteNameType
|
|
5
5
|
from opentrons_shared_data.pipette.pipette_load_name_conversions import (
|
|
@@ -16,6 +16,7 @@ from ..legacy.legacy_module_core import LegacyModuleCore
|
|
|
16
16
|
from ..legacy.load_info import InstrumentLoadInfo
|
|
17
17
|
|
|
18
18
|
from .legacy_instrument_core import LegacyInstrumentCoreSimulator
|
|
19
|
+
from .tasks import LegacyTaskCore
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
@@ -23,7 +24,10 @@ logger = logging.getLogger(__name__)
|
|
|
23
24
|
class LegacyProtocolCoreSimulator(
|
|
24
25
|
LegacyProtocolCore,
|
|
25
26
|
AbstractProtocol[
|
|
26
|
-
LegacyInstrumentCoreSimulator,
|
|
27
|
+
LegacyInstrumentCoreSimulator,
|
|
28
|
+
LegacyLabwareCore,
|
|
29
|
+
LegacyModuleCore,
|
|
30
|
+
LegacyTaskCore,
|
|
27
31
|
],
|
|
28
32
|
):
|
|
29
33
|
_instruments: Dict[Mount, Optional[LegacyInstrumentCoreSimulator]] # type: ignore[assignment]
|
|
@@ -83,3 +87,11 @@ class LegacyProtocolCoreSimulator(
|
|
|
83
87
|
)
|
|
84
88
|
|
|
85
89
|
return new_instr
|
|
90
|
+
|
|
91
|
+
def wait_for_tasks(self, task: Sequence[LegacyTaskCore]) -> None:
|
|
92
|
+
"""Wait for list of tasks to complete before executing subsequent commands."""
|
|
93
|
+
assert False, "wait_for_tasks only supported on engine core"
|
|
94
|
+
|
|
95
|
+
def create_timer(self, seconds: float) -> LegacyTaskCore:
|
|
96
|
+
"""Create a timer task that runs in the background."""
|
|
97
|
+
assert False, "create_timer only supported on engine core"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ..tasks import AbstractTaskCore
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LegacyTaskCore(AbstractTaskCore):
|
|
6
|
+
def __init__(self, created_at: datetime) -> None:
|
|
7
|
+
raise NotImplementedError("Legacy protocols do not implement tasks.")
|
|
8
|
+
|
|
9
|
+
def get_created_at_timestamp(self) -> datetime:
|
|
10
|
+
raise NotImplementedError("Legacy protocols do not implement tasks.")
|
|
11
|
+
|
|
12
|
+
def is_done(self) -> bool:
|
|
13
|
+
raise NotImplementedError("Legacy protocols do not implement tasks.")
|
|
14
|
+
|
|
15
|
+
def is_started(self) -> bool:
|
|
16
|
+
raise NotImplementedError("Legacy protocols do not implement tasks.")
|
|
17
|
+
|
|
18
|
+
def get_finished_at_timestamp(self) -> datetime | None:
|
|
19
|
+
raise NotImplementedError("Legacy protocols do not implement tasks.")
|
|
@@ -18,6 +18,7 @@ from opentrons.hardware_control.modules.types import (
|
|
|
18
18
|
SpeedStatus,
|
|
19
19
|
)
|
|
20
20
|
from .labware import LabwareCoreType, AbstractLabware
|
|
21
|
+
from .tasks import AbstractTaskCore
|
|
21
22
|
from opentrons.protocol_engine.types import ABSMeasureMode
|
|
22
23
|
from opentrons.types import DeckSlotName
|
|
23
24
|
|
|
@@ -59,7 +60,7 @@ class AbstractTemperatureModuleCore(
|
|
|
59
60
|
"""Get the module's unique hardware serial number."""
|
|
60
61
|
|
|
61
62
|
@abstractmethod
|
|
62
|
-
def set_target_temperature(self, celsius: float) ->
|
|
63
|
+
def set_target_temperature(self, celsius: float) -> AbstractTaskCore:
|
|
63
64
|
"""Set the Temperature Module's target temperature in °C."""
|
|
64
65
|
|
|
65
66
|
@abstractmethod
|
|
@@ -161,9 +162,10 @@ class AbstractThermocyclerCore(
|
|
|
161
162
|
def set_target_block_temperature(
|
|
162
163
|
self,
|
|
163
164
|
celsius: float,
|
|
165
|
+
ramp_rate: Optional[float],
|
|
164
166
|
hold_time_seconds: Optional[float] = None,
|
|
165
167
|
block_max_volume: Optional[float] = None,
|
|
166
|
-
) ->
|
|
168
|
+
) -> AbstractTaskCore:
|
|
167
169
|
"""Set the target temperature for the well block, in °C.
|
|
168
170
|
|
|
169
171
|
Note:
|
|
@@ -183,7 +185,7 @@ class AbstractThermocyclerCore(
|
|
|
183
185
|
"""Wait for target block temperature to be reached."""
|
|
184
186
|
|
|
185
187
|
@abstractmethod
|
|
186
|
-
def set_target_lid_temperature(self, celsius: float) ->
|
|
188
|
+
def set_target_lid_temperature(self, celsius: float) -> AbstractTaskCore:
|
|
187
189
|
"""Set the target temperature for the heated lid, in °C."""
|
|
188
190
|
|
|
189
191
|
@abstractmethod
|
|
@@ -217,6 +219,33 @@ class AbstractThermocyclerCore(
|
|
|
217
219
|
will default to 25µL/well.
|
|
218
220
|
"""
|
|
219
221
|
|
|
222
|
+
@abstractmethod
|
|
223
|
+
def start_execute_profile(
|
|
224
|
+
self,
|
|
225
|
+
steps: List[ThermocyclerStep],
|
|
226
|
+
repetitions: int,
|
|
227
|
+
block_max_volume: Optional[float] = None,
|
|
228
|
+
) -> AbstractTaskCore:
|
|
229
|
+
"""Start a Thermocycler Profile.
|
|
230
|
+
|
|
231
|
+
Profile defined as a cycle of ``steps`` to repeat for a given number of ``repetitions``
|
|
232
|
+
|
|
233
|
+
Note:
|
|
234
|
+
Unlike the :py:meth:`execute_profile`, once the profile has started
|
|
235
|
+
the protocol will immediately move on to the next command, rather than waiting
|
|
236
|
+
for it to finish.
|
|
237
|
+
Args:
|
|
238
|
+
steps: List of unique steps that make up a single cycle.
|
|
239
|
+
Each list item should be a dictionary that maps to
|
|
240
|
+
the parameters of the :py:meth:`set_block_temperature`
|
|
241
|
+
method with keys 'temperature', 'hold_time_seconds',
|
|
242
|
+
and 'hold_time_minutes'.
|
|
243
|
+
repetitions: The number of times to repeat the cycled steps.
|
|
244
|
+
block_max_volume: The maximum volume of any individual well
|
|
245
|
+
of the loaded labware. If not supplied, the thermocycler
|
|
246
|
+
will default to 25µL/well.
|
|
247
|
+
"""
|
|
248
|
+
|
|
220
249
|
@abstractmethod
|
|
221
250
|
def deactivate_lid(self) -> None:
|
|
222
251
|
"""Turn off the heated lid."""
|
|
@@ -294,7 +323,7 @@ class AbstractHeaterShakerCore(
|
|
|
294
323
|
"""Get the module's unique hardware serial number."""
|
|
295
324
|
|
|
296
325
|
@abstractmethod
|
|
297
|
-
def set_target_temperature(self, celsius: float) ->
|
|
326
|
+
def set_target_temperature(self, celsius: float) -> AbstractTaskCore:
|
|
298
327
|
"""Set the labware plate's target temperature in °C."""
|
|
299
328
|
|
|
300
329
|
@abstractmethod
|
|
@@ -305,6 +334,10 @@ class AbstractHeaterShakerCore(
|
|
|
305
334
|
def set_and_wait_for_shake_speed(self, rpm: int) -> None:
|
|
306
335
|
"""Set the shaker's target shake speed and wait for it to spin up."""
|
|
307
336
|
|
|
337
|
+
@abstractmethod
|
|
338
|
+
def set_shake_speed(self, rpm: int) -> AbstractTaskCore:
|
|
339
|
+
"""Set the shaker's target shake speed."""
|
|
340
|
+
|
|
308
341
|
@abstractmethod
|
|
309
342
|
def open_labware_latch(self) -> None:
|
|
310
343
|
"""Open the labware latch."""
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from abc import abstractmethod, ABC
|
|
6
|
-
from typing import Generic, List, Optional, Union, Tuple, Dict, TYPE_CHECKING
|
|
6
|
+
from typing import Generic, List, Optional, Union, Tuple, Dict, TYPE_CHECKING, Sequence
|
|
7
7
|
|
|
8
8
|
from opentrons_shared_data.deck.types import DeckDefinitionV5, SlotDefV3
|
|
9
9
|
from opentrons_shared_data.pipette.types import PipetteNameType
|
|
@@ -24,6 +24,7 @@ from opentrons.protocols.api_support.util import AxisMaxSpeeds
|
|
|
24
24
|
from .instrument import InstrumentCoreType
|
|
25
25
|
from .labware import LabwareCoreType, LabwareLoadParams
|
|
26
26
|
from .module import ModuleCoreType
|
|
27
|
+
from .tasks import TaskCoreType
|
|
27
28
|
from .._liquid import Liquid, LiquidClass
|
|
28
29
|
from .robot import AbstractRobot
|
|
29
30
|
from .._types import OffDeckType
|
|
@@ -34,7 +35,7 @@ if TYPE_CHECKING:
|
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
class AbstractProtocol(
|
|
37
|
-
ABC, Generic[InstrumentCoreType, LabwareCoreType, ModuleCoreType]
|
|
38
|
+
ABC, Generic[InstrumentCoreType, LabwareCoreType, ModuleCoreType, TaskCoreType]
|
|
38
39
|
):
|
|
39
40
|
@property
|
|
40
41
|
@abstractmethod
|
|
@@ -192,6 +193,14 @@ class AbstractProtocol(
|
|
|
192
193
|
def delay(self, seconds: float, msg: Optional[str]) -> None:
|
|
193
194
|
...
|
|
194
195
|
|
|
196
|
+
@abstractmethod
|
|
197
|
+
def wait_for_tasks(self, task_cores: Sequence[TaskCoreType]) -> None:
|
|
198
|
+
...
|
|
199
|
+
|
|
200
|
+
@abstractmethod
|
|
201
|
+
def create_timer(self, seconds: float) -> TaskCoreType:
|
|
202
|
+
...
|
|
203
|
+
|
|
195
204
|
@abstractmethod
|
|
196
205
|
def home(self) -> None:
|
|
197
206
|
...
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from abc import abstractmethod, ABC
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AbstractTaskCore(ABC):
|
|
7
|
+
@abstractmethod
|
|
8
|
+
def get_created_at_timestamp(self) -> datetime:
|
|
9
|
+
"""Get the createdAt timestamp of the task."""
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def is_done(self) -> bool:
|
|
14
|
+
"""Returns ``True`` if the task is done."""
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def is_started(self) -> bool:
|
|
19
|
+
"""Returns ``True`` if the task has started."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def get_finished_at_timestamp(self) -> datetime | None:
|
|
24
|
+
"""The timestamp of the when the task finished.
|
|
25
|
+
|
|
26
|
+
Returns ``None`` if the task hasn't finished yet.
|
|
27
|
+
"""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
TaskCoreType = TypeVar("TaskCoreType", bound=AbstractTaskCore)
|
|
@@ -112,5 +112,9 @@ class AbstractWellCore(ABC):
|
|
|
112
112
|
def volume_from_height(self, height: LiquidTrackingType) -> LiquidTrackingType:
|
|
113
113
|
"""Return the volume contained in a well at any height."""
|
|
114
114
|
|
|
115
|
+
@abstractmethod
|
|
116
|
+
def has_tracked_liquid(self) -> bool:
|
|
117
|
+
"""Return true if liquid has been loaded or probed."""
|
|
118
|
+
|
|
115
119
|
|
|
116
120
|
WellCoreType = TypeVar("WellCoreType", bound=AbstractWellCore)
|