Skip to content

Commit 638ff4c

Browse files
authored
feat(api): Speed up instrument calibration (#18469)
<!-- Thanks for taking the time to open a Pull Request (PR)! Please make sure you've read the "Opening Pull Requests" section of our Contributing Guide: https://github.com/Opentrons/opentrons/blob/edge/CONTRIBUTING.md#opening-pull-requests GitHub provides robust markdown to format your PR. Links, diagrams, pictures, and videos along with text formatting make it possible to create a rich and informative PR. For more information on GitHub markdown, see: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax To ensure your code is reviewed quickly and thoroughly, please fill out the sections below to the best of your ability! --> # Overview So previously we were limiting the gantry speed for the calibration process. This was a remnant of when we were considering doing a x/y capacitive sweep instead of a z probe. This however does not give us anything and just slows down the process. Additionally we probe all 4 edges to account for any error, however we also probe quite a large bounds on each edge. To preserve the ability to overcome the same level of positional error I made it so that the first x and the first y edges still use the older, larger sweeping region with the same number of strides, however once we have a good idea of where the first ones are we don't have to sweep such a large area for the second two, so I reduced the starting stride distance and the number of probes required for this. All of this together the time to calibrate the pipette changed from ~2:10 from hitting go until the time it tells you to take off the probe to 1:45. The gripper also has benefits going from about ~4:10 to ~3:25 <!-- Describe your PR at a high level. State acceptance criteria and how this PR fits into other work. Link issues, PRs, and other relevant resources. --> ## Test Plan and Hands on Testing <!-- Describe your testing of the PR. Emphasize testing not reflected in the code. Attach protocols, logs, screenshots and any other assets that support your testing. --> ## Changelog <!-- List changes introduced by this PR considering future developers and the end user. Give careful thought and clear documentation to breaking changes. --> ## Review requests <!-- - What do you need from reviewers to feel confident this PR is ready to merge? - Ask questions. --> ## Risk assessment <!-- - Indicate the level of attention this PR needs. - Provide context to guide reviewers. - Discuss trade-offs, coupling, and side effects. - Look for the possibility, even if you think it's small, that your change may affect some other part of the system. - For instance, changing return tip behavior may also change the behavior of labware calibration. - How do your unit tests and on hands on testing mitigate this PR's risks and the risk of future regressions? - Especially in high risk PRs, explain how you know your testing is enough. -->
1 parent 8c92593 commit 638ff4c

File tree

9 files changed

+76
-91
lines changed

9 files changed

+76
-91
lines changed

api/src/opentrons/config/defaults_ot3.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@
7474
DEFAULT_RIGHT_MOUNT_OFFSET: Final[Offset] = (40.5, -60.5, 255.675)
7575
DEFAULT_GRIPPER_MOUNT_OFFSET: Final[Offset] = (84.55, -12.75, 93.85)
7676
DEFAULT_SAFE_HOME_DISTANCE: Final = 5
77-
DEFAULT_CALIBRATION_AXIS_MAX_SPEED: Final = 30
7877
DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED: Final = 90
7978

8079
DEFAULT_MAX_SPEEDS: Final[ByGantryLoad[Dict[OT3AxisKind, float]]] = ByGantryLoad(

api/src/opentrons/hardware_control/backends/flex_protocol.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,6 @@ def get_pressure_sensor_available(self, pipette_axis: Axis) -> bool:
7373
def update_constraints_for_gantry_load(self, gantry_load: GantryLoad) -> None:
7474
...
7575

76-
def update_constraints_for_calibration_with_gantry_load(
77-
self,
78-
gantry_load: GantryLoad,
79-
) -> None:
80-
...
81-
8276
def update_constraints_for_plunger_acceleration(
8377
self,
8478
mount: OT3Mount,

api/src/opentrons/hardware_control/backends/ot3controller.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
moving_pipettes_in_move_group,
4949
gripper_jaw_state_from_fw,
5050
get_system_constraints,
51-
get_system_constraints_for_calibration,
5251
get_system_constraints_for_plunger_acceleration,
5352
)
5453
from .tip_presence_manager import TipPresenceManager
@@ -409,19 +408,6 @@ def get_pressure_sensor_available(self, pipette_axis: Axis) -> bool:
409408
pip_node = axis_to_node(pipette_axis)
410409
return self._pressure_sensor_available[pip_node]
411410

412-
def update_constraints_for_calibration_with_gantry_load(
413-
self,
414-
gantry_load: GantryLoad,
415-
) -> None:
416-
self._move_manager.update_constraints(
417-
get_system_constraints_for_calibration(
418-
self._configuration.motion_settings, gantry_load
419-
)
420-
)
421-
log.debug(
422-
f"Set system constraints for calibration: {self._move_manager.get_constraints()}"
423-
)
424-
425411
def update_constraints_for_gantry_load(self, gantry_load: GantryLoad) -> None:
426412
self._move_manager.update_constraints(
427413
get_system_constraints(self._configuration.motion_settings, gantry_load)

api/src/opentrons/hardware_control/backends/ot3simulator.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,6 @@ async def restore_system_constraints(self) -> AsyncIterator[None]:
230230
def update_constraints_for_gantry_load(self, gantry_load: GantryLoad) -> None:
231231
self._sim_gantry_load = gantry_load
232232

233-
def update_constraints_for_calibration_with_gantry_load(
234-
self,
235-
gantry_load: GantryLoad,
236-
) -> None:
237-
self._sim_gantry_load = gantry_load
238-
239233
def update_constraints_for_plunger_acceleration(
240234
self,
241235
mount: OT3Mount,

api/src/opentrons/hardware_control/backends/ot3utils.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from typing_extensions import Literal
44
from logging import getLogger
55
from opentrons.config.defaults_ot3 import (
6-
DEFAULT_CALIBRATION_AXIS_MAX_SPEED,
76
DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED,
87
)
98
from opentrons.config.types import OT3MotionSettings, OT3CurrentSettings, GantryLoad
@@ -256,29 +255,6 @@ def get_system_constraints(
256255
return constraints
257256

258257

259-
def get_system_constraints_for_calibration(
260-
config: OT3MotionSettings,
261-
gantry_load: GantryLoad,
262-
) -> "SystemConstraints[Axis]":
263-
conf_by_pip = config.by_gantry_load(gantry_load)
264-
constraints = {}
265-
for axis_kind in [
266-
OT3AxisKind.P,
267-
OT3AxisKind.X,
268-
OT3AxisKind.Y,
269-
OT3AxisKind.Z,
270-
OT3AxisKind.Z_G,
271-
]:
272-
for axis in Axis.of_kind(axis_kind):
273-
constraints[axis] = AxisConstraints.build(
274-
conf_by_pip["acceleration"][axis_kind],
275-
conf_by_pip["max_speed_discontinuity"][axis_kind],
276-
conf_by_pip["direction_change_speed_discontinuity"][axis_kind],
277-
DEFAULT_CALIBRATION_AXIS_MAX_SPEED,
278-
)
279-
return constraints
280-
281-
282258
def get_system_constraints_for_plunger_acceleration(
283259
config: OT3MotionSettings,
284260
gantry_load: GantryLoad,

api/src/opentrons/hardware_control/ot3_calibration.py

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Functions and utilites for OT3 calibration."""
22
from __future__ import annotations
33
from functools import lru_cache
4-
from dataclasses import dataclass
4+
from dataclasses import dataclass, replace
55
from typing_extensions import Final, Literal, TYPE_CHECKING
66
from typing import Tuple, List, Dict, Any, Optional, Union
77
import datetime
@@ -181,6 +181,7 @@ async def find_edge_binary(
181181
mount: OT3Mount,
182182
slot_edge_nominal: Point,
183183
search_axis: Union[Literal[Axis.X, Axis.Y]],
184+
edge_settings: EdgeSenseSettings,
184185
direction_if_hit: Literal[1, -1],
185186
raise_verify_error: bool = True,
186187
probe: InstrumentProbeType = InstrumentProbeType.PRIMARY,
@@ -209,7 +210,6 @@ async def find_edge_binary(
209210
The absolute position at which the center of the effector is inside the slot
210211
and its edge is aligned with the calibration slot edge.
211212
"""
212-
edge_settings = hcapi.config.calibration.edge_sense
213213
# Our first search position is at the slot edge nominal at the probe's edge
214214
checking_pos = slot_edge_nominal + critical_edge_offset(
215215
search_axis, direction_if_hit
@@ -281,12 +281,14 @@ async def find_slot_center_binary(
281281
"""Find the center of the calibration slot by binary-searching its edges.
282282
Returns the XY-center of the slot.
283283
"""
284+
edge_settings = hcapi.config.calibration.edge_sense
284285
# Find all four edges of the calibration slot
285286
plus_x_edge = await find_edge_binary(
286287
hcapi,
287288
mount,
288289
estimated_center + EDGES["right"],
289290
Axis.X,
291+
edge_settings,
290292
-1,
291293
raise_verify_error,
292294
probe=probe,
@@ -299,18 +301,26 @@ async def find_slot_center_binary(
299301
mount,
300302
estimated_center + EDGES["top"],
301303
Axis.Y,
304+
edge_settings,
302305
-1,
303306
raise_verify_error,
304307
probe=probe,
305308
)
306309
LOG.info(f"Found +y edge at {plus_y_edge.y}mm")
307310
estimated_center = estimated_center._replace(y=plus_y_edge.y - EDGES["top"].y)
308311

312+
# since we have a good idea where the edges are now we can reduce the second set of sweeps
313+
fast_edge_settings = replace(
314+
edge_settings,
315+
search_initial_tolerance_mm=edge_settings.search_initial_tolerance_mm / 4,
316+
search_iteration_limit=edge_settings.search_iteration_limit - 2,
317+
)
309318
minus_x_edge = await find_edge_binary(
310319
hcapi,
311320
mount,
312321
estimated_center + EDGES["left"],
313322
Axis.X,
323+
fast_edge_settings,
314324
1,
315325
raise_verify_error,
316326
probe=probe,
@@ -323,6 +333,7 @@ async def find_slot_center_binary(
323333
mount,
324334
estimated_center + EDGES["bottom"],
325335
Axis.Y,
336+
fast_edge_settings,
326337
1,
327338
raise_verify_error,
328339
probe=probe,
@@ -369,7 +380,6 @@ async def _probe_deck_at(
369380
mount: OT3Mount,
370381
target: Point,
371382
settings: CapacitivePassSettings,
372-
speed: float = 50,
373383
probe: InstrumentProbeType = InstrumentProbeType.PRIMARY,
374384
) -> Tuple[float, bool]:
375385
here = await api.gantry_position(mount)
@@ -378,7 +388,7 @@ async def _probe_deck_at(
378388
)
379389
safe_height = max(here.z, target.z, abs_transit_height)
380390
await api.move_to(mount, here._replace(z=safe_height))
381-
await api.move_to(mount, target._replace(z=safe_height), speed=speed)
391+
await api.move_to(mount, target._replace(z=safe_height))
382392
await api.move_to(mount, target._replace(z=abs_transit_height))
383393
_found_pos, contact = await api.capacitive_probe(
384394
mount, Axis.by_mount(mount), target.z, settings, probe=probe
@@ -610,34 +620,32 @@ async def _calibrate_mount(
610620
pip = hcapi.hardware_instruments[mount.to_mount()]
611621
num_channels = int(pip.channels) # type: ignore[union-attr]
612622
nominal_center += OFFSET_SECONDARY_PROBE.get(num_channels, Point())
613-
async with hcapi.restore_system_constrants():
614-
await hcapi.set_system_constraints_for_calibration()
615-
try:
616-
# find the center of the calibration sqaure
617-
offset = await find_calibration_structure_position(
618-
hcapi,
619-
mount,
620-
nominal_center,
621-
method=method,
622-
raise_verify_error=raise_verify_error,
623-
probe=probe,
624-
)
625-
# update center with values obtained during calibration
626-
LOG.info(f"Found calibration value {offset} for mount {mount.name}")
627-
return offset
628-
629-
except (
630-
InaccurateNonContactSweepError,
631-
EarlyCapacitiveSenseTrigger,
632-
CalibrationStructureNotFoundError,
633-
EdgeNotFoundError,
634-
):
635-
LOG.info(
636-
"Error occurred during calibration. Resetting to current saved calibration value."
637-
)
638-
await hcapi.reset_instrument_offset(mount, to_default=False)
639-
# re-raise exception after resetting instrument offset
640-
raise
623+
try:
624+
# find the center of the calibration sqaure
625+
offset = await find_calibration_structure_position(
626+
hcapi,
627+
mount,
628+
nominal_center,
629+
method=method,
630+
raise_verify_error=raise_verify_error,
631+
probe=probe,
632+
)
633+
# update center with values obtained during calibration
634+
LOG.info(f"Found calibration value {offset} for mount {mount.name}")
635+
return offset
636+
637+
except (
638+
InaccurateNonContactSweepError,
639+
EarlyCapacitiveSenseTrigger,
640+
CalibrationStructureNotFoundError,
641+
EdgeNotFoundError,
642+
):
643+
LOG.info(
644+
"Error occurred during calibration. Resetting to current saved calibration value."
645+
)
646+
await hcapi.reset_instrument_offset(mount, to_default=False)
647+
# re-raise exception after resetting instrument offset
648+
raise
641649

642650

643651
async def find_calibration_structure_position(

api/src/opentrons/hardware_control/ot3api.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -303,11 +303,6 @@ async def set_gantry_load(self, gantry_load: GantryLoad) -> None:
303303
async def get_serial_number(self) -> Optional[str]:
304304
return await self._backend.get_serial_number()
305305

306-
async def set_system_constraints_for_calibration(self) -> None:
307-
self._backend.update_constraints_for_calibration_with_gantry_load(
308-
self._gantry_load
309-
)
310-
311306
async def set_system_constraints_for_plunger_acceleration(
312307
self, mount: OT3Mount, acceleration: float
313308
) -> None:

api/src/opentrons/hardware_control/protocols/flex_calibratable.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,6 @@ async def capacitive_sweep(
7474
async def restore_system_constrants(self) -> AsyncIterator[None]:
7575
yield
7676

77-
async def set_system_constraints_for_calibration(self) -> None:
78-
...
79-
8077
async def reset_instrument_offset(
8178
self, mount: Union[top_types.Mount, OT3Mount], to_default: bool = True
8279
) -> None:

api/tests/opentrons/hardware_control/test_ot3_calibration.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ def mock_move_to(ot3_hardware: ThreadManager[OT3API]) -> Iterator[AsyncMock]:
5555
yield mock_move
5656

5757

58+
@pytest.fixture
59+
def mock_find_edge_binary() -> Iterator[AsyncMock]:
60+
with patch(
61+
"opentrons.hardware_control.ot3_calibration.find_edge_binary",
62+
AsyncMock(
63+
spec=find_edge_binary,
64+
),
65+
) as mock_find_edge_binary:
66+
yield mock_find_edge_binary
67+
68+
5869
@pytest.fixture
5970
def mock_capacitive_probe(ot3_hardware: ThreadManager[OT3API]) -> Iterator[AsyncMock]:
6071
managed = ot3_hardware.managed_obj
@@ -183,11 +194,13 @@ async def test_find_edge(
183194
) -> None:
184195
await ot3_hardware.home()
185196
mock_capacitive_probe.side_effect = probe_results
197+
edge_settings = ot3_hardware.config.calibration.edge_sense
186198
result = await find_edge_binary(
187199
ot3_hardware,
188200
OT3Mount.RIGHT,
189201
Point(0, 0, 0),
190202
Axis.X,
203+
edge_settings,
191204
direction_if_hit,
192205
False,
193206
)
@@ -202,6 +215,27 @@ async def test_find_edge(
202215
)
203216

204217

218+
async def test_find_slot_center(
219+
ot3_hardware: ThreadManager[OT3API], mock_find_edge_binary: AsyncMock
220+
) -> None:
221+
await ot3_hardware.home()
222+
mock_find_edge_binary.return_value = Point(0, 0, 0)
223+
await find_slot_center_binary(
224+
ot3_hardware,
225+
OT3Mount.RIGHT,
226+
Point(0, 0, 0),
227+
False,
228+
)
229+
# we want to make sure that the final edge probe is at the same distance as the settings change
230+
final_pass_distance = 0.046875
231+
for call in mock_find_edge_binary.mock_calls:
232+
# call[1] is positional args and [4] is the EdgeSenseSettings arg
233+
settings = call[1][4]
234+
start_tolerance = settings.search_initial_tolerance_mm
235+
search_limit = settings.search_iteration_limit
236+
assert start_tolerance / (2**search_limit) == final_pass_distance
237+
238+
205239
@pytest.mark.parametrize(
206240
"search_axis,direction_if_hit,probe_results",
207241
[
@@ -238,12 +272,14 @@ async def test_find_edge_early_trigger(
238272
) -> None:
239273
await ot3_hardware.home()
240274
mock_capacitive_probe.side_effect = ((3, True), ())
275+
edge_settings = ot3_hardware.config.calibration.edge_sense
241276
with pytest.raises(EarlyCapacitiveSenseTrigger):
242277
await find_edge_binary(
243278
ot3_hardware,
244279
OT3Mount.RIGHT,
245280
Point(0.0, 0.0, 0.0),
246281
Axis.Y,
282+
edge_settings,
247283
-1,
248284
)
249285

0 commit comments

Comments
 (0)