Skip to content

Commit

Permalink
Merge branch 'edge' into pd_redesign-pause-stepform
Browse files Browse the repository at this point in the history
  • Loading branch information
ncdiehl11 committed Sep 23, 2024
2 parents 52c1bb9 + 426ecab commit 55bdf70
Show file tree
Hide file tree
Showing 468 changed files with 2,711 additions and 2,385 deletions.
12 changes: 8 additions & 4 deletions api/docs/v2/basic_commands/liquids.rst
Original file line number Diff line number Diff line change
Expand Up @@ -263,14 +263,15 @@ This example aspirates enough air to fill the remaining volume in a pipette::
Detect Liquids
==============

The :py:meth:`.InstrumentContext.detect_liquid_presence` method tells a Flex pipette to check for liquid in a well. It returns ``True`` if the pressure sensors in the pipette detect a liquid and ``False`` if the sensors do not.
The :py:meth:`.InstrumentContext.detect_liquid_presence` method tells a Flex pipette to check for liquid in a well. It returns ``True`` if the pressure sensors in the pipette detect a liquid and ``False`` if the sensors do not. When ``detect_liquid_presence()`` finds an empty well it won't raise an error or stop your protocol.

Aspiration isn't required to use ``detect_liquid_presence()``. This is a standalone method that can be called when you want the robot to record the presence or absence of a liquid. When ``detect_liquid_presence()`` finds an empty well it won't raise an error or stop your protocol.
``detect_liquid_presence()`` is a standalone method to record the presence or absence of a liquid. You don't have to aspirate after detecting liquid presence. However, you should always pick up a tip immediately prior to checking for liquid, and either aspirate or drop the tip immediately after. This ensures that the pipette uses a clean, dry tip to check for liquid, and prevents cross-contamination.

A potential use of liquid detection is to try aspirating from another well if the first well is found to contain no liquid.

.. code-block:: python
pipette.pick_up_tip()
if pipette.detect_liquid_presence(reservoir["A1"]):
pipette.aspirate(100, reservoir["A1"])
else:
Expand All @@ -283,13 +284,16 @@ A potential use of liquid detection is to try aspirating from another well if th
Require Liquids
===============

The :py:meth:`.InstrumentContext.require_liquid_presence` method tells a Flex pipette to check for `and require` liquid in a well.
The :py:meth:`.InstrumentContext.require_liquid_presence` method tells a Flex pipette to check for `and require` liquid in a well. When ``require_liquid_presence()`` finds an empty well, it raises an error and pauses the protocol to let you resolve the problem.

Aspiration isn't required to use ``require_liquid_presence()``. This is a standalone method that can be called when you want the robot to react to a missing liquid or empty well. When ``require_liquid_presence()`` finds an empty well, it raises an error and pauses the protocol to let you resolve the problem. See also :ref:`lpd`.
``require_liquid_presence()`` is a standalone method to react to a missing liquid or empty well. You don't have to aspirate after requiring liquid presence. However, you should always pick up a tip immediately prior to checking for liquid, and either aspirate or drop the tip immediately after. This ensures that the pipette uses a clean, dry tip to check for liquid, and prevents cross-contamination.

.. code-block:: python
pipette.pick_up_tip()
pipette.require_liquid_presence(reservoir["A1"])
pipette.aspirate(100, reservoir["A1"]) # only occurs if liquid found
You can also require liquid presence for all aspirations performed with a given pipette. See :ref:`lpd`.

.. versionadded:: 2.20
14 changes: 9 additions & 5 deletions api/docs/v2/pipettes/loading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -221,15 +221,17 @@ Another example is a Flex protocol that uses a waste chute. Say you want to only
Liquid Presence Detection
=========================

Liquid presence detection is a pressure-based feature that allows Opentrons Flex pipettes to detect the presence or absence of liquids in a well, reservoir, tube, or other container. It gives you the ability to identify, avoid, and recover from liquid-related protocol errors. You can enable this feature for an entire protocol run or toggle it on and off as required. Liquid presence detection is disabled by default.
Liquid presence detection is a pressure-based feature that allows Opentrons Flex pipettes to detect the presence or absence of liquids in a well, reservoir, tube, or other container. It gives you the ability to identify, avoid, and recover from liquid-related protocol errors.

When detecting liquid, the pipette slowly moves a fresh, empty tip downward from the top of the well until it contacts the liquid. The downward probing motion can take anywhere from 5 to 50 seconds, depending on the depth of the well and how much liquid it contains. For example, it will take much less time to detect liquid in a full flat well plate than in an empty (or nearly empty) large tube.

You can enable this feature for an entire protocol run or toggle it on and off as required. Consider the amount of time automatic detection will add to your protocol. If you only need to detect liquid infrequently, use the :ref:`corresponding building block commands <detect-liquid-presence>` instead. Automatic liquid presence detection is disabled by default.

Pipette Compatibility
---------------------

Liquid presence detection works with Flex 1-, 8-, and 96-channel pipettes only. 1-channel pipettes have one pressure sensor. The 8-channel pipette pressure sensors are on channels 1 and 8 (positions A1 and H1). The 96-channel pipette pressure sensors are on channels 1 and 96 (positions A1 and H12). Other channels on multi-channel pipettes do not have sensors and cannot detect liquid.

.. add text with link to revised pipette sensor section in manual?
Enabling Globally
-----------------

Expand All @@ -245,9 +247,11 @@ To automatically use liquid presence detection, add the optional Boolean argumen
)
.. note::
Accurate liquid detection requires fresh, dry pipette tips. Protocols using this feature must discard used tips after an aspirate/dispense cycle and pick up new tips before the next cycle. The API will raise an error if liquid detection is active and your protocol attempts to reuse a pipette tip or if the robot thinks the tip is wet.
Accurate liquid detection requires fresh, dry pipette tips. Protocols using this feature must discard used tips after an aspirate/dispense cycle and pick up new tips before the next cycle. :ref:`Complex commands <v2-complex-commands>` may include aspirate steps after a tip is already wet. When global liquid detection is enabled, use :ref:`building block commands <v2-atomic-commands>` to ensure that your protocol picks up a tip immediately before aspiration.

The API will not raise an error during liquid detection if a tip is empty but wet. It will raise an error if liquid detection is active and your protocol attempts to aspirate with liquid in the tip.

Let's take a look at how all this works. First, tell the robot to pick up a clean tip, aspirate 100 µL from a reservoir, and dispense that volume into a well plate.
Let's take a look at how all this works. With automatic liquid detection enabled, tell the robot to pick up a clean tip, aspirate 100 µL from a reservoir, and dispense that volume into a well plate:

.. code-block:: python
Expand Down
2 changes: 2 additions & 0 deletions api/docs/v2/versioning.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ Changes in API Versions
Version 2.20
------------

- Detect liquid presence within a well. The :py:meth:`.InstrumentContext.detect_liquid_presence()` and :py:meth:`.InstrumentContext.require_liquid_presence()` building block commands check for liquid any point in your protocol. You can also :ref:`enable liquid presence detection <lpd>` for all aspirations when loading a pipette, although this will add significant time to your protocol.
- Define CSV runtime parameters and use their contents in a protocol with new :ref:`data manipulation methods <rtp-csv-data>`. See the :ref:`cherrypicking use case <use-case-cherrypicking>` for a full example.
- :py:meth:`.configure_nozzle_layout` now accepts row, single, and partial column layout constants. See :ref:`partial-tip-pickup`.
- You can now call :py:obj:`.ProtocolContext.define_liquid()` without supplying a ``description`` or ``display_color``.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from ...errors.error_occurrence import ErrorOccurrence
from ...state import update_types

if TYPE_CHECKING:
from opentrons.protocol_engine.state.state import StateView
Expand Down Expand Up @@ -54,6 +55,8 @@ async def execute(
self, params: OpenLabwareLatchParams
) -> SuccessData[OpenLabwareLatchResult, None]:
"""Open a Heater-Shaker's labware latch."""
state_update = update_types.StateUpdate()

# Allow propagation of ModuleNotLoadedError and WrongModuleTypeError.
hs_module_substate = self._state_view.modules.get_heater_shaker_module_substate(
module_id=params.moduleId
Expand All @@ -72,6 +75,7 @@ async def execute(
await self._movement.home(
axes=self._state_view.motion.get_robot_mount_axes()
)
state_update.clear_all_pipette_locations()

# Allow propagation of ModuleNotAttachedError.
hs_hardware_module = self._equipment.get_module_hardware_api(
Expand All @@ -84,6 +88,7 @@ async def execute(
return SuccessData(
public=OpenLabwareLatchResult(pipetteRetracted=pipette_should_retract),
private=None,
state_update=state_update,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from ...errors.error_occurrence import ErrorOccurrence
from ...state import update_types

if TYPE_CHECKING:
from opentrons.protocol_engine.state.state import StateView
Expand Down Expand Up @@ -56,6 +57,8 @@ async def execute(
params: SetAndWaitForShakeSpeedParams,
) -> SuccessData[SetAndWaitForShakeSpeedResult, None]:
"""Set and wait for a Heater-Shaker's target shake speed."""
state_update = update_types.StateUpdate()

# Allow propagation of ModuleNotLoadedError and WrongModuleTypeError.
hs_module_substate = self._state_view.modules.get_heater_shaker_module_substate(
module_id=params.moduleId
Expand All @@ -77,6 +80,7 @@ async def execute(
await self._movement.home(
axes=self._state_view.motion.get_robot_mount_axes()
)
state_update.clear_all_pipette_locations()

# Allow propagation of ModuleNotAttachedError.
hs_hardware_module = self._equipment.get_module_hardware_api(
Expand All @@ -91,6 +95,7 @@ async def execute(
pipetteRetracted=pipette_should_retract
),
private=None,
state_update=state_update,
)


Expand Down
10 changes: 9 additions & 1 deletion api/src/opentrons/protocol_engine/commands/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing_extensions import Literal

from opentrons.types import MountType
from ..state import update_types
from ..types import MotorAxis
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from ..errors.error_occurrence import ErrorOccurrence
Expand Down Expand Up @@ -51,14 +52,21 @@ def __init__(self, movement: MovementHandler, **kwargs: object) -> None:

async def execute(self, params: HomeParams) -> SuccessData[HomeResult, None]:
"""Home some or all motors to establish positional accuracy."""
state_update = update_types.StateUpdate()

if (
params.skipIfMountPositionOk is None
or not await self._movement.check_for_valid_position(
mount=params.skipIfMountPositionOk
)
):
await self._movement.home(axes=params.axes)
return SuccessData(public=HomeResult(), private=None)

# todo(mm, 2024-09-17): Clearing all pipette locations *unconditionally* is to
# preserve prior behavior, but we might only want to do this if we actually home.
state_update.clear_all_pipette_locations()

return SuccessData(public=HomeResult(), private=None, state_update=state_update)


class Home(BaseCommand[HomeParams, HomeResult, ErrorOccurrence]):
Expand Down
22 changes: 21 additions & 1 deletion api/src/opentrons/protocol_engine/commands/move_labware.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from typing_extensions import Literal

from opentrons.types import Point
from ..state import update_types
from ..types import (
CurrentWell,
LabwareLocation,
DeckSlotLocation,
OnLabwareLocation,
Expand Down Expand Up @@ -96,6 +98,8 @@ async def execute( # noqa: C901
self, params: MoveLabwareParams
) -> SuccessData[MoveLabwareResult, None]:
"""Move a loaded labware to a new location."""
state_update = update_types.StateUpdate()

# Allow propagation of LabwareNotLoadedError.
current_labware = self._state_view.labware.get(labware_id=params.labwareId)
current_labware_definition = self._state_view.labware.get_definition(
Expand Down Expand Up @@ -209,12 +213,28 @@ async def execute( # noqa: C901
user_offset_data=user_offset_data,
post_drop_slide_offset=post_drop_slide_offset,
)
# All mounts will have been retracted as part of the gripper move.
state_update.clear_all_pipette_locations()
elif params.strategy == LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE:
# Pause to allow for manual labware movement
await self._run_control.wait_for_resume()

# We may have just moved the labware that contains the current well out from
# under the pipette. Clear the current location to reflect the fact that the
# pipette is no longer over any labware. This is necessary for safe path
# planning in case the next movement goes to the same labware (now in a new
# place).
pipette_location = self._state_view.pipettes.get_current_location()
if (
isinstance(pipette_location, CurrentWell)
and pipette_location.labware_id == params.labwareId
):
state_update.clear_all_pipette_locations()

return SuccessData(
public=MoveLabwareResult(offsetId=new_offset_id), private=None
public=MoveLabwareResult(offsetId=new_offset_id),
private=None,
state_update=state_update,
)


Expand Down
15 changes: 14 additions & 1 deletion api/src/opentrons/protocol_engine/commands/move_relative.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from typing import TYPE_CHECKING, Optional, Type
from typing_extensions import Literal


from ..state import update_types
from ..types import MovementAxis, DeckPoint
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from ..errors.error_occurrence import ErrorOccurrence
Expand Down Expand Up @@ -48,14 +50,25 @@ async def execute(
self, params: MoveRelativeParams
) -> SuccessData[MoveRelativeResult, None]:
"""Move (jog) a given pipette a relative distance."""
state_update = update_types.StateUpdate()

x, y, z = await self._movement.move_relative(
pipette_id=params.pipetteId,
axis=params.axis,
distance=params.distance,
)
deck_point = DeckPoint.construct(x=x, y=y, z=z)
state_update.pipette_location = update_types.PipetteLocationUpdate(
pipette_id=params.pipetteId,
# TODO(jbl 2023-02-14): Need to investigate whether move relative should clear current location
new_location=update_types.NO_CHANGE,
new_deck_point=deck_point,
)

return SuccessData(
public=MoveRelativeResult(position=DeckPoint(x=x, y=y, z=z)), private=None
public=MoveRelativeResult(position=deck_point),
private=None,
state_update=state_update,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
from typing import TYPE_CHECKING, Optional, Type
from typing_extensions import Literal

from opentrons_shared_data.pipette.types import PipetteNameType

from ..errors import LocationNotAccessibleByPipetteError
from ..state import update_types
from ..types import DeckPoint, AddressableOffsetVector
from ..resources import fixture_validation
from .pipetting_common import (
Expand Down Expand Up @@ -88,9 +91,24 @@ async def execute(
self, params: MoveToAddressableAreaParams
) -> SuccessData[MoveToAddressableAreaResult, None]:
"""Move the requested pipette to the requested addressable area."""
state_update = update_types.StateUpdate()

self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration(
params.addressableAreaName
)
loaded_pipette = self._state_view.pipettes.get(params.pipetteId)
if loaded_pipette.pipetteName in (
PipetteNameType.P10_SINGLE,
PipetteNameType.P10_MULTI,
PipetteNameType.P50_MULTI,
PipetteNameType.P50_SINGLE,
PipetteNameType.P300_SINGLE,
PipetteNameType.P300_MULTI,
PipetteNameType.P1000_SINGLE,
):
extra_z_offset: Optional[float] = 5.0
else:
extra_z_offset = None

if fixture_validation.is_staging_slot(params.addressableAreaName):
raise LocationNotAccessibleByPipetteError(
Expand All @@ -105,11 +123,19 @@ async def execute(
minimum_z_height=params.minimumZHeight,
speed=params.speed,
stay_at_highest_possible_z=params.stayAtHighestPossibleZ,
highest_possible_z_extra_offset=extra_z_offset,
)
deck_point = DeckPoint.construct(x=x, y=y, z=z)
state_update.set_pipette_location(
pipette_id=params.pipetteId,
new_addressable_area_name=params.addressableAreaName,
new_deck_point=deck_point,
)

return SuccessData(
public=MoveToAddressableAreaResult(position=DeckPoint(x=x, y=y, z=z)),
private=None,
state_update=state_update,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing_extensions import Literal

from ..errors import LocationNotAccessibleByPipetteError
from ..state import update_types
from ..types import DeckPoint, AddressableOffsetVector
from ..resources import fixture_validation
from .pipetting_common import (
Expand Down Expand Up @@ -100,6 +101,8 @@ async def execute(
self, params: MoveToAddressableAreaForDropTipParams
) -> SuccessData[MoveToAddressableAreaForDropTipResult, None]:
"""Move the requested pipette to the requested addressable area in preperation of a drop tip."""
state_update = update_types.StateUpdate()

self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration(
params.addressableAreaName
)
Expand All @@ -126,12 +129,19 @@ async def execute(
speed=params.speed,
ignore_tip_configuration=params.ignoreTipConfiguration,
)
deck_point = DeckPoint.construct(x=x, y=y, z=z)
state_update.set_pipette_location(
pipette_id=params.pipetteId,
new_addressable_area_name=params.addressableAreaName,
new_deck_point=deck_point,
)

return SuccessData(
public=MoveToAddressableAreaForDropTipResult(
position=DeckPoint(x=x, y=y, z=z)
),
private=None,
state_update=state_update,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from typing import Optional, Type, TYPE_CHECKING
from typing_extensions import Literal


from ..state import update_types
from ..types import DeckPoint
from .pipetting_common import PipetteIdMixin, MovementMixin, DestinationPositionResult
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
Expand Down Expand Up @@ -50,17 +52,24 @@ async def execute(
self, params: MoveToCoordinatesParams
) -> SuccessData[MoveToCoordinatesResult, None]:
"""Move the requested pipette to the requested coordinates."""
state_update = update_types.StateUpdate()

x, y, z = await self._movement.move_to_coordinates(
pipette_id=params.pipetteId,
deck_coordinates=params.coordinates,
direct=params.forceDirect,
additional_min_travel_z=params.minimumZHeight,
speed=params.speed,
)
deck_point = DeckPoint.construct(x=x, y=y, z=z)
state_update.pipette_location = update_types.PipetteLocationUpdate(
pipette_id=params.pipetteId, new_location=None, new_deck_point=deck_point
)

return SuccessData(
public=MoveToCoordinatesResult(position=DeckPoint(x=x, y=y, z=z)),
private=None,
state_update=state_update,
)


Expand Down
Loading

0 comments on commit 55bdf70

Please sign in to comment.