Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 60 additions & 15 deletions api/src/opentrons/protocol_api/_liquid.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional, Dict, Union, TYPE_CHECKING
from typing import Optional, Dict, Union, TYPE_CHECKING, Tuple

from opentrons_shared_data.liquid_classes.liquid_class_definition import (
LiquidClassSchemaV1,
Expand Down Expand Up @@ -63,6 +63,20 @@ def create(cls, liquid_class_definition: LiquidClassSchemaV1) -> "LiquidClass":
_by_pipette_setting=by_pipette_settings,
)

@classmethod
def create_from(
cls,
name: str,
display_name: str,
by_pipette_setting: Dict[str, Dict[str, TransferProperties]],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include in the doc-string (or a comment) that this is keyed by pipette name and tiprack and the specific variety of strings we use for those?

) -> "LiquidClass":
"""Create a liquid class from the passed in args."""
return cls(
_name=name,
_display_name=display_name,
_by_pipette_setting=by_pipette_setting,
)

@property
def name(self) -> str:
return self._name
Expand All @@ -71,10 +85,54 @@ def name(self) -> str:
def display_name(self) -> str:
return self._display_name

def update_for(
self,
pipette: Union[str, InstrumentContext],
tip_rack: Union[str, Labware],
transfer_properties: TransferProperties,
) -> None:
"""Update the transfer properties for the given pipette and tip combo.

If an entry does not exist, it will be created.
Copy link
Copy Markdown
Contributor

@ddcc4 ddcc4 May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this true? If self._by_pipette_setting[pipette_name] didn't exist, in your code on line 102, wouldn't this function just crash instead of creating it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops you are right. Will fix it

"""
pipette_name, tiprack_uri = self._get_pipette_and_tiprack_names(
pipette, tip_rack
)
try:
self._by_pipette_setting[pipette_name].update(
{tiprack_uri: transfer_properties}
)
except KeyError:
self._by_pipette_setting[pipette_name] = {tiprack_uri: transfer_properties}

def get_for(
self, pipette: Union[str, InstrumentContext], tip_rack: Union[str, Labware]
) -> TransferProperties:
"""Get liquid class transfer properties for the specified pipette and tip."""
pipette_name, tiprack_uri = self._get_pipette_and_tiprack_names(
pipette, tip_rack
)

try:
settings_for_pipette = self._by_pipette_setting[pipette_name]
except KeyError:
raise NoLiquidClassPropertyError(
f"No properties found for {pipette_name} in {self._name} liquid class"
)
try:
transfer_properties = settings_for_pipette[tiprack_uri]
except KeyError:
raise NoLiquidClassPropertyError(
f"No properties found for {tiprack_uri} for {pipette_name} in {self._name} liquid class"
)
return transfer_properties

@staticmethod
def _get_pipette_and_tiprack_names(
pipette: Union[str, InstrumentContext],
tip_rack: Union[str, Labware],
) -> Tuple[str, str]:
"""Return the pipette and tip rack name strings from the given pipette and tip rack."""
from . import InstrumentContext, Labware

if isinstance(pipette, InstrumentContext):
Expand All @@ -96,17 +154,4 @@ def get_for(
f"{tip_rack} should either be a tiprack Labware object"
f" or a tiprack URI string."
)

try:
settings_for_pipette = self._by_pipette_setting[pipette_name]
except KeyError:
raise NoLiquidClassPropertyError(
f"No properties found for {pipette_name} in {self._name} liquid class"
)
try:
transfer_properties = settings_for_pipette[tiprack_uri]
except KeyError:
raise NoLiquidClassPropertyError(
f"No properties found for {tiprack_uri} for {pipette_name} in {self._name} liquid class"
)
return transfer_properties
return pipette_name, tiprack_uri
20 changes: 14 additions & 6 deletions api/src/opentrons/protocol_api/_liquid_properties.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from dataclasses import dataclass
from numpy import interp
from typing import Optional, Dict, Sequence, Tuple, List
from typing import Optional, Dict, Sequence, Tuple, List, Union

from opentrons_shared_data.liquid_classes.liquid_class_definition import (
TransferProperties as SharedDataTransferProperties,
AspirateProperties as SharedDataAspirateProperties,
SingleDispenseProperties as SharedDataSingleDispenseProperties,
MultiDispenseProperties as SharedDataMultiDispenseProperties,
Expand All @@ -23,7 +24,6 @@
PositionReference,
Coordinate,
)

from . import validation


Expand Down Expand Up @@ -778,12 +778,20 @@ def build_multi_dispense_properties(


def build_transfer_properties(
by_tip_type_setting: SharedByTipTypeSetting,
transfer_properties: Union[SharedDataTransferProperties, SharedByTipTypeSetting],
) -> TransferProperties:
if isinstance(transfer_properties, SharedByTipTypeSetting):
_transfer_properties = SharedDataTransferProperties(
aspirate=transfer_properties.aspirate,
singleDispense=transfer_properties.singleDispense,
multiDispense=transfer_properties.multiDispense,
)
else:
_transfer_properties = transfer_properties
return TransferProperties(
_aspirate=build_aspirate_properties(by_tip_type_setting.aspirate),
_dispense=build_single_dispense_properties(by_tip_type_setting.singleDispense),
_aspirate=build_aspirate_properties(_transfer_properties.aspirate),
_dispense=build_single_dispense_properties(_transfer_properties.singleDispense),
_multi_dispense=build_multi_dispense_properties(
by_tip_type_setting.multiDispense
_transfer_properties.multiDispense
),
)
9 changes: 3 additions & 6 deletions api/src/opentrons/protocol_api/core/engine/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
from typing import Dict, Optional, Type, Union, List, Tuple, TYPE_CHECKING

from opentrons_shared_data.liquid_classes import LiquidClassDefinitionDoesNotExist

from opentrons.protocol_engine import commands as cmd
from opentrons.protocol_engine.commands import LoadModuleResult

from opentrons_shared_data.deck.types import DeckDefinitionV5, SlotDefV3
from opentrons_shared_data.labware.labware_definition import (
labware_definition_type_adapter,
Expand Down Expand Up @@ -35,7 +31,8 @@
from opentrons.protocols.api_support.util import AxisMaxSpeeds
from opentrons.protocols.api_support.types import APIVersion


from opentrons.protocol_engine import commands as cmd
from opentrons.protocol_engine.commands import LoadModuleResult
from opentrons.protocol_engine import (
DeckSlotLocation,
AddressableAreaLocation,
Expand Down Expand Up @@ -1071,7 +1068,7 @@ def define_liquid(
display_color=(liquid.displayColor.root if liquid.displayColor else None),
)

def define_liquid_class(self, name: str, version: int = 1) -> LiquidClass:
def define_liquid_class(self, name: str, version: int) -> LiquidClass:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, why did you take out @jbleon95's default of version=1 here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm passing the default version in the caller for define_liquid_class so there is no reason for this function to assign a default. Besides, mypy doesn't like this. This function was being called in protocol_context.py as define_liquid_class(name) which passes None value to version but version is not marked as Optional so it raises a mypy error. I am a bit surprised that mypy didn't flag this in @jbleon95 's PR because it did on this PR as soon as I merged edge in. Unless something changed in edge after the merge or something messed up during merge into my PR.

"""Define a liquid class for use in transfer functions."""
try:
# Check if we have already loaded this liquid class' definition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ def define_liquid(
"""Define a liquid to load into a well."""
assert False, "define_liquid only supported on engine core"

def define_liquid_class(self, name: str) -> LiquidClass:
def define_liquid_class(self, name: str, version: int) -> LiquidClass:
"""Define a liquid class."""
assert False, "define_liquid_class is only supported on engine core"

Expand Down
2 changes: 1 addition & 1 deletion api/src/opentrons/protocol_api/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ def define_liquid(
"""Define a liquid to load into a well."""

@abstractmethod
def define_liquid_class(self, name: str) -> LiquidClass:
def define_liquid_class(self, name: str, version: int) -> LiquidClass:
"""Define a liquid class for use in transfer functions."""

@abstractmethod
Expand Down
69 changes: 68 additions & 1 deletion api/src/opentrons/protocol_api/protocol_context.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import logging
from copy import deepcopy
from typing import (
Callable,
Dict,
Expand All @@ -13,6 +14,11 @@
)

from opentrons_shared_data.labware.types import LabwareDefinition
from opentrons_shared_data.liquid_classes.liquid_class_definition import (
TransferProperties as SharedTransferProperties,
)
from opentrons_shared_data.liquid_classes import DEFAULT_LC_VERSION, definition_exists
from opentrons_shared_data.liquid_classes.types import TransferPropertiesDict
from opentrons_shared_data.pipette.types import PipetteNameType

from opentrons.types import Mount, Location, DeckLocation, DeckSlotName, StagingSlotName
Expand Down Expand Up @@ -48,6 +54,7 @@
)
from opentrons_shared_data.errors.exceptions import CommandPreconditionViolated
from opentrons.protocol_engine.errors import LabwareMovementNotAllowedError
from ._liquid_properties import build_transfer_properties

from ._types import OffDeckType
from .core.common import ModuleCore, LabwareCore, ProtocolCore
Expand Down Expand Up @@ -1376,7 +1383,67 @@ def define_liquid_class(

:meta private:
"""
return self._core.define_liquid_class(name=name)
return self._core.define_liquid_class(name=name, version=DEFAULT_LC_VERSION)

@requires_version(2, 24)
def define_custom_liquid_class(
self,
name: str,
properties: Dict[str, Dict[str, TransferPropertiesDict]],
base_liquid_class: Optional[LiquidClass] = None,
display_name: Optional[str] = None,
) -> LiquidClass:
"""Define a custom liquid class, either a completely new one or based on an existing one.

Args:
name: The name to give to the new liquid class. Cannot use names of existing in-built liquid classes.
properties: A dict of transfer properties per tip per pipette.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, let's describe what pipette name and how the tiprack name should be formatted

Accepts a nested dictionary in the following format:

.. code-block:: python

{
<pipette_name>: {
<tiprack_uri>: <properties in the shape of TransferPropertiesDict>

# TransferPropertiesDict is a dictionary representation of the
# transfer properties returned by the `LiquidClass.get_for(..)` function.
}}

base_liquid_class: A LiquidClass to base this liquid class on. The properties
specified in transfer_properties will override any existing ones
for the specified pipettes & tips.
display_name: An optional human-readable name for the liquid. If not provided,
will default to title-cased name.

"""
if definition_exists(name, DEFAULT_LC_VERSION):
raise ValueError(
f"Liquid class named {name} already exists. Please specify a different name."
)
new_liquid_class: LiquidClass
if base_liquid_class:
# If base liquid is provided, copy to new class
# and replace the entries mentioned in transfer props arg
new_liquid_class = deepcopy(base_liquid_class)
else:
new_liquid_class = LiquidClass.create_from(
name=name,
display_name=display_name or name.title(),
by_pipette_setting={},
)
for pipette, by_tiprack_props in properties.items():
for tiprack, transfer_props in by_tiprack_props.items():
new_liquid_class.update_for(
pipette=pipette,
tip_rack=tiprack,
transfer_properties=build_transfer_properties(
transfer_properties=SharedTransferProperties.model_validate(
transfer_props
)
),
)
return new_liquid_class

@property
@requires_version(2, 5)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

def generate_command_schema(version: str) -> str:
"""Generate a JSON Schema that all valid create commands can validate against."""
schema_as_dict = CommandCreateAdapter.json_schema(mode="validation")
schema_as_dict = CommandCreateAdapter.json_schema(mode="validation", by_alias=False)
schema_as_dict["$id"] = f"opentronsCommandSchemaV{version}"
schema_as_dict["$schema"] = "http://json-schema.org/draft-07/schema#"
return json.dumps(schema_as_dict, indent=2, sort_keys=True)
Expand Down
Loading
Loading