Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add WellVolumeOffset to WellLocation #16302

Merged
merged 56 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
dd74c58
initial implementation
pmoegenburg Sep 19, 2024
e210544
update
pmoegenburg Sep 19, 2024
0deed9a
updated get_well_position
pmoegenburg Sep 19, 2024
8f713df
updated implementation
pmoegenburg Sep 19, 2024
64fc6e2
update schema 9
pmoegenburg Sep 19, 2024
1bcc578
updated docstring
pmoegenburg Sep 20, 2024
33e6108
updated schema
pmoegenburg Sep 20, 2024
fd93d0e
updated docstring
pmoegenburg Sep 20, 2024
a40b66b
updated schema and formatted
pmoegenburg Sep 20, 2024
add6638
fleshed out height-to-volume-to-height
pmoegenburg Sep 23, 2024
f2b6bc7
created command-specific WellLocations and move_to_well() subfunctions
pmoegenburg Sep 23, 2024
087caaf
added LiquidHandlingWellLocation and PickUpTipWellLocation
pmoegenburg Sep 24, 2024
3d53137
split get_well_position into submethods
pmoegenburg Sep 24, 2024
68981a3
test fixes
pmoegenburg Sep 25, 2024
a9b6dfb
updated typescript
pmoegenburg Sep 25, 2024
8c0f5ce
schema and test fixes
pmoegenburg Sep 25, 2024
2bc1d66
revert typescript work
pmoegenburg Sep 25, 2024
8731a19
cleaned up comments
pmoegenburg Sep 25, 2024
32760c3
updated command schema and formatted
pmoegenburg Sep 25, 2024
957c573
removed comment
pmoegenburg Sep 25, 2024
df07616
added validate_well_position()
pmoegenburg Sep 26, 2024
843a5b5
Merge branch 'edge' into EXEC-643-add-WellVolumeOffset
pmoegenburg Sep 26, 2024
da7f479
added TODO
pmoegenburg Sep 26, 2024
a9ca313
removed TODO and fixed robot-server tavern tests
pmoegenburg Sep 27, 2024
22d2c39
added test, need to fix. added well_location guardrails
pmoegenburg Sep 30, 2024
17ce9b1
refactored geometry methods and added helper function in labware
pmoegenburg Sep 30, 2024
9d559a6
refactored geometry and added validate_dispense_volume_into_well
pmoegenburg Sep 30, 2024
54205b3
updated long doc strings
pmoegenburg Sep 30, 2024
d2af54e
cleaned up comments and doc strings
pmoegenburg Sep 30, 2024
c2a8271
Merge branch 'edge' into EXEC-643-add-WellVolumeOffset
pmoegenburg Sep 30, 2024
ad334cc
updated command schema 9
pmoegenburg Sep 30, 2024
a671aad
refactored geometry and test
pmoegenburg Oct 2, 2024
726ffc1
Merge branch 'edge' into EXEC-643-add-WellVolumeOffset
pmoegenburg Oct 2, 2024
910d940
updated tests
pmoegenburg Oct 9, 2024
a3ac146
Merge branch 'edge' into EXEC-643-add-WellVolumeOffset
pmoegenburg Oct 9, 2024
38afbc3
update needed after merging in edge
pmoegenburg Oct 9, 2024
b688aac
follow-up format and lint
pmoegenburg Oct 9, 2024
f03bcb4
fixed up frustum_helpers and tests
pmoegenburg Oct 10, 2024
67d53a4
Merge branch 'edge' into EXEC-643-add-WellVolumeOffset
pmoegenburg Oct 10, 2024
053a47c
updated validate_well_position and command schema 10
pmoegenburg Oct 10, 2024
05a075c
added tiprack check to move_to_well
pmoegenburg Oct 10, 2024
97858d8
updates to relax WellLocation contraints based on PR review
pmoegenburg Oct 11, 2024
4e6a6ba
reverted command schema 9
pmoegenburg Oct 11, 2024
f364bc9
linted, removed test, reverted protocol/models/ work
pmoegenburg Oct 11, 2024
029ecea
eliminated test that doesn't do anything
pmoegenburg Oct 11, 2024
38235fe
Created PickUpTipWellOrigin without MENISCUS enum and updated BlowOut…
pmoegenburg Oct 14, 2024
cedc8b4
added Papi support for Well.meniscus Location
pmoegenburg Oct 15, 2024
7fe0451
reduce PE constraints
pmoegenburg Oct 16, 2024
fc0572f
Papi refactor to eliminate calculations prior to PE execution
pmoegenburg Oct 16, 2024
81da6d0
fixed Papi to not calculate meniscus location prior to PE execution
pmoegenburg Oct 17, 2024
b9aba1f
fixed simulation/analysis
pmoegenburg Oct 17, 2024
618e92f
Hid Location.is_meniscus from docs
pmoegenburg Oct 17, 2024
fb8a20c
added geometry test
pmoegenburg Oct 17, 2024
6feaff0
tidied up
pmoegenburg Oct 17, 2024
936b91c
addressed TODO to more appropriately name exceptions
pmoegenburg Oct 17, 2024
665e815
eliminated unneeded method
pmoegenburg Oct 17, 2024
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
1 change: 1 addition & 0 deletions api/src/opentrons/protocol_engine/commands/aspirate.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn:
well_name=well_name,
well_location=params.wellLocation,
current_well=current_well,
operation_volume=-params.volume,
)
deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z)
state_update.set_pipette_location(
Expand Down
12 changes: 8 additions & 4 deletions api/src/opentrons/protocol_engine/commands/liquid_probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@
PipetteLiquidNotFoundError,
)

from ..types import DeckPoint
from ..types import LiquidProbeWellLocation, DeckPoint
from .pipetting_common import (
LiquidNotFoundError,
PipetteIdMixin,
WellLocationMixin,
DestinationPositionResult,
)
from .command import (
Expand All @@ -42,8 +41,13 @@
# Both command variants should have identical parameters.
# But we need two separate parameter model classes because
# `command_unions.CREATE_TYPES_BY_PARAMS_TYPE` needs to be a 1:1 mapping.
class _CommonParams(PipetteIdMixin, WellLocationMixin):
pass
class _CommonParams(PipetteIdMixin):
labwareId: str = Field(..., description="Identifier of labware to use.")
wellName: str = Field(..., description="Name of well to use in labware.")
wellLocation: LiquidProbeWellLocation = Field(
default_factory=LiquidProbeWellLocation,
description="Relative well location at which to liquid probe.",
)


class LiquidProbeParams(_CommonParams):
Expand Down
12 changes: 8 additions & 4 deletions api/src/opentrons/protocol_engine/commands/pick_up_tip.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@
from ..errors import ErrorOccurrence, TipNotAttachedError
from ..resources import ModelUtils
from ..state import update_types
from ..types import DeckPoint
from ..types import PickUpTipWellLocation, DeckPoint
from .pipetting_common import (
PipetteIdMixin,
WellLocationMixin,
DestinationPositionResult,
)
from .command import (
Expand All @@ -31,10 +30,15 @@
PickUpTipCommandType = Literal["pickUpTip"]


class PickUpTipParams(PipetteIdMixin, WellLocationMixin):
class PickUpTipParams(PipetteIdMixin):
"""Payload needed to move a pipette to a specific well."""

pass
labwareId: str = Field(..., description="Identifier of labware to use.")
wellName: str = Field(..., description="Name of well to use in labware.")
wellLocation: PickUpTipWellLocation = Field(
default_factory=PickUpTipWellLocation,
description="Relative well location at which to pick up the tip.",
)


class PickUpTipResult(DestinationPositionResult):
Expand Down
2 changes: 2 additions & 0 deletions api/src/opentrons/protocol_engine/execution/movement.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ async def move_to_well(
force_direct: bool = False,
minimum_z_height: Optional[float] = None,
speed: Optional[float] = None,
operation_volume: Optional[float] = None,
) -> Point:
"""Move to a specific well."""
self._state_store.labware.raise_if_labware_inaccessible_by_pipette(
Expand Down Expand Up @@ -129,6 +130,7 @@ async def move_to_well(
current_well=current_well,
force_direct=force_direct,
minimum_z_height=minimum_z_height,
operation_volume=operation_volume,
)

speed = self._state_store.pipettes.get_movement_speed(
Expand Down
35 changes: 32 additions & 3 deletions api/src/opentrons/protocol_engine/state/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ def get_well_position(
labware_id: str,
well_name: str,
well_location: Optional[WellLocation] = None,
operation_volume: Optional[float] = None,
) -> Point:
"""Given relative well location in a labware, get absolute position."""
labware_pos = self.get_labware_position(labware_id)
Expand All @@ -434,11 +435,18 @@ def get_well_position(
elif well_location.origin == WellOrigin.CENTER:
offset = offset.copy(update={"z": offset.z + well_depth / 2.0})
elif well_location.origin == WellOrigin.MENISCUS:
liquid_height = self._wells.get_last_measured_liquid_height(
starting_liquid_height = self._wells.get_last_measured_liquid_height(
labware_id, well_name
)
if liquid_height is not None:
offset = offset.copy(update={"z": offset.z + liquid_height})
if starting_liquid_height is not None:
sfoster1 marked this conversation as resolved.
Show resolved Hide resolved
if well_location.volumeOffset.volumeOffset == "operationVolume":
volume = operation_volume or 0.0
else:
volume = well_location.volumeOffset.volumeOffset
height_after_operation = self.get_height_after_volume(labware_id=labware_id, well_name=well_name, starting_height=starting_liquid_height, volume=volume)
offset = offset.copy(
update={"z": offset.z + height_after_operation}
)
else:
raise errors.LiquidHeightUnknownError(
"Must liquid probe before specifying WellOrigin.MENISCUS."
Expand Down Expand Up @@ -1218,3 +1226,24 @@ def get_well_volumetric_capacity(
message=f"No InnerWellGeometry found for well id: {well_id}"
)
return get_well_volumetric_capacity(well_geometry)

def get_volume_at_height(
self, labware_id: str, well_id: str, target_height: float
) -> float:
pass

def get_height_at_volume(
self, labware_id: str, well_id: str, target_volume: float
) -> float:
pass

def get_height_after_volume(
self, labware_id: str, well_name: str, starting_height: float, volume: float
) -> float:
well_def = self._labware.get_well_definition(labware_id, well_name)
well_id = well_def.geometryDefinitionId
starting_volume = self.get_volume_at_height(labware_id=labware_id, well_id=well_id, target_height=starting_height)
ending_volume = starting_volume + volume
ending_height = self.get_height_at_volume(labware_id=labware_id, well_id=well_id, target_volume=ending_volume)
# return ending_height
return starting_height # delete, use line above once sub-methods implemented
Copy link
Member

Choose a reason for hiding this comment

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

if we merge this, we should merge the return ending_height, and not this comment

8 changes: 5 additions & 3 deletions api/src/opentrons/protocol_engine/state/motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def get_movement_waypoints_to_well(
current_well: Optional[CurrentWell] = None,
force_direct: bool = False,
minimum_z_height: Optional[float] = None,
operation_volume: Optional[float] = None,
) -> List[motion_planning.Waypoint]:
"""Calculate waypoints to a destination that's specified as a well."""
location = current_well or self._pipettes.get_current_location()
Expand All @@ -107,9 +108,10 @@ def get_movement_waypoints_to_well(
destination_cp = CriticalPoint.XY_CENTER

destination = self._geometry.get_well_position(
labware_id,
well_name,
well_location,
labware_id=labware_id,
well_name=well_name,
well_location=well_location,
operation_volume=operation_volume,
)

move_type = _move_types.get_move_type_to_well(
Expand Down
25 changes: 24 additions & 1 deletion api/src/opentrons/protocol_engine/types.py
pmoegenburg marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,31 @@ class WellOffset(BaseModel):
z: float = 0


# get rid of operationVolume below and operation_volume parameter in methods?
# TODO(pm, 2024-09-23): PE should raise error if volumeOffset specified with a tip rack location
class WellLocation(BaseModel):
Copy link
Member

Choose a reason for hiding this comment

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

I think we should call this LiquidHandlingWellLocation, and then have WellLocation = LiquidHandlingWellLocation if we don't want to change all the papi code

"""A relative location in reference to a well's location."""
"""A relative location in reference to a well's location.""" # update

origin: WellOrigin = WellOrigin.TOP
offset: WellOffset = Field(default_factory=WellOffset)
volumeOffset: Union[float, Literal["operationVolume"]] = Field(default=0.0, description="""A volume of liquid to account for when
executing commands with an origin of WellOrigin.MENISCUS. Specifying
pmoegenburg marked this conversation as resolved.
Show resolved Hide resolved
`operationVolume` results in this class acting as a sentinel and should
be used when volume can be determined from the command parameters, for
example commanding Aspirate. A volume should be specified when it cannot
be determined from the command parameters, for example commanding
MoveToWell prior to AspirateInPlace.""") # update comment


class LiquidProbeWellLocation(BaseModel):
pmoegenburg marked this conversation as resolved.
Show resolved Hide resolved
"""A relative location in reference to a well's location.""" # update

origin: WellOrigin = WellOrigin.TOP
offset: WellOffset = Field(default_factory=WellOffset)


class PickUpTipWellLocation(BaseModel):
"""A relative location in reference to a well's location.""" # update

origin: WellOrigin = WellOrigin.TOP
offset: WellOffset = Field(default_factory=WellOffset)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ async def test_aspirate_implementation_no_prep(
well_name="A3",
well_location=location,
current_well=None,
operation_volume=-50,
),
).then_return(Point(x=1, y=2, z=3))

Expand Down Expand Up @@ -145,6 +146,7 @@ async def test_aspirate_implementation_with_prep(
labware_id="123",
well_name="A3",
),
operation_volume=-50,
),
).then_return(Point(x=1, y=2, z=3))

Expand Down Expand Up @@ -210,6 +212,7 @@ async def test_aspirate_raises_volume_error(
well_name="A3",
well_location=location,
current_well=None,
operation_volume=-50,
),
).then_return(Point(1, 2, 3))

Expand Down Expand Up @@ -267,6 +270,7 @@ async def test_overpressure_error(
well_name=well_name,
well_location=well_location,
current_well=None,
operation_volume=-50,
),
).then_return(position)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ async def test_move_to_well(
current_well=None,
force_direct=True,
minimum_z_height=12.3,
operation_volume=None,
)
).then_return(
[Waypoint(Point(1, 2, 3), CriticalPoint.XY_CENTER), Waypoint(Point(4, 5, 6))]
Expand Down Expand Up @@ -257,6 +258,7 @@ async def test_move_to_well_from_starting_location(
well_location=well_location,
force_direct=False,
minimum_z_height=None,
operation_volume=None,
)
).then_return([Waypoint(Point(1, 2, 3), CriticalPoint.XY_CENTER)])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1543,6 +1543,7 @@ def test_get_well_position_with_meniscus_offset(
origin=WellOrigin.MENISCUS,
offset=WellOffset(x=2, y=3, z=4),
),
operation_volume=0.0,
)

assert result == Point(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ def test_get_movement_waypoints_to_well_for_y_center(
).then_return(False)

decoy.when(
geometry_view.get_well_position("labware-id", "well-name", WellLocation())
geometry_view.get_well_position("labware-id", "well-name", WellLocation(), None)
).then_return(Point(x=4, y=5, z=6))

decoy.when(
Expand Down Expand Up @@ -391,7 +391,7 @@ def test_get_movement_waypoints_to_well_for_xy_center(
).then_return(True)

decoy.when(
geometry_view.get_well_position("labware-id", "well-name", WellLocation())
geometry_view.get_well_position("labware-id", "well-name", WellLocation(), None)
).then_return(Point(x=4, y=5, z=6))

decoy.when(
Expand Down Expand Up @@ -460,6 +460,7 @@ def test_get_movement_waypoints_to_well_raises(
labware_id="labware-id",
well_name="A1",
well_location=None,
operation_volume=None,
)
).then_return(Point(x=4, y=5, z=6))
decoy.when(pipette_view.get_current_location()).then_return(None)
Expand Down
23 changes: 23 additions & 0 deletions shared-data/command/schemas/9.json
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,26 @@
}
}
},
"WellVolumeOffset": {
"title": "WellVolumeOffset",
"description": "A volume of liquid to account for when executing commands with an origin of WellOrigin.MENISCUS.\n\nSpecifying `operationVolume` results in this class acting as a sentinel and should be used when\nvolume can be determined from the command parameters, for example commanding Aspirate. A volume\nshould be specified when it cannot be determined from the command parameters, for example commanding\nMoveToWell prior to AspirateInPlace.",
"type": "object",
"properties": {
"volumeOffset": {
"title": "Volumeoffset",
"default": 0.0,
"anyOf": [
{
"type": "number"
},
{
"enum": ["operationVolume"],
"type": "string"
}
]
}
}
},
"WellLocation": {
"title": "WellLocation",
"description": "A relative location in reference to a well's location.",
Expand All @@ -333,6 +353,9 @@
},
"offset": {
"$ref": "#/definitions/WellOffset"
},
"volumeOffset": {
"$ref": "#/definitions/WellVolumeOffset"
}
}
},
Expand Down
Loading