Encapsulate pipette batch scheduling into backend-agnostic module#949
Encapsulate pipette batch scheduling into backend-agnostic module#949BioCam wants to merge 56 commits intoPyLabRobot:mainfrom
Conversation
Replace hamilton/planning.py with pipette_batch_scheduling.py, a self-contained module for channel-batch planning, Y-position computation, and X-group scheduling. Refactor STAR_backend's probe_liquid_heights and execute_batched to use the new API. Add volume-tracker-based probe_liquid_heights mock to chatterbox. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR extracts pipette channel batching/scheduling logic from the Hamilton STARBackend into a new backend-agnostic module, and rewrites probe_liquid_heights to use the new planner while adding tests for the scheduling algorithm.
Changes:
- Added
pipette_batch_scheduling.pywithplan_batches()(X grouping + Y sub-batching), phantom-channel interpolation, pairwise span validation, and optional batch-transition lookahead optimization. - Rewrote
STARBackend.probe_liquid_heights()to useplan_batches()and the new helper utilities (input validation, offset computation, absolute position computation). - Replaced the old Hamilton-only
planning.pyand its tests with new dedicated unit tests for the new scheduling module.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
pylabrobot/liquid_handling/pipette_batch_scheduling.py |
New backend-agnostic batching/scheduling module (plan_batches, transition optimization, helpers). |
pylabrobot/liquid_handling/pipette_batch_scheduling_tests.py |
New unit tests covering spacing logic, phantom interpolation, batching, and transition optimization. |
pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py |
Integrates new planner into probe_liquid_heights, adjusts spacing-related logic, removes legacy batching helpers. |
pylabrobot/liquid_handling/backends/hamilton/STAR_chatterbox.py |
Adds missing mock methods and a simulation-friendly probe_liquid_heights implementation using shared validation. |
pylabrobot/liquid_handling/backends/hamilton/planning.py |
Removed legacy Hamilton-only batching module. |
pylabrobot/liquid_handling/backends/hamilton/planning_tests.py |
Removed tests for the deleted legacy planner. |
pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py |
Refactors tests to exercise the new probe_liquid_heights flow (and rehomes some test classes). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…odd-span wall crash - plan_batches now takes targets (Containers or Coordinates) and handles position computation and same-container spreading internally, replacing the external compute_offsets + compute_positions + plan_batches sequence - Restore execute_batched on STARBackend; probe_liquid_heights uses it via _probe_batch_heights closure instead of an inline batch loop - Make +5.5mm odd-span center-avoidance offset conditional on container width to prevent tip-wall collisions on narrow containers - Generalize compute_positions to accept any Resource (wrt_resource), not just Deck - Remove dead code: _optimize_batch_transitions (LATER :), _find_next_y_target - Rename validate_probing_inputs -> validate_channel_selections - Clean up redundant tests, add container-path coverage
| def validate_channel_selections( | ||
| containers: List[Container], | ||
| use_channels: Optional[List[int]], | ||
| num_channels: int, | ||
| ) -> List[int]: | ||
| """Validate and normalize channel selection. | ||
|
|
||
| If *use_channels* is ``None``, defaults to ``[0, 1, ..., len(containers)-1]``. | ||
|
|
||
| Returns: |
There was a problem hiding this comment.
this function should probably live outside of batch scheduling, and can also be used elsewhere in LH
There was a problem hiding this comment.
I agree; but not sure where it should go. Do you have a preferred home for it?
…s, handle no-go-zone fit failures
…ral pipette scheduling Batch scheduling allows duplicate channels since `aspirate`/`dispense` will reuse the same channel across batches. `probe_liquid_heights` rejects them because each channel probes exactly one container per call. Callers needing more containers than channels should call `probe_liquid_heights` multiple times in sequence and this has to be explicit.
…eights` around it Step toward evolving the chatterbox from a command echo layer into a simulator. The chatterbox `execute_batched` iterates batches and calls the callback without physical moves. `probe_liquid_heights` now flows through it with a mock LLD callback, running the same validation, target resolution, and batch planning as the real backend. Protocols developed off-hardware will encounter the same errors and batch structure as on the instrument. The `execute_batched` override will also serve future batched aspirate/dispense.
Extract `_run_lld_on_channel_batch` from the probe_liquid_heights closure to an instance method so chatterbox overrides only the sensing step instead of the entire function. Drops ~85 lines of chatterbox duplication (full probe_liquid_heights and redundant execute_batched override) - validation, target resolution, batch planning, merge, and Z-safety now have a single source of truth. Adds `verbose: bool = False` to `execute_batched`/`probe_liquid_heights` for optional plan printing, makes `print_batches`'s `use_channels`/`containers` parameters optional, and promotes `lld_mode` validation to fail-fast before any I/O.
Allows mixing GAMMA and PRESSURE LLD across containers in a single call (e.g. capacitive for aqueous, pressure for organic) by widening `lld_mode` to `Union[LLDMode, List[LLDMode], None]` and dispatching `detect_func` per channel within each batch. Matches the singular-name-list-type convention of aspirate/dispense. Backward-compatible: scalar values still work with a `DeprecationWarning` (remove in v1b1); None default silently broadcasts to GAMMA.
….com/BioCam/pylabrobot into encapsulate-pipette-batch-scheduling
…anner `plan_batches` now takes containers + wrt_resource directly (instead of pre-resolved Coordinates) and decides per-batch container spread via `compute_nonconsecutive_channel_offsets`, so subsets that fit in a no-go-zone-divided container can batch even when the full set can't. Pipeline: `is_valid_batch` → `enumerate_valid_batches` → `minimum_exact_cover` → `plan_batches`. `ChannelBatch` is the currency throughout; partition is optimal (branch-and-bound), greedy-only on realistic inputs but a strict win on any container that fits K but not K+1 channels. Removes the prior two-step `resolve_container_targets` + greedy `plan_batches` pipeline and updates STAR's `_prepare_batched` to the single call.
Replace greedy first-fit batching with container-aware exact-cover planner
Reads each container's tracked volume and converts to height via compute_height_from_volume. Parameters matching the real-backend signature are accepted but have no effect in simulation. Will be upgraded in PyLabRobot#949 (pipette batch scheduling refactor). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads each container's tracked volume and converts to height via compute_height_from_volume. Parameters matching the real-backend signature are accepted but have no effect in simulation. Will be upgraded in #949 (pipette batch scheduling refactor). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…spacing tests - Rename print_batches → log_batches (uses logger.info, no custom label) - Remove verbose parameter from execute_batched and probe_liquid_heights - Remove LLDMode alias from chatterbox, remove chatterbox probe_liquid_heights override - Deprecate move_to_z_safety_after (warn-only), migrate internal callers to z_position_at_end_of_command - Restore TestChannelsMinimumYSpacing tests (can_reach_position, position_channels_in_y_direction) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| logger = logging.getLogger(__name__) | ||
|
|
||
| from pylabrobot.liquid_handling.channel_positioning import ( | ||
| compute_nonconsecutive_channel_offsets, | ||
| ) | ||
| from pylabrobot.resources.container import Container | ||
| from pylabrobot.resources.coordinate import Coordinate | ||
| from pylabrobot.resources.resource import Resource |
There was a problem hiding this comment.
Ruff will flag E402 here: imports from pylabrobot.* occur after logger = logging.getLogger(__name__). Please move all imports to the top of the module (or move logger initialization below the imports) so linting passes consistently.
| x_groups: Dict[float, list] = {} | ||
| for b in batches: | ||
| x_key = round(b.x_position, 1) | ||
| x_groups.setdefault(x_key, []).append(b) | ||
|
|
There was a problem hiding this comment.
log_batches() hard-codes X grouping as round(x, 1), which (a) ignores the caller’s configured x_tolerance/x_grouping_tolerance and (b) reintroduces banker's rounding. Consider grouping by the same X key used by planning/execution (e.g., store an x_group_key in ChannelBatch, or accept a tolerance parameter) to keep logs accurate when tolerance != 0.1mm.
| """Pressure liquid level detection mode.""" | ||
|
|
||
| LIQUID = 0 |
There was a problem hiding this comment.
execute_batched() decides when to move X via batch.x_position != prev_batch.x_position. Since x_position is a float (and is computed as an average in the planner), exact inequality can trigger unnecessary X moves between batches that are in the same X group. Use a tolerance-based comparison (e.g. math.isclose / compare within configured X tolerance) or carry a normalized X-group key from the planner.
| """Probe liquid surface heights in containers using liquid level detection. | ||
|
|
||
| Performs capacitive or pressure-based liquid level detection (LLD) by moving channels to | ||
| container positions and sensing the liquid surface. Heights are measured from the bottom | ||
| of each container's cavity. | ||
|
|
||
| Uses ``plan_batches`` for X/Y partitioning with per-batch container spread | ||
| (respecting no-go zones), then ``execute_batched`` to iterate batches with | ||
| Z safety. |
There was a problem hiding this comment.
probe_liquid_heights() now warns whenever move_to_z_safety_after is not None, but internal callers (e.g. probe_liquid_volumes) pass this argument unconditionally. This will emit a deprecation warning on every call. Consider updating internal call sites to the new API (map the boolean to z_position_at_end_of_command behavior) and/or only warn when the deprecated parameter is explicitly used by external callers.
| use_channels: Channel indices to use for probing (0-indexed). | ||
| resource_offsets: Optional XYZ offsets from container centers. Auto-calculated for single | ||
| containers with odd channel counts to avoid center dividers. Defaults to container centers. | ||
| lld_mode: Detection mode - LLDMode(1) for capacitive, LLDMode(2) for pressure-based. | ||
| Defaults to capacitive. | ||
| resource_offsets: Optional XYZ offsets from container centers. When not provided, | ||
| ``plan_batches`` auto-spreads channels targeting the same container. | ||
| lld_mode: Detection mode. Either a single ``LLDMode`` applied to all containers | ||
| (deprecated, removed in v1b1) or a list of ``LLDMode``s (one per container) | ||
| allowing mixed GAMMA/PRESSURE within one call. ``None`` (default) applies |
There was a problem hiding this comment.
This change rejects duplicate channels in a single probe_liquid_heights() call. Previously the batching layer could serialize duplicate channel assignments (and the new scheduling module still supports that), which is useful for probing >N containers with N channels. If duplicates need to remain supported, the result aggregation should be keyed by job/container index (not channel) so a channel can appear in multiple batches without mixing measurements; otherwise, confirm no existing call paths rely on duplicate use_channels (e.g. probe_liquid_height inside aspirate/dispense).
Extracts batch scheduling logic from
STARBackendintopipette_batch_scheduling.py- a standalone, backend-agnostic module with full test coverage.The Problem
probe_liquid_heightsrelied on several tightly coupled private methods (execute_batched,_probe_liquid_heights_batch,_compute_channels_in_resource_locations,_move_to_traverse_height) embedded inSTARBackend. The scheduling algorithm (X grouping, Y sub-batching) was inseparable from Hamilton-specific hardware calls, making it untestable in isolation and not easily reusable by other backends.While per-channel spacing support was already merged (PRs #862, #870, #915), the scheduling layer had gaps: non-consecutive channel batches (e.g. [0,1,2,5,6,7]) left intermediate physical channels 3,4 unpositioned, Y batch feasibility used a single-pair check rather than full pairwise span validation, and there was no lookahead between batches.
PR Content/Solution
New module:
pipette_batch_scheduling.pyplan_batches()partitions channel-position pairs into executable batches. Backend-agnostic - depends only on channel indices, positions, and spacing constraints.New capabilities
_span_required(sum of adjacent pairwise spacings), not just the gap between the candidate and the batch's lowest-Y member. This matters for mixed-channel instruments wherespacing(ch0->ch3)iss(0,1) + s(1,2) + s(2,3), not3 * max(spacings).round(x, 1)(Python's banker's rounding) withmath.floor(x / tolerance) * toleranceand exposesx_grouping_toleranceas a parameter.Structural changes
probe_liquid_heightsrewritten - replaces the 5-method delegation chain (execute_batchedwith callback closure ->_probe_liquid_heights_batch->_compute_channels_in_resource_locations->_move_to_traverse_height) with a single linear flow callingplan_batches(). The method is longer (262 vs 124 lines) but reads top-to-bottom without jumping between methods or files.planning.py- the old module provided X grouping and Y sub-batching but lacked phantom interpolation, span validation, and transition lookahead. Deleted along with its tests.num_channels(not justmax(use_channels)+1), preventingIndexErrorin transition optimization.Not in scope
Detection parameter exposure (cLLD/pLLD kwargs) is intentionally deferred to a follow-up PR to keep this change focused on scheduling encapsulation.
Preview
a small taste of the new functionalities (GitHub doesn't allow larger videos)
WellPlateScene_with_logo.mp4