Repository: Opentrons/opentrons
Pull request #17714: feat(api): dynamic liquid tracking papi changes
Author: login: caila-marashaj
Labels: name: gen-analyses-snapshot-pr
description: Generate a healing PR if the analyses snapshot test fails
Created: 2025-03-10T22:40:11Z | Merged: 2025-03-14T17:20:27Z

## Overview
This is the protocol api/engine portion of dynamic liquid tracking. It adds to the `aspirate_while_tracking` command, so with this pr, dynamic liquid tracking will be fully operational on edge. 

## Changelog
- introduce `MeniscusTrackingTarget`- this can be passed in as a location to `InstrumentContext::aspirate/dispense`, and gives the caller choices of `start`, `end`, or `dynamic`.
- call the `AspirateWhileTrackingCommand` from `instrument.py`
- calculate the z distance needed to travel given the requested volume inside `geometry.py`
- call `move_to_well` in the `AspirateWhileTrackingCommand` to get to the well_location specified in the params

## TODO
Disallow users from calling aspirate with target `start` and write a test for it


Diff:
diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py
index bd54277355d5..4d5f485c4482 100644
--- a/api/src/opentrons/protocol_api/core/engine/instrument.py
+++ b/api/src/opentrons/protocol_api/core/engine/instrument.py
@@ -16,6 +16,7 @@
     Mount,
     NozzleConfigurationType,
     NozzleMapInterface,
+    MeniscusTrackingTarget,
 )
 from opentrons.hardware_control import SyncHardwareAPI
 from opentrons.hardware_control.dev_types import PipetteDict
@@ -169,7 +170,7 @@ def aspirate(
         rate: float,
         flow_rate: float,
         in_place: bool,
-        is_meniscus: Optional[bool] = None,
+        meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
         correction_volume: Optional[float] = None,
     ) -> None:
         """Aspirate a given volume of liquid from the specified location.
@@ -180,7 +181,10 @@ def aspirate(
             rate: Not used in this core.
             flow_rate: The flow rate in µL/s to aspirate at.
             in_place: whether this is a in-place command.
+            meniscus_tracking: Optional data about where to aspirate from.
         """
+        if meniscus_tracking == MeniscusTrackingTarget.START:
+            raise ValueError("Cannot aspirate at the starting liquid height.")
         if well_core is None:
             if not in_place:
                 self._engine_client.execute_command(
@@ -208,14 +212,15 @@ def aspirate(
             well_name = well_core.get_name()
             labware_id = well_core.labware_id
 
-            well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
+            (
+                well_location,
+                dynamic_liquid_tracking,
+            ) = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
                 labware_id=labware_id,
                 well_name=well_name,
                 absolute_point=location.point,
-                is_meniscus=is_meniscus,
+                meniscus_tracking=meniscus_tracking,
             )
-            if well_location.origin == WellOrigin.MENISCUS:
-                well_location.volumeOffset = "operationVolume"
             pipette_movement_conflict.check_safe_for_pipette_movement(
                 engine_state=self._engine_client.state,
                 pipette_id=self._pipette_id,
@@ -223,17 +228,31 @@ def aspirate(
                 well_name=well_name,
                 well_location=well_location,
             )
-            self._engine_client.execute_command(
-                cmd.AspirateParams(
-                    pipetteId=self._pipette_id,
-                    labwareId=labware_id,
-                    wellName=well_name,
-                    wellLocation=well_location,
-                    volume=volume,
-                    flowRate=flow_rate,
-                    correctionVolume=correction_volume,
+            if dynamic_liquid_tracking:
+
+                self._engine_client.execute_command(
+                    cmd.AspirateWhileTrackingParams(
+                        pipetteId=self._pipette_id,
+                        labwareId=labware_id,
+                        wellName=well_name,
+                        wellLocation=well_location,
+                        volume=volume,
+                        flowRate=flow_rate,
+                        correctionVolume=correction_volume,
+                    )
+                )
+            else:
+                self._engine_client.execute_command(
+                    cmd.AspirateParams(
+                        pipetteId=self._pipette_id,
+                        labwareId=labware_id,
+                        wellName=well_name,
+                        wellLocation=well_location,
+                        volume=volume,
+                        flowRate=flow_rate,
+                        correctionVolume=correction_volume,
+                    )
                 )
-            )
 
         self._protocol_core.set_last_location(location=location, mount=self.get_mount())
 
@@ -246,7 +265,7 @@ def dispense(
         flow_rate: float,
         in_place: bool,
         push_out: Optional[float],
-        is_meniscus: Optional[bool] = None,
+        meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
         correction_volume: Optional[float] = None,
     ) -> None:
         """Dispense a given volume of liquid into the specified location.
@@ -258,6 +277,7 @@ def dispense(
             flow_rate: The flow rate in µL/s to dispense at.
             in_place: whether this is a in-place command.
             push_out: The amount to push the plunger below bottom position.
+            meniscus_tracking: Optional data about where to dispense from.
         """
         if self._protocol_core.api_version < _DISPENSE_VOLUME_VALIDATION_ADDED_IN:
             # In older API versions, when you try to dispense more than you can,
@@ -304,11 +324,14 @@ def dispense(
             well_name = well_core.get_name()
             labware_id = well_core.labware_id
 
-            well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
+            (
+                well_location,
+                dynamic_liquid_tracking,
+            ) = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
                 labware_id=labware_id,
                 well_name=well_name,
                 absolute_point=location.point,
-                is_meniscus=is_meniscus,
+                meniscus_tracking=meniscus_tracking,
             )
             pipette_movement_conflict.check_safe_for_pipette_movement(
                 engine_state=self._engine_client.state,
@@ -317,18 +340,32 @@ def dispense(
                 well_name=well_name,
                 well_location=well_location,
             )
-            self._engine_client.execute_command(
-                cmd.DispenseParams(
-                    pipetteId=self._pipette_id,
-                    labwareId=labware_id,
-                    wellName=well_name,
-                    wellLocation=well_location,
-                    volume=volume,
-                    flowRate=flow_rate,
-                    pushOut=push_out,
-                    correctionVolume=correction_volume,
+            if dynamic_liquid_tracking:
+                self._engine_client.execute_command(
+                    cmd.DispenseWhileTrackingParams(
+                        pipetteId=self._pipette_id,
+                        labwareId=labware_id,
+                        wellName=well_name,
+                        wellLocation=well_location,
+                        volume=volume,
+                        flowRate=flow_rate,
+                        pushOut=push_out,
+                        correctionVolume=correction_volume,
+                    )
+                )
+            else:
+                self._engine_client.execute_command(
+                    cmd.DispenseParams(
+                        pipetteId=self._pipette_id,
+                        labwareId=labware_id,
+                        wellName=well_name,
+                        wellLocation=well_location,
+                        volume=volume,
+                        flowRate=flow_rate,
+                        pushOut=push_out,
+                        correctionVolume=correction_volume,
+                    )
                 )
-            )
 
         if isinstance(location, (TrashBin, WasteChute)):
             self._protocol_core.set_last_location(location=None, mount=self.get_mount())
@@ -813,11 +850,13 @@ def resin_tip_dispense(
         if flow_rate is None:
             flow_rate = _RESIN_TIP_DEFAULT_FLOW_RATE
 
-        well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
+        (
+            well_location,
+            dynamic_tracking,
+        ) = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
             labware_id=labware_id,
             well_name=well_name,
             absolute_point=location.point,
-            is_meniscus=None,
         )
         pipette_movement_conflict.check_safe_for_pipette_movement(
             engine_state=self._engine_client.state,
diff --git a/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py b/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py
index 5afae82df8a1..766414b319c1 100644
--- a/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py
+++ b/api/src/opentrons/protocol_api/core/engine/transfer_components_executor.py
@@ -164,9 +164,6 @@ def aspirate_and_wait(self, volume: float) -> None:
         # TODO: handle volume correction
         aspirate_props = self._transfer_properties.aspirate
         correction_volume = aspirate_props.correction_by_volume.get_for_volume(volume)
-        is_meniscus = bool(
-            aspirate_props.position_reference == PositionReference.LIQUID_MENISCUS
-        )
         self._instrument.aspirate(
             location=self._target_location,
             well_core=None,
@@ -174,7 +171,6 @@ def aspirate_and_wait(self, volume: float) -> None:
             rate=1,
             flow_rate=aspirate_props.flow_rate_by_volume.get_for_volume(volume),
             in_place=True,
-            is_meniscus=is_meniscus,
             correction_volume=correction_volume,
         )
         self._tip_state.append_liquid(volume)
@@ -194,9 +190,6 @@ def dispense_and_wait(
         correction_volume = dispense_properties.correction_by_volume.get_for_volume(
             volume
         )
-        is_meniscus = bool(
-            dispense_properties.position_reference == PositionReference.LIQUID_MENISCUS
-        )
         self._instrument.dispense(
             location=self._target_location,
             well_core=None,
@@ -205,7 +198,6 @@ def dispense_and_wait(
             flow_rate=dispense_properties.flow_rate_by_volume.get_for_volume(volume),
             in_place=True,
             push_out=push_out_override,
-            is_meniscus=is_meniscus,
             correction_volume=correction_volume,
         )
         if push_out_override:
@@ -745,7 +737,6 @@ def _remove_air_gap(self, location: Location) -> None:
             rate=1,
             flow_rate=flow_rate,
             in_place=True,
-            is_meniscus=False,
             push_out=0,
             correction_volume=correction_volume,
         )
diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py
index fd4c7e571b37..7c0aab3f2358 100644
--- a/api/src/opentrons/protocol_api/core/instrument.py
+++ b/api/src/opentrons/protocol_api/core/instrument.py
@@ -47,7 +47,7 @@ def aspirate(
         rate: float,
         flow_rate: float,
         in_place: bool,
-        is_meniscus: Optional[bool] = None,
+        meniscus_tracking: Optional[types.MeniscusTrackingTarget] = None,
         correction_volume: Optional[float] = None,
     ) -> None:
         """Aspirate a given volume of liquid from the specified location.
@@ -58,6 +58,7 @@ def aspirate(
             rate: The rate for how quickly to aspirate.
             flow_rate: The flow rate in µL/s to aspirate at.
             in_place: Whether this is in-place.
+            meniscus_tracking: Optional data about where to aspirate from.
             correction_volume: The correction volume in uL
         """
         ...
@@ -72,7 +73,7 @@ def dispense(
         flow_rate: float,
         in_place: bool,
         push_out: Optional[float],
-        is_meniscus: Optional[bool] = None,
+        meniscus_tracking: Optional[types.MeniscusTrackingTarget] = None,
         correction_volume: Optional[float] = None,
     ) -> None:
         """Dispense a given volume of liquid into the specified location.
@@ -85,6 +86,7 @@ def dispense(
             in_place: Whether this is in-place.
             push_out: The amount to push the plunger below bottom position.
             correction_volume: The correction volume in uL
+            meniscus_tracking: Optional data about where to dispense from.
         """
         ...
 
diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py
index 443349cb0bdf..5140a3f0a78d 100644
--- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py
+++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py
@@ -89,7 +89,7 @@ def aspirate(
         rate: float,
         flow_rate: float,
         in_place: bool,
-        is_meniscus: Optional[bool] = None,
+        meniscus_tracking: Optional[types.MeniscusTrackingTarget] = None,
         correction_volume: Optional[float] = None,
     ) -> None:
         """Aspirate a given volume of liquid from the specified location.
@@ -101,6 +101,7 @@ def aspirate(
             flow_rate: Not used in this core.
             in_place: Whether we should move_to location.
             correction_volume: Not used in this core
+            meniscus_tracking: Optional data about where to aspirate from.
         """
         if self.get_current_volume() == 0:
             # Make sure we're at the top of the labware and clear of any
@@ -134,7 +135,7 @@ def dispense(
         flow_rate: float,
         in_place: bool,
         push_out: Optional[float],
-        is_meniscus: Optional[bool] = None,
+        meniscus_tracking: Optional[types.MeniscusTrackingTarget] = None,
         correction_volume: Optional[float] = None,
     ) -> None:
         """Dispense a given volume of liquid into the specified location.
@@ -147,6 +148,7 @@ def dispense(
             in_place: Whether we should move_to location.
             correction_volume: Not used in this core.
             push_out: The amount to push the plunger below bottom position.
+            meniscus_tracking: Optional data about where to dispense from.
         """
         if isinstance(location, (TrashBin, WasteChute)):
             raise APIVersionError(
diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py
index a1dd3a6ddcb6..388c26bfd70e 100644
--- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py
+++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py
@@ -102,7 +102,7 @@ def aspirate(
         rate: float,
         flow_rate: float,
         in_place: bool,
-        is_meniscus: Optional[bool] = None,
+        meniscus_tracking: Optional[types.MeniscusTrackingTarget] = None,
         correction_volume: Optional[float] = None,
     ) -> None:
         if self.get_current_volume() == 0:
@@ -145,7 +145,7 @@ def dispense(
         flow_rate: float,
         in_place: bool,
         push_out: Optional[float],
-        is_meniscus: Optional[bool] = None,
+        meniscus_tracking: Optional[types.MeniscusTrackingTarget] = None,
         correction_volume: Optional[float] = None,
     ) -> None:
         if isinstance(location, (TrashBin, WasteChute)):
diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py
index b2a8479b21ce..48a175824eb9 100644
--- a/api/src/opentrons/protocol_api/instrument_context.py
+++ b/api/src/opentrons/protocol_api/instrument_context.py
@@ -232,7 +232,6 @@ def aspirate(
 
         move_to_location: types.Location
         well: Optional[labware.Well] = None
-        is_meniscus: Optional[bool] = None
         last_location = self._get_last_location_by_api_version()
         try:
             target = validation.validate_location(
@@ -250,7 +249,7 @@ def aspirate(
             raise ValueError(
                 "Trash Bin and Waste Chute are not acceptable location parameters for Aspirate commands."
             )
-        move_to_location, well, is_meniscus = self._handle_aspirate_target(
+        move_to_location, well, meniscus_tracking = self._handle_aspirate_target(
             target=target
         )
         if self.api_version >= APIVersion(2, 11):
@@ -293,13 +292,13 @@ def aspirate(
                 rate=rate,
                 flow_rate=flow_rate,
                 in_place=target.in_place,
-                is_meniscus=is_meniscus,
+                meniscus_tracking=meniscus_tracking,
             )
 
         return self
 
     @requires_version(2, 0)
-    def dispense(  # noqa: C901
+    def dispense(
         self,
         volume: Optional[float] = None,
         location: Optional[
@@ -395,8 +394,6 @@ def dispense(  # noqa: C901
                 volume, location if location else "current position", rate
             )
         )
-        well: Optional[labware.Well] = None
-        is_meniscus: Optional[bool] = None
         last_location = self._get_last_location_by_api_version()
 
         try:
@@ -411,29 +408,6 @@ def dispense(  # noqa: C901
                 "knows where it is."
             ) from e
 
-        if isinstance(target, validation.WellTarget):
-            well = target.well
-            if target.location:
-                move_to_location = target.location
-                is_meniscus = target.location.is_meniscus
-            elif well.parent._core.is_fixed_trash():
-                move_to_location = target.well.top()
-            else:
-                move_to_location = target.well.bottom(
-                    z=self._well_bottom_clearances.dispense
-                )
-        if isinstance(target, validation.PointTarget):
-            move_to_location = target.location
-
-        if self.api_version >= APIVersion(2, 11) and not isinstance(
-            target, (TrashBin, WasteChute)
-        ):
-            instrument.validate_takes_liquid(
-                location=move_to_location,
-                reject_module=self.api_version >= APIVersion(2, 13),
-                reject_adapter=self.api_version >= APIVersion(2, 15),
-            )
-
         if self.api_version >= APIVersion(2, 16):
             c_vol = self._core.get_current_volume() if volume is None else volume
         else:
@@ -460,9 +434,21 @@ def dispense(  # noqa: C901
                     flow_rate=flow_rate,
                     in_place=False,
                     push_out=push_out,
+                    meniscus_tracking=None,
                 )
             return self
 
+        move_to_location, well, meniscus_tracking = self._handle_dispense_target(
+            target=target
+        )
+
+        if self.api_version >= APIVersion(2, 11):
+            instrument.validate_takes_liquid(
+                location=move_to_location,
+                reject_module=self.api_version >= APIVersion(2, 13),
+                reject_adapter=self.api_version >= APIVersion(2, 15),
+            )
+
         with publisher.publish_context(
             broker=self.broker,
             command=cmds.dispense(
@@ -481,7 +467,7 @@ def dispense(  # noqa: C901
                 flow_rate=flow_rate,
                 in_place=target.in_place,
                 push_out=push_out,
-                is_meniscus=is_meniscus,
+                meniscus_tracking=meniscus_tracking,
             )
 
         return self
@@ -2543,24 +2529,41 @@ def _raise_if_pressure_not_supported_by_pipette(self) -> None:
             )
 
     def _handle_aspirate_target(
-        self, target: validation.ValidTarget
-    ) -> tuple[types.Location, Optional[labware.Well], Optional[bool]]:
-        move_to_location: types.Location
-        well: Optional[labware.Well] = None
-        is_meniscus: Optional[bool] = None
+        self, target: Union[validation.WellTarget, validation.PointTarget]
+    ) -> tuple[
+        types.Location, Optional[labware.Well], Optional[types.MeniscusTrackingTarget]
+    ]:
         if isinstance(target, validation.WellTarget):
-            well = target.well
             if target.location:
-                move_to_location = target.location
-                is_meniscus = target.location.is_meniscus
+                return target.location, target.well, target.location.meniscus_tracking
 
             else:
-                move_to_location = target.well.bottom(
-                    z=self._well_bottom_clearances.aspirate
+                return (
+                    target.well.bottom(z=self._well_bottom_clearances.aspirate),
+                    target.well,
+                    None,
                 )
         if isinstance(target, validation.PointTarget):
-            move_to_location = target.location
-        return (move_to_location, well, is_meniscus)
+            return target.location, None, None
+
+    def _handle_dispense_target(
+        self, target: Union[validation.WellTarget, validation.PointTarget]
+    ) -> tuple[
+        types.Location, Optional[labware.Well], Optional[types.MeniscusTrackingTarget]
+    ]:
+        if isinstance(target, validation.WellTarget):
+            if target.location:
+                return target.location, target.well, target.location.meniscus_tracking
+            elif target.well.parent._core.is_fixed_trash():
+                return target.well.top(), target.well, None
+            else:
+                return (
+                    target.well.bottom(z=self._well_bottom_clearances.dispense),
+                    target.well,
+                    None,
+                )
+        if isinstance(target, validation.PointTarget):
+            return target.location, None, None
 
 
 class AutoProbeDisable:
diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py
index 57394aee1141..b58c0a44b3ef 100644
--- a/api/src/opentrons/protocol_api/labware.py
+++ b/api/src/opentrons/protocol_api/labware.py
@@ -24,6 +24,7 @@
     Sequence,
     Mapping,
     Union,
+    Literal,
 )
 
 from opentrons_shared_data.labware.types import (
@@ -33,7 +34,12 @@
     LabwareParameters3,
 )
 
-from opentrons.types import Location, Point, NozzleMapInterface
+from opentrons.types import (
+    Location,
+    Point,
+    NozzleMapInterface,
+    MeniscusTrackingTarget,
+)
 from opentrons.protocols.api_support.types import APIVersion
 from opentrons.protocols.api_support.util import (
     requires_version,
@@ -256,16 +262,21 @@ def center(self) -> Location:
         return Location(self._core.get_center(), self)
 
     @requires_version(2, 21)
-    def meniscus(self, z: float = 0.0) -> Location:
+    def meniscus(
+        self, target: Literal["start", "end", "dynamic"], z: float = 0.0
+    ) -> Location:
         """
         :param z: An offset on the z-axis, in mm. Positive offsets are higher and
             negative offsets are lower.
+        :param target: The relative position inside the well to target when performing a liquid handling operation.
         :return: A :py:class:`~opentrons.types.Location` that indicates location is meniscus and that holds the ``z`` offset in its point.z field.
 
         :meta private:
         """
         return Location(
-            point=Point(x=0, y=0, z=z), labware=self, _ot_internal_is_meniscus=True
+            point=Point(x=0, y=0, z=z),
+            labware=self,
+            _meniscus_tracking=MeniscusTrackingTarget(target),
         )
 
     @requires_version(2, 8)
diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_while_tracking.py b/api/src/opentrons/protocol_engine/commands/aspirate_while_tracking.py
index eb2328eeee8a..91ade3f32b78 100644
--- a/api/src/opentrons/protocol_engine/commands/aspirate_while_tracking.py
+++ b/api/src/opentrons/protocol_engine/commands/aspirate_while_tracking.py
@@ -16,6 +16,7 @@
     LiquidHandlingWellLocationMixin,
     DestinationPositionResult,
     StallOrCollisionError,
+    move_to_well,
 )
 from .command import (
     AbstractCommandImpl,
@@ -24,13 +25,14 @@
     DefinedErrorData,
     SuccessData,
 )
+from ..state.update_types import StateUpdate
 from ..errors.exceptions import PipetteNotReadyToAspirateError
 from opentrons.hardware_control import HardwareControlAPI
 from ..state.update_types import CLEAR
 from ..types import CurrentWell, DeckPoint
 
 if TYPE_CHECKING:
-    from ..execution import PipettingHandler, GantryMover
+    from ..execution import PipettingHandler, GantryMover, MovementHandler
     from ..resources import ModelUtils
     from ..state.state import StateView
     from ..notes import CommandNoteAdder
@@ -75,6 +77,7 @@ def __init__(
         command_note_adder: CommandNoteAdder,
         model_utils: ModelUtils,
         gantry_mover: GantryMover,
+        movement: MovementHandler,
         **kwargs: object,
     ) -> None:
         self._pipetting = pipetting
@@ -83,6 +86,7 @@ def __init__(
         self._command_note_adder = command_note_adder
         self._model_utils = model_utils
         self._gantry_mover = gantry_mover
+        self._movement = movement
 
     async def execute(self, params: AspirateWhileTrackingParams) -> _ExecuteReturn:
         """Move to and aspirate from the requested well.
@@ -104,6 +108,28 @@ async def execute(self, params: AspirateWhileTrackingParams) -> _ExecuteReturn:
         current_position = await self._gantry_mover.get_position(params.pipetteId)
         current_location = self._state_view.pipettes.get_current_location()
 
+        state_update = StateUpdate()
+        current_well = CurrentWell(
+            pipette_id=params.pipetteId,
+            labware_id=params.labwareId,
+            well_name=params.wellName,
+        )
+        move_result = await move_to_well(
+            movement=self._movement,
+            model_utils=self._model_utils,
+            pipette_id=params.pipetteId,
+            labware_id=params.labwareId,
+            well_name=params.wellName,
+            well_location=params.wellLocation,
+            current_well=current_well,
+            operation_volume=-params.volume,
+        )
+        state_update.append(move_result.state_update)
+        if isinstance(move_result, DefinedErrorData):
+            return DefinedErrorData(
+                public=move_result.public, state_update=state_update
+            )
+
         aspirate_result = await aspirate_while_tracking(
             pipette_id=params.pipetteId,
             labware_id=params.labwareId,
diff --git a/api/src/opentrons/protocol_engine/commands/dispense_while_tracking.py b/api/src/opentrons/protocol_engine/commands/dispense_while_tracking.py
index 180377e1b337..f84c3fda1900 100644
--- a/api/src/opentrons/protocol_engine/commands/dispense_while_tracking.py
+++ b/api/src/opentrons/protocol_engine/commands/dispense_while_tracking.py
@@ -8,7 +8,7 @@
 from pydantic import Field
 from pydantic.json_schema import SkipJsonSchema
 
-from ..state.update_types import CLEAR
+from ..state.update_types import CLEAR, StateUpdate
 from ..types import CurrentWell, DeckPoint
 from .pipetting_common import (
     PipetteIdMixin,
@@ -22,6 +22,7 @@
     LiquidHandlingWellLocationMixin,
     DestinationPositionResult,
     StallOrCollisionError,
+    move_to_well,
 )
 from .command import (
     AbstractCommandImpl,
@@ -32,7 +33,7 @@
 )
 
 if TYPE_CHECKING:
-    from ..execution import PipettingHandler, GantryMover
+    from ..execution import PipettingHandler, GantryMover, MovementHandler
     from ..resources import ModelUtils
     from ..state.state import StateView
 
@@ -82,12 +83,14 @@ def __init__(
         pipetting: PipettingHandler,
         model_utils: ModelUtils,
         gantry_mover: GantryMover,
+        movement: MovementHandler,
         **kwargs: object,
     ) -> None:
         self._state_view = state_view
         self._pipetting = pipetting
         self._model_utils = model_utils
         self._gantry_mover = gantry_mover
+        self._movement = movement
 
     async def execute(self, params: DispenseWhileTrackingParams) -> _ExecuteReturn:
         """Move to and dispense to the requested well."""
@@ -98,6 +101,28 @@ async def execute(self, params: DispenseWhileTrackingParams) -> _ExecuteReturn:
 
         current_location = self._state_view.pipettes.get_current_location()
         current_position = await self._gantry_mover.get_position(params.pipetteId)
+        current_well = CurrentWell(
+            pipette_id=params.pipetteId,
+            labware_id=params.labwareId,
+            well_name=params.wellName,
+        )
+        state_update = StateUpdate()
+        move_result = await move_to_well(
+            movement=self._movement,
+            model_utils=self._model_utils,
+            pipette_id=params.pipetteId,
+            labware_id=params.labwareId,
+            well_name=params.wellName,
+            well_location=params.wellLocation,
+            current_well=current_well,
+            operation_volume=-params.volume,
+        )
+        state_update.append(move_result.state_update)
+        if isinstance(move_result, DefinedErrorData):
+            return DefinedErrorData(
+                public=move_result.public, state_update=state_update
+            )
+
         dispense_result = await dispense_while_tracking(
             pipette_id=params.pipetteId,
             labware_id=labware_id,
diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py
index 01c3e3aee0a5..3c4e974d32f0 100644
--- a/api/src/opentrons/protocol_engine/execution/pipetting.py
+++ b/api/src/opentrons/protocol_engine/execution/pipetting.py
@@ -13,6 +13,7 @@
     InvalidAspirateVolumeError,
     InvalidPushOutVolumeError,
     InvalidDispenseVolumeError,
+    InvalidLiquidHeightFound,
 )
 from opentrons.protocol_engine.types import WellLocation
 from opentrons.protocol_engine.types.liquid_level_detection import (
@@ -177,7 +178,27 @@ async def aspirate_while_tracking(
         Raises:
             PipetteOverpressureError, propagated as-is from the hardware controller.
         """
-        return 0.0
+        # get mount and config data from state and hardware controller
+        hw_pipette, adjusted_volume = self.get_hw_aspirate_params(
+            pipette_id, volume, command_note_adder
+        )
+        aspirate_z_distance = self._state_view.geometry.get_liquid_handling_z_change(
+            labware_id=labware_id,
+            well_name=well_name,
+            operation_volume=volume * -1,
+        )
+        if isinstance(aspirate_z_distance, SimulatedProbeResult):
+            raise InvalidLiquidHeightFound(
+                "Aspirate distance must be a float in Hardware pipetting handler."
+            )
+        with self._set_flow_rate(pipette=hw_pipette, aspirate_flow_rate=flow_rate):
+            await self._hardware_api.aspirate_while_tracking(
+                mount=hw_pipette.mount,
+                z_distance=aspirate_z_distance,
+                flow_rate=flow_rate,
+                volume=adjusted_volume,
+            )
+        return adjusted_volume
 
     async def dispense_while_tracking(
         self,
@@ -193,7 +214,26 @@ async def dispense_while_tracking(
         Raises:
             PipetteOverpressureError, propagated as-is from the hardware controller.
         """
-        return 0.0
+        # get mount and config data from state and hardware controller
+        hw_pipette, adjusted_volume = self.get_hw_dispense_params(pipette_id, volume)
+        dispense_z_distance = self._state_view.geometry.get_liquid_handling_z_change(
+            labware_id=labware_id,
+            well_name=well_name,
+            operation_volume=volume,
+        )
+        if isinstance(dispense_z_distance, SimulatedProbeResult):
+            raise InvalidLiquidHeightFound(
+                "Dispense distance must be a float in Hardware pipetting handler."
+            )
+        with self._set_flow_rate(pipette=hw_pipette, dispense_flow_rate=flow_rate):
+            await self._hardware_api.dispense_while_tracking(
+                mount=hw_pipette.mount,
+                z_distance=dispense_z_distance,
+                flow_rate=flow_rate,
+                volume=adjusted_volume,
+                push_out=push_out,
+            )
+        return adjusted_volume
 
     async def aspirate_in_place(
         self,
diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py
index 24cb5ab36ad3..14e31309d291 100644
--- a/api/src/opentrons/protocol_engine/state/geometry.py
+++ b/api/src/opentrons/protocol_engine/state/geometry.py
@@ -13,6 +13,7 @@
     DeckSlotName,
     StagingSlotName,
     MountType,
+    MeniscusTrackingTarget,
 )
 
 from opentrons_shared_data.errors.exceptions import InvalidStoredData
@@ -567,23 +568,26 @@ def get_relative_liquid_handling_well_location(
         labware_id: str,
         well_name: str,
         absolute_point: Point,
-        is_meniscus: Optional[bool] = None,
-    ) -> LiquidHandlingWellLocation:
-        """Given absolute position, get relative location of a well in a labware.
-
-        If is_meniscus is True, absolute_point will hold the z-offset in its z field.
-        """
-        if is_meniscus:
-            return LiquidHandlingWellLocation(
+        meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
+    ) -> Tuple[LiquidHandlingWellLocation, bool]:
+        """Given absolute position, get relative location of a well in a labware."""
+        dynamic_liquid_tracking = False
+        if meniscus_tracking:
+            location = LiquidHandlingWellLocation(
                 origin=WellOrigin.MENISCUS,
                 offset=WellOffset(x=0, y=0, z=absolute_point.z),
             )
+            if meniscus_tracking == MeniscusTrackingTarget.END:
+                location.volumeOffset = "operationVolume"
+            elif meniscus_tracking == MeniscusTrackingTarget.DYNAMIC:
+                dynamic_liquid_tracking = True
         else:
             well_absolute_point = self.get_well_position(labware_id, well_name)
             delta = absolute_point - well_absolute_point
-            return LiquidHandlingWellLocation(
+            location = LiquidHandlingWellLocation(
                 offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)
             )
+        return location, dynamic_liquid_tracking
 
     def get_relative_pick_up_tip_well_location(
         self,
@@ -1789,6 +1793,29 @@ def _recurse_labware_offset_location(
         else:  # Off deck
             return None
 
+    def get_liquid_handling_z_change(
+        self,
+        labware_id: str,
+        well_name: str,
+        operation_volume: float,
+    ) -> float:
+        """Get the change in height from a liquid handling operation."""
+        initial_handling_height = self.get_meniscus_height(
+            labware_id=labware_id, well_name=well_name
+        )
+        final_height = self.get_well_height_after_liquid_handling(
+            labware_id=labware_id,
+            well_name=well_name,
+            initial_height=initial_handling_height,
+            volume=operation_volume,
+        )
+        # this function is only called by
+        # HardwarePipetteHandler::aspirate/dispense while_tracking, and shouldn't
+        # be reached in the case of a simulated liquid_probe
+        assert not isinstance(initial_handling_height, SimulatedProbeResult)
+        assert not isinstance(final_height, SimulatedProbeResult)
+        return final_height - initial_handling_height
+
     def get_well_offset_adjustment(
         self,
         labware_id: str,
@@ -1809,7 +1836,6 @@ def get_well_offset_adjustment(
             well_location=well_location,
             well_depth=well_depth,
         )
-
         # if we're tracking a MENISCUS origin, and targeting either the beginning
         #   position of the liquid or doing dynamic tracking, return the initial height
         if (
diff --git a/api/src/opentrons/types.py b/api/src/opentrons/types.py
index 09e138513c1c..17b97fda5174 100644
--- a/api/src/opentrons/types.py
+++ b/api/src/opentrons/types.py
@@ -88,6 +88,15 @@ def magnitude_to(self, other: Any) -> float:
 ]
 
 
+class MeniscusTrackingTarget(enum.Enum):
+    START = "start"
+    END = "end"
+    DYNAMIC = "dynamic"
+
+    def __str__(self) -> str:
+        return self.name
+
+
 class Location:
     """Location(point: Point, labware: Union["Labware", "Well", str, "ModuleGeometry", LabwareLike, None, "ModuleContext"])
 
@@ -129,12 +138,12 @@ def __init__(
             "ModuleContext",
         ],
         *,
-        _ot_internal_is_meniscus: Optional[bool] = None,
+        _meniscus_tracking: Optional[MeniscusTrackingTarget] = None,
     ):
         self._point = point
         self._given_labware = labware
         self._labware = LabwareLike(labware)
-        self._is_meniscus = _ot_internal_is_meniscus
+        self._meniscus_tracking = _meniscus_tracking
 
     # todo(mm, 2021-10-01): Figure out how to get .point and .labware to show up
     # in the rendered docs, and then update the class docstring to use cross-references.
@@ -148,8 +157,8 @@ def labware(self) -> LabwareLike:
         return self._labware
 
     @property
-    def is_meniscus(self) -> Optional[bool]:
-        return self._is_meniscus
+    def meniscus_tracking(self) -> Optional[MeniscusTrackingTarget]:
+        return self._meniscus_tracking
 
     def __iter__(self) -> Iterator[Union[Point, LabwareLike]]:
         """Iterable interface to support unpacking. Like a tuple.
@@ -167,7 +176,7 @@ def __eq__(self, other: object) -> bool:
             isinstance(other, Location)
             and other._point == self._point
             and other._labware == self._labware
-            and other._is_meniscus == self._is_meniscus
+            and other._meniscus_tracking == self._meniscus_tracking
         )
 
     def move(self, point: Point) -> "Location":
@@ -193,7 +202,7 @@ def move(self, point: Point) -> "Location":
         return Location(point=self.point + point, labware=self._given_labware)
 
     def __repr__(self) -> str:
-        return f"Location(point={repr(self._point)}, labware={self._labware}, is_meniscus={self._is_meniscus if self._is_meniscus is not None else False})"
+        return f"Location(point={repr(self._point)}, labware={self._labware}, meniscus_tracking={self._meniscus_tracking})"
 
 
 # TODO(mc, 2020-10-22): use MountType implementation for Mount
diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py
index a7fae6fb34ba..206c527eb0a7 100644
--- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py
+++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py
@@ -71,7 +71,14 @@
 from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION
 from opentrons.protocols.api_support.types import APIVersion
 from opentrons.protocols.advanced_control.transfers import common as tx_commons
-from opentrons.types import Location, Mount, MountType, Point, NozzleConfigurationType
+from opentrons.types import (
+    Location,
+    Mount,
+    MountType,
+    Point,
+    NozzleConfigurationType,
+    MeniscusTrackingTarget,
+)
 
 from ... import versions_below, versions_at_or_above
 
@@ -619,12 +626,15 @@ def test_aspirate_from_well(
             labware_id="123abc",
             well_name="my cool well",
             absolute_point=Point(1, 2, 3),
-            is_meniscus=None,
+            meniscus_tracking=None,
         )
     ).then_return(
-        LiquidHandlingWellLocation(
-            origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1)
-        )
+        (
+            LiquidHandlingWellLocation(
+                origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1)
+            ),
+            False,
+        ),
     )
 
     subject.aspirate(
@@ -635,6 +645,7 @@ def test_aspirate_from_well(
         flow_rate=7.8,
         in_place=False,
         correction_volume=123,
+        meniscus_tracking=None,
     )
 
     decoy.verify(
@@ -679,6 +690,7 @@ def test_aspirate_from_coordinates(
         well_core=None,
         location=location,
         in_place=False,
+        meniscus_tracking=None,
     )
 
     decoy.verify(
@@ -721,11 +733,16 @@ def test_aspirate_from_meniscus(
             labware_id="123abc",
             well_name="my cool well",
             absolute_point=Point(1, 2, 3),
-            is_meniscus=True,
+            meniscus_tracking=MeniscusTrackingTarget.END,
         )
     ).then_return(
-        LiquidHandlingWellLocation(
-            origin=WellOrigin.MENISCUS, offset=WellOffset(x=3, y=2, z=1), volumeOffset=0
+        (
+            LiquidHandlingWellLocation(
+                origin=WellOrigin.MENISCUS,
+                offset=WellOffset(x=3, y=2, z=1),
+                volumeOffset="operationVolume",
+            ),
+            False,
         )
     )
 
@@ -736,7 +753,7 @@ def test_aspirate_from_meniscus(
         rate=5.6,
         flow_rate=7.8,
         in_place=False,
-        is_meniscus=True,
+        meniscus_tracking=MeniscusTrackingTarget.END,
     )
 
     decoy.verify(
@@ -785,6 +802,7 @@ def test_aspirate_in_place(
         well_core=None,
         location=location,
         in_place=True,
+        meniscus_tracking=None,
     )
 
     decoy.verify(
@@ -921,11 +939,14 @@ def test_dispense_to_well(
             labware_id="123abc",
             well_name="my cool well",
             absolute_point=Point(1, 2, 3),
-            is_meniscus=None,
+            meniscus_tracking=None,
         )
     ).then_return(
-        LiquidHandlingWellLocation(
-            origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1)
+        (
+            LiquidHandlingWellLocation(
+                origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1)
+            ),
+            False,
         )
     )
 
@@ -938,6 +959,7 @@ def test_dispense_to_well(
         in_place=False,
         correction_volume=321,
         push_out=7,
+        meniscus_tracking=None,
     )
 
     decoy.verify(
@@ -985,6 +1007,7 @@ def test_dispense_in_place(
         location=location,
         in_place=True,
         push_out=None,
+        meniscus_tracking=None,
     )
 
     decoy.verify(
@@ -1017,6 +1040,7 @@ def test_dispense_to_coordinates(
         location=location,
         in_place=False,
         push_out=None,
+        meniscus_tracking=None,
     )
 
     decoy.verify(
@@ -1067,6 +1091,7 @@ def test_dispense_conditionally_clamps_volume(
         location=Location(point=Point(1, 2, 3), labware=None),
         in_place=True,
         push_out=None,
+        meniscus_tracking=None,
     )
 
     if expect_clampage:
diff --git a/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py b/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py
index 51c038172ad0..1956573760ea 100644
--- a/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py
+++ b/api/tests/opentrons/protocol_api/core/engine/test_transfer_components_executor.py
@@ -110,7 +110,6 @@ def test_submerge(
             rate=1,
             flow_rate=air_gap_removal_flow_rate,
             in_place=True,
-            is_meniscus=False,
             push_out=0,
             correction_volume=air_gap_correction_vol,
         ),
@@ -127,10 +126,10 @@ def test_submerge(
 
 
 @pytest.mark.parametrize(
-    argnames=["position_reference", "expected_is_meniscus"],
+    argnames=["position_reference"],
     argvalues=[
-        [PositionReference.WELL_BOTTOM, False],
-        [PositionReference.LIQUID_MENISCUS, True],
+        [PositionReference.WELL_BOTTOM],
+        [PositionReference.LIQUID_MENISCUS],
     ],
 )
 def test_aspirate_and_wait(
@@ -138,7 +137,6 @@ def test_aspirate_and_wait(
     mock_instrument_core: InstrumentCore,
     sample_transfer_props: TransferProperties,
     position_reference: PositionReference,
-    expected_is_meniscus: bool,
 ) -> None:
     """It should execute an aspirate and a delay according to properties."""
     source_well = decoy.mock(cls=WellCore)
@@ -166,7 +164,6 @@ def test_aspirate_and_wait(
             rate=1,
             flow_rate=aspirate_flow_rate,
             in_place=True,
-            is_meniscus=expected_is_meniscus,
             correction_volume=correction_volume,
         ),
         mock_instrument_core.delay(0.2),
@@ -198,10 +195,10 @@ def test_aspirate_and_wait_skips_delay(
 
 
 @pytest.mark.parametrize(
-    argnames=["position_reference", "expected_is_meniscus"],
+    argnames=["position_reference"],
     argvalues=[
-        [PositionReference.WELL_BOTTOM, False],
-        [PositionReference.LIQUID_MENISCUS, True],
+        [PositionReference.WELL_BOTTOM],
+        [PositionReference.LIQUID_MENISCUS],
     ],
 )
 def test_dispense_and_wait(
@@ -209,7 +206,6 @@ def test_dispense_and_wait(
     mock_instrument_core: InstrumentCore,
     sample_transfer_props: TransferProperties,
     position_reference: PositionReference,
-    expected_is_meniscus: bool,
 ) -> None:
     """It should execute a dispense and a delay according to properties."""
     source_well = decoy.mock(cls=WellCore)
@@ -242,7 +238,6 @@ def test_dispense_and_wait(
             flow_rate=dispense_flow_rate,
             in_place=True,
             push_out=123,
-            is_meniscus=expected_is_meniscus,
             correction_volume=correction_volume,
         ),
         mock_instrument_core.delay(0.5),
@@ -317,7 +312,6 @@ def test_mix(
             rate=1,
             flow_rate=aspirate_flow_rate,
             in_place=True,
-            is_meniscus=False,
             correction_volume=aspirate_correction_volume,
         ),
         mock_instrument_core.delay(0.2),
@@ -329,7 +323,6 @@ def test_mix(
             flow_rate=dispense_flow_rate,
             in_place=True,
             push_out=2.0,
-            is_meniscus=False,
             correction_volume=dispense_correction_volume,
         ),
         mock_instrument_core.delay(0.5),
@@ -370,7 +363,6 @@ def test_mix_disabled(
             rate=1,
             flow_rate=aspirate_flow_rate,
             in_place=True,
-            is_meniscus=False,
             correction_volume=correction_volume,
         ),
         times=0,
@@ -414,7 +406,6 @@ def test_pre_wet(
             rate=1,
             flow_rate=aspirate_flow_rate,
             in_place=True,
-            is_meniscus=False,
             correction_volume=aspirate_correction_volume,
         ),
         mock_instrument_core.delay(0.2),
@@ -426,7 +417,6 @@ def test_pre_wet(
             flow_rate=dispense_flow_rate,
             in_place=True,
             push_out=0,
-            is_meniscus=False,
             correction_volume=dispense_correction_volume,
         ),
         mock_instrument_core.delay(0.5),
@@ -465,7 +455,6 @@ def test_pre_wet_disabled(
             rate=1,
             flow_rate=aspirate_flow_rate,
             in_place=True,
-            is_meniscus=False,
             correction_volume=aspirate_correction_volume,
         ),
         times=0,
diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py
index 42ec959eec2f..9b7828ade65a 100644
--- a/api/tests/opentrons/protocol_api/test_instrument_context.py
+++ b/api/tests/opentrons/protocol_api/test_instrument_context.py
@@ -341,7 +341,7 @@ def test_aspirate(
             volume=42.0,
             rate=1.23,
             flow_rate=5.67,
-            is_meniscus=None,
+            meniscus_tracking=None,
         ),
         times=1,
     )
@@ -379,7 +379,7 @@ def test_aspirate_well_location(
             volume=42.0,
             rate=1.23,
             flow_rate=5.67,
-            is_meniscus=None,
+            meniscus_tracking=None,
         ),
         times=1,
     )
@@ -393,9 +393,7 @@ def test_aspirate_meniscus_well_location(
 ) -> None:
     """It should aspirate to a well."""
     mock_well = decoy.mock(cls=Well)
-    input_location = Location(
-        point=Point(2, 2, 2), labware=mock_well, _ot_internal_is_meniscus=True
-    )
+    input_location = Location(point=Point(2, 2, 2), labware=mock_well)
     last_location = Location(point=Point(9, 9, 9), labware=None)
     decoy.when(mock_instrument_core.get_mount()).then_return(Mount.RIGHT)
 
@@ -419,7 +417,7 @@ def test_aspirate_meniscus_well_location(
             volume=42.0,
             rate=1.23,
             flow_rate=5.67,
-            is_meniscus=True,
+            meniscus_tracking=None,
         ),
         times=1,
     )
@@ -456,7 +454,7 @@ def test_aspirate_from_coordinates(
             volume=42.0,
             rate=1.23,
             flow_rate=5.67,
-            is_meniscus=None,
+            meniscus_tracking=None,
         ),
         times=1,
     )
@@ -970,7 +968,7 @@ def test_dispense_with_location(
             rate=1.23,
             flow_rate=5.67,
             push_out=None,
-            is_meniscus=None,
+            meniscus_tracking=None,
         ),
         times=1,
     )
@@ -1009,7 +1007,7 @@ def test_dispense_with_well_location(
             rate=1.23,
             flow_rate=3.0,
             push_out=7,
-            is_meniscus=None,
+            meniscus_tracking=None,
         ),
         times=1,
     )
@@ -1050,7 +1048,7 @@ def test_dispense_with_well(
             rate=1.23,
             flow_rate=5.67,
             push_out=None,
-            is_meniscus=None,
+            meniscus_tracking=None,
         ),
         times=1,
     )
@@ -1305,7 +1303,7 @@ def test_dispense_0_volume_means_dispense_everything(
             rate=1.23,
             flow_rate=5.67,
             push_out=None,
-            is_meniscus=None,
+            meniscus_tracking=None,
         ),
         times=1,
     )
@@ -1335,7 +1333,7 @@ def test_dispense_0_volume_means_dispense_nothing(
             rate=1.23,
             flow_rate=5.67,
             push_out=None,
-            is_meniscus=None,
+            meniscus_tracking=None,
         ),
         times=1,
     )
@@ -1375,7 +1373,7 @@ def test_aspirate_0_volume_means_aspirate_everything(
             volume=200,
             rate=1.23,
             flow_rate=5.67,
-            is_meniscus=None,
+            meniscus_tracking=None,
         ),
         times=1,
     )
@@ -1415,7 +1413,7 @@ def test_aspirate_0_volume_means_aspirate_nothing(
             volume=0,
             rate=1.23,
             flow_rate=5.67,
-            is_meniscus=None,
+            meniscus_tracking=None,
         ),
         times=1,
     )
diff --git a/api/tests/opentrons/protocol_api/test_well.py b/api/tests/opentrons/protocol_api/test_well.py
index c0ef530289bb..1c4d3371426f 100644
--- a/api/tests/opentrons/protocol_api/test_well.py
+++ b/api/tests/opentrons/protocol_api/test_well.py
@@ -104,11 +104,10 @@ def test_well_center(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> N
 
 def test_well_meniscus(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> None:
     """It should get a Location representing the meniscus of the well."""
-    result = subject.meniscus(4.2)
+    result = subject.meniscus(z=4.2, target="end")
 
     assert isinstance(result, Location)
     assert result.point == Point(0, 0, 4.2)
-    assert result.is_meniscus is True
     assert result.labware.as_well() is subject
 
 
diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate_while_tracking.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate_while_tracking.py
index f787b9d98923..71778c27fe21 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_aspirate_while_tracking.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate_while_tracking.py
@@ -10,7 +10,11 @@
 from opentrons.types import Point
 from opentrons.hardware_control import API as HardwareAPI
 
-from opentrons.protocol_engine.execution import PipettingHandler, GantryMover
+from opentrons.protocol_engine.execution import (
+    PipettingHandler,
+    GantryMover,
+    MovementHandler,
+)
 from opentrons.protocol_engine.commands.aspirate_while_tracking import (
     AspirateWhileTrackingParams,
     AspirateWhileTrackingResult,
@@ -54,6 +58,12 @@ def pipetting(decoy: Decoy) -> PipettingHandler:
     return decoy.mock(cls=PipettingHandler)
 
 
+@pytest.fixture
+def movement(decoy: Decoy) -> MovementHandler:
+    """Get a mock in the shape of a MovementHandler."""
+    return decoy.mock(cls=MovementHandler)
+
+
 @pytest.fixture
 def subject(
     pipetting: PipettingHandler,
@@ -62,6 +72,7 @@ def subject(
     mock_command_note_adder: CommandNoteAdder,
     model_utils: ModelUtils,
     gantry_mover: GantryMover,
+    movement: MovementHandler,
 ) -> AspirateWhileTrackingImplementation:
     """Get the impelementation subject."""
     return AspirateWhileTrackingImplementation(
@@ -71,6 +82,7 @@ def subject(
         command_note_adder=mock_command_note_adder,
         model_utils=model_utils,
         gantry_mover=gantry_mover,
+        movement=movement,
     )
 
 
@@ -81,11 +93,11 @@ def subject(
         (
             CurrentWell(
                 pipette_id="pipette-id-abc",
-                labware_id="labware-id-1",
-                well_name="well-name-1",
+                labware_id="funky-labware",
+                well_name="funky-well",
             ),
-            "labware-id-1",
-            "well-name-1",
+            "funky-labware",
+            "funky-well",
         ),
         (
             CurrentAddressableArea("pipette-id-abc", "addressable-area-1"),
@@ -155,6 +167,26 @@ async def test_aspirate_while_tracking_implementation(
 
     decoy.when(state_store.pipettes.get_current_location()).then_return(location)
 
+    _well_location = LiquidHandlingWellLocation(
+        origin=WellOrigin.MENISCUS, offset=WellOffset(x=0.0, y=0.0, z=1.0)
+    )
+    _current_well = CurrentWell(
+        pipette_id="pipette-id-abc", labware_id="funky-labware", well_name="funky-well"
+    )
+    decoy.when(
+        await subject._movement.move_to_well(
+            pipette_id="pipette-id-abc",
+            labware_id="funky-labware",
+            well_name="funky-well",
+            well_location=_well_location,
+            current_well=_current_well,
+            force_direct=False,
+            minimum_z_height=None,
+            speed=None,
+            operation_volume=-123.0,
+        ),
+    ).then_return(Point(x=4, y=5, z=6))
+
     result = await subject.execute(params=data)
 
     if isinstance(location, CurrentWell):
@@ -266,6 +298,26 @@ async def test_aspirate_raises_volume_error(
         )
     ).then_raise(AssertionError("blah blah"))
 
+    _well_location = LiquidHandlingWellLocation(
+        origin=WellOrigin.MENISCUS, offset=WellOffset(x=0.0, y=0.0, z=1.0)
+    )
+    _current_well = CurrentWell(
+        pipette_id="pipette-id-abc", labware_id="funky-labware", well_name="funky-well"
+    )
+    decoy.when(
+        await subject._movement.move_to_well(
+            pipette_id="pipette-id-abc",
+            labware_id="funky-labware",
+            well_name="funky-well",
+            well_location=_well_location,
+            current_well=_current_well,
+            force_direct=False,
+            minimum_z_height=None,
+            speed=None,
+            operation_volume=-50.0,
+        ),
+    ).then_return(Point(x=4, y=5, z=6))
+
     with pytest.raises(AssertionError):
         await subject.execute(data)
 
@@ -275,15 +327,15 @@ async def test_aspirate_raises_volume_error(
     [
         (
             CurrentWell(
-                pipette_id="pipette-id",
-                labware_id="labware-id-1",
-                well_name="well-name-1",
+                pipette_id="pipette-id-abc",
+                labware_id="funky-labware",
+                well_name="funky-well",
             ),
-            "labware-id-1",
-            "well-name-1",
+            "funky-labware",
+            "funky-well",
         ),
         (None, None, None),
-        (CurrentAddressableArea("pipette-id", "addressable-area-1"), None, None),
+        (CurrentAddressableArea("pipette-id-abc", "addressable-area-1"), None, None),
     ],
 )
 async def test_overpressure_error(
@@ -299,7 +351,7 @@ async def test_overpressure_error(
     stateupdateWell: str,
 ) -> None:
     """It should return an overpressure error if the hardware API indicates that."""
-    pipette_id = "pipette-id"
+    pipette_id = "pipette-id-abc"
 
     position = Point(x=1, y=2, z=3)
 
@@ -309,13 +361,13 @@ async def test_overpressure_error(
         state_store.geometry.get_nozzles_per_well(
             labware_id=stateupdateLabware,
             target_well_name=stateupdateWell,
-            pipette_id="pipette-id",
+            pipette_id="pipette-id-abc",
         )
     ).then_return(2)
 
     decoy.when(
         state_store.geometry.get_wells_covered_by_pipette_with_active_well(
-            stateupdateLabware, stateupdateWell, "pipette-id"
+            stateupdateLabware, stateupdateWell, "pipette-id-abc"
         )
     ).then_return(["A3", "A4"])
     well_location = LiquidHandlingWellLocation(
@@ -351,6 +403,26 @@ async def test_overpressure_error(
     decoy.when(await gantry_mover.get_position(pipette_id)).then_return(position)
     decoy.when(state_store.pipettes.get_current_location()).then_return(location)
 
+    _well_location = LiquidHandlingWellLocation(
+        origin=WellOrigin.MENISCUS, offset=WellOffset(x=0.0, y=0.0, z=1.0)
+    )
+    _current_well = CurrentWell(
+        pipette_id="pipette-id-abc", labware_id="funky-labware", well_name="funky-well"
+    )
+    decoy.when(
+        await subject._movement.move_to_well(
+            pipette_id=pipette_id,
+            labware_id="funky-labware",
+            well_name="funky-well",
+            well_location=_well_location,
+            current_well=_current_well,
+            force_direct=False,
+            minimum_z_height=None,
+            speed=None,
+            operation_volume=-50.0,
+        ),
+    ).then_return(Point(x=4, y=5, z=6))
+
     result = await subject.execute(data)
 
     if isinstance(location, CurrentWell):
@@ -368,7 +440,7 @@ async def test_overpressure_error(
                     volume_added=update_types.CLEAR,
                 ),
                 pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate(
-                    pipette_id="pipette-id"
+                    pipette_id="pipette-id-abc"
                 ),
             ),
         )
@@ -382,7 +454,7 @@ async def test_overpressure_error(
             ),
             state_update=update_types.StateUpdate(
                 pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate(
-                    pipette_id="pipette-id"
+                    pipette_id="pipette-id-abc"
                 )
             ),
         )
diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense_while_tracking.py b/api/tests/opentrons/protocol_engine/commands/test_dispense_while_tracking.py
index 20526d6e6de5..0f1b53f4b9b0 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_dispense_while_tracking.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_dispense_while_tracking.py
@@ -8,7 +8,11 @@
 from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
 
 from opentrons.types import Point
-from opentrons.protocol_engine.execution import PipettingHandler, GantryMover
+from opentrons.protocol_engine.execution import (
+    PipettingHandler,
+    GantryMover,
+    MovementHandler,
+)
 
 from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData
 from opentrons.protocol_engine.commands.dispense_while_tracking import (
@@ -36,6 +40,7 @@ def subject(
     pipetting: PipettingHandler,
     state_view: StateView,
     gantry_mover: GantryMover,
+    movement: MovementHandler,
     model_utils: ModelUtils,
 ) -> DispenseWhileTrackingImplementation:
     """Build a command implementation."""
@@ -44,20 +49,27 @@ def subject(
         state_view=state_view,
         gantry_mover=gantry_mover,
         model_utils=model_utils,
+        movement=movement,
     )
 
 
+@pytest.fixture
+def movement(decoy: Decoy) -> MovementHandler:
+    """Get a mock in the shape of a MovementHandler."""
+    return decoy.mock(cls=MovementHandler)
+
+
 @pytest.mark.parametrize(
     "location,stateupdateLabware,stateupdateWell",
     [
         (
             CurrentWell(
                 pipette_id="pipette-id-abc",
-                labware_id="labware-id-1",
-                well_name="well-name-1",
+                labware_id="funky-labware",
+                well_name="funky-well",
             ),
-            "labware-id-1",
-            "well-name-1",
+            "funky-labware",
+            "funky-well",
         ),
         (
             CurrentAddressableArea("pipette-id-abc", "addressable-area-1"),
@@ -124,6 +136,25 @@ async def test_dispense_while_tracking_implementation(
     decoy.when(await gantry_mover.get_position("pipette-id-abc")).then_return(
         Point(1, 2, 3)
     )
+    _well_location = LiquidHandlingWellLocation(
+        origin=WellOrigin.MENISCUS, offset=WellOffset(x=0.0, y=0.0, z=1.0)
+    )
+    _current_well = CurrentWell(
+        pipette_id="pipette-id-abc", labware_id="funky-labware", well_name="funky-well"
+    )
+    decoy.when(
+        await subject._movement.move_to_well(
+            pipette_id="pipette-id-abc",
+            labware_id="funky-labware",
+            well_name="funky-well",
+            well_location=_well_location,
+            current_well=_current_well,
+            force_direct=False,
+            minimum_z_height=None,
+            speed=None,
+            operation_volume=-123.0,
+        ),
+    ).then_return(Point(x=4, y=5, z=6))
 
     result = await subject.execute(data)
 
@@ -162,11 +193,11 @@ async def test_dispense_while_tracking_implementation(
         (
             CurrentWell(
                 pipette_id="pipette-id",
-                labware_id="labware-id-1",
-                well_name="well-name-1",
+                labware_id="funky-labware",
+                well_name="funky-well",
             ),
-            "labware-id-1",
-            "well-name-1",
+            "funky-labware",
+            "funky-well",
         ),
         (
             CurrentAddressableArea("pipette-id", "addressable-area-1"),
@@ -237,6 +268,26 @@ async def test_overpressure_error(
     decoy.when(await gantry_mover.get_position(pipette_id)).then_return(position)
     decoy.when(state_view.pipettes.get_current_location()).then_return(location)
 
+    _well_location = LiquidHandlingWellLocation(
+        origin=WellOrigin.MENISCUS, offset=WellOffset(x=0.0, y=0.0, z=1.0)
+    )
+    _current_well = CurrentWell(
+        pipette_id="pipette-id", labware_id="funky-labware", well_name="funky-well"
+    )
+    decoy.when(
+        await subject._movement.move_to_well(
+            pipette_id="pipette-id",
+            labware_id="funky-labware",
+            well_name="funky-well",
+            well_location=_well_location,
+            current_well=_current_well,
+            force_direct=False,
+            minimum_z_height=None,
+            speed=None,
+            operation_volume=-50.0,
+        ),
+    ).then_return(Point(x=4, y=5, z=6))
+
     result = await subject.execute(data)
 
     if isinstance(location, CurrentWell):
diff --git a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py
index f39853cb894f..17d72a146847 100644
--- a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py
+++ b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py
@@ -10,12 +10,15 @@
 
 from opentrons.protocol_engine.state.state import StateView
 from opentrons.protocol_engine.state.pipettes import HardwarePipette
+from opentrons.protocol_engine.state.labware import LabwareView
+from opentrons.protocol_engine.state.wells import WellView
 from opentrons.protocol_engine.types import TipGeometry
 from opentrons.protocol_engine.execution.pipetting import (
     HardwarePipettingHandler,
     VirtualPipettingHandler,
     create_pipetting_handler,
 )
+
 from opentrons.protocol_engine.errors.exceptions import (
     TipNotAttachedError,
     InvalidAspirateVolumeError,
@@ -26,6 +29,43 @@
 from ..note_utils import CommandNoteMatcher
 
 
+from opentrons_shared_data.labware.labware_definition import (
+    CuboidalFrustum,
+    InnerWellGeometry,
+    SphericalSegment,
+    RectangularWellDefinition3,
+)
+
+_TEST_INNER_WELL_GEOMETRY = InnerWellGeometry(
+    sections=[
+        CuboidalFrustum(
+            shape="cuboidal",
+            topXDimension=7.6,
+            topYDimension=8.5,
+            bottomXDimension=5.6,
+            bottomYDimension=6.5,
+            topHeight=45,
+            bottomHeight=20,
+        ),
+        CuboidalFrustum(
+            shape="cuboidal",
+            topXDimension=5.6,
+            topYDimension=6.5,
+            bottomXDimension=4.5,
+            bottomYDimension=4.0,
+            topHeight=20,
+            bottomHeight=10,
+        ),
+        SphericalSegment(
+            shape="spherical",
+            radiusOfCurvature=6,
+            topHeight=10,
+            bottomHeight=0.0,
+        ),
+    ],
+)
+
+
 @pytest.fixture
 def mock_hardware_api(decoy: Decoy) -> HardwareAPI:
     """Get a mock in the shape of a HardwareAPI."""
@@ -49,6 +89,18 @@ def hardware_subject(
     )
 
 
+@pytest.fixture
+def mock_labware_view(decoy: Decoy) -> LabwareView:
+    """Get a mock in the shape of a LabwareView."""
+    return decoy.mock(cls=LabwareView)
+
+
+@pytest.fixture
+def mock_well_view(decoy: Decoy) -> WellView:
+    """Get a mock in the shape of a WellView."""
+    return decoy.mock(cls=WellView)
+
+
 async def test_create_pipette_handler(
     decoy: Decoy,
     mock_state_view: StateView,
@@ -217,6 +269,68 @@ async def test_hw_dispense_in_place_raises_invalid_push_out(
         )
 
 
+async def test_hw_aspirate_while_tracking(
+    decoy: Decoy,
+    mock_state_view: StateView,
+    mock_hardware_api: HardwareAPI,
+    mock_labware_view: LabwareView,
+    mock_well_view: WellView,
+    hardware_subject: HardwarePipettingHandler,
+    mock_command_note_adder: CommandNoteAdder,
+) -> None:
+    """Should set flow_rate and call hardware_api aspirate."""
+    decoy.when(mock_labware_view.get_well_definition("labware-id", "A1")).then_return(
+        RectangularWellDefinition3.model_construct(totalLiquidVolume=1100000)  # type: ignore[call-arg]
+    )
+    decoy.when(mock_labware_view.get_well_geometry("labware-id", "A1")).then_return(
+        _TEST_INNER_WELL_GEOMETRY
+    )
+
+    decoy.when(mock_state_view.pipettes.get_working_volume("pipette-id")).then_return(
+        25
+    )
+    decoy.when(mock_state_view.pipettes.get_aspirated_volume("pipette-id")).then_return(
+        0
+    )
+
+    decoy.when(mock_hardware_api.attached_instruments).then_return({})
+    decoy.when(
+        mock_state_view.pipettes.get_hardware_pipette(
+            pipette_id="pipette-id",
+            attached_pipettes={},
+        )
+    ).then_return(
+        HardwarePipette(
+            mount=Mount.LEFT,
+            config=cast(
+                PipetteDict,
+                {
+                    "aspirate_flow_rate": 1.23,
+                    "dispense_flow_rate": 4.56,
+                    "blow_out_flow_rate": 7.89,
+                },
+            ),
+        )
+    )
+
+    decoy.when(
+        mock_state_view.geometry.get_liquid_handling_z_change(
+            labware_id="labware-id", well_name="A1", operation_volume=25.0
+        )
+    ).then_return(4.544)
+
+    result = await hardware_subject.aspirate_while_tracking(
+        pipette_id="pipette-id",
+        labware_id="labware-id",
+        well_name="A1",
+        volume=25,
+        flow_rate=2.5,
+        command_note_adder=mock_command_note_adder,
+    )
+    # make sure hw aspirate_while_tracking runs without error
+    assert result == 25
+
+
 async def test_hw_aspirate_in_place(
     decoy: Decoy,
     mock_state_view: StateView,
diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py
index 4e336dc03322..f725a4a476e2 100644
--- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py
+++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py
@@ -23,7 +23,13 @@
 from opentrons_shared_data.labware.types import LabwareUri
 from opentrons_shared_data.pipette import pipette_definition
 from opentrons.calibration_storage.helpers import uri_from_details
-from opentrons.types import Point, DeckSlotName, MountType, StagingSlotName
+from opentrons.types import (
+    Point,
+    DeckSlotName,
+    MountType,
+    StagingSlotName,
+    MeniscusTrackingTarget,
+)
 from opentrons_shared_data.pipette.types import PipetteNameType
 from opentrons_shared_data.labware.labware_definition import (
     CuboidalFrustum,
@@ -2074,11 +2080,14 @@ def test_get_relative_liquid_handling_well_location(
     subject: GeometryView,
 ) -> None:
     """It should get the relative location of a well given an absolute position."""
-    result = subject.get_relative_liquid_handling_well_location(
+    (
+        result,
+        dynamic_liquid_tracking,
+    ) = subject.get_relative_liquid_handling_well_location(
         labware_id="labware-id",
         well_name="B2",
         absolute_point=Point(x=0, y=0, z=-2),
-        is_meniscus=True,
+        meniscus_tracking=MeniscusTrackingTarget.END,
     )
 
     assert result == LiquidHandlingWellLocation(
@@ -2088,6 +2097,7 @@ def test_get_relative_liquid_handling_well_location(
             y=0.0,
             z=cast(float, pytest.approx(-2)),
         ),
+        volumeOffset="operationVolume",
     )
 
 
@@ -4289,3 +4299,35 @@ def test_virtual_find_height_and_volume(
     #  SimulatedProbeResult
     if isinstance(target_height_volume, SimulatedProbeResult):
         assert height_estimate == volume_estimate == target_height_volume
+
+
+def test_get_liquid_handling_z_change(
+    decoy: Decoy,
+    subject: GeometryView,
+    mock_labware_view: LabwareView,
+    mock_well_view: WellView,
+) -> None:
+    """Test for get_liquid_handling_z_change math."""
+    decoy.when(mock_labware_view.get_well_definition("labware-id", "A1")).then_return(
+        RectangularWellDefinition3.model_construct(totalLiquidVolume=1100000)  # type: ignore[call-arg]
+    )
+    decoy.when(mock_labware_view.get_well_geometry("labware-id", "A1")).then_return(
+        _TEST_INNER_WELL_GEOMETRY
+    )
+    probe_time = datetime.now()
+    decoy.when(mock_well_view.get_last_liquid_update("labware-id", "A1")).then_return(
+        probe_time
+    )
+    decoy.when(mock_well_view.get_well_liquid_info("labware-id", "A1")).then_return(
+        WellLiquidInfo(
+            loaded_volume=None,
+            probed_height=ProbedHeightInfo(height=40.0, last_probed=probe_time),
+            probed_volume=None,
+        )
+    )
+    # make sure that liquid handling z change math stays the same
+    change = subject.get_liquid_handling_z_change(
+        labware_id="labware-id", well_name="A1", operation_volume=199.0
+    )
+    expected_change = 3.2968
+    assert isclose(change, expected_change)
diff --git a/api/tests/opentrons/test_types.py b/api/tests/opentrons/test_types.py
index 77249fa04923..ab3ab6df0c9b 100644
--- a/api/tests/opentrons/test_types.py
+++ b/api/tests/opentrons/test_types.py
@@ -29,7 +29,7 @@ def test_location_repr_labware(min_lw: Labware) -> None:
     loc = Location(point=Point(x=1.1, y=2.1, z=3.5), labware=min_lw)
     assert (
         f"{loc}"
-        == "Location(point=Point(x=1.1, y=2.1, z=3.5), labware=minimal labware on deck, is_meniscus=False)"
+        == "Location(point=Point(x=1.1, y=2.1, z=3.5), labware=minimal labware on deck, meniscus_tracking=None)"
     )
 
 
@@ -38,7 +38,7 @@ def test_location_repr_well(min_lw: Labware) -> None:
     loc = Location(point=Point(x=1, y=2, z=3), labware=min_lw.wells()[0])
     assert (
         f"{loc}"
-        == "Location(point=Point(x=1, y=2, z=3), labware=A1 of minimal labware on deck, is_meniscus=False)"
+        == "Location(point=Point(x=1, y=2, z=3), labware=A1 of minimal labware on deck, meniscus_tracking=None)"
     )
 
 
@@ -47,7 +47,7 @@ def test_location_repr_slot() -> None:
     loc = Location(point=Point(x=-1, y=2, z=3), labware="1")
     assert (
         f"{loc}"
-        == "Location(point=Point(x=-1, y=2, z=3), labware=1, is_meniscus=False)"
+        == "Location(point=Point(x=-1, y=2, z=3), labware=1, meniscus_tracking=None)"
     )
 
 


Reviews:
user: login: ryanthecoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T15:03:30Z
user: login: ryanthecoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T15:04:26Z
user: login: ryanthecoder
body: LGTM

state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T18:26:46Z
user: login: sfoster1
body: Excellent!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-14T17:02:25Z

Review comments:
diff_hunk: @@ -304,31 +323,66 @@ def dispense(
             well_name = well_core.get_name()
             labware_id = well_core.labware_id
 
-            well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
+            (
+                well_location,
+                dynamic_liquid_tracking,
+            ) = self._engine_client.state.geometry.get_relative_liquid_handling_well_location(
                 labware_id=labware_id,
                 well_name=well_name,
                 absolute_point=location.point,
-                is_meniscus=is_meniscus,
+                meniscus_tracking=meniscus_tracking,
             )
+            # raise ValueError(f"well location = {well_location}")
path: api/src/opentrons/protocol_api/core/engine/instrument.py
user: login: ryanthecoder
body: leftover comment
created_at: 2025-03-11T15:03:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2532,23 +2520,49 @@ def _raise_if_pressure_not_supported_by_pipette(self) -> None:
 
     def _handle_aspirate_target(
         self, target: validation.ValidTarget
-    ) -> tuple[types.Location, Optional[labware.Well], Optional[bool]]:
+    ) -> tuple[
+        types.Location, Optional[labware.Well], Optional[types.MeniscusTrackingTarget]
+    ]:
         move_to_location: types.Location
         well: Optional[labware.Well] = None
-        is_meniscus: Optional[bool] = None
+        meniscus_tracking: Optional[types.MeniscusTrackingTarget] = None
         if isinstance(target, validation.WellTarget):
             well = target.well
             if target.location:
                 move_to_location = target.location
-                is_meniscus = target.location.is_meniscus
+                # raise ValueError(f"location = {target.location}")
path: api/src/opentrons/protocol_api/instrument_context.py
user: login: ryanthecoder
body: leftover comment

created_at: 2025-03-11T15:04:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-12T21:03:57Z
body: ## [Codecov](https://app.codecov.io/gh/Opentrons/opentrons/pull/17714?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 22.76%. Comparing base [(`fa1bba9`)](https://app.codecov.io/gh/Opentrons/opentrons/commit/fa1bba970728f2f121f7dbfcc75c3dd82c6e19c8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) to head [(`7111b19`)](https://app.codecov.io/gh/Opentrons/opentrons/commit/7111b195747f568e0252eb1f88b69394284eba23?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons).
> Report is 1 commits behind head on edge.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/Opentrons/opentrons/pull/17714/graphs/tree.svg?width=650&height=150&src=pr&token=DIJPWI1JiU&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons)](https://app.codecov.io/gh/Opentrons/opentrons/pull/17714?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons)

```diff
@@             Coverage Diff             @@
##             edge   #17714       +/-   ##
===========================================
- Coverage   61.93%   22.76%   -39.18%     
===========================================
  Files        2824     2824               
  Lines      217106   216920      -186     
  Branches    18437    18362       -75     
===========================================
- Hits       134458    49371    -85087     
- Misses      82463   167543    +85080     
+ Partials      185        6      -179     
```

| [Flag](https://app.codecov.io/gh/Opentrons/opentrons/pull/17714/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) | Coverage Δ | |
|---|---|---|
| [app](https://app.codecov.io/gh/Opentrons/opentrons/pull/17714/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) | `2.34% <ø> (-44.06%)` | :arrow_down: |
| [protocol-designer](https://app.codecov.io/gh/Opentrons/opentrons/pull/17714/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons) | `18.84% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons#carryforward-flags-in-the-pull-request-comment) to find out more.

[see 1601 files with indirect coverage changes](https://app.codecov.io/gh/Opentrons/opentrons/pull/17714/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Opentrons)

</details>
<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-13T03:21:44Z
body: A PR has been opened to address analyses snapshot changes. Please review the changes here: https://github.com/Opentrons/opentrons/pull/17748
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-13T18:38:40Z
body: A PR has been opened to address analyses snapshot changes. Please review the changes here: https://github.com/Opentrons/opentrons/pull/17753
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-13T20:51:18Z
body: A PR has been opened to address analyses snapshot changes. Please review the changes here: https://github.com/Opentrons/opentrons/pull/17757
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-13T22:03:50Z
body: A PR has been opened to address analyses snapshot changes. Please review the changes here: https://github.com/Opentrons/opentrons/pull/17757
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-14T16:12:11Z
body: A PR has been opened to address analyses snapshot changes. Please review the changes here: https://github.com/Opentrons/opentrons/pull/17768
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-14T16:57:25Z
body: A PR has been opened to address analyses snapshot changes. Please review the changes here: https://github.com/Opentrons/opentrons/pull/17770
author_association: CONTRIBUTOR