Skip to content

Commit f2c2db9

Browse files
sanni-tb-cooper
andauthored
fix(api, app): thermocycler hold time on API v2.14 (#12814)
* added holdTimeSeconds param to TC setTargetBlockTemperature, wired up to TC module context in API * updated setTargetBlockTemperature in v7 schema * updated app run log command text for setTargetBlockTemperature & waitForBlockTemperature --------- Co-authored-by: Brian Cooper <[email protected]>
1 parent 092e4e1 commit f2c2db9

File tree

15 files changed

+118
-14
lines changed

15 files changed

+118
-14
lines changed

api/src/opentrons/protocol_api/core/engine/module_core.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,10 @@ def set_target_block_temperature(
225225
) -> None:
226226
"""Set the target temperature for the well block, in °C."""
227227
self._engine_client.thermocycler_set_target_block_temperature(
228-
module_id=self.module_id, celsius=celsius, block_max_volume=block_max_volume
228+
module_id=self.module_id,
229+
celsius=celsius,
230+
block_max_volume=block_max_volume,
231+
hold_time_seconds=hold_time_seconds,
229232
)
230233

231234
def wait_for_block_temperature(self) -> None:

api/src/opentrons/protocol_engine/clients/sync_client.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,12 +465,19 @@ def thermocycler_set_target_lid_temperature(
465465
return cast(commands.thermocycler.SetTargetLidTemperatureResult, result)
466466

467467
def thermocycler_set_target_block_temperature(
468-
self, module_id: str, celsius: float, block_max_volume: Optional[float]
468+
self,
469+
module_id: str,
470+
celsius: float,
471+
block_max_volume: Optional[float],
472+
hold_time_seconds: Optional[float],
469473
) -> commands.thermocycler.SetTargetBlockTemperatureResult:
470474
"""Execute a `thermocycler/setTargetLidTemperature` command and return the result."""
471475
request = commands.thermocycler.SetTargetBlockTemperatureCreate(
472476
params=commands.thermocycler.SetTargetBlockTemperatureParams(
473-
moduleId=module_id, celsius=celsius, blockMaxVolumeUl=block_max_volume
477+
moduleId=module_id,
478+
celsius=celsius,
479+
blockMaxVolumeUl=block_max_volume,
480+
holdTimeSeconds=hold_time_seconds,
474481
)
475482
)
476483
result = self._transport.execute_command(request=request)

api/src/opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ class SetTargetBlockTemperatureParams(BaseModel):
2525
description="Amount of liquid in uL of the most-full well"
2626
" in labware loaded onto the thermocycler.",
2727
)
28+
holdTimeSeconds: Optional[float] = Field(
29+
None,
30+
description="Amount of time, in seconds, to hold the temperature for."
31+
" If specified, a waitForBlockTemperature command will block until"
32+
" the given hold time has elapsed.",
33+
)
2834

2935

3036
class SetTargetBlockTemperatureResult(BaseModel):
@@ -71,13 +77,19 @@ async def execute(
7177
)
7278
else:
7379
target_volume = None
80+
hold_time: Optional[float]
81+
if params.holdTimeSeconds is not None:
82+
hold_time = thermocycler_state.validate_hold_time(params.holdTimeSeconds)
83+
else:
84+
hold_time = None
85+
7486
thermocycler_hardware = self._equipment.get_module_hardware_api(
7587
thermocycler_state.module_id
7688
)
7789

7890
if thermocycler_hardware is not None:
7991
await thermocycler_hardware.set_target_block_temperature(
80-
target_temperature, volume=target_volume
92+
target_temperature, volume=target_volume, hold_time_seconds=hold_time
8193
)
8294

8395
return SetTargetBlockTemperatureResult(

api/src/opentrons/protocol_engine/errors/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
InvalidTargetSpeedError,
4040
InvalidTargetTemperatureError,
4141
InvalidBlockVolumeError,
42+
InvalidHoldTimeError,
4243
CannotPerformModuleAction,
4344
PauseNotAllowedError,
4445
ProtocolCommandFailedError,
@@ -90,6 +91,7 @@
9091
"InvalidTargetTemperatureError",
9192
"InvalidTargetSpeedError",
9293
"InvalidBlockVolumeError",
94+
"InvalidHoldTimeError",
9395
"CannotPerformModuleAction",
9496
"PauseNotAllowedError",
9597
"ProtocolCommandFailedError",

api/src/opentrons/protocol_engine/errors/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ class InvalidBlockVolumeError(ProtocolEngineError):
180180
"""An error raised when attempting to set an invalid block max volume."""
181181

182182

183+
class InvalidHoldTimeError(ProtocolEngineError):
184+
"""An error raised when attempting to set an invalid temperature hold time."""
185+
186+
183187
class InvalidTargetSpeedError(ProtocolEngineError):
184188
"""An error raised when attempting to set an invalid target speed."""
185189

api/src/opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
InvalidTargetTemperatureError,
77
InvalidBlockVolumeError,
88
NoTargetTemperatureSetError,
9+
InvalidHoldTimeError,
910
)
1011

1112
# TODO(mc, 2022-04-25): move to module definition
@@ -79,6 +80,26 @@ def validate_max_block_volume(volume: float) -> float:
7980
f" {BLOCK_VOL_MIN} and {BLOCK_VOL_MAX}, but got {volume}."
8081
)
8182

83+
@staticmethod
84+
def validate_hold_time(hold_time: float) -> float:
85+
"""Validate a given temperature hold time.
86+
87+
Args:
88+
hold_time: The requested hold time in seconds.
89+
90+
Raises:
91+
InvalidHoldTimeError: The given time is invalid
92+
93+
Returns:
94+
The validated time in seconds
95+
"""
96+
if hold_time < 0:
97+
raise InvalidHoldTimeError(
98+
"Thermocycler target temperature hold time must be a positive number,"
99+
f" but received {hold_time}."
100+
)
101+
return hold_time
102+
82103
@staticmethod
83104
def validate_target_lid_temperature(celsius: float) -> float:
84105
"""Validate a given target lid temperature.

api/tests/opentrons/protocol_api/core/engine/test_thermocycler_core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def test_set_target_block_temperature(
9494
module_id="1234",
9595
celsius=42.0,
9696
block_max_volume=3.4,
97+
hold_time_seconds=1.2,
9798
),
9899
times=1,
99100
)

api/tests/opentrons/protocol_engine/clients/test_sync_client.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -637,15 +637,21 @@ def test_thermocycler_set_target_block_temperature(
637637
"""It should execute a Thermocycler's set target block temperature command."""
638638
request = commands.thermocycler.SetTargetBlockTemperatureCreate(
639639
params=commands.thermocycler.SetTargetBlockTemperatureParams(
640-
moduleId="module-id", celsius=45.6, blockMaxVolumeUl=12.3
640+
moduleId="module-id",
641+
celsius=45.6,
642+
blockMaxVolumeUl=12.3,
643+
holdTimeSeconds=123.4,
641644
)
642645
)
643646
response = commands.thermocycler.SetTargetBlockTemperatureResult(
644647
targetBlockTemperature=45.6
645648
)
646649
decoy.when(transport.execute_command(request=request)).then_return(response)
647650
result = subject.thermocycler_set_target_block_temperature(
648-
module_id="module-id", celsius=45.6, block_max_volume=12.3
651+
module_id="module-id",
652+
celsius=45.6,
653+
block_max_volume=12.3,
654+
hold_time_seconds=123.4,
649655
)
650656

651657
assert result == response

api/tests/opentrons/protocol_engine/commands/thermocycler/test_set_target_block_temperature.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async def test_set_target_block_temperature(
2727
moduleId="input-thermocycler-id",
2828
celsius=12.3,
2929
blockMaxVolumeUl=50.2,
30+
holdTimeSeconds=123456,
3031
)
3132
expected_result = tc_commands.SetTargetBlockTemperatureResult(
3233
targetBlockTemperature=45.6
@@ -43,14 +44,15 @@ async def test_set_target_block_temperature(
4344
ThermocyclerModuleId("thermocycler-id")
4445
)
4546

46-
# Stub temperature validation from hs module view
47+
# Stub temperature validation from TC module view
4748
decoy.when(tc_module_substate.validate_target_block_temperature(12.3)).then_return(
4849
45.6
4950
)
5051

51-
# Stub volume validation from hs module view
52+
# Stub volume validation from TC module view
5253
decoy.when(tc_module_substate.validate_max_block_volume(50.2)).then_return(77.6)
53-
54+
# Stub hold time validation from TC module view
55+
decoy.when(tc_module_substate.validate_hold_time(123456)).then_return(654321)
5456
# Get attached hardware modules
5557
decoy.when(
5658
equipment.get_module_hardware_api(ThermocyclerModuleId("thermocycler-id"))
@@ -59,7 +61,9 @@ async def test_set_target_block_temperature(
5961
result = await subject.execute(data)
6062

6163
decoy.verify(
62-
await tc_hardware.set_target_block_temperature(celsius=45.6, volume=77.6),
64+
await tc_hardware.set_target_block_temperature(
65+
celsius=45.6, volume=77.6, hold_time_seconds=654321
66+
),
6367
times=1,
6468
)
6569
assert result == expected_result

api/tests/opentrons/protocol_engine/state/test_module_view.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,38 @@ def test_thermocycler_validate_target_block_temperature(
13931393
assert result == input_temperature
13941394

13951395

1396+
@pytest.mark.parametrize(
1397+
argnames=["input_time", "validated_time"],
1398+
argvalues=[(0.0, 0.0), (0.123, 0.123), (123.456, 123.456), (1234567, 1234567)],
1399+
)
1400+
def test_thermocycler_validate_hold_time(
1401+
module_view_with_thermocycler: ModuleView,
1402+
input_time: float,
1403+
validated_time: float,
1404+
) -> None:
1405+
"""It should return a valid hold time."""
1406+
subject = module_view_with_thermocycler.get_thermocycler_module_substate(
1407+
"module-id"
1408+
)
1409+
result = subject.validate_hold_time(input_time)
1410+
1411+
assert result == validated_time
1412+
1413+
1414+
@pytest.mark.parametrize("input_time", [-0.1, -123])
1415+
def test_thermocycler_validate_hold_time_raises(
1416+
module_view_with_thermocycler: ModuleView,
1417+
input_time: float,
1418+
) -> None:
1419+
"""It should raise on invalid hold time."""
1420+
subject = module_view_with_thermocycler.get_thermocycler_module_substate(
1421+
"module-id"
1422+
)
1423+
1424+
with pytest.raises(errors.InvalidHoldTimeError):
1425+
subject.validate_hold_time(input_time)
1426+
1427+
13961428
@pytest.mark.parametrize("input_temperature", [-0.001, 99.001])
13971429
def test_thermocycler_validate_target_block_temperature_raises(
13981430
module_view_with_thermocycler: ModuleView,

0 commit comments

Comments
 (0)