Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b903551
STAR: a simulated driver may hand each command to a motion listener, …
stefangolas Oct 3, 2026
4cfd684
STAR: read each firmware command for the motion its drives make
stefangolas Oct 3, 2026
8c40bf8
Viewer: act a command's motion out on the page, and hold the command …
stefangolas Oct 3, 2026
3dfbff2
STAR: satisfy the type checker, the formatter, and the spell checker
stefangolas Oct 3, 2026
15d0a8f
STAR: the fallback tip length is read as a float
stefangolas Oct 3, 2026
1182915
Viewer: the Y ripple runs back along the direction of travel - the ch…
stefangolas Oct 3, 2026
be8b3b0
Viewer: satisfy biome in the motion player
stefangolas Oct 4, 2026
c485eca
Viewer: the iSWAP demo works on an empty deck, within the turned grip…
stefangolas Oct 3, 2026
15f87ec
Viewer: demos for the Y ripple, the 96 head, and the CO-RE gripper
stefangolas Oct 3, 2026
5447821
Viewer: satisfy the type checker in the demos, and biome in the motio…
stefangolas Oct 4, 2026
3a16c9d
Collisions: swept paths against the resource tree
stefangolas Oct 4, 2026
083f3bb
iSWAP transport: plate and lid moves planned from the resource model
stefangolas Oct 4, 2026
a83304f
iSWAP collisions: every mover a plan sweeps is checked, riders included
stefangolas Oct 4, 2026
6bac8b1
Tests: the kept-scene check is timed against the first, not a wall
stefangolas Oct 4, 2026
f7b4944
Tests: the quick check is said in the scenes it builds, not the secon…
stefangolas Oct 4, 2026
4bb60df
STAR collisions: the checks' shared scaffolding out of the iSWAP's file
stefangolas Oct 4, 2026
43500fb
Collisions: a lid seated on an itemized resource is solid with it
stefangolas Oct 4, 2026
ea811bc
Pipette and head commands: their movers swept and judged against the …
stefangolas Oct 4, 2026
4a4985d
Tests: the pipette and head checks' imports in order
stefangolas Oct 5, 2026
7385603
Viewer: a raise_on_collision switch, and what a refused command would…
stefangolas Oct 5, 2026
3897336
STAR driver: every command that moves something is handed to a collis…
stefangolas Oct 5, 2026
10489d0
STAR collisions: the viewer's flag turns the checks into gates, sayin…
stefangolas Oct 5, 2026
3b17c0e
Collisions: a reported sweep walked finely to where the meeting happe…
stefangolas Oct 5, 2026
ba4e32c
Collisions: the meeting walked finely between groups too, and the who…
stefangolas Oct 5, 2026
8236bad
Viewer: what would have met is placed at the instant together, one pi…
stefangolas Oct 5, 2026
7d7d93b
Viewer: what would have met eases along its way to the meeting, and i…
stefangolas Oct 5, 2026
9de1fea
Viewer: what the refusal marks is said in boxes alone, the mover driv…
stefangolas Oct 5, 2026
6ed7286
Viewer: a demo of an open wider than the space beside a gripped plate
stefangolas Oct 5, 2026
97f06c1
Viewer: the collision page's imports in order
stefangolas Oct 6, 2026
0c1ad9b
Viewer: the replay stays out of the page for now
stefangolas Oct 6, 2026
2ac3cec
Collisions: the tip carrier's declared shape, and the deck keeps the …
stefangolas Oct 7, 2026
e228405
Collisions: sweeps carried a point set at a time, the fine walk walke…
stefangolas Oct 7, 2026
db1367e
Viewer: the collision demo grips beside a flush carrier, judged befor…
stefangolas Oct 7, 2026
2873f40
Collisions: the gate judges a tipped channel's Z move at its stop disc
stefangolas Oct 7, 2026
ed206dd
Collisions: resources addressed by their attributes, not reflectively
stefangolas Oct 7, 2026
1994428
Viewer: the collision demo shows a meeting the walk cannot place, and…
stefangolas Oct 7, 2026
5c29f9b
Viewer: what a refusal hits is said in boxes alone, with nothing the …
stefangolas Oct 7, 2026
60f94a7
Collisions: the ODTC's hulls wait for the resource they belong to
stefangolas Oct 7, 2026
2a92275
Collisions: sweep hulls carry each pose once, and a group's pose afte…
stefangolas Oct 7, 2026
e718748
Collisions: the box sweeps carry each corner once, at the plan's own x
stefangolas Oct 7, 2026
24c3962
Collisions: the audit's dead ends swept out, and every helper says wh…
stefangolas Oct 7, 2026
34b30ec
Collisions: a pair on one frame walked relative to it, a group's pose…
stefangolas Oct 7, 2026
ee4123f
Collisions: the checks on plain points, without numpy
stefangolas Oct 7, 2026
753227d
STAR: the imports of the motion glue and the iSWAP transport in order
stefangolas Oct 7, 2026
226d0d9
Viewer: the page's trailing blank line gone
stefangolas Oct 7, 2026
0d0c4ed
Viewer: the page is settled on what the scene names, not a deck's old…
stefangolas Oct 7, 2026
d1aaed2
Collisions: the pairwise sweeps computed once per group, not once per…
stefangolas Oct 7, 2026
c1e00ab
Collisions: a pose's points carried with the turn's cosine taken once
stefangolas Oct 7, 2026
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
459 changes: 459 additions & 0 deletions pylabrobot/hamilton/star/MOTION_PROFILES.md

Large diffs are not rendered by default.

38 changes: 30 additions & 8 deletions pylabrobot/hamilton/star/driver/features/core_grippers.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ def __init__(
self._holding_resource_width: Optional[float] = None
self._held_resource: Optional[Resource] = None
self._taken_from: Optional[Tuple[Resource, Optional[Coordinate]]] = None
# For whoever acts a command out (the viewer), set before it is sent: what a grip takes and
# where it will hang from the front tool, or what a release puts down and where. Cleared by
# nothing in particular: each grip or release overwrites it.
self._handover: Optional[Tuple[Resource, Optional[Resource], Optional[Coordinate]]] = None

# -- what carries them ---------------------------------------------------------------------------

Expand Down Expand Up @@ -168,28 +172,40 @@ def _front_tool(self) -> Optional[Resource]:
shaft = self._pipettes.shaft(self._front_channel)
return shaft.tip if shaft is not None and shaft.has_tip() else None

def _hang_held_resource_on_the_front_tool(self) -> None:
"""Hang the held resource from the front tool where the jaws hold it, so it rides with them.
def _hang_location(self, held: Resource, from_top: Optional[float]) -> Optional[Coordinate]:
"""Where `held` hangs from the front tool, as its child location, or None without the model.

Its centre at the jaws' centre, its top `pickup_distance_from_top` above the grip line - the
front channel's stop disc less the tool's overhang. Nothing happens while nothing models them.
Its centre at the jaws' centre, its top `from_top` below the grip line - the front channel's
stop disc less the tool's overhang.
"""
held, from_top, tool = self._held_resource, self._pickup_distance_from_top, self._front_tool()
tool = self._front_tool()
if held is None or from_top is None or not isinstance(tool, HamiltonCoreGripperTool):
return
return None
back = self._pipettes.get_reference_point_location(cast(int, self._back_channel))
front = self._pipettes.get_reference_point_location(cast(int, self._front_channel))
if back is None or front is None:
return
return None
grip_line = front.z - (tool.get_size_z() - tool.fitting_depth - tool.grip_line_height)
center = held.center().rotated(held.get_absolute_rotation())
lfb = Coordinate(
front.x - center.x,
(back.y + front.y) / 2 - center.y,
grip_line + from_top - held.get_absolute_size_z(),
)
return lfb - tool.get_location_wrt(self._deck)

def _hang_held_resource_on_the_front_tool(self) -> None:
"""Hang the held resource from the front tool where the jaws hold it, so it rides with them.

Nothing happens while nothing models them.
"""
held, from_top = self._held_resource, self._pickup_distance_from_top
location = self._hang_location(held, from_top) if held is not None else None
tool = self._front_tool()
if held is None or location is None or not isinstance(tool, HamiltonCoreGripperTool):
return
held.unassign()
tool.assign_child_resource(held, location=lfb - tool.get_location_wrt(self._deck))
tool.assign_child_resource(held, location=location)

def _put_held_resource_on_the_deck(self) -> None:
"""Put a resource hanging from the front tool on the deck, where it is now."""
Expand Down Expand Up @@ -856,6 +872,9 @@ async def pick_up_resource(
raise ValueError(f"the jaws would close to {closed} mm; squeeze_mm is too large")
await pipettes._require_iswap_parked()

# For whoever acts the command out: what is taken, and where it will hang.
self._handover = (resource, self._front_tool(), self._hang_location(resource, from_top))

source = (resource.parent, resource.location)
try:
async with pipettes._temporary_z_drive_profile(
Expand Down Expand Up @@ -966,6 +985,9 @@ async def drop_resource(
raise ValueError(f"y_clearance must be 0 or more, is {y_clearance}")
await pipettes._require_iswap_parked()

# For whoever acts the command out: what is put down, and where it lands.
self._handover = (held, destination, child)

try:
async with pipettes._temporary_z_drive_profile(
acceleration=z_acceleration, channels=[back, front]
Expand Down
22 changes: 22 additions & 0 deletions pylabrobot/hamilton/star/driver/features/head.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,28 @@ def update_location_by_reference_point(
here.z if z is None else z - on_the_arm.z - shaft.z,
)

def get_reference_point_location(self) -> Optional[Coordinate]:
"""Where the model has the head's reference point, in mm on the deck.

The inverse of `update_location_by_reference_point`: it converts a reported position into a
location, and this converts a location back into the position that would be reported. X is the
arm's, so it is carried through unread.

Returns:
Where the model has it, or None when there is nothing modelling the head yet.
"""
deck = self._driver.deck
if self.resource is None or self.resource.location is None or deck is None:
return None
arm = self.resource.parent
if arm is None:
return None
# The drives report channel A1's axis: the shaft's centre, not the corner it is placed by.
shaft = self.resource.get_item(HEAD_REFERENCE_SHAFT).get_location_wrt(
self.resource, "c", "c", "b"
)
return self.resource.location + arm.get_location_wrt(deck) + shaft

# ----------------------------------------
# Movement
# ----------------------------------------
Expand Down
21 changes: 21 additions & 0 deletions pylabrobot/hamilton/star/driver/features/head96.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,27 @@ async def pick_up_tips(
await self._record_after_tip_command(command_error)
await self.dispensing_drive_request_uL_position()

def _spots_under_shafts(self, offset: Optional[Coordinate]) -> List[Optional[int]]:
"""For each shaft, in spot order (A1, B1, ..., H12), the index of the rack spot it stands over
when head channel A1 is sent to spot A1 plus `offset`, or None where it is past the rack.

Raises:
ValueError: If the offset leaves the channels between spots rather than over them.
"""
pitch = self.configuration.channel_pitch
offset = offset or Coordinate.zero()
columns, rows = round(offset.x / pitch), round(-offset.y / pitch)
for residual in (offset.x - columns * pitch, -offset.y - rows * pitch):
if abs(residual) > 1.0:
raise ValueError(
f"an offset of ({offset.x}, {offset.y}) puts the channels between the rack's spots"
)
under: List[Optional[int]] = []
for shaft in range(96):
column, row = shaft // 8 + columns, shaft % 8 + rows
under.append(column * 8 + row if 0 <= column < 12 and 0 <= row < 8 else None)
return under

# -- tip drop ------------------------------------------------------------------------------------

async def drop_tips(
Expand Down
Loading
Loading