Skip to content

Commit 1e8c527

Browse files
authored
refactor(api): improve error messages for group_well argument in liquid class transfers (#18757)
Updates error messages to make them more clear when using group_wells=True with liquid class transfer functions
1 parent e5f7ba5 commit 1e8c527

File tree

4 files changed

+76
-53
lines changed

4 files changed

+76
-53
lines changed

api/src/opentrons/protocol_api/_transfer_liquid_validation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ def verify_and_normalize_transfer_args(
4747
flat_dests_list = []
4848
if group_wells_for_multi_channel and nozzle_map.tip_count > 1:
4949
flat_sources_list = tx_liquid_utils.group_wells_for_multi_channel_transfer(
50-
flat_sources_list, nozzle_map
50+
flat_sources_list, nozzle_map, "source"
5151
)
5252
flat_dests_list = tx_liquid_utils.group_wells_for_multi_channel_transfer(
53-
flat_dests_list, nozzle_map
53+
flat_dests_list, nozzle_map, "destination"
5454
)
5555
for well in flat_sources_list + flat_dests_list:
5656
instrument.validate_takes_liquid(

api/src/opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def raise_if_location_inside_liquid(
7272
def group_wells_for_multi_channel_transfer(
7373
targets: Sequence[Well],
7474
nozzle_map: NozzleMapInterface,
75+
target_name: Literal["source", "destination"],
7576
) -> List[Well]:
7677
"""Takes a list of wells and a nozzle map and returns a list of target wells to address every well given
7778
@@ -94,13 +95,20 @@ def group_wells_for_multi_channel_transfer(
9495
or (configuration == NozzleConfigurationType.ROW and active_nozzles == 12)
9596
or active_nozzles == 96
9697
):
97-
return _group_wells_for_nozzle_configuration(list(targets), nozzle_map)
98+
return _group_wells_for_nozzle_configuration(
99+
list(targets), nozzle_map, target_name
100+
)
98101
else:
99-
raise ValueError("Unsupported tip configuration for well grouping")
102+
raise ValueError(
103+
"Unsupported nozzle configuration for well grouping. Set group_wells to False"
104+
" to only target wells with the primary nozzle for this configuration."
105+
)
100106

101107

102108
def _group_wells_for_nozzle_configuration( # noqa: C901
103-
targets: List[Well], nozzle_map: NozzleMapInterface
109+
targets: List[Well],
110+
nozzle_map: NozzleMapInterface,
111+
target_name: Literal["source", "destination"],
104112
) -> List[Well]:
105113
"""Groups wells together for a column, row, or full 96 configuration and returns a reduced list of target wells."""
106114
grouped_wells = []
@@ -132,8 +140,9 @@ def _group_wells_for_nozzle_configuration( # noqa: C901
132140
if active_wells_covered:
133141
if well.parent != active_labware:
134142
raise ValueError(
135-
"Could not resolve wells provided to pipette's nozzle configuration. "
136-
"Please ensure wells are ordered to match pipette's nozzle layout."
143+
f"Could not group {target_name} wells to match pipette's nozzle configuration. Ensure that the"
144+
" wells are ordered correctly (e.g. rows() for a row layout or columns() for a column layout), or"
145+
" set group_wells to False to only target wells with the primary nozzle."
137146
)
138147

139148
if well.well_name in active_wells_covered:
@@ -165,8 +174,9 @@ def _group_wells_for_nozzle_configuration( # noqa: C901
165174
alternate_384_well_coverage_count += 1
166175
else:
167176
raise ValueError(
168-
"Could not resolve wells provided to pipette's nozzle configuration. "
169-
"Please ensure wells are ordered to match pipette's nozzle layout."
177+
f"Could not group {target_name} wells to match pipette's nozzle configuration. Ensure that the"
178+
" wells are ordered correctly (e.g. rows() for a row layout or columns() for a column layout), or"
179+
" set group_wells to False to only target wells with the primary nozzle."
170180
)
171181
# If we have no active wells covered to account for, add a new target well and list of covered wells to check
172182
else:
@@ -193,8 +203,8 @@ def _group_wells_for_nozzle_configuration( # noqa: C901
193203

194204
if active_wells_covered:
195205
raise ValueError(
196-
"Could not target all wells provided without aspirating or dispensing from other wells. "
197-
f"Other wells that would be targeted: {active_wells_covered}"
206+
f"Pipette will access {target_name} wells not provided in the liquid handling command."
207+
f" Set group_wells to False or include these wells: {active_wells_covered}"
198208
)
199209

200210
# If we reversed the lookup of wells, reverse the grouped wells we will return

api/tests/opentrons/protocol_api/test_instrument_context.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2525,7 +2525,12 @@ def test_transfer_liquid_multi_channel_delegates_to_engine_core(
25252525
decoy.when(mock_instrument_core.get_nozzle_map()).then_return(mock_nozzle_map)
25262526
decoy.when(
25272527
mock_tx_liquid_utils.group_wells_for_multi_channel_transfer(
2528-
[mock_well, mock_well], mock_nozzle_map
2528+
[mock_well, mock_well], mock_nozzle_map, "source"
2529+
)
2530+
).then_return([mock_well])
2531+
decoy.when(
2532+
mock_tx_liquid_utils.group_wells_for_multi_channel_transfer(
2533+
[mock_well, mock_well], mock_nozzle_map, "destination"
25292534
)
25302535
).then_return([mock_well])
25312536

@@ -2862,12 +2867,12 @@ def test_distribute_liquid_multi_channel_delegates_to_engine_core(
28622867
decoy.when(mock_instrument_core.get_nozzle_map()).then_return(mock_nozzle_map)
28632868
decoy.when(
28642869
mock_tx_liquid_utils.group_wells_for_multi_channel_transfer(
2865-
[mock_well, mock_well, mock_well], mock_nozzle_map
2870+
[mock_well, mock_well, mock_well], mock_nozzle_map, "source"
28662871
)
28672872
).then_return([mock_well])
28682873
decoy.when(
28692874
mock_tx_liquid_utils.group_wells_for_multi_channel_transfer(
2870-
[mock_well, mock_well], mock_nozzle_map
2875+
[mock_well, mock_well], mock_nozzle_map, "destination"
28712876
)
28722877
).then_return([mock_well, mock_well])
28732878

@@ -3149,14 +3154,14 @@ def test_consolidate_liquid_multi_channel_delegates_to_engine_core(
31493154
decoy.when(mock_instrument_core.get_nozzle_map()).then_return(mock_nozzle_map)
31503155
decoy.when(
31513156
mock_tx_liquid_utils.group_wells_for_multi_channel_transfer(
3152-
[mock_well, mock_well, mock_well], mock_nozzle_map
3157+
[mock_well, mock_well], mock_nozzle_map, "source"
31533158
)
3154-
).then_return([mock_well])
3159+
).then_return([mock_well, mock_well])
31553160
decoy.when(
31563161
mock_tx_liquid_utils.group_wells_for_multi_channel_transfer(
3157-
[mock_well, mock_well], mock_nozzle_map
3162+
[mock_well, mock_well, mock_well], mock_nozzle_map, "destination"
31583163
)
3159-
).then_return([mock_well, mock_well])
3164+
).then_return([mock_well])
31603165

31613166
decoy.when(mock_instrument_core.get_active_channels()).then_return(2)
31623167
decoy.when(mock_instrument_core.get_current_volume()).then_return(0)

api/tests/opentrons/protocols/advanced_control/transfers/test_transfer_liquid_utils.py

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ def test_grouping_wells_for_column_96_plate(
263263
decoy.when(mock_well.well_name).then_return(well_name)
264264
decoy.when(mock_well.parent).then_return(mock_96_well_labware)
265265

266-
wells = group_wells_for_multi_channel_transfer(mock_wells, nozzle_map)
266+
wells = group_wells_for_multi_channel_transfer(mock_wells, nozzle_map, "source")
267267
assert len(wells) == 2
268268
assert wells[0].well_name == "A1"
269269
assert wells[1].well_name == "A2"
@@ -283,7 +283,7 @@ def test_grouping_wells_for_column_384_plate(
283283
decoy.when(mock_well.well_name).then_return(well_name)
284284
decoy.when(mock_well.parent).then_return(mock_384_well_labware)
285285

286-
wells = group_wells_for_multi_channel_transfer(mock_wells, nozzle_map)
286+
wells = group_wells_for_multi_channel_transfer(mock_wells, nozzle_map, "source")
287287
assert len(wells) == 4
288288
assert wells[0].well_name == "A1"
289289
assert wells[1].well_name == "B1"
@@ -306,13 +306,13 @@ def test_grouping_wells_for_column_96_plate_raises(
306306
decoy.when(mock_well.parent).then_return(mock_96_well_labware)
307307

308308
# leftover wells
309-
with pytest.raises(ValueError, match="Could not target all wells"):
310-
group_wells_for_multi_channel_transfer(mock_wells, nozzle_map)
309+
with pytest.raises(ValueError, match="Pipette will access source wells"):
310+
group_wells_for_multi_channel_transfer(mock_wells, nozzle_map, "source")
311311

312312
# non-contiguous wells from the same labware
313-
with pytest.raises(ValueError, match="Could not resolve wells"):
313+
with pytest.raises(ValueError, match="Could not group source wells"):
314314
group_wells_for_multi_channel_transfer(
315-
mock_wells[:7] + [mock_wells[-1], mock_wells[7]], nozzle_map
315+
mock_wells[:7] + [mock_wells[-1], mock_wells[7]], nozzle_map, "source"
316316
)
317317

318318
other_labware = decoy.mock(cls=Labware)
@@ -322,9 +322,9 @@ def test_grouping_wells_for_column_96_plate_raises(
322322
decoy.when(other_well.parent).then_return(other_labware)
323323

324324
# non-contiguous wells from different labware, well name is correct though
325-
with pytest.raises(ValueError, match="Could not resolve wells"):
325+
with pytest.raises(ValueError, match="Could not group source wells"):
326326
group_wells_for_multi_channel_transfer(
327-
mock_wells[:7] + [other_well], nozzle_map
327+
mock_wells[:7] + [other_well], nozzle_map, "source"
328328
)
329329

330330

@@ -343,16 +343,18 @@ def test_grouping_wells_for_column_384_plate_raises(
343343
decoy.when(mock_well.parent).then_return(mock_384_well_labware)
344344

345345
# leftover wells
346-
with pytest.raises(ValueError, match="Could not target all wells"):
347-
group_wells_for_multi_channel_transfer(mock_wells[:-1], nozzle_map)
346+
with pytest.raises(ValueError, match="Pipette will access destination wells"):
347+
group_wells_for_multi_channel_transfer(
348+
mock_wells[:-1], nozzle_map, "destination"
349+
)
348350

349351
# non-contiguous or every other wells from the same labware
350-
with pytest.raises(ValueError, match="Could not resolve wells"):
352+
with pytest.raises(ValueError, match="Could not group"):
351353
group_wells_for_multi_channel_transfer(
352-
mock_wells[:2] + [mock_wells[-1]], nozzle_map
354+
mock_wells[:2] + [mock_wells[-1]], nozzle_map, "source"
353355
)
354356
group_wells_for_multi_channel_transfer(
355-
mock_wells[:-1] + [mock_wells[0]], nozzle_map
357+
mock_wells[:-1] + [mock_wells[0]], nozzle_map, "destination"
356358
)
357359

358360
other_labware = decoy.mock(cls=Labware)
@@ -362,9 +364,9 @@ def test_grouping_wells_for_column_384_plate_raises(
362364
decoy.when(other_well.parent).then_return(other_labware)
363365

364366
# non-contiguous wells from different labware, well name is correct though
365-
with pytest.raises(ValueError, match="Could not resolve wells"):
367+
with pytest.raises(ValueError, match="Could not group source wells"):
366368
group_wells_for_multi_channel_transfer(
367-
mock_wells[:15] + [other_well], nozzle_map
369+
mock_wells[:15] + [other_well], nozzle_map, "source"
368370
)
369371

370372

@@ -383,7 +385,9 @@ def test_grouping_wells_for_row_96_plate(
383385
decoy.when(mock_well.well_name).then_return(well_name)
384386
decoy.when(mock_well.parent).then_return(mock_96_well_labware)
385387

386-
wells = group_wells_for_multi_channel_transfer(mock_wells, nozzle_map)
388+
wells = group_wells_for_multi_channel_transfer(
389+
mock_wells, nozzle_map, "destination"
390+
)
387391
assert len(wells) == 2
388392
assert wells[0].well_name == "A1"
389393
assert wells[1].well_name == "B1"
@@ -404,7 +408,7 @@ def test_grouping_wells_for_row_384_plate(
404408
decoy.when(mock_well.well_name).then_return(well_name)
405409
decoy.when(mock_well.parent).then_return(mock_384_well_labware)
406410

407-
wells = group_wells_for_multi_channel_transfer(mock_wells, nozzle_map)
411+
wells = group_wells_for_multi_channel_transfer(mock_wells, nozzle_map, "source")
408412
assert len(wells) == 4
409413
assert wells[0].well_name == "A1"
410414
assert wells[1].well_name == "A2"
@@ -428,13 +432,13 @@ def test_grouping_wells_for_row_96_plate_raises(
428432
decoy.when(mock_well.parent).then_return(mock_96_well_labware)
429433

430434
# leftover wells
431-
with pytest.raises(ValueError, match="Could not target all wells"):
432-
group_wells_for_multi_channel_transfer(mock_wells[:-1], nozzle_map)
435+
with pytest.raises(ValueError, match="Pipette will access source wells"):
436+
group_wells_for_multi_channel_transfer(mock_wells[:-1], nozzle_map, "source")
433437

434438
# non-contiguous wells from the same labware
435-
with pytest.raises(ValueError, match="Could not resolve wells"):
439+
with pytest.raises(ValueError, match="Could not group source wells"):
436440
group_wells_for_multi_channel_transfer(
437-
mock_wells[:11] + [mock_wells[-1], mock_wells[11]], nozzle_map
441+
mock_wells[:11] + [mock_wells[-1], mock_wells[11]], nozzle_map, "source"
438442
)
439443

440444
other_labware = decoy.mock(cls=Labware)
@@ -444,9 +448,9 @@ def test_grouping_wells_for_row_96_plate_raises(
444448
decoy.when(other_well.parent).then_return(other_labware)
445449

446450
# non-contiguous wells from different labware, well name is correct though
447-
with pytest.raises(ValueError, match="Could not resolve wells"):
451+
with pytest.raises(ValueError, match="Could not group destination wells"):
448452
group_wells_for_multi_channel_transfer(
449-
mock_wells[:11] + [other_well], nozzle_map
453+
mock_wells[:11] + [other_well], nozzle_map, "destination"
450454
)
451455

452456

@@ -466,16 +470,18 @@ def test_grouping_wells_for_row_384_plate_raises(
466470
decoy.when(mock_well.parent).then_return(mock_384_well_labware)
467471

468472
# leftover wells
469-
with pytest.raises(ValueError, match="Could not target all wells"):
470-
group_wells_for_multi_channel_transfer(mock_wells[:-1], nozzle_map)
473+
with pytest.raises(ValueError, match="Pipette will access destination wells"):
474+
group_wells_for_multi_channel_transfer(
475+
mock_wells[:-1], nozzle_map, "destination"
476+
)
471477

472478
# non-contiguous or every other wells from the same labware
473-
with pytest.raises(ValueError, match="Could not resolve wells"):
479+
with pytest.raises(ValueError, match="Could not group"):
474480
group_wells_for_multi_channel_transfer(
475-
mock_wells[:2] + [mock_wells[-1]], nozzle_map
481+
mock_wells[:2] + [mock_wells[-1]], nozzle_map, "destination"
476482
)
477483
group_wells_for_multi_channel_transfer(
478-
mock_wells[:-1] + [mock_wells[0]], nozzle_map
484+
mock_wells[:-1] + [mock_wells[0]], nozzle_map, "source"
479485
)
480486

481487
other_labware = decoy.mock(cls=Labware)
@@ -485,9 +491,9 @@ def test_grouping_wells_for_row_384_plate_raises(
485491
decoy.when(other_well.parent).then_return(other_labware)
486492

487493
# non-contiguous wells from different labware, well name is correct though
488-
with pytest.raises(ValueError, match="Could not resolve wells"):
494+
with pytest.raises(ValueError, match="Could not group destination wells"):
489495
group_wells_for_multi_channel_transfer(
490-
mock_wells[:23] + [other_well], nozzle_map
496+
mock_wells[:23] + [other_well], nozzle_map, "destination"
491497
)
492498

493499

@@ -500,7 +506,9 @@ def test_grouping_wells_for_full_96_plate(
500506
decoy.when(mock_well.well_name).then_return(well_name)
501507
decoy.when(mock_well.parent).then_return(mock_96_well_labware)
502508

503-
wells = group_wells_for_multi_channel_transfer(mock_wells, _96_FULL_MAP)
509+
wells = group_wells_for_multi_channel_transfer(
510+
mock_wells, _96_FULL_MAP, "destination"
511+
)
504512
assert len(wells) == 1
505513
assert wells[0].well_name == "A1"
506514

@@ -515,7 +523,7 @@ def test_grouping_wells_for_full_384_plate(
515523
decoy.when(mock_well.well_name).then_return(well_name)
516524
decoy.when(mock_well.parent).then_return(mock_384_well_labware)
517525

518-
wells = group_wells_for_multi_channel_transfer(mock_wells, _96_FULL_MAP)
526+
wells = group_wells_for_multi_channel_transfer(mock_wells, _96_FULL_MAP, "source")
519527
assert len(wells) == 4
520528
assert wells[0].well_name == "A1"
521529
assert wells[1].well_name == "B1"
@@ -534,8 +542,8 @@ def test_grouping_wells_raises_for_unsupported_configuration() -> None:
534542
front_right_nozzle="D1",
535543
valid_nozzle_maps=ValidNozzleMaps(maps={"Half": ["A1", "B1", "C1", "D1"]}),
536544
)
537-
with pytest.raises(ValueError, match="Unsupported tip configuration"):
538-
group_wells_for_multi_channel_transfer([], nozzle_map)
545+
with pytest.raises(ValueError, match="Unsupported nozzle configuration"):
546+
group_wells_for_multi_channel_transfer([], nozzle_map, "source")
539547

540548

541549
@pytest.mark.parametrize(
@@ -561,5 +569,5 @@ def test_grouping_well_returns_all_wells_for_non_96_or_384_plate(
561569
decoy.when(mock_well.well_name).then_return(well_name)
562570
decoy.when(mock_well.parent).then_return(mock_reservoir)
563571

564-
result = group_wells_for_multi_channel_transfer(mock_wells, nozzle_map)
572+
result = group_wells_for_multi_channel_transfer(mock_wells, nozzle_map, "source")
565573
assert result == mock_wells

0 commit comments

Comments
 (0)