Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions api/src/opentrons/protocol_api/core/engine/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,7 @@ def define_liquid(
display_color=(liquid.displayColor.root if liquid.displayColor else None),
)

def define_liquid_class(self, name: str) -> LiquidClass:
def define_liquid_class(self, name: str, version: int = 1) -> LiquidClass:
"""Define a liquid class for use in transfer functions."""
try:
# Check if we have already loaded this liquid class' definition
Expand All @@ -1081,7 +1081,7 @@ def define_liquid_class(self, name: str) -> LiquidClass:
# Fetching the liquid class data from file and parsing it
# is an expensive operation and should be avoided.
# Calling this often will degrade protocol execution performance.
liquid_class_def = liquid_classes.load_definition(name)
liquid_class_def = liquid_classes.load_definition(name, version=version)
self._defined_liquid_class_defs_by_name[name] = liquid_class_def
except LiquidClassDefinitionDoesNotExist:
raise ValueError(f"Liquid class definition not found for '{name}'.")
Expand Down
3 changes: 3 additions & 0 deletions api/tests/opentrons/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,7 @@ def minimal_liquid_class_def1() -> LiquidClassSchemaV1:
displayName="water 1",
description="some water",
schemaVersion=1,
version=1,
namespace="test-fixture-1",
byPipette=[],
)
Expand All @@ -810,6 +811,7 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1:
displayName="water 2",
description="some water",
schemaVersion=1,
version=1,
namespace="test-fixture-2",
byPipette=[
ByPipetteSetting(
Expand Down Expand Up @@ -896,6 +898,7 @@ def maximal_liquid_class_def() -> LiquidClassSchemaV1:
displayName="Test Water",
description="some water",
schemaVersion=1,
version=1,
namespace="opentrons",
byPipette=[
ByPipetteSetting(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1876,15 +1876,15 @@ def test_define_liquid_class(
expected_liquid_class = LiquidClass(
_name="water1", _display_name="water 1", _by_pipette_setting={}
)
decoy.when(liquid_classes.load_definition("water")).then_return(
decoy.when(liquid_classes.load_definition("water", version=123)).then_return(
minimal_liquid_class_def1
)
assert subject.define_liquid_class("water") == expected_liquid_class
assert subject.define_liquid_class("water", version=123) == expected_liquid_class

decoy.when(liquid_classes.load_definition("water")).then_return(
decoy.when(liquid_classes.load_definition("water", version=456)).then_return(
minimal_liquid_class_def2
)
assert subject.define_liquid_class("water") == expected_liquid_class
assert subject.define_liquid_class("water", version=456) == expected_liquid_class

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.

Wait, why do we want this?

If the user explicitly wants to load version=456 after loading version=123, are you saying we have to return version=123 no matter what?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ooh I did not look closely enough at this test when I was fixing it. The way I have it now is that if you load say version 1 of a liquid class, and then try loading version 2, we will return version 1 because that's what we have cached. This is obviously not ideal, so I will fix that



def test_get_labware_location_deck_slot(
Expand Down
6 changes: 3 additions & 3 deletions shared-data/js/liquidClasses.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ethanol80V1Uncasted from '../liquid-class/definitions/1/ethanol_80.json'
import glycerol50V1Uncasted from '../liquid-class/definitions/1/glycerol_50.json'
import waterV1Uncasted from '../liquid-class/definitions/1/water.json'
import ethanol80V1Uncasted from '../liquid-class/definitions/1/ethanol_80/1.json'
import glycerol50V1Uncasted from '../liquid-class/definitions/1/glycerol_50/1.json'
import waterV1Uncasted from '../liquid-class/definitions/1/water/1.json'

import type { LiquidClass } from '.'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"displayName": "Volatile",
"description": "80% ethanol",
"schemaVersion": 1,
"version": 1,
"namespace": "opentrons",
"byPipette": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"displayName": "Viscous",
"description": "50% glycerol",
"schemaVersion": 1,
"version": 1,
"namespace": "opentrons",
"byPipette": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"displayName": "Aqueous",
"description": "Deionized water",
"schemaVersion": 1,
"version": 1,
"namespace": "opentrons",
"byPipette": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"displayName": "Viscous",
"description": "50% glycerol",
"schemaVersion": 1,
"version": 1,
"namespace": "opentrons",
"byPipette": [
{
Expand Down
6 changes: 6 additions & 0 deletions shared-data/liquid-class/schemas/1.json
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,11 @@
"type": "number",
"enum": [1]
},
"version": {
"description": "Version of the liquid class definition itself (eg water v1/v2/v3). An incrementing integer",
"type": "integer",
"minimum": 1
},
"namespace": {
"$ref": "#/definitions/safeString"
},
Expand Down Expand Up @@ -492,6 +497,7 @@
"displayName",
"description",
"schemaVersion",
"version",
"namespace",
"byPipette"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,27 @@
from .liquid_class_definition import LiquidClassSchemaV1


DEFAULT_VERSION = 1
DEFAULT_SCHEMA_VERSION = 1


class LiquidClassDefinitionDoesNotExist(Exception):
"""Specified liquid class definition does not exist."""


def load_definition(name: str, version: int = DEFAULT_VERSION) -> LiquidClassSchemaV1:
def load_definition(
name: str, version: int, schema_version: int = DEFAULT_SCHEMA_VERSION
) -> LiquidClassSchemaV1:
"""Load the specified liquid class' definition as a LiquidClassSchemaV1 object.

Note: this is an expensive operation and should be called sparingly.
"""
try:
return LiquidClassSchemaV1.model_validate_json(
load_shared_data(f"liquid-class/definitions/{version}/{name}.json")
load_shared_data(
f"liquid-class/definitions/{schema_version}/{name}/{version}.json"
)
)
except FileNotFoundError:
raise LiquidClassDefinitionDoesNotExist(
f"No definition found for liquid class '{name}'"
f"No definition found for liquid class '{name}' version {version}"
)
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,9 @@ class LiquidClassSchemaV1(BaseModel):
schemaVersion: Literal[1] = Field(
..., description="Which schema version a liquid class is using"
)
version: int = Field(
..., description="Version of the liquid class within the schema"
)

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.

Ah. So the version numbers will start over from 1 if we ever change the schemaVersion?

@SyntaxColoring SyntaxColoring May 22, 2025

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.

FYI we're not doing it that way for labware definitions. We're doing it like:

  • Schema 2
    • Labware XYZ version 1
    • Labware XYZ version 2
  • Schema 3
    • Labware XYZ version 3
    • Labware XYZ version 4

Which I'm pretty sure is what we want. Otherwise it's ambiguous what "labware XYZ version 1" refers to, among other problems.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Noted, I can update the doc string so it doesn't imply otherwise

namespace: str = Field(...)
byPipette: Sequence[ByPipetteSetting] = Field(
...,
Expand Down
4 changes: 2 additions & 2 deletions shared-data/python/tests/liquid_classes/test_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@


def test_load_liquid_class_schema_v1() -> None:
fixture_data = load_shared_data("liquid-class/definitions/1/water.json")
fixture_data = load_shared_data("liquid-class/definitions/1/water/1.json")
liquid_class_model = LiquidClassSchemaV1.model_validate_json(fixture_data)
liquid_class_def_from_model = json.loads(
liquid_class_model.model_dump_json(exclude_unset=True)
Expand All @@ -24,7 +24,7 @@ def test_load_liquid_class_schema_v1() -> None:


def test_load_definition() -> None:
water_definition = load_definition(name="water", version=1)
water_definition = load_definition(name="water", version=1, schema_version=1)
assert type(water_definition) is LiquidClassSchemaV1
assert water_definition.byPipette[0].pipetteModel == "flex_1channel_50"
assert water_definition.byPipette[0].byTipType[0].aspirate.submerge == Submerge(
Expand Down
4 changes: 2 additions & 2 deletions shared-data/python/tests/liquid_classes/test_validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ def _get_all_liquid_classes() -> List[str]:
@pytest.mark.parametrize("liquid_class_name", list(_get_all_liquid_classes()))
def test_validate_unique_pipette_keys(liquid_class_name: str) -> None:
"""A liquid class definition should contain only one set of properties per pipette model."""
definition_dict = load_definition(liquid_class_name, version=1)
definition_dict = load_definition(liquid_class_name, version=1, schema_version=1)
pipette_models = [prop.pipetteModel for prop in definition_dict.byPipette]
assert len(pipette_models) == len(set(pipette_models))


@pytest.mark.parametrize("liquid_class_name", list(_get_all_liquid_classes()))
def test_validate_unique_tip_keys(liquid_class_name: str) -> None:
"""A liquid class definition should contain only one set of properties per tip type."""
definition_dict = load_definition(liquid_class_name, version=1)
definition_dict = load_definition(liquid_class_name, version=1, schema_version=1)

for by_pip_prop in definition_dict.byPipette:
tipracks = [tip_prop.tiprack for tip_prop in by_pip_prop.byTipType]
Expand Down