Skip to content

Commit f36195a

Browse files
sanni-tddcc4
authored andcommitted
fix(shared-data): correct multi dispense alias (#18939)
Fixes AUTH-2105 Fixes a typo where alias for `multiDispense` was using a hyphen instead of underscore (`multi_dispense`). Adds tests to make sure a correct liquid class dictionary creates correct custom liquid class object. This should now fix the linked issue where `distribute_with_liquid_class()` was doing only 1:1 transfers instead of doing multi-dispenses when used with a custom liquid class. - Added integration and unit tests - Tested with PD protocol for distribute - Usual stuff None. Bug fix only. (cherry picked from commit 6ea8ec1)
1 parent 9efbc96 commit f36195a

File tree

6 files changed

+315
-3
lines changed

6 files changed

+315
-3
lines changed

api/tests/opentrons/conftest.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,3 +1230,107 @@ def custom_pip_n_tip_transfer_properties_dict() -> Dict[
12301230
)
12311231
}
12321232
}
1233+
1234+
1235+
@pytest.fixture
1236+
def custom_pip_n_tip_transfer_properties_dict_v2() -> Dict[str, Dict[str, Any]]:
1237+
"""A minimal dictionary representation of transfer properties for a custom pipette and tiprack."""
1238+
return {
1239+
"a_custom_pipette_type": {
1240+
"a_custom_tiprack_uri": {
1241+
"aspirate": {
1242+
"aspirate_position": {
1243+
"offset": {"x": 1, "y": 2, "z": 3},
1244+
"position_reference": "well-bottom",
1245+
},
1246+
"correction_by_volume": [(0.0, 0.0)],
1247+
"delay": {"enable": False},
1248+
"flow_rate_by_volume": [(10.0, 40.0), (20.0, 30.0)],
1249+
"mix": {"enable": False},
1250+
"pre_wet": True,
1251+
"retract": {
1252+
"air_gap_by_volume": [(5.0, 3.0), (10.0, 4.0)],
1253+
"delay": {"enable": False},
1254+
"end_position": {
1255+
"offset": {"x": 1, "y": 2, "z": 3},
1256+
"position_reference": "well-bottom",
1257+
},
1258+
"speed": 40,
1259+
"touch_tip": {"enable": False},
1260+
},
1261+
"submerge": {
1262+
"delay": {"enable": False},
1263+
"speed": 100,
1264+
"start_position": {
1265+
"offset": {"x": 1, "y": 2, "z": 3},
1266+
"position_reference": "well-bottom",
1267+
},
1268+
},
1269+
},
1270+
"dispense": {
1271+
"dispense_position": {
1272+
"offset": {"x": 1, "y": 2, "z": 3},
1273+
"position_reference": "well-bottom",
1274+
},
1275+
"correction_by_volume": [(0.0, 0.0)],
1276+
"delay": {"enable": False},
1277+
"flow_rate_by_volume": [(10.0, 40.0), (20.0, 30.0)],
1278+
"mix": {"enable": False},
1279+
"push_out_by_volume": [(10.0, 7.0), (20.0, 10.0)],
1280+
"retract": {
1281+
"air_gap_by_volume": [(5.0, 3.0), (10.0, 4.0)],
1282+
"blowout": {"enable": False},
1283+
"delay": {"enable": False},
1284+
"end_position": {
1285+
"offset": {"x": 1, "y": 2, "z": 3},
1286+
"position_reference": "well-bottom",
1287+
},
1288+
"speed": 40,
1289+
"touch_tip": {"enable": False},
1290+
},
1291+
"submerge": {
1292+
"delay": {"enable": False},
1293+
"speed": 100,
1294+
"start_position": {
1295+
"offset": {"x": 1, "y": 2, "z": 3},
1296+
"position_reference": "well-bottom",
1297+
},
1298+
},
1299+
},
1300+
"multi_dispense": {
1301+
"dispense_position": {
1302+
"offset": {"x": 0, "y": 0, "z": 1},
1303+
"position_reference": "well-bottom",
1304+
},
1305+
"flow_rate_by_volume": [(0, 318)],
1306+
"correction_by_volume": [(0, 0)],
1307+
"delay": {"enabled": False},
1308+
"submerge": {
1309+
"delay": {"enabled": False},
1310+
"speed": 100,
1311+
"start_position": {
1312+
"offset": {"x": 0, "y": 0, "z": 2},
1313+
"position_reference": "well-top",
1314+
},
1315+
},
1316+
"retract": {
1317+
"air_gap_by_volume": [(0, 0)],
1318+
"delay": {"enabled": False},
1319+
"end_position": {
1320+
"offset": {"x": 0, "y": 0, "z": 2},
1321+
"position_reference": "well-top",
1322+
},
1323+
"speed": 50,
1324+
"touch_tip": {"enabled": False},
1325+
"blowout": {
1326+
"enabled": True,
1327+
"location": "trash",
1328+
"flow_rate": 478,
1329+
},
1330+
},
1331+
"conditioning_by_volume": [(0, 0)],
1332+
"disposal_by_volume": [(0, 5)],
1333+
},
1334+
}
1335+
}
1336+
}

api/tests/opentrons/protocol_api_integration/test_liquid_classes.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Tests for the APIs around liquid classes."""
2-
from typing import Dict
2+
from typing import Dict, Any
33

44
import pytest
55

@@ -77,3 +77,36 @@ def test_custom_liquid_class_creation_and_property_fetching(
7777
custom_water.get_for(
7878
"flex_8channel_50", "opentrons/opentrons_flex_96_tiprack_50ul/1"
7979
)
80+
81+
82+
@pytest.mark.ot3_only
83+
@pytest.mark.parametrize(
84+
"simulated_protocol_context", [("2.24", "Flex")], indirect=True
85+
)
86+
def test_custom_liquid_class_w_multi_dispense_creation_and_property_fetching(
87+
simulated_protocol_context: ProtocolContext,
88+
custom_pip_n_tip_transfer_properties_dict_v2: Dict[str, Dict[str, Any]],
89+
) -> None:
90+
"""It should create the liquid class with specified properties and provide access to its properties."""
91+
p50 = "a_custom_pipette_type"
92+
tiprack = "a_custom_tiprack_uri"
93+
custom_water = simulated_protocol_context.define_liquid_class(
94+
name="water_50",
95+
properties=custom_pip_n_tip_transfer_properties_dict_v2,
96+
display_name="Custom Aqueous",
97+
)
98+
99+
assert custom_water.name == "water_50"
100+
assert custom_water.display_name == "Custom Aqueous"
101+
custom_water_props = custom_water.get_for(p50, tiprack)
102+
assert custom_water_props.aspirate.retract.speed == 40
103+
assert custom_water_props.dispense.submerge.speed == 100
104+
assert custom_water_props.multi_dispense.retract.blowout.location.value == "trash" # type: ignore[union-attr]
105+
106+
with pytest.raises(
107+
ValueError,
108+
match="No properties found for flex_8channel_50 in water_50 liquid class",
109+
):
110+
custom_water.get_for(
111+
"flex_8channel_50", "opentrons/opentrons_flex_96_tiprack_50ul/1"
112+
)

shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ class TransferProperties(BaseLiquidClassModel):
508508
)
509509
multiDispense: MultiDispenseProperties | SkipJsonSchema[None] = Field(
510510
None,
511-
alias="multi-dispense",
511+
alias="multi_dispense",
512512
description="Optional multi-dispense parameters for this tip type.",
513513
json_schema_extra=_remove_default,
514514
)

shared-data/python/opentrons_shared_data/liquid_classes/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class MultiDispensePropertiesDict(TypedDict):
119119
dispense_position: TipPositionDict
120120
retract: RetractDispenseDict
121121
conditioning_by_volume: Sequence[Tuple[float, float]]
122+
disposal_by_volume: Sequence[Tuple[float, float]]
122123

123124

124125
class TransferPropertiesDict(TypedDict):

shared-data/python/tests/conftest.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""Fixtures and helpers for python tests."""
2+
3+
import pytest
4+
from typing import Any, Dict
5+
6+
7+
@pytest.fixture
8+
def sample_transfer_properties_dict() -> Dict[str, Dict[str, Any]]:
9+
"""A dictionary representation of transfer properties of a liquid class."""
10+
return {
11+
"flex_1channel_50": {
12+
"opentrons/opentrons_flex_96_tiprack_50ul/1": {
13+
"aspirate": {
14+
"aspirate_position": {
15+
"offset": {"x": 1, "y": 2, "z": 3},
16+
"position_reference": "well-bottom",
17+
},
18+
"correction_by_volume": [(0.0, 0.0)],
19+
"delay": {"enable": False},
20+
"flow_rate_by_volume": [(10.0, 40.0), (20.0, 30.0)],
21+
"mix": {"enable": False},
22+
"pre_wet": True,
23+
"retract": {
24+
"air_gap_by_volume": [(5.0, 3.0), (10.0, 4.0)],
25+
"delay": {"enable": False},
26+
"end_position": {
27+
"offset": {"x": 1, "y": 2, "z": 3},
28+
"position_reference": "well-bottom",
29+
},
30+
"speed": 40,
31+
"touch_tip": {"enable": False},
32+
},
33+
"submerge": {
34+
"delay": {"enable": False},
35+
"speed": 100,
36+
"start_position": {
37+
"offset": {"x": 1, "y": 2, "z": 3},
38+
"position_reference": "well-bottom",
39+
},
40+
},
41+
},
42+
"dispense": {
43+
"dispense_position": {
44+
"offset": {"x": 1, "y": 2, "z": 3},
45+
"position_reference": "well-bottom",
46+
},
47+
"correction_by_volume": [(0.0, 0.0)],
48+
"delay": {"enable": False},
49+
"flow_rate_by_volume": [(10.0, 40.0), (20.0, 30.0)],
50+
"mix": {"enable": False},
51+
"push_out_by_volume": [(10.0, 7.0), (20.0, 10.0)],
52+
"retract": {
53+
"air_gap_by_volume": [(5.0, 3.0), (10.0, 4.0)],
54+
"blowout": {"enable": False},
55+
"delay": {"enable": False},
56+
"end_position": {
57+
"offset": {"x": 1, "y": 2, "z": 3},
58+
"position_reference": "well-bottom",
59+
},
60+
"speed": 40,
61+
"touch_tip": {"enable": False},
62+
},
63+
"submerge": {
64+
"delay": {"enable": False},
65+
"speed": 100,
66+
"start_position": {
67+
"offset": {"x": 1, "y": 2, "z": 3},
68+
"position_reference": "well-bottom",
69+
},
70+
},
71+
},
72+
"multi_dispense": {
73+
"dispense_position": {
74+
"offset": {"x": 0, "y": 0, "z": 1},
75+
"position_reference": "well-bottom",
76+
},
77+
"flow_rate_by_volume": [(0, 318)],
78+
"correction_by_volume": [(0, 0)],
79+
"delay": {"enabled": False},
80+
"submerge": {
81+
"delay": {"enabled": False},
82+
"speed": 100,
83+
"start_position": {
84+
"offset": {"x": 0, "y": 0, "z": 2},
85+
"position_reference": "well-top",
86+
},
87+
},
88+
"retract": {
89+
"air_gap_by_volume": [(0, 0)],
90+
"delay": {"enabled": False},
91+
"end_position": {
92+
"offset": {"x": 0, "y": 0, "z": 2},
93+
"position_reference": "well-top",
94+
},
95+
"speed": 50,
96+
"touch_tip": {"enabled": False},
97+
"blowout": {
98+
"enabled": True,
99+
"location": "trash",
100+
"flow_rate": 478,
101+
},
102+
},
103+
"conditioning_by_volume": [(0, 0)],
104+
"disposal_by_volume": [(0, 5)],
105+
},
106+
}
107+
}
108+
}

shared-data/python/tests/liquid_classes/test_validations.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Tests that validate the built-in liquid class definitions."""
22
import pytest
3-
from typing import List
3+
from typing import List, Dict, Any
44

55
from opentrons_shared_data import get_shared_data_root
66
from opentrons_shared_data.liquid_classes import load_definition
@@ -10,6 +10,11 @@
1010
TouchTipProperties,
1111
BlowoutProperties,
1212
BlowoutLocation,
13+
AspirateProperties,
14+
PositionReference,
15+
SingleDispenseProperties,
16+
MultiDispenseProperties,
17+
TransferProperties,
1318
)
1419

1520

@@ -124,3 +129,64 @@ def test_validate_blowout_properties_dict() -> None:
124129
"params": {"foo": "bar"},
125130
}
126131
)
132+
133+
134+
def test_validate_aspirate_properties_dict(
135+
sample_transfer_properties_dict: Dict[str, Dict[str, Any]],
136+
) -> None:
137+
"""Aspirate properties model validator should convert valid dict to AspirateProperties."""
138+
obj = AspirateProperties.model_validate(
139+
sample_transfer_properties_dict["flex_1channel_50"][
140+
"opentrons/opentrons_flex_96_tiprack_50ul/1"
141+
]["aspirate"]
142+
)
143+
assert isinstance(obj, AspirateProperties)
144+
assert obj.aspiratePosition.positionReference == PositionReference.WELL_BOTTOM
145+
assert obj.mix.enable is False
146+
147+
148+
def test_validate_single_dispense_properties_dict(
149+
sample_transfer_properties_dict: Dict[str, Dict[str, Any]],
150+
) -> None:
151+
"""Single dispense properties model validator should convert valid dict to SingleDispenseProperties."""
152+
obj = SingleDispenseProperties.model_validate(
153+
sample_transfer_properties_dict["flex_1channel_50"][
154+
"opentrons/opentrons_flex_96_tiprack_50ul/1"
155+
]["dispense"]
156+
)
157+
assert isinstance(obj, SingleDispenseProperties)
158+
assert obj.dispensePosition.positionReference == PositionReference.WELL_BOTTOM
159+
assert obj.mix.enable is False
160+
161+
162+
def test_validate_multi_dispense_properties_dict(
163+
sample_transfer_properties_dict: Dict[str, Dict[str, Any]],
164+
) -> None:
165+
"""Multi dispense properties model validator should convert valid dict to MultiDispenseProperties."""
166+
obj = MultiDispenseProperties.model_validate(
167+
sample_transfer_properties_dict["flex_1channel_50"][
168+
"opentrons/opentrons_flex_96_tiprack_50ul/1"
169+
]["multi_dispense"]
170+
)
171+
assert isinstance(obj, MultiDispenseProperties)
172+
assert obj.dispensePosition.positionReference == PositionReference.WELL_BOTTOM
173+
assert obj.conditioningByVolume == [(0, 0)]
174+
assert obj.disposalByVolume == [(0, 5)]
175+
176+
177+
def test_validate_transfer_properties_dict(
178+
sample_transfer_properties_dict: Dict[str, Dict[str, Any]],
179+
) -> None:
180+
"""Transfer properties model validator should convert valid dict to TransferProperties."""
181+
obj = TransferProperties.model_validate(
182+
sample_transfer_properties_dict["flex_1channel_50"][
183+
"opentrons/opentrons_flex_96_tiprack_50ul/1"
184+
]
185+
)
186+
assert isinstance(obj, TransferProperties)
187+
assert (
188+
obj.aspirate.aspiratePosition.positionReference == PositionReference.WELL_BOTTOM
189+
)
190+
assert obj.singleDispense.pushOutByVolume == [(10.0, 7.0), (20.0, 10.0)]
191+
assert obj.multiDispense.conditioningByVolume == [(0, 0)] # type: ignore[union-attr]
192+
assert obj.multiDispense.disposalByVolume == [(0, 5)] # type: ignore[union-attr]

0 commit comments

Comments
 (0)