Skip to content

Commit

Permalink
Merge branch 'edge' into stacker_evt-script-test-axes
Browse files Browse the repository at this point in the history
  • Loading branch information
ahiuchingau authored Nov 19, 2024
2 parents 626a471 + 29d7e87 commit 175d100
Show file tree
Hide file tree
Showing 156 changed files with 14,928 additions and 1,628 deletions.
6 changes: 5 additions & 1 deletion abr-testing/abr_testing/data_collection/abr_google_drive.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ def create_data_dictionary(
file_path = os.path.join(storage_directory, filename)
if file_path.endswith(".json"):
with open(file_path) as file:
file_results = json.load(file)
try:
file_results = json.load(file)
except json.decoder.JSONDecodeError:
print(f"Skipped file {file_path} bc no data.")
continue
else:
continue
if not isinstance(file_results, dict):
Expand Down
2 changes: 1 addition & 1 deletion abr-testing/abr_testing/data_collection/get_run_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def get_run_ids_from_robot(ip: str) -> Set[str]:
f"http://{ip}:31950/runs", headers={"opentrons-version": "3"}
)
run_data = response.json()
run_list = run_data["data"]
run_list = run_data.get("data", "")
except requests.exceptions.RequestException:
print(f"Could not connect to robot with IP {ip}")
run_list = []
Expand Down
2 changes: 1 addition & 1 deletion api/docs/v2/parameters/use_case_sample_count.rst
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Now we'll bring sample count into consideration as we :ref:`load the liquids <lo
* - Tagmentation Wash Buffer
- 900

To calculate the total volume for each liquid, we'll multiply these numbers by ``column_count`` and by 1.1 (to ensure that the pipette can aspirate the required volume without drawing in air at the bottom of the well). This calculation can be done inline as the ``volume`` value of :py:meth:`.load_liquid`::
To calculate the total volume for each liquid, we'll multiply these numbers by ``column_count`` and by 1.1 (to ensure that the pipette can aspirate the required volume without drawing in air at the bottom of the well). This calculation can be done inline as the ``volume`` value of :py:meth:`~.Well.load_liquid`::

reservoir["A1"].load_liquid(
liquid=ampure_liquid, volume=180 * column_count * 1.1
Expand Down
2 changes: 1 addition & 1 deletion api/docs/v2/pipettes/partial_tip_pickup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ The following table summarizes the limitations in place along each side of the d
* - A1–D1 (left edge)
- Rightmost column
- None (all wells accessible)
* - A1–D3 (back edge)
* - A1–A3 (back edge)
- Frontmost row
- Rows A–G
* - A3–D3 (right edge)
Expand Down
6 changes: 4 additions & 2 deletions api/src/opentrons/protocol_api/_liquid_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ def delete_for_volume(self, volume: float) -> None:

def _sort_volume_and_values(self) -> None:
"""Sort volume in increasing order along with corresponding values in matching order."""
self._sorted_volumes, self._sorted_values = zip(
*sorted(self._properties_by_volume.items())
self._sorted_volumes, self._sorted_values = (
zip(*sorted(self._properties_by_volume.items()))
if len(self._properties_by_volume) > 0
else [(), ()]
)


Expand Down
23 changes: 22 additions & 1 deletion api/src/opentrons/protocol_api/core/engine/labware.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""ProtocolEngine-based Labware core implementations."""
from typing import List, Optional, cast

from typing import List, Optional, cast, Dict

from opentrons_shared_data.labware.types import (
LabwareParameters as LabwareParametersDict,
Expand All @@ -22,7 +23,9 @@
from opentrons.types import DeckSlotName, NozzleMapInterface, Point, StagingSlotName


from ..._liquid import Liquid
from ..labware import AbstractLabware, LabwareLoadParams

from .well import WellCore


Expand Down Expand Up @@ -202,3 +205,21 @@ def get_deck_slot(self) -> Optional[DeckSlotName]:
LocationIsStagingSlotError,
):
return None

def load_liquid(self, volumes: Dict[str, float], liquid: Liquid) -> None:
"""Load liquid into wells of the labware."""
self._engine_client.execute_command(
cmd.LoadLiquidParams(
labwareId=self._labware_id, liquidId=liquid._id, volumeByWell=volumes
)
)

def load_empty(self, wells: List[str]) -> None:
"""Mark wells of the labware as empty."""
self._engine_client.execute_command(
cmd.LoadLiquidParams(
labwareId=self._labware_id,
liquidId="EMPTY",
volumeByWell={well: 0.0 for well in wells},
)
)
16 changes: 0 additions & 16 deletions api/src/opentrons/protocol_api/core/engine/well.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,22 +142,6 @@ def load_liquid(
)
)

def load_empty(
self,
) -> None:
"""Inform the system that a well is known to be empty.
This should be done early in the protocol, at the same time as a load_liquid command might
be used.
"""
self._engine_client.execute_command(
cmd.LoadLiquidParams(
labwareId=self._labware_id,
liquidId="EMPTY",
volumeByWell={self._name: 0.0},
)
)

def from_center_cartesian(self, x: float, y: float, z: float) -> Point:
"""Gets point in deck coordinates based on percentage of the radius of each axis."""
well_size = self._engine_client.state.labware.get_well_size(
Expand Down
12 changes: 11 additions & 1 deletion api/src/opentrons/protocol_api/core/labware.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""The interface that implements InstrumentContext."""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any, Generic, List, NamedTuple, Optional, TypeVar
from typing import Any, Generic, List, NamedTuple, Optional, TypeVar, Dict

from opentrons_shared_data.labware.types import (
LabwareUri,
Expand All @@ -11,6 +12,7 @@
)

from opentrons.types import DeckSlotName, Point, NozzleMapInterface
from .._liquid import Liquid

from .well import WellCoreType

Expand Down Expand Up @@ -133,5 +135,13 @@ def get_well_core(self, well_name: str) -> WellCoreType:
def get_deck_slot(self) -> Optional[DeckSlotName]:
"""Get the deck slot the labware or its parent is in, if any."""

@abstractmethod
def load_liquid(self, volumes: Dict[str, float], liquid: Liquid) -> None:
"""Load liquid into wells of the labware."""

@abstractmethod
def load_empty(self, wells: List[str]) -> None:
"""Mark wells of the labware as empty."""


LabwareCoreType = TypeVar("LabwareCoreType", bound=AbstractLabware[Any])
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Optional
from typing import List, Optional, Dict

from opentrons.calibration_storage import helpers
from opentrons.protocols.geometry.labware_geometry import LabwareGeometry
Expand All @@ -8,6 +8,7 @@

from opentrons_shared_data.labware.types import LabwareParameters, LabwareDefinition

from ..._liquid import Liquid
from ..labware import AbstractLabware, LabwareLoadParams
from .legacy_well_core import LegacyWellCore
from .well_geometry import WellGeometry
Expand Down Expand Up @@ -220,3 +221,11 @@ def get_deck_slot(self) -> Optional[DeckSlotName]:
"""Get the deck slot the labware is in, if in a deck slot."""
slot = self._geometry.parent.labware.first_parent()
return DeckSlotName.from_primitive(slot) if slot is not None else None

def load_liquid(self, volumes: Dict[str, float], liquid: Liquid) -> None:
"""Load liquid into wells of the labware."""
assert False, "load_liquid only supported in API version 2.22 & later"

def load_empty(self, wells: List[str]) -> None:
"""Mark wells of the labware as empty."""
assert False, "load_empty only supported in API version 2.22 & later"
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,6 @@ def load_liquid(
"""Load liquid into a well."""
raise APIVersionError(api_element="Loading a liquid")

def load_empty(self) -> None:
"""Mark a well as empty."""
assert False, "load_empty only supported on engine core"

def from_center_cartesian(self, x: float, y: float, z: float) -> Point:
"""Gets point in deck coordinates based on percentage of the radius of each axis."""
return self._geometry.from_center_cartesian(x, y, z)
Expand Down
4 changes: 0 additions & 4 deletions api/src/opentrons/protocol_api/core/well.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,6 @@ def load_liquid(
) -> None:
"""Load liquid into a well."""

@abstractmethod
def load_empty(self) -> None:
"""Mark a well as containing no liquid."""

@abstractmethod
def from_center_cartesian(self, x: float, y: float, z: float) -> Point:
"""Gets point in deck coordinates based on percentage of the radius of each axis."""
Expand Down
160 changes: 151 additions & 9 deletions api/src/opentrons/protocol_api/labware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
""" opentrons.protocol_api.labware: classes and functions for labware handling
"""opentrons.protocol_api.labware: classes and functions for labware handling
This module provides things like :py:class:`Labware`, and :py:class:`Well`
to encapsulate labware instances used in protocols
Expand All @@ -13,7 +13,18 @@
import logging

from itertools import dropwhile
from typing import TYPE_CHECKING, Any, List, Dict, Optional, Union, Tuple, cast
from typing import (
TYPE_CHECKING,
Any,
List,
Dict,
Optional,
Union,
Tuple,
cast,
Sequence,
Mapping,
)

from opentrons_shared_data.labware.types import LabwareDefinition, LabwareParameters

Expand Down Expand Up @@ -281,19 +292,15 @@ def load_liquid(self, liquid: Liquid, volume: float) -> None:
:param Liquid liquid: The liquid to load into the well.
:param float volume: The volume of liquid to load, in µL.
.. note::
In API version 2.22 and later, use :py:meth:`~.Well.load_empty()` to mark a well as empty at the beginning of a protocol, rather than using this method with ``volume=0``.
.. deprecated:: 2.22
In API version 2.22 and later, use :py:meth:`~Labware.load_liquid`, :py:meth:`~Labware.load_liquid_by_well`,
or :py:meth:`~Labware.load_empty` to load liquid into a well.
"""
self._core.load_liquid(
liquid=liquid,
volume=volume,
)

@requires_version(2, 22)
def load_empty(self) -> None:
"""Mark a well as empty."""
self._core.load_empty()

def _from_center_cartesian(self, x: float, y: float, z: float) -> Point:
"""
Private version of from_center_cartesian. Present only for backward
Expand Down Expand Up @@ -1113,6 +1120,141 @@ def reset(self) -> None:
"""
self._core.reset_tips()

@requires_version(2, 22)
def load_liquid(
self, wells: Sequence[Union[str, Well]], volume: float, liquid: Liquid
) -> None:
"""Mark several wells as containing the same amount of liquid.
This method should be called at the beginning of a protocol, soon after loading the labware and before
liquid handling operations begin. It is a base of information for liquid tracking functionality. If a well in a labware
has not been named in a call to :py:meth:`~Labware.load_empty`, :py:meth:`~Labware.load_liquid`, or
:py:meth:`~Labware.load_liquid_by_well`, the volume it contains is unknown and the well's liquid will not be tracked.
For example, to load 10µL of a liquid named ``water`` (defined with :py:meth:`~ProtocolContext.define_liquid`)
into all the wells of a labware, you could call ``labware.load_liquid(labware.wells(), 10, water)``.
If you want to load different volumes of liquid into different wells, use :py:meth:`~Labware.load_liquid_by_well`.
If you want to mark the well as containing no liquid, use :py:meth:`~Labware.load_empty`.
:param wells: The wells to load the liquid into.
:type wells: List of well names or list of Well objects, for instance from :py:meth:`~Labware.wells`.
:param volume: The volume of liquid to load into each well, in 10µL.
:type volume: float
:param liquid: The liquid to load into each well, previously defined by :py:meth:`~ProtocolContext.define_liquid`
:type liquid: Liquid
"""
well_names: List[str] = []
for well in wells:
if isinstance(well, str):
if well not in self.wells_by_name():
raise KeyError(
f"{well} is not a well in labware {self.name}. The elements of wells should name wells in this labware."
)
well_names.append(well)
elif isinstance(well, Well):
if well.parent is not self:
raise KeyError(
f"{well.well_name} is not a well in labware {self.name}. The elements of wells should be wells of this labware."
)
well_names.append(well.well_name)
else:
raise TypeError(
f"Unexpected type for element {repr(well)}. The elements of wells should be Well instances or well names."
)
if not isinstance(volume, (float, int)):
raise TypeError(
f"Unexpected type for volume {repr(volume)}. Volume should be a number in microliters."
)
self._core.load_liquid({well_name: volume for well_name in well_names}, liquid)

@requires_version(2, 22)
def load_liquid_by_well(
self, volumes: Mapping[Union[str, Well], float], liquid: Liquid
) -> None:
"""Mark several wells as containing unique volumes of liquid.
This method should be called at the beginning of a protocol, soon after loading the labware and before
liquid handling operations begin. It is a base of information for liquid tracking functionality. If a well in a labware
has not been named in a call to :py:meth:`~Labware.load_empty`, :py:meth:`~Labware.load_liquid`, or
:py:meth:`~Labware.load_liquid_by_well`, the volume it contains is unknown and the well's liquid will not be tracked.
For example, to load a decreasing amount of of a liquid named ``water`` (defined with :py:meth:`~ProtocolContext.define_liquid`)
into each successive well of a row, you could call
``labware.load_liquid_by_well({'A1': 1000, 'A2': 950, 'A3': 900, ..., 'A12': 600}, water)``
If you want to load the same volume of a liquid into multiple wells, it is often easier to use :py:meth:`~Labware.load_liquid`.
If you want to mark the well as containing no liquid, use :py:meth:`~Labware.load_empty`.
:param volumes: A dictionary of well names (or :py:class:`Well` objects, for instance from ``labware['A1']``)
:type wells: Dict[Union[str, Well], float]
:param liquid: The liquid to load into each well, previously defined by :py:meth:`~ProtocolContext.define_liquid`
:type liquid: Liquid
"""
verified_volumes: Dict[str, float] = {}
for well, volume in volumes.items():
if isinstance(well, str):
if well not in self.wells_by_name():
raise KeyError(
f"{well} is not a well in {self.name}. The keys of volumes should name wells in this labware"
)
verified_volumes[well] = volume
elif isinstance(well, Well):
if well.parent is not self:
raise KeyError(
f"{well.well_name} is not a well in {self.name}. The keys of volumes should be wells of this labware"
)
verified_volumes[well.well_name] = volume
else:
raise TypeError(
f"Unexpected type for well name {repr(well)}. The keys of volumes should be Well instances or well names."
)
if not isinstance(volume, (float, int)):
raise TypeError(
f"Unexpected type for volume {repr(volume)}. The values of volumes should be numbers in microliters."
)
self._core.load_liquid(verified_volumes, liquid)

@requires_version(2, 22)
def load_empty(self, wells: Sequence[Union[Well, str]]) -> None:
"""Mark several wells as empty.
This method should be called at the beginning of a protocol, soon after loading the labware and before liquid handling
operations begin. It is a base of information for liquid tracking functionality. If a well in a labware has not been named
in a call to :py:meth:`Labware.load_empty`, :py:meth:`Labware.load_liquid`, or :py:meth:`Labware.load_liquid_by_well`, the
volume it contains is unknown and the well's liquid will not be tracked.
For instance, to mark all wells in the labware as empty, you can call ``labware.load_empty(labware.wells())``.
:param wells: The list of wells to mark empty. To mark all wells as empty, pass ``labware.wells()``. You can also specify
wells by their names (for instance, ``labware.load_empty(['A1', 'A2'])``).
:type wells: Union[List[Well], List[str]]
"""
well_names: List[str] = []
for well in wells:
if isinstance(well, str):
if well not in self.wells_by_name():
raise KeyError(
f"{well} is not a well in {self.name}. The elements of wells should name wells in this labware."
)
well_names.append(well)
elif isinstance(well, Well):
if well.parent is not self:
raise KeyError(
f"{well.well_name} is not a well in {self.name}. The elements of wells should be wells of this labware."
)
well_names.append(well.well_name)
else:
raise TypeError(
f"Unexpected type for well name {repr(well)}. The elements of wells should be Well instances or well names."
)
self._core.load_empty(well_names)


# TODO(mc, 2022-11-09): implementation detail, move to core
def split_tipracks(tip_racks: List[Labware]) -> Tuple[Labware, List[Labware]]:
Expand Down
Loading

0 comments on commit 175d100

Please sign in to comment.