Skip to content

Commit

Permalink
feat(api): add new pick up tip function (#15275)
Browse files Browse the repository at this point in the history
  • Loading branch information
caila-marashaj authored May 29, 2024
1 parent 538b774 commit 9def841
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 53 deletions.
44 changes: 26 additions & 18 deletions api/src/opentrons/hardware_control/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
RobotCalibration,
)
from .protocols import HardwareControlInterface
from .instruments.ot2.pipette_handler import PipetteHandlerProvider
from .instruments.ot2.pipette_handler import PipetteHandlerProvider, PickUpTipSpec
from .instruments.ot2.instrument_calibration import load_pipette_offset
from .motion_utilities import (
target_position_from_absolute,
Expand Down Expand Up @@ -1152,6 +1152,30 @@ async def update_nozzle_configuration_for_mount(
mount, back_left_nozzle, front_right_nozzle, starting_nozzle
)

async def tip_pickup_moves(
self,
mount: top_types.Mount,
spec: PickUpTipSpec,
) -> None:
for press in spec.presses:
with self._backend.save_current():
self._backend.set_active_current(press.current)
target_down = target_position_from_relative(
mount, press.relative_down, self._current_position
)
await self._move(target_down, speed=press.speed)
target_up = target_position_from_relative(
mount, press.relative_up, self._current_position
)
await self._move(target_up)
# neighboring tips tend to get stuck in the space between
# the volume chamber and the drop-tip sleeve on p1000.
# This extra shake ensures those tips are removed
for rel_point, speed in spec.shake_off_list:
await self.move_rel(mount, rel_point, speed=speed)

await self.retract(mount, spec.retract_target)

async def pick_up_tip(
self,
mount: top_types.Mount,
Expand All @@ -1176,25 +1200,9 @@ async def pick_up_tip(
home_flagged_axes=False,
)

for press in spec.presses:
with self._backend.save_current():
self._backend.set_active_current(press.current)
target_down = target_position_from_relative(
mount, press.relative_down, self._current_position
)
await self._move(target_down, speed=press.speed)
target_up = target_position_from_relative(
mount, press.relative_up, self._current_position
)
await self._move(target_up)
await self.tip_pickup_moves(mount, spec)
_add_tip_to_instrs()
# neighboring tips tend to get stuck in the space between
# the volume chamber and the drop-tip sleeve on p1000.
# This extra shake ensures those tips are removed
for rel_point, speed in spec.shake_off_list:
await self.move_rel(mount, rel_point, speed=speed)

await self.retract(mount, spec.retract_target)
if prep_after:
await self.prepare_for_aspirate(mount)

Expand Down
85 changes: 50 additions & 35 deletions api/src/opentrons/hardware_control/ot3api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1796,6 +1796,55 @@ async def hold_jaw_width(self, jaw_width_mm: int) -> None:
self._gripper_handler.check_ready_for_jaw_move("hold_jaw_width")
await self._hold_jaw_width(jaw_width_mm)

async def tip_pickup_moves(
self,
mount: OT3Mount,
presses: Optional[int] = None,
increment: Optional[float] = None,
) -> None:
"""This is a slightly more barebones variation of pick_up_tip. This is only the motor routine
directly involved in tip pickup, and leaves any state updates and plunger moves to the caller."""
realmount = OT3Mount.from_mount(mount)
instrument = self._pipette_handler.get_pipette(realmount)

if (
self.gantry_load == GantryLoad.HIGH_THROUGHPUT
and instrument.nozzle_manager.current_configuration.configuration
== NozzleConfigurationType.FULL
):
spec = self._pipette_handler.plan_ht_pick_up_tip(
instrument.nozzle_manager.current_configuration.tip_count
)
if spec.z_distance_to_tiprack:
await self.move_rel(
realmount, top_types.Point(z=spec.z_distance_to_tiprack)
)
await self._tip_motor_action(realmount, spec.tip_action_moves)
else:
spec = self._pipette_handler.plan_lt_pick_up_tip(
realmount,
instrument.nozzle_manager.current_configuration.tip_count,
presses,
increment,
)
await self._force_pick_up_tip(realmount, spec)

# neighboring tips tend to get stuck in the space between
# the volume chamber and the drop-tip sleeve on p1000.
# This extra shake ensures those tips are removed
for rel_point, speed in spec.shake_off_moves:
await self.move_rel(realmount, rel_point, speed=speed)

if isinstance(self._backend, OT3Simulator):
self._backend._update_tip_state(realmount, True)

# fixme: really only need this during labware position check so user
# can verify if a tip is properly attached
if spec.ending_z_retract_distance:
await self.move_rel(
realmount, top_types.Point(z=spec.ending_z_retract_distance)
)

async def _move_to_plunger_bottom(
self,
mount: OT3Mount,
Expand Down Expand Up @@ -2156,44 +2205,10 @@ async def pick_up_tip(
def add_tip_to_instr() -> None:
instrument.add_tip(tip_length=tip_length)
instrument.set_current_volume(0)
if isinstance(self._backend, OT3Simulator):
self._backend._update_tip_state(realmount, True)

await self._move_to_plunger_bottom(realmount, rate=1.0)
if (
self.gantry_load == GantryLoad.HIGH_THROUGHPUT
and instrument.nozzle_manager.current_configuration.configuration
== NozzleConfigurationType.FULL
):
spec = self._pipette_handler.plan_ht_pick_up_tip(
instrument.nozzle_manager.current_configuration.tip_count
)
if spec.z_distance_to_tiprack:
await self.move_rel(
realmount, top_types.Point(z=spec.z_distance_to_tiprack)
)
await self._tip_motor_action(realmount, spec.tip_action_moves)
else:
spec = self._pipette_handler.plan_lt_pick_up_tip(
realmount,
instrument.nozzle_manager.current_configuration.tip_count,
presses,
increment,
)
await self._force_pick_up_tip(realmount, spec)

# neighboring tips tend to get stuck in the space between
# the volume chamber and the drop-tip sleeve on p1000.
# This extra shake ensures those tips are removed
for rel_point, speed in spec.shake_off_moves:
await self.move_rel(realmount, rel_point, speed=speed)

# fixme: really only need this during labware position check so user
# can verify if a tip is properly attached
if spec.ending_z_retract_distance:
await self.move_rel(
realmount, top_types.Point(z=spec.ending_z_retract_distance)
)
await self.tip_pickup_moves(realmount, presses, increment)

add_tip_to_instr()

Expand Down
23 changes: 23 additions & 0 deletions api/tests/opentrons/hardware_control/test_instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,29 @@ async def test_no_pipette(sim_and_instr):
assert not hw_api._current_volume[types.Mount.RIGHT]


async def test_tip_pickup_moves(sim_and_instr):
"""Make sure that tip_pickup_moves does not add a tip to the instrument."""
sim_builder, (dummy_instruments, _) = sim_and_instr
hw_api = await sim_builder(
attached_instruments=dummy_instruments, loop=asyncio.get_running_loop()
)
mount = types.Mount.LEFT
await hw_api.home()
await hw_api.cache_instruments()

config = hw_api.get_config()

if config.model == "OT-2 Standard":
spec, _ = hw_api.plan_check_pick_up_tip(
mount=mount, tip_length=40.0, presses=None, increment=None
)
await hw_api.tip_pickup_moves(mount, spec)
else:
await hw_api.tip_pickup_moves(mount)

assert not hw_api.hardware_instruments[mount].has_tip


async def test_pick_up_tip(is_robot, sim_and_instr):
sim_builder, (dummy_instruments, _) = sim_and_instr
hw_api = await sim_builder(
Expand Down
31 changes: 31 additions & 0 deletions api/tests/opentrons/hardware_control/test_moves.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,37 @@ async def test_critical_point_applied(hardware_api):
assert await hardware_api.current_position(types.Mount.RIGHT) == target


async def test_tip_pickup_routine(hardware_api, monkeypatch):

_move = mock.Mock(side_effect=hardware_api._move)
monkeypatch.setattr(hardware_api, "_move", _move)

await hardware_api.home()
hardware_api._backend._attached_instruments = {
types.Mount.LEFT: {"model": None, "id": None},
types.Mount.RIGHT: {"model": "p10_single_v1", "id": "testyness"},
}
await hardware_api.cache_instruments()
mount = types.Mount.RIGHT

spec, _ = hardware_api.plan_check_pick_up_tip(
mount=mount, tip_length=40.0, presses=None, increment=None
)
await hardware_api.tip_pickup_moves(mount, spec)

tip_motor_routine_num_moves = 2 * len(spec.presses)

# the tip motor routine should only make the immediate 'press' moves happen
assert len(_move.call_args_list) == tip_motor_routine_num_moves
_move.reset_mock()

# pick_up_tip should have the press moves + a plunger move both before and after
await hardware_api.pick_up_tip(
mount=mount, tip_length=40.0, presses=None, increment=None, prep_after=True
)
assert len(_move.call_args_list) == tip_motor_routine_num_moves + 2


async def test_new_critical_point_applied(hardware_api):
await hardware_api.home()
hardware_api._backend._attached_instruments = {
Expand Down
19 changes: 19 additions & 0 deletions api/tests/opentrons/hardware_control/test_ot3_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ async def test_pickup_moves(
pipette_handler.get_pipette(
OT3Mount.LEFT
).nozzle_manager.current_configuration.configuration = NozzleConfigurationType.FULL
pipette_handler.get_pipette(OT3Mount.LEFT).current_volume = 0
z_tiprack_distance = 8.0
end_z_retract_dist = 9.0
move_plan_return_val = TipActionSpec(
Expand Down Expand Up @@ -643,6 +644,24 @@ async def test_pickup_moves(
]
else:
assert move_call_list == [(OT3Mount.LEFT, Point(z=end_z_retract_dist))]
# pick up tip should have two calls to move_to_plunger_bottom, one before and one after
# the tip pickup
assert len(mock_move_to_plunger_bottom.call_args_list) == 2
mock_move_to_plunger_bottom.reset_mock()
mock_move_rel.reset_mock()

# make sure that tip_pickup_moves has the same set of moves,
# except no calls to move_to_plunger_bottom
await ot3_hardware.tip_pickup_moves(Mount.LEFT, 40.0)
move_call_list = [call.args for call in mock_move_rel.call_args_list]
if gantry_load == GantryLoad.HIGH_THROUGHPUT:
assert move_call_list == [
(OT3Mount.LEFT, Point(z=z_tiprack_distance)),
(OT3Mount.LEFT, Point(z=end_z_retract_dist)),
]
else:
assert move_call_list == [(OT3Mount.LEFT, Point(z=end_z_retract_dist))]
assert len(mock_move_to_plunger_bottom.call_args_list) == 0


@pytest.mark.parametrize("load_configs", load_pipette_configs)
Expand Down

0 comments on commit 9def841

Please sign in to comment.