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 new pick up tip function #15275

Merged
merged 4 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
)
Copy link
Member

Choose a reason for hiding this comment

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

we need to do the simulator state update thing that's on line 2159 too, this:

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why do we want this? My impression was that we want to leave any kind of state updates to the caller

Copy link
Member

Choose a reason for hiding this comment

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

This specifically is a hack to get the simulated tip presence sensors returning the right thing - that's why it's checking the backend. You're right in general, but this is an exception


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 @@ -609,6 +609,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 @@ -644,6 +645,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
Loading