Skip to content
Open
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
535 changes: 211 additions & 324 deletions pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py
Copy link
Member

Choose a reason for hiding this comment

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

with the removal of execute_batched, the code becomes a lot less modular. and we do want to use execute_batched for other commands in the future

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

interesting, the current execute_batched seemed very limited to probing actions, which is why I removed it, but you're saying that it actually acts as an abstraction layer for any function that is meant to be called after the channels have moved to their target locations - I really like this

I will work on an implementation

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I returned it and tried to make it even more adaptive:

Screenshot 2026-03-22 at 20 15 18

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

with small adjustment to this workflow we can use it for...

  • probing (ztouch, cLLD, pLLD)
  • aspirate
  • dispense

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions pylabrobot/liquid_handling/backends/hamilton/STAR_chatterbox.py
Copy link
Member

Choose a reason for hiding this comment

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

why does CB need probe_liquid_heights? ideally it should share the same logic as the star backend equivalent. previously I just overrode the measurement bit because that part we can't know in CB

Copy link
Collaborator Author

@BioCam BioCam Mar 22, 2026

Choose a reason for hiding this comment

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

because....

  1. Chatterbox code needs to work completely interchangeably with its backend/driver counterpart without the need to say if protocol_mode == "simulation" every time.
  2. I disagree with...

previously I just overrode the measurement bit because that part we can't know in CB

...the simulation/chatterbox actually has a unique opportunity here:
I made it return what the liquid tracker returns - this enables true simulation :)

Copy link
Member

Choose a reason for hiding this comment

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

ok yes this makes sense

Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,18 @@
MachineConfiguration,
STARBackend,
)
from pylabrobot.liquid_handling.pipette_batch_scheduling import (
plan_batches,
print_batches,
validate_channel_selections,
)
from pylabrobot.resources.container import Container
from pylabrobot.resources.coordinate import Coordinate
from pylabrobot.resources.well import Well

# Type alias for nested enum (for cleaner signatures)
LLDMode = STARBackend.LLDMode

_DEFAULT_MACHINE_CONFIGURATION = MachineConfiguration(
pip_type_1000ul=True,
kb_iswap_installed=True,
Expand Down Expand Up @@ -213,6 +223,10 @@ async def channel_request_y_minimum_spacing(self, channel_idx: int) -> float:
)
return self._channels_minimum_y_spacing[channel_idx]

async def channels_request_y_minimum_spacing(self) -> List[float]:
"""Return mock per-channel minimum Y spacings for all channels."""
return list(self._channels_minimum_y_spacing)

async def move_channel_y(self, channel: int, y: float):
print(f"moving channel {channel} to y: {y}")

Expand Down Expand Up @@ -316,3 +330,73 @@ async def position_channels_in_y_direction(self, ys, make_space=True):

async def request_pip_height_last_lld(self):
return list(range(12))

async def probe_liquid_heights(
self,
containers: List[Container],
use_channels: Optional[List[int]] = None,
resource_offsets: Optional[List[Coordinate]] = None,
lld_mode: LLDMode = LLDMode.GAMMA,
search_speed: float = 10.0,
n_replicates: int = 1,
move_to_z_safety_after: bool = True,
min_traverse_height_at_beginning_of_command: Optional[float] = None,
min_traverse_height_during_command: Optional[float] = None,
z_position_at_end_of_command: Optional[float] = None,
x_grouping_tolerance: Optional[float] = None,
) -> List[float]:
"""Probe liquid heights by computing from tracked container volumes.

Instead of simulating hardware LLD, this mock computes liquid heights directly from
each container's volume tracker using ``container.compute_height_from_volume()``.

Args:
containers: List of Container objects to probe, one per channel.
use_channels: Channel indices to use (0-indexed). Defaults to ``[0, ..., len(containers)-1]``.
resource_offsets: Passed to ``plan_batches`` for auto-spreading. See ``plan_batches``.
All other parameters: Accepted for API compatibility but unused in mock.

Returns:
Liquid heights in mm from cavity bottom for each container, computed from tracked volumes.

Raises:
ValueError: If ``use_channels`` is empty, contains out-of-range indices, or if
``containers`` and ``use_channels`` have different lengths.
NoTipError: If any specified channel lacks a tip.
"""
if x_grouping_tolerance is None:
x_grouping_tolerance = self._x_grouping_tolerance_mm

use_channels = validate_channel_selections(
containers=containers,
use_channels=use_channels,
num_channels=self.num_channels,
)

# Validate tip presence using tip tracker
for ch in use_channels:
self.head[ch].get_tip() # Raises NoTipError if no tip

batches = plan_batches(
use_channels=use_channels,
targets=containers,
channel_spacings=self._channels_minimum_y_spacing,
x_tolerance=x_grouping_tolerance,
wrt_resource=self.deck,
resource_offsets=resource_offsets,
)

print_batches(batches, use_channels, containers, label="probe_liquid_heights plan")

# Compute heights from volume trackers
heights: List[float] = []
for container in containers:
volume = container.tracker.get_used_volume()
if volume == 0:
heights.append(0.0)
else:
height = container.compute_height_from_volume(volume)
heights.append(height)

print(f" heights: {[f'{h:.2f}' for h in heights]} mm")
return heights
Loading
Loading