Skip to content
Open
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
73 changes: 72 additions & 1 deletion pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4530,11 +4530,82 @@ async def move_channel_y(self, channel: int, y: float):
)

async def move_channel_z(self, channel: int, z: float):
"""Move a channel in the z direction."""
"""Move a channel in the Z direction.

The Hamilton firmware interprets this Z position based on its internal
"tip mounted" state for the specified channel. When the firmware state
indicates that no tip is mounted, the absolute Z position refers to the
bottom of the stop disc. In that case, this command is effectively
equivalent to :meth:`move_channel_probe_z` for the same numeric Z value.

When the firmware state indicates that a tip is mounted on the channel,
the same Z position instead refers to the physical end of the tip. In
this case, the numeric Z value used with this method may differ from the
probe Z position used with :meth:`move_channel_probe_z` for the same
physical height above the deck.
"""
await self.position_single_pipetting_channel_in_z_direction(
Comment on lines 4532 to 4547
Copy link
Member

Choose a reason for hiding this comment

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

should we make this only for the case when tips are mounted, and ask people to use move_channel_probe_z for other cases? this conditional method behavior is confusing

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes I think you're right - should be a separate PR though

Copy link
Member

Choose a reason for hiding this comment

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

Lets do it here since they go nicely together

Also we can consider renaming tips method to move tip bottom or something to make it extra clear

pipetting_channel_index=channel + 1, z_position=round(z * 10)
)

async def move_channel_probe_z(
self,
channel: int,
z: float,
speed: float = 125.0,
acceleration: float = 800.0,
current_limit: int = 3,
):
"""Move a channel's probe Z-drive to an absolute position, communicating directly
with the individual channel rather than through the master module.

"Probe" refers to the lowest point of the stop disc / entire channel assembly
without a tip attached.

Use this instead of `move_channel_z` when the firmware's internal "tip picked up"
flag has been incorrectly set (e.g. after sleeve-sensing displacement during
tip-presence probing), which causes master-routed Z moves to misbehave.

Args:
channel: Channel index (0-based, backmost = 0).
z: Target Z position in mm.
speed: Max Z-drive speed in mm/sec. Default 125.0 mm/s.
acceleration: Acceleration in mm/sec². Default 800.0. Valid range: ~53.6 to 1609.
current_limit: Current limit (0-7). Default 3.
"""

z_increment = STARBackend.mm_to_z_drive_increment(z)
speed_increment = STARBackend.mm_to_z_drive_increment(speed)
acceleration_increment = STARBackend.mm_to_z_drive_increment(acceleration / 1000)

if not isinstance(channel, int):
raise ValueError(f"channel must be an int, got {type(channel).__name__}")
if not (0 <= channel < self.num_channels):
raise ValueError(
f"channel index {channel} out of range for instrument with {self.num_channels} channels"
)
assert 9320 <= z_increment <= 31200, (
f"z must be between {STARBackend.z_drive_increment_to_mm(9320)} and "
f"{STARBackend.z_drive_increment_to_mm(31200)} mm, got {z} mm"
)
assert 20 <= speed_increment <= 15000, (
f"speed must be between {STARBackend.z_drive_increment_to_mm(20)} and "
f"{STARBackend.z_drive_increment_to_mm(15000)} mm/s, got {speed} mm/s"
)
assert 5 <= acceleration_increment <= 150, (
f"acceleration must be between ~53.6 and ~1609 mm/s², got {acceleration} mm/s²"
)
assert 0 <= current_limit <= 7, f"current_limit must be between 0 and 7, got {current_limit}"

return await self.send_command(
module=STARBackend.channel_id(channel),
command="ZA",
za=f"{z_increment:05}",
zv=f"{speed_increment:05}",
zr=f"{acceleration_increment:03}",
zw=f"{current_limit:01}",
)

async def move_channel_x_relative(self, channel: int, distance: float):
"""Move a channel in the x direction by a relative amount."""
current_x = await self.request_x_pos_channel_n(channel)
Expand Down