Skip to content

Commit f982d56

Browse files
BioCamclaude
andcommitted
STAR: iSWAP.request_is_parked, and two channels a pitch apart are not too close
The name answers a yes/no question, so it reads as one: `request_is_parked`, callers and tests renamed with it. Spots exactly one pitch apart were refused as too close for a tip command: the Y difference of two float positions is off by float error (130.7 - 121.7 is 8.999999999999986), while the command carries tenths. The gap is rounded to 0.1 mm before it is compared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent be42e00 commit f982d56

4 files changed

Lines changed: 27 additions & 19 deletions

File tree

‎pylabrobot/hamilton/star/driver/features/iswap.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3049,7 +3049,7 @@ async def request_pose(self) -> iSWAPPose:
30493049

30503050
# -- parking ---------------------------------------------------------------
30513051

3052-
async def request_parked(self, tolerance_increments: int = 2) -> bool:
3052+
async def request_is_parked(self, tolerance_increments: int = 2) -> bool:
30533053
"""Whether the arm is parked, judged from where its drives are.
30543054
30553055
Each drive is checked against the parking stop in its stored table, which setup reads:

‎pylabrobot/hamilton/star/driver/features/iswap_tests.py‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async def test_the_arm_is_parked_when_every_drive_is_at_its_parking_position(sel
8989
}
9090
),
9191
):
92-
self.assertTrue(await iswap.request_parked())
92+
self.assertTrue(await iswap.request_is_parked())
9393

9494
async def test_the_arm_is_not_parked_when_a_drive_is_moved(self):
9595
iswap, _ = await gripper()
@@ -100,7 +100,7 @@ async def test_the_arm_is_not_parked_when_a_drive_is_moved(self):
100100
c.elbow_predefined_y_positions_increments.parking - 10
101101
)
102102
with patch.object(iswap, "request_joint_state", new=AsyncMock(return_value=joints)):
103-
self.assertFalse(await iswap.request_parked())
103+
self.assertFalse(await iswap.request_is_parked())
104104

105105
async def test_z_above_the_parking_position_is_still_parked(self):
106106
iswap, _ = await gripper()
@@ -112,7 +112,7 @@ async def test_z_above_the_parking_position_is_still_parked(self):
112112
+ c.elbow_z_offset_above_finger
113113
)
114114
with patch.object(iswap, "request_joint_state", new=AsyncMock(return_value=joints)):
115-
self.assertTrue(await iswap.request_parked())
115+
self.assertTrue(await iswap.request_is_parked())
116116

117117

118118
class TestJawMoves(unittest.IsolatedAsyncioTestCase):
@@ -385,7 +385,7 @@ async def test_it_never_asks_the_master_for_its_parked_flag(self):
385385
"""`C0 RG` is answered wrongly by the firmware, so nothing may read it."""
386386
iswap, sent = await gripper()
387387

388-
await iswap.request_parked()
388+
await iswap.request_is_parked()
389389

390390
self.assertEqual([command for command in sent if command.startswith("C0RG")], [])
391391

@@ -394,13 +394,13 @@ async def test_a_parked_arm_is_parked_and_a_moved_one_is_not(self):
394394
c = iswap.configuration
395395

396396
await iswap.park()
397-
self.assertTrue(await iswap.request_parked())
397+
self.assertTrue(await iswap.request_is_parked())
398398

399399
assert c.elbow_predefined_y_positions_increments is not None
400400
await iswap.elbow_move_to_y_position(
401401
c.y_increments_to_mm(c.elbow_predefined_y_positions_increments.parking) - 50.0
402402
)
403-
self.assertFalse(await iswap.request_parked())
403+
self.assertFalse(await iswap.request_is_parked())
404404

405405
async def test_the_jaws_are_part_of_it(self):
406406
"""Parking closes them, and the gripper's table names that stop its home."""
@@ -412,7 +412,7 @@ async def test_the_jaws_are_part_of_it(self):
412412
home = c.gripper_increments_to_mm(c.gripper_drive_predefined_increments.home)
413413
await iswap.gripper_move_to_jaw_position(home + 5.0)
414414

415-
self.assertFalse(await iswap.request_parked())
415+
self.assertFalse(await iswap.request_is_parked())
416416

417417
async def test_a_drive_within_the_tolerance_still_counts_as_parked(self):
418418
iswap, _ = await gripper()
@@ -424,9 +424,9 @@ async def test_a_drive_within_the_tolerance_still_counts_as_parked(self):
424424
assert c.elbow_predefined_y_positions_increments is not None
425425
stop = c.elbow_predefined_y_positions_increments.parking
426426
iswap.update_location_by_reference_point(y=c.y_increments_to_mm(stop + 2))
427-
self.assertTrue(await iswap.request_parked())
427+
self.assertTrue(await iswap.request_is_parked())
428428
iswap.update_location_by_reference_point(y=c.y_increments_to_mm(stop + 20))
429-
self.assertFalse(await iswap.request_parked())
429+
self.assertFalse(await iswap.request_is_parked())
430430

431431
async def test_a_parked_arm_above_its_z_stop_is_parked_and_below_it_is_not(self):
432432
"""Parked from 284 mm, the device stands at `rz` 26660 against a stop of 25400."""
@@ -438,9 +438,9 @@ async def test_a_parked_arm_above_its_z_stop_is_parked_and_below_it_is_not(self)
438438
offset = c.elbow_z_offset_above_finger
439439

440440
iswap.update_location_by_reference_point(z=c.z_increments_to_mm(stop + 1260) + offset)
441-
self.assertTrue(await iswap.request_parked())
441+
self.assertTrue(await iswap.request_is_parked())
442442
iswap.update_location_by_reference_point(z=c.z_increments_to_mm(stop - 20) + offset)
443-
self.assertFalse(await iswap.request_parked())
443+
self.assertFalse(await iswap.request_is_parked())
444444

445445

446446
class TestElbowXMoves(unittest.IsolatedAsyncioTestCase):

‎pylabrobot/hamilton/star/driver/features/pipettes.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ async def _require_iswap_parked(self) -> None:
465465
RuntimeError: If it is not parked.
466466
"""
467467
iswap = self.arm.iswap
468-
if iswap is not None and not await iswap.request_parked():
468+
if iswap is not None and not await iswap.request_is_parked():
469469
raise RuntimeError(
470470
"the iSWAP is not parked, and the channels move where it stands. "
471471
"Call `await star.iswap.park()` first."
@@ -3661,15 +3661,15 @@ def _tip_command_positions(
36613661
ys.append(round(centre.y * 10))
36623662
placed[channel] = (centre.x, centre.y)
36633663

3664-
# As legacy checks them: each pair taking part by itself, not the channels between them. Where
3665-
# those leave the pair too little room, the firmware arranges the channels, as it does for
3666-
# legacy's commands.
3664+
# Each pair taking part by itself, as legacy checks them; the firmware arranges the channels
3665+
# between. Distances in tenths, as the command carries them: 9.0 mm is 9.0 mm, float or not.
36673666
for i, (xi, yi) in placed.items():
36683667
for j, (xj, yj) in placed.items():
36693668
# Channels in different columns are separate moves on the device.
3670-
if i < j and round(xi, 1) == round(xj, 1) and abs(yi - yj) < self._min_pair_spacing(i, j):
3669+
apart = round(abs(yi - yj), 1)
3670+
if i < j and round(xi, 1) == round(xj, 1) and apart < self._min_pair_spacing(i, j):
36713671
raise ValueError(
3672-
f"channels {i} and {j} would be {abs(yi - yj):.1f} mm apart in Y, closer than "
3672+
f"channels {i} and {j} would be {apart} mm apart in Y, closer than "
36733673
f"{self._min_pair_spacing(i, j)} mm"
36743674
)
36753675

‎pylabrobot/hamilton/star/driver/features/pipettes_tests.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ async def test_not_parked_refuses(self):
457457
async def not_parked(*args: Any, **kwargs: Any) -> bool:
458458
return False
459459

460-
iswap.request_parked = not_parked # type: ignore[method-assign]
460+
iswap.request_is_parked = not_parked # type: ignore[method-assign]
461461
with self.assertRaises(RuntimeError):
462462
await self.pipettes._require_iswap_parked()
463463

@@ -2144,6 +2144,14 @@ async def test_a_pickup_brings_the_other_channels_along_in_y(self):
21442144
f"channels {channel} and {channel + 1} stand too close: {order}",
21452145
)
21462146

2147+
async def test_spots_a_pitch_apart_in_floating_point_are_accepted(self):
2148+
# 130.7 - 121.7 is 8.999999999999986 in floating point; the command carries 9.0 mm.
2149+
pipettes, _, _ = await channels_over_a_rack()
2150+
_, ys, _ = pipettes._tip_command_positions(
2151+
{0: Coordinate(300.0, 130.7, 150.0), 1: Coordinate(300.0, 121.7, 150.0)}
2152+
)
2153+
self.assertEqual(ys[:2], [1307, 1217])
2154+
21472155

21482156
class TestTipsOfDifferentKinds(unittest.IsolatedAsyncioTestCase):
21492157
"""A command names one tip type, so spots holding different tips go out in separate commands."""

0 commit comments

Comments
 (0)