Skip to content

feat(api): add custom liquid class creator#18416

Merged
sanni-t merged 14 commits intoedgefrom
api-add_custom_liquid_class_creator
May 29, 2025
Merged

feat(api): add custom liquid class creator#18416
sanni-t merged 14 commits intoedgefrom
api-add_custom_liquid_class_creator

Conversation

@sanni-t
Copy link
Copy Markdown
Member

@sanni-t sanni-t commented May 22, 2025

Closes AUTH-839, AUTH-838

Overview

  • Adds the ability to create a new liquid class based on user-specified properties
  • Adds the ability to customize an existing liquid class based on user-specified properties

Test Plan and Hands on Testing

  1. Test LC-based transfer for OT-2 protocol
requirements = {
	"robotType": "OT-2",
	"apiLevel": "2.24"
}

def run(protocol_context):
	# trash = simulated_protocol_context.load_trash_bin("A3")
	tiprack = protocol_context.load_labware(
		"opentrons_96_tiprack_20ul", "1"
		)
	pipette_20 = protocol_context.load_instrument(
		"p20_single_gen2", mount="left", tip_racks=[tiprack]
		)
	nest_plate = protocol_context.load_labware(
		"nest_96_wellplate_200ul_flat", "3"
		)
	arma_plate = protocol_context.load_labware(
		"armadillo_96_wellplate_200ul_pcr_full_skirt", "2"
		)

	######## Create a custom liquid class using transfer properties ########
	custom_water = protocol_context.define_custom_liquid_class(
		name="custom_water",
        properties=custom_liquid_class_properties,
        display_name="Custom Aqueous",
		)
	
    ######## Still possible to edit the liquid class ##########
	custom_water_props = custom_water.get_for(pipette_20, tiprack)
	custom_water_props.aspirate.submerge.speed = 80
	
	######## Use the custom liquid class in your transfer call #########
	pipette_20.consolidate_with_liquid_class(
		liquid_class=custom_water,
		volume=30,
		source=nest_plate.rows()[0][:2],
		dest=arma_plate.wells()[0],
		new_tip="once",
		trash_location=pipette_20.trash_container,
	)

######## Properties to use for defining the custom liquid class ##########
custom_liquid_class_properties = {
    "p20_single_gen2": {
        "opentrons/opentrons_96_tiprack_20ul/1": {
            "aspirate": {
                "aspirate_position": {
                    "offset": {"x": 1, "y": 2, "z": 3},
                    "position_reference": "well-bottom",
                },
                "correction_by_volume": [(0.0, 0.0)],
                "delay": {"enabled": False},
                "flow_rate_by_volume": [(10.0, 40.0), (20.0, 30.0)],
                "mix": {"enabled": False},
                "pre_wet": True,
                "retract": {
                    "air_gap_by_volume": [(5.0, 3.0), (10.0, 4.0)],
                    "delay": {"enabled": False},
                    "end_position": {
                        "offset": {"x": 1, "y": 2, "z": 3},
                        "position_reference": "well-bottom",
                    },
                    "speed": 40,
                    "touch_tip": {"enabled": False},
                },
                "submerge": {
                    "delay": {"enabled": False},
                    "speed": 100,
                    "start_position": {
                        "offset": {"x": 1, "y": 2, "z": 3},
                        "position_reference": "well-bottom",
                    },
                },
            },
            "dispense": {
                "correction_by_volume": [(0.0, 0.0)],
                "delay": {"enabled": False},
                "dispense_position": {
                    "offset": {"x": 1, "y": 2, "z": 3},
                    "position_reference": "well-bottom",
                },
                "flow_rate_by_volume": [(10.0, 40.0), (20.0, 30.0)],
                "mix": {"enabled": False},
                "push_out_by_volume": [(10.0, 7.0), (20.0, 10.0)],
                "retract": {
                    "air_gap_by_volume": [(5.0, 3.0), (10.0, 4.0)],
                    "blowout": {"enabled": False},
                    "delay": {"enabled": False},
                    "end_position": {
                        "offset": {"x": 1, "y": 2, "z": 3},
                        "position_reference": "well-bottom",
                    },
                    "speed": 40,
                    "touch_tip": {"enabled": False},
                },
                "submerge": {
                    "delay": {"enabled": False},
                    "speed": 100,
                    "start_position": {
                        "offset": {"x": 1, "y": 2, "z": 3},
                        "position_reference": "well-bottom",
                    },
                },
            },
        }
    }
}
  1. Test customizing existing liquid class for Flex protocol. Use the same properties dictionary from above (not duplicating it here to avoid making this too long)
requirements = {
	"robotType": "Flex",
	"apiLevel": "2.24"
}

def run(protocol_context):
	trash = protocol_context.load_trash_bin("A3")
	tiprack = protocol_context.load_labware(
		"opentrons_flex_96_tiprack_50ul", "D1"
		)
	pipette_50 = protocol_context.load_instrument(
		"flex_1channel_50", mount="left", tip_racks=[tiprack]
		)
	nest_plate = protocol_context.load_labware(
		"nest_96_wellplate_200ul_flat", "C3"
		)
	arma_plate = protocol_context.load_labware(
		"armadillo_96_wellplate_200ul_pcr_full_skirt", "C2"
		)

	water = protocol_context.define_liquid_class("water")
	
	custom_water_class = protocol_context.define_custom_liquid_class(
		name="custom_water",
        	properties=custom_liquid_class_properties,
        	base_liquid_class=water,
        	display_name="Custom Aqueous",
		)
	pipette_50.consolidate_with_liquid_class(
			liquid_class=custom_water_class,
			volume=30,
			source=nest_plate.rows()[0][:2],
			dest=arma_plate.wells()[0],
			new_tip="once",
			trash_location=trash,
		)

Changelog

  • adds liquid class modifiers to enable custom liquid classes
  • adds the custom liquid class creator API
  • small changes to 'version' arg typing

Review requests

  • Does the custom LC interface make sense and is it usable for PD?

Risk assessment

Low. Supplementary stuff. Doesn't modify existing infrastructure.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 22, 2025

Codecov Report

Attention: Patch coverage is 46.66667% with 80 lines in your changes missing coverage. Please review.

Project coverage is 24.34%. Comparing base (3c3a1ca) to head (1b81fcd).
Report is 26 commits behind head on edge.

Files with missing lines Patch % Lines
...thon/opentrons_shared_data/liquid_classes/types.py 0.00% 73 Missing ⚠️
...red_data/liquid_classes/liquid_class_definition.py 90.27% 7 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             edge   #18416      +/-   ##
==========================================
- Coverage   25.55%   24.34%   -1.21%     
==========================================
  Files        3254     3100     -154     
  Lines      277247   262542   -14705     
  Branches    32256    32465     +209     
==========================================
- Hits        70849    63915    -6934     
+ Misses     206375   198609    -7766     
+ Partials       23       18       -5     
Flag Coverage Δ
hardware ?
labware-library ?
protocol-designer 19.39% <ø> (-0.01%) ⬇️
react-api-client ?
step-generation 4.60% <ø> (-0.01%) ⬇️
system-server ?
update-server ?
usb-bridge ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...n/opentrons_shared_data/liquid_classes/__init__.py 84.61% <100.00%> (+13.18%) ⬆️
...red_data/liquid_classes/liquid_class_definition.py 93.84% <90.27%> (-2.66%) ⬇️
...thon/opentrons_shared_data/liquid_classes/types.py 0.00% <0.00%> (ø)

... and 997 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ddcc4 ddcc4 self-requested a review May 23, 2025 00:05
properties_dict: A dict of transfer properties per tip per pipette
If you want these transfer properties to be used as a fallback, i.e.
use them as a default if no transfer properties for the requested pipette
&/or tip are found, then use the key 'default' for the pipette &/or tip name.
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.

Hey, did you say last week that we're not supporting default after all?

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.

Yep. I had a todo to remove this from docstring. Removed it now

@sanni-t sanni-t marked this pull request as ready for review May 28, 2025 15:08
@sanni-t sanni-t requested a review from a team as a code owner May 28, 2025 15:08
@sanni-t sanni-t requested a review from jbleon95 May 28, 2025 15:13
submerge=SubmergeDict(
start_position=TipPositionDict(
position_reference=PositionReference.WELL_BOTTOM,
position_reference="well-bottom",
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.

So you made these a string now?
There wasn't a way to make it take a PositionReference instance when called from TipPositionDict() and a string when called with a raw dict, was there?

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.

So you made these a string now?

Yep, String literal types

There wasn't a way to make it take a PositionReference instance when called from TipPositionDict() and a string when called with a raw dict, was there?

Not unless we wanted to suppress mypy errors (which is a valid option but I try to avoid it)

) -> 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

)

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.

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?


Args:
name: The name to give to the new liquid class
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

@sanni-t sanni-t requested a review from a team as a code owner May 28, 2025 19:31
def reshape(cls, data: Any) -> Any:
"""Move any params specified as top-level keys into the 'params' value."""
if isinstance(data, dict):
if None not in (data.get("enable"), data.get("enabled")):
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 are we supporting both variants?

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.

The json schema (and hence pydantic model) defines an enable while the python properties use enabled.

We had switched to using enabled in python to make it more intuitive and readable. For example, water_props.aspirate.delay.enabled is True reads more correctly than water_props.aspirate.delay.enable is True.

)
speed: _GreaterThanZeroNumber = Field(
..., description="Touch-tip speed, in millimeters per second."
..., alias="speed", description="Touch-tip speed, in millimeters per second."
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.

Does this alias do anything?

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.

Ya, it's needed so that the reshape_glob function can fetch all the field names by alias. If there's no alias specified for a field, fetching the field name returns a None.

Comment on lines +1435 to +1442
for pipette, by_tiprack_props in properties.items():
for tiprack, transfer_props in by_tiprack_props.items():
new_tiprack_props[tiprack] = build_transfer_properties(
transfer_properties=SharedTransferProperties.model_validate(
transfer_props
)
)
by_pipette_setting[pipette] = {tiprack: new_tiprack_props[tiprack]}
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.

Er.. made a wrong change in a previous commit- this would totally replace the entire entry for the pipette, even if we only want to update an entry related to a specific tiprack only. Fix coming up

@sanni-t sanni-t merged commit 88da5fa into edge May 29, 2025
53 checks passed
ddcc4 pushed a commit that referenced this pull request May 29, 2025
Closes AUTH-839

# Overview

- Adds the ability to create a new liquid class based on user-specified
properties
- Adds the ability to customize an existing liquid class based on
user-specified properties

## Changelog

- adds liquid class modifiers to enable custom liquid classes
- adds the custom liquid class creator API
- small changes to 'version' arg typing

## Risk assessment

Low. Supplementary stuff. Doesn't modify existing infrastructure.

(cherry picked from commit 88da5fa)
ddcc4 pushed a commit that referenced this pull request May 29, 2025
Closes AUTH-839

# Overview

- Adds the ability to create a new liquid class based on user-specified
properties
- Adds the ability to customize an existing liquid class based on
user-specified properties

## Changelog

- adds liquid class modifiers to enable custom liquid classes
- adds the custom liquid class creator API
- small changes to 'version' arg typing

## Risk assessment

Low. Supplementary stuff. Doesn't modify existing infrastructure.

(cherry picked from commit 88da5fa)
ddcc4 pushed a commit that referenced this pull request May 29, 2025
Closes AUTH-839

# Overview

- Adds the ability to create a new liquid class based on user-specified
properties
- Adds the ability to customize an existing liquid class based on
user-specified properties

## Changelog

- adds liquid class modifiers to enable custom liquid classes
- adds the custom liquid class creator API
- small changes to 'version' arg typing

## Risk assessment

Low. Supplementary stuff. Doesn't modify existing infrastructure.

(cherry picked from commit 88da5fa)
ddcc4 pushed a commit that referenced this pull request May 29, 2025
Closes AUTH-839

# Overview

- Adds the ability to create a new liquid class based on user-specified
properties
- Adds the ability to customize an existing liquid class based on
user-specified properties

## Changelog

- adds liquid class modifiers to enable custom liquid classes
- adds the custom liquid class creator API
- small changes to 'version' arg typing

## Risk assessment

Low. Supplementary stuff. Doesn't modify existing infrastructure.

(cherry picked from commit 88da5fa)
ddcc4 pushed a commit that referenced this pull request May 29, 2025
Closes AUTH-839

# Overview

- Adds the ability to create a new liquid class based on user-specified
properties
- Adds the ability to customize an existing liquid class based on
user-specified properties

## Changelog

- adds liquid class modifiers to enable custom liquid classes
- adds the custom liquid class creator API
- small changes to 'version' arg typing

## Risk assessment

Low. Supplementary stuff. Doesn't modify existing infrastructure.

(cherry picked from commit 88da5fa)
ddcc4 pushed a commit that referenced this pull request May 29, 2025
Closes AUTH-839

# Overview

- Adds the ability to create a new liquid class based on user-specified
properties
- Adds the ability to customize an existing liquid class based on
user-specified properties

## Changelog

- adds liquid class modifiers to enable custom liquid classes
- adds the custom liquid class creator API
- small changes to 'version' arg typing

## Risk assessment

Low. Supplementary stuff. Doesn't modify existing infrastructure.

(cherry picked from commit 88da5fa)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants