-
Notifications
You must be signed in to change notification settings - Fork 179
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(engine): thermocycler run profile (#10921)
Adds thermocycler runProfile command to Protocol Engine and jsonv6 Co-authored-by: Shlok Amin <[email protected]>
- Loading branch information
Showing
10 changed files
with
244 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
111 changes: 111 additions & 0 deletions
111
api/src/opentrons/protocol_engine/commands/thermocycler/run_profile.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
"""Command models to execute a Thermocycler profile.""" | ||
from __future__ import annotations | ||
from typing import List, Optional, TYPE_CHECKING | ||
from typing_extensions import Literal, Type | ||
|
||
from pydantic import BaseModel, Field | ||
|
||
from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate | ||
|
||
if TYPE_CHECKING: | ||
from opentrons.protocol_engine.state import StateView | ||
from opentrons.protocol_engine.execution import EquipmentHandler | ||
|
||
|
||
RunProfileCommandType = Literal["thermocycler/runProfile"] | ||
|
||
|
||
class RunProfileStepParams(BaseModel): | ||
"""Input parameters for an individual Thermocycler profile step.""" | ||
|
||
celsius: float = Field(..., description="Target temperature in °C.") | ||
holdSeconds: int = Field( | ||
..., description="Time to hold target temperature at in seconds." | ||
) | ||
|
||
|
||
class RunProfileParams(BaseModel): | ||
"""Input parameters to run a Thermocycler profile.""" | ||
|
||
moduleId: str = Field(..., description="Unique ID of the Thermocycler.") | ||
profile: List[RunProfileStepParams] = Field( | ||
..., | ||
description="Array of profile steps with target temperature and temperature hold time.", | ||
) | ||
blockMaxVolumeUl: Optional[float] = Field( | ||
None, | ||
description="Amount of liquid in uL of the most-full well" | ||
" in labware loaded onto the thermocycler.", | ||
) | ||
|
||
|
||
class RunProfileResult(BaseModel): | ||
"""Result data from running a Thermocycler profile.""" | ||
|
||
|
||
class RunProfileImpl(AbstractCommandImpl[RunProfileParams, RunProfileResult]): | ||
"""Execution implementation of a Thermocycler's run profile command.""" | ||
|
||
def __init__( | ||
self, | ||
state_view: StateView, | ||
equipment: EquipmentHandler, | ||
**unused_dependencies: object, | ||
) -> None: | ||
self._state_view = state_view | ||
self._equipment = equipment | ||
|
||
async def execute(self, params: RunProfileParams) -> RunProfileResult: | ||
"""Run a Thermocycler profile.""" | ||
thermocycler_state = self._state_view.modules.get_thermocycler_module_substate( | ||
params.moduleId | ||
) | ||
thermocycler_hardware = self._equipment.get_module_hardware_api( | ||
thermocycler_state.module_id | ||
) | ||
|
||
steps = [ | ||
{ | ||
"temperature": thermocycler_state.validate_target_block_temperature( | ||
profile_step.celsius | ||
), | ||
"hold_time_seconds": profile_step.holdSeconds, | ||
} | ||
for profile_step in params.profile | ||
] | ||
|
||
target_volume: Optional[float] | ||
if params.blockMaxVolumeUl is not None: | ||
target_volume = thermocycler_state.validate_max_block_volume( | ||
params.blockMaxVolumeUl | ||
) | ||
else: | ||
target_volume = None | ||
|
||
if thermocycler_hardware is not None: | ||
# TODO(jbl 2022-06-27) hardcoded constant 1 for `repetitions` should be | ||
# moved from HardwareControlAPI to the Python ProtocolContext | ||
await thermocycler_hardware.cycle_temperatures( | ||
steps=steps, repetitions=1, volume=target_volume | ||
) | ||
|
||
return RunProfileResult() | ||
|
||
|
||
class RunProfile(BaseCommand[RunProfileParams, RunProfileResult]): | ||
"""A command to execute a Thermocycler profile run.""" | ||
|
||
commandType: RunProfileCommandType = "thermocycler/runProfile" | ||
params: RunProfileParams | ||
result: Optional[RunProfileResult] | ||
|
||
_ImplementationCls: Type[RunProfileImpl] = RunProfileImpl | ||
|
||
|
||
class RunProfileCreate(BaseCommandCreate[RunProfileParams]): | ||
"""A request to execute a Thermocycler profile run.""" | ||
|
||
commandType: RunProfileCommandType = "thermocycler/runProfile" | ||
params: RunProfileParams | ||
|
||
_CommandCls: Type[RunProfile] = RunProfile |
77 changes: 77 additions & 0 deletions
77
api/tests/opentrons/protocol_engine/commands/thermocycler/test_run_profile.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
"""Test Thermocycler run profile command implementation.""" | ||
from decoy import Decoy | ||
|
||
from opentrons.hardware_control.modules import Thermocycler | ||
|
||
from opentrons.protocol_engine.state import StateView | ||
from opentrons.protocol_engine.state.module_substates import ( | ||
ThermocyclerModuleSubState, | ||
ThermocyclerModuleId, | ||
) | ||
from opentrons.protocol_engine.execution import EquipmentHandler | ||
from opentrons.protocol_engine.commands import thermocycler as tc_commands | ||
from opentrons.protocol_engine.commands.thermocycler.run_profile import ( | ||
RunProfileImpl, | ||
) | ||
|
||
|
||
async def test_run_profile( | ||
decoy: Decoy, | ||
state_view: StateView, | ||
equipment: EquipmentHandler, | ||
) -> None: | ||
"""It should be able to execute the specified module's profile run.""" | ||
subject = RunProfileImpl(state_view=state_view, equipment=equipment) | ||
|
||
step_data = [ | ||
tc_commands.RunProfileStepParams(celsius=12.3, holdSeconds=45), | ||
tc_commands.RunProfileStepParams(celsius=45.6, holdSeconds=78), | ||
] | ||
data = tc_commands.RunProfileParams( | ||
moduleId="input-thermocycler-id", | ||
profile=step_data, | ||
blockMaxVolumeUl=56.7, | ||
) | ||
expected_result = tc_commands.RunProfileResult() | ||
|
||
tc_module_substate = decoy.mock(cls=ThermocyclerModuleSubState) | ||
tc_hardware = decoy.mock(cls=Thermocycler) | ||
|
||
decoy.when( | ||
state_view.modules.get_thermocycler_module_substate("input-thermocycler-id") | ||
).then_return(tc_module_substate) | ||
|
||
decoy.when(tc_module_substate.module_id).then_return( | ||
ThermocyclerModuleId("thermocycler-id") | ||
) | ||
|
||
# Stub temperature validation from hs module view | ||
decoy.when(tc_module_substate.validate_target_block_temperature(12.3)).then_return( | ||
32.1 | ||
) | ||
decoy.when(tc_module_substate.validate_target_block_temperature(45.6)).then_return( | ||
65.4 | ||
) | ||
|
||
# Stub volume validation from hs module view | ||
decoy.when(tc_module_substate.validate_max_block_volume(56.7)).then_return(76.5) | ||
|
||
# Get attached hardware modules | ||
decoy.when( | ||
equipment.get_module_hardware_api(ThermocyclerModuleId("thermocycler-id")) | ||
).then_return(tc_hardware) | ||
|
||
result = await subject.execute(data) | ||
|
||
decoy.verify( | ||
await tc_hardware.cycle_temperatures( | ||
steps=[ | ||
{"temperature": 32.1, "hold_time_seconds": 45}, | ||
{"temperature": 65.4, "hold_time_seconds": 78}, | ||
], | ||
repetitions=1, | ||
volume=76.5, | ||
), | ||
times=1, | ||
) | ||
assert result == expected_result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.