Skip to content

Commit 7a53675

Browse files
committed
Merge back 'chore_release-8.5.0' into 'chore_release-pd-8.5.0'
Resolved conflicts in `api/tests/opentrons/protocol_api_integration/test_transfer_with_liquid_classes.py`.
2 parents 3a54180 + 1e8c527 commit 7a53675

File tree

14 files changed

+342
-668
lines changed

14 files changed

+342
-668
lines changed

.github/workflows/api-test-lint-deploy.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
- name: Lint with opentrons_hardware
6868
run: make -C api lint
6969
test:
70-
name: 'opentrons package tests on ${{ matrix.os }}, python ${{ matrix.python }}'
70+
name: 'opentrons package tests on ${{ matrix.os }}, python ${{ matrix.python }}, ot-hardware ${{ matrix.with-ot-hardware }}'
7171
timeout-minutes: 30
7272
needs: [lint]
7373
strategy:

api/release-notes.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,20 @@ By installing and using Opentrons software, you agree to the Opentrons End-User
1010

1111
## Opentrons Robot Software Changes in 8.5.0
1212

13-
### TODO
13+
Welcome to the v8.5.0 release of the Opentrons robot software! This release features the ability to pipette more accurately by using liquid classes in your protocols.
14+
15+
### New Features
16+
17+
- Use Opentrons-verified liquid classes (aqueous, viscous, and volatile) in the Python Protocol API to automatically adjust submerge speed, flow rate, touch tip, air gap, and more.
18+
- Customize and create your own liquid classes for even more control.
19+
20+
### Improvements
21+
22+
- Several Python API methods have new parameters that add capabilities available in Protocol Designer.
23+
24+
### Bug Fixes
25+
26+
- The Absorbance Plate Reader no longer reports measurements below 0.
1427

1528
---
1629

@@ -51,13 +64,12 @@ The 8.3.2 hotfix release fixes a bug where protocol commands could time out, esp
5164

5265
The 8.3.1 hotfix release contains two bug fixes:
5366

54-
- Exported data from the Absorbance Plate Reader no longer contains invalid values.
55-
- A small fix allows all robots to properly reboot after an upgrade to v8.3.0.
67+
- Exported data from the Absorbance Plate Reader no longer contains invalid values.
68+
- A small fix allows all robots to properly reboot after an upgrade to v8.3.0.
5669

5770
## Opentrons Robot Software Changes in 8.3.0
5871

59-
Welcome to the v8.3.0 release of the Opentrons robot software! This release includes improvements to error recovery on the Flex, as well as beta features for our commercial partners.
60-
72+
Welcome to the v8.3.0 release of the Opentrons robot software! This release includes improvements to error recovery on the Flex, as well as beta features for our commercial partners.
6173

6274
### Improved Features
6375

@@ -172,7 +184,7 @@ Welcome to the v7.3.0 release of the Opentrons robot software!
172184
### Bug Fixes
173185

174186
- Fixed an edge case where capitalizing part of a labware load name could cause unexpected behavior or collisions.
175-
- Fixed Python packages installed on the OT-2 with `pip` not being found by `import` statements.
187+
- Fixed Python packages installed on the OT-2 with `pip` not being found by `import` statements.
176188

177189
---
178190

@@ -208,7 +220,7 @@ Welcome to the v7.2.1 release of the Opentrons robot software!
208220

209221
Welcome to the v7.2.0 release of the Opentrons robot software!
210222

211-
This update may take longer than usual if your robot has a lot of long protocols and runs stored on it. Allow *approximately 20 minutes* for your robot to restart. This delay will only happen once.
223+
This update may take longer than usual if your robot has a lot of long protocols and runs stored on it. Allow _approximately 20 minutes_ for your robot to restart. This delay will only happen once.
212224

213225
If you don't care about preserving your labware offsets and run history, you can avoid the delay by clearing your runs and protocols before starting this update. Go to **Robot Settings** > **Device Reset** and select **Clear protocol run history**.
214226

@@ -343,12 +355,12 @@ Some protocols can't be simulated with the `opentrons_simulate` command-line too
343355
Welcome to the v6.3.1 release of the OT-2 software! This hotfix release addresses a few problems.
344356

345357
### Improved Features
358+
346359
- Changed the Thermocycler GEN2 plate ejection behavior to prevent plates from getting stuck after PCR cycles or being ejected too forcefully.
347360

348361
### Bug Fixes
349362

350-
- Specifying Python API version 2.14 no longer prevents ``set_block_temperature`` from executing a hold time.
351-
363+
- Specifying Python API version 2.14 no longer prevents `set_block_temperature` from executing a hold time.
352364

353365
---
354366

@@ -374,6 +386,7 @@ Some protocols can't be simulated with the `opentrons_simulate` command-line too
374386
- Python protocols specifying an `apiLevel` of 2.14
375387

376388
---
389+
377390
## OT-2 Software Changes in 6.2.1
378391

379392
Welcome to the v6.2.1 release of the OT-2 software! This hotfix release addresses a few problems.

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/protocol_api/core/engine/instrument.py

Lines changed: 43 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""ProtocolEngine-based InstrumentContext core implementation."""
22

33
from __future__ import annotations
4-
from contextlib import contextmanager
54
from itertools import dropwhile
65
from copy import deepcopy
76
from typing import (
@@ -13,7 +12,6 @@
1312
Sequence,
1413
Tuple,
1514
NamedTuple,
16-
Generator,
1715
Literal,
1816
)
1917
from opentrons.types import (
@@ -1384,43 +1382,25 @@ def _pick_up_tip() -> Tuple[Location, WellCore]:
13841382
air_gap=0,
13851383
)
13861384
]
1387-
# Enable LPD only if all of these apply:
1388-
# - LPD is globally enabled for this pipette
1389-
# - it is the first time visiting this well
1390-
# - pipette tip is unused
1391-
enable_lpd = (
1392-
self.get_liquid_presence_detection() and step_source != prev_src
1393-
)
1394-
elif new_tip == TransferTipPolicyV2.ONCE:
1395-
# Enable LPD only if:
1396-
# - LPD is globally enabled for this pipette
1397-
# - this is the first source well of the entire transfer, which means
1398-
# that the current tip is unused
1399-
enable_lpd = self.get_liquid_presence_detection() and prev_src is None
1400-
else:
1401-
enable_lpd = False
14021385

1403-
with self.lpd_for_transfer(enable_lpd):
1404-
post_asp_tip_contents = self.aspirate_liquid_class(
1405-
volume=step_volume,
1406-
source=step_source,
1407-
transfer_properties=transfer_props,
1408-
transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
1409-
tip_contents=post_disp_tip_contents,
1410-
volume_for_pipette_mode_configuration=step_volume,
1411-
)
1412-
post_disp_tip_contents = self.dispense_liquid_class(
1413-
volume=step_volume,
1414-
dest=step_destination,
1415-
source=step_source,
1416-
transfer_properties=transfer_props,
1417-
transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
1418-
tip_contents=post_asp_tip_contents,
1419-
add_final_air_gap=(
1420-
False if is_last_step and keep_last_tip else True
1421-
),
1422-
trash_location=trash_location,
1423-
)
1386+
post_asp_tip_contents = self.aspirate_liquid_class(
1387+
volume=step_volume,
1388+
source=step_source,
1389+
transfer_properties=transfer_props,
1390+
transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
1391+
tip_contents=post_disp_tip_contents,
1392+
volume_for_pipette_mode_configuration=step_volume,
1393+
)
1394+
post_disp_tip_contents = self.dispense_liquid_class(
1395+
volume=step_volume,
1396+
dest=step_destination,
1397+
source=step_source,
1398+
transfer_properties=transfer_props,
1399+
transfer_type=tx_comps_executor.TransferType.ONE_TO_ONE,
1400+
tip_contents=post_asp_tip_contents,
1401+
add_final_air_gap=(False if is_last_step and keep_last_tip else True),
1402+
trash_location=trash_location,
1403+
)
14241404
prev_src = step_source
14251405
prev_dest = step_destination
14261406

@@ -1695,17 +1675,6 @@ def _pick_up_tip() -> Tuple[Location, WellCore]:
16951675
" Specify a blowout location and enable blowout when using a disposal volume."
16961676
)
16971677

1698-
if (
1699-
self.get_liquid_presence_detection()
1700-
and new_tip != TransferTipPolicyV2.NEVER
1701-
and is_first_step
1702-
):
1703-
# Do probing only once, regardless of whether you are coming back to refill
1704-
# with a fresh tip since there's only one source well
1705-
enable_lpd = True
1706-
else:
1707-
enable_lpd = False
1708-
17091678
if not is_first_step and new_tip == TransferTipPolicyV2.ALWAYS:
17101679
_drop_tip()
17111680
last_tip = _pick_up_tip()
@@ -1715,20 +1684,19 @@ def _pick_up_tip() -> Tuple[Location, WellCore]:
17151684
air_gap=0,
17161685
)
17171686
]
1718-
with self.lpd_for_transfer(enable=enable_lpd):
1719-
# Aspirate the total volume determined by the loop above
1720-
tip_contents = self.aspirate_liquid_class(
1721-
volume=total_aspirate_volume + conditioning_vol + disposal_vol,
1722-
source=source,
1723-
transfer_properties=transfer_props,
1724-
transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
1725-
tip_contents=tip_contents,
1726-
# We configure the mode based on the last dispense volume and disposal volume
1727-
# since the mode is only used to determine the dispense push out volume
1728-
# and we can do a push out only at the last dispense, that too if there is no disposal volume.
1729-
volume_for_pipette_mode_configuration=vol_dest_combo[-1][0],
1730-
conditioning_volume=conditioning_vol,
1731-
)
1687+
# Aspirate the total volume determined by the loop above
1688+
tip_contents = self.aspirate_liquid_class(
1689+
volume=total_aspirate_volume + conditioning_vol + disposal_vol,
1690+
source=source,
1691+
transfer_properties=transfer_props,
1692+
transfer_type=tx_comps_executor.TransferType.ONE_TO_MANY,
1693+
tip_contents=tip_contents,
1694+
# We configure the mode based on the last dispense volume and disposal volume
1695+
# since the mode is only used to determine the dispense push out volume
1696+
# and we can do a push out only at the last dispense, that too if there is no disposal volume.
1697+
volume_for_pipette_mode_configuration=vol_dest_combo[-1][0],
1698+
conditioning_volume=conditioning_vol,
1699+
)
17321700

17331701
# If the tip has volumes correspoinding to multiple destinations, then
17341702
# multi-dispense in those destinations.
@@ -1963,7 +1931,6 @@ def _pick_up_tip() -> Tuple[Location, WellCore]:
19631931
next_step_volume, next_source = next(source_per_volume_step)
19641932
is_first_step = True
19651933
is_last_step = False
1966-
prev_src: Optional[tuple[Location, WellCore]] = None
19671934
while not is_last_step:
19681935
total_dispense_volume = 0.0
19691936
vol_aspirate_combo = []
@@ -1981,13 +1948,7 @@ def _pick_up_tip() -> Tuple[Location, WellCore]:
19811948
is_last_step = True
19821949
break
19831950

1984-
if (
1985-
self.get_liquid_presence_detection()
1986-
and new_tip != TransferTipPolicyV2.NEVER
1987-
and is_first_step
1988-
):
1989-
enable_lpd = True
1990-
elif not is_first_step and new_tip == TransferTipPolicyV2.ALWAYS:
1951+
if not is_first_step and new_tip == TransferTipPolicyV2.ALWAYS:
19911952
_drop_tip()
19921953
last_tip = _pick_up_tip()
19931954
tip_contents = [
@@ -1996,33 +1957,22 @@ def _pick_up_tip() -> Tuple[Location, WellCore]:
19961957
air_gap=0,
19971958
)
19981959
]
1999-
# Enable LPD if it's globally enabled, as long as
2000-
# the next source is different from previous source
2001-
enable_lpd = (
2002-
self.get_liquid_presence_detection()
2003-
and prev_src != vol_aspirate_combo[0][1]
2004-
)
2005-
else:
2006-
enable_lpd = False
20071960

20081961
total_aspirated_volume = 0.0
20091962
for step_num, (step_volume, step_source) in enumerate(vol_aspirate_combo):
2010-
with self.lpd_for_transfer(enable=enable_lpd):
2011-
tip_contents = self.aspirate_liquid_class(
2012-
volume=step_volume,
2013-
source=step_source,
2014-
transfer_properties=transfer_props,
2015-
transfer_type=tx_comps_executor.TransferType.MANY_TO_ONE,
2016-
tip_contents=tip_contents,
2017-
volume_for_pipette_mode_configuration=(
2018-
total_dispense_volume if step_num == 0 else None
2019-
),
2020-
current_volume=total_aspirated_volume,
2021-
)
2022-
prev_src = step_source
1963+
tip_contents = self.aspirate_liquid_class(
1964+
volume=step_volume,
1965+
source=step_source,
1966+
transfer_properties=transfer_props,
1967+
transfer_type=tx_comps_executor.TransferType.MANY_TO_ONE,
1968+
tip_contents=tip_contents,
1969+
volume_for_pipette_mode_configuration=(
1970+
total_dispense_volume if step_num == 0 else None
1971+
),
1972+
current_volume=total_aspirated_volume,
1973+
)
20231974
total_aspirated_volume += step_volume
20241975
is_first_step = False
2025-
enable_lpd = False
20261976
tip_contents = self.dispense_liquid_class(
20271977
volume=total_dispense_volume,
20281978
dest=dest,
@@ -2119,10 +2069,6 @@ def aspirate_liquid_class(
21192069
location=prep_location,
21202070
)
21212071
last_liquid_and_airgap_in_tip.air_gap = 0
2122-
if self.get_liquid_presence_detection():
2123-
self.liquid_probe_with_recovery(
2124-
well_core=source_well, loc=prep_location
2125-
)
21262072
# TODO: do volume configuration + prepare for aspirate only if the mode needs to be changed
21272073
self.configure_for_volume(volume_for_pipette_mode_configuration)
21282074
self.prepare_to_aspirate()
@@ -2541,14 +2487,6 @@ def delay(self, seconds: float) -> None:
25412487
"""Call a protocol delay."""
25422488
self._protocol_core.delay(seconds=seconds, msg=None)
25432489

2544-
@contextmanager
2545-
def lpd_for_transfer(self, enable: bool) -> Generator[None, None, None]:
2546-
"""Context manager for the instrument's LPD state during a transfer."""
2547-
global_lpd_enabled = self.get_liquid_presence_detection()
2548-
self.set_liquid_presence_detection(enable=enable)
2549-
yield
2550-
self.set_liquid_presence_detection(enable=global_lpd_enabled)
2551-
25522490

25532491
class _TipInfo(NamedTuple):
25542492
tiprack_location: Location

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

0 commit comments

Comments
 (0)