Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
639b3b7
Add no_go_zones attribute to Container and all subclasses
BioCam Mar 23, 2026
46843d7
Add compartment utilities for no-go zone channel distribution
BioCam Mar 23, 2026
771cb48
Integrate no-go zone channel distribution into LiquidHandler
BioCam Mar 23, 2026
9300fc1
Add no-go zones to Hamilton 60mL and 120mL troughs
BioCam Mar 23, 2026
e71b850
Add tests for no-go zone compartment distribution
BioCam Mar 23, 2026
6d8a293
Add container no-go zones investigation notebook
BioCam Mar 23, 2026
f1c9bb1
make format
BioCam Mar 23, 2026
19c5335
normalize no_go_zones deserialization to tuples, extract `_compute_sp…
BioCam Mar 23, 2026
ea00182
normalize no_go_zones deserialization, validate channel_spacings leng…
BioCam Mar 23, 2026
cb39a1b
Merge branch 'PyLabRobot:main' into create-no_go_zones
BioCam Mar 23, 2026
83e046e
improve notebook: header standard, narrative flow, channel diameter c…
BioCam Mar 23, 2026
12983be
add empirical data for Hamilton troughs
BioCam Mar 23, 2026
7557615
make no_go_zones respect spread mode (wide/tight) within compartments
BioCam Mar 23, 2026
5cabe7b
Update pylabrobot/liquid_handling/utils.py
BioCam Mar 23, 2026
4e661ca
Update pylabrobot/resources/hamilton/troughs.py
BioCam Mar 23, 2026
23859f6
Update pylabrobot/resources/container.py
BioCam Mar 23, 2026
93d4d83
Merge branch 'main' into create-no_go_zones
BioCam Mar 23, 2026
73619a2
address PR review - spacing_idx bug, spread validation, wide per-gap …
BioCam Mar 23, 2026
f43cd78
add type narrowing asserts for mypy in container_tests
BioCam Mar 23, 2026
930236d
simplify into independent `compute_channel_offsets`
BioCam Mar 23, 2026
b1adfa4
refactor: consolidate channel positioning into compute_channel_offset…
BioCam Mar 23, 2026
4bb3be4
Merge branch 'main' into create-no_go_zones
BioCam Mar 24, 2026
f48d50b
wip: per-channel spacing model, proportional compartment assignment, …
BioCam Mar 24, 2026
035feee
cross-compartment gap enforcement, centered edge margins, distance an…
BioCam Mar 24, 2026
0bd91de
fix: occupancy diameter model (sum of radii), decouple STAR firmware …
BioCam Mar 24, 2026
f3c8436
fix: radius-aware edge clearance, remove post-hoc enforcement, cross-…
BioCam Mar 24, 2026
d76356d
add TODO for handling LHs like OT2
BioCam Mar 24, 2026
8c32685
fix: reverse back-to-front spacings to front-to-back for compartment …
BioCam Mar 24, 2026
1f3ff28
Merge branch 'main' into create-no_go_zones
BioCam Mar 24, 2026
b8f81b2
Merge branch 'main' into create-no_go_zones
BioCam Mar 26, 2026
5b2effc
Merge branch 'main' into create-no_go_zones
BioCam Mar 26, 2026
db273ab
Merge branch 'main' into create-no_go_zones
BioCam Mar 29, 2026
672c18d
Add ChannelsDoNotFitError, use logger.info in _get_compartments, refa…
rickwierenga Mar 29, 2026
6fb8396
Rename utils.py to channel_positioning.py and update all imports
rickwierenga Mar 29, 2026
cb3753d
Fix unused imports and int/float type errors in channel_positioning_t…
rickwierenga Mar 30, 2026
8d97c7c
Fix import sorting in files changed by this PR
rickwierenga Mar 30, 2026
c8fa74d
Format channel_positioning.py
rickwierenga Mar 30, 2026
8af3a42
Merge branch 'main' into create-no_go_zones
BioCam Mar 30, 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
1 change: 1 addition & 0 deletions docs/user_guide/00_liquid-handling/_liquid-handling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ Examples:
moving-channels-around
tutorial_tip_inventory_consolidation
mixing
container_no_go_zones
882 changes: 882 additions & 0 deletions docs/user_guide/00_liquid-handling/container_no_go_zones.ipynb

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions pylabrobot/liquid_handling/backends/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from abc import ABCMeta, abstractmethod
from typing import Dict, List, Optional, Union

from pylabrobot.liquid_handling.channel_positioning import GENERIC_LH_MIN_SPACING_BETWEEN_CHANNELS
from pylabrobot.liquid_handling.standard import (
Drop,
DropTipRack,
Expand Down Expand Up @@ -158,6 +159,27 @@ async def request_tip_presence(self) -> List[Optional[bool]]:

raise NotImplementedError()

def get_channel_spacings(self, use_channels: List[int]) -> List[float]:
"""Get the per-channel occupancy diameter for each channel being used.

Each value is the channel's occupancy diameter - the physical space it takes up.
The required center-to-center distance between two adjacent channels is the sum of
their radii: ``spacing[i]/2 + spacing[j]/2``.

Args:
use_channels: The channels being used, in order.

Returns:
List of per-channel occupancy diameters (mm), length = ``len(use_channels)``.
Defaults to ``GENERIC_LH_MIN_SPACING_BETWEEN_CHANNELS`` (9mm) for all channels.
Backends with variable channel spacing should override this.

Note: This assumes channels spread along Y. Backends where channels are fixed in Y
(e.g. OT2 with 2 channels at fixed X spacing) should either override this or
signal that Y-spread is not supported via a ``supports_y_spread`` property.
"""
return [GENERIC_LH_MIN_SPACING_BETWEEN_CHANNELS] * len(use_channels)

@abstractmethod
def can_pick_up_tip(self, channel_idx: int, tip: Tip) -> bool:
"""Check if the tip can be picked up by the specified channel. Does not consider
Expand Down
37 changes: 18 additions & 19 deletions pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import enum
import functools
import logging
import math
import re
import sys
import warnings
Expand Down Expand Up @@ -40,6 +39,11 @@
)
from pylabrobot.liquid_handling.backends.hamilton.common import fill_in_defaults
from pylabrobot.liquid_handling.backends.hamilton.planning import group_by_x_batch_by_xy
from pylabrobot.liquid_handling.channel_positioning import (
MIN_SPACING_EDGE,
get_tight_single_resource_liquid_op_offsets,
get_wide_single_resource_liquid_op_offsets,
)
from pylabrobot.liquid_handling.errors import ChannelizedError
from pylabrobot.liquid_handling.liquid_classes.hamilton import (
HamiltonLiquidClass,
Expand All @@ -62,11 +66,6 @@
SingleChannelAspiration,
SingleChannelDispense,
)
from pylabrobot.liquid_handling.utils import (
MIN_SPACING_EDGE,
get_tight_single_resource_liquid_op_offsets,
get_wide_single_resource_liquid_op_offsets,
)
from pylabrobot.resources import (
Carrier,
Container,
Expand Down Expand Up @@ -1358,15 +1357,16 @@ def __init__(
self._setup_done = False

def _min_spacing_between(self, i: int, j: int) -> float:
"""Return the conservative minimum Y spacing required between channels *i* and *j*.

For adjacent channels, the constraint is the larger of the two channels' individual minimum
spacings, ceiling'd to 1 decimal place for safe movement.
"""Return the firmware-safe minimum Y spacing between channels *i* and *j*.

For non-adjacent channels, the spacing is the sum of all intermediate adjacent-pair spacings.
Uses max() of both channels' spacings for firmware safety (conservative).
For adjacent channels, ceiling-rounded to 0.1mm.
For non-adjacent channels, the sum of all intermediate adjacent-pair spacings.
"""
lo, hi = min(i, j), max(i, j)
if hi - lo == 1:
import math

spacing = max(self._channels_minimum_y_spacing[lo], self._channels_minimum_y_spacing[hi])
return math.ceil(spacing * 10) / 10
return sum(self._min_spacing_between(k, k + 1) for k in range(lo, hi))
Expand Down Expand Up @@ -2214,10 +2214,6 @@ def _compute_channels_in_resource_locations(
)
min_ch = min(use_channels)
offsets = [all_offsets[ch - min_ch] for ch in use_channels]

if num_channels_in_span % 2 != 0:
y_offset = 5.5
offsets = [offset + Coordinate(0, y_offset, 0) for offset in offsets]
# else: container too small to fit all channels — fall back to center offsets.
# Y sub-batching will serialize channels that can't coexist.

Expand Down Expand Up @@ -2328,8 +2324,8 @@ async def probe_liquid_heights(
Notes:
- All specified channels must have tips attached
- Containers at different X positions are probed in sequential groups (single X carriage)
- For single containers with odd channel counts, Y-offsets are applied to avoid
center dividers (Hamilton 1000 uL spacing: 9mm, offset: 5.5mm)
- For single containers with no-go zones, Y-offsets are computed to avoid
obstructed regions (e.g. center dividers in troughs)
"""

if move_to_z_safety_after is not None:
Expand Down Expand Up @@ -4548,9 +4544,9 @@ async def move_channel_z(self, channel: int, z: float):
stop disk Z position used with :meth:`move_channel_stop_disk_z` for the same
physical height above the deck.
"""
# TODO: remove after 2026-09
# TODO: remove in v1
warnings.warn(
"move_channel_z is deprecated and will be removed after 2026-09 in legacy PLR. "
"move_channel_z is deprecated and will be removed in v1. "
"Use move_channel_stop_disk_z() for moves without a tip attached "
"or move_channel_tool_z() when a tip/tool is attached.",
DeprecationWarning,
Expand Down Expand Up @@ -4677,6 +4673,9 @@ async def move_channel_z_relative(self, channel: int, distance: float):
current_z = await self.request_z_pos_channel_n(channel)
await self.move_channel_z(channel, current_z + distance)

def get_channel_spacings(self, use_channels: List[int]) -> List[float]:
return [self._channels_minimum_y_spacing[ch] for ch in sorted(use_channels)]

def can_pick_up_tip(self, channel_idx: int, tip: Tip) -> bool:
if not isinstance(tip, HamiltonTip):
return False
Expand Down
2 changes: 1 addition & 1 deletion pylabrobot/liquid_handling/backends/hamilton/planning.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Callable, Dict, List

from pylabrobot.liquid_handling.utils import MIN_SPACING_BETWEEN_CHANNELS
from pylabrobot.liquid_handling.channel_positioning import MIN_SPACING_BETWEEN_CHANNELS
from pylabrobot.resources import Coordinate


Expand Down
Loading
Loading