Make STARBackend.probe_liquid_heights() Smart#876
Make STARBackend.probe_liquid_heights() Smart#876BioCam wants to merge 30 commits intoPyLabRobot:mainfrom
STARBackend.probe_liquid_heights() Smart#876Conversation
…on methods important for usage in... 1.- LLD (based on these inner detection methods rather than the default aspirate) 2.- auto-liquid level following
useful for pooling (?)
i.e. channels not used in between those that are used in spread behaviour across large
There was a problem hiding this comment.
Pull request overview
This PR enhances the probe_liquid_heights() method to automatically handle complex channel and container configurations that previously required manual batching or raised errors. The changes enable the method to intelligently group containers by X position, partition channels into compatible Y sub-batches, compute channel-aware offsets for non-consecutive channel configurations, and provide fine-grained Z-axis control during probing operations.
Changes:
- Added automatic X-position grouping and Y sub-batching to handle any channel/container configuration
- Implemented channel-index-aware offset calculation for non-consecutive channels (e.g., [0,1,2,5,6,7])
- Added three new traverse height parameters and a flag to control Z positioning at different stages
- Added duplicate channel validation with an override flag for legitimate multi-probe scenarios
- Exposed all cLLD and pLLD detection parameters as method arguments
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
using the trackers :)
important: thorough in-line documentation to counter complexity
|
This is step 3 from #822 (comment) |
| plld_mode: Optional[PressureLLDMode] = None, # defaults to PressureLLDMode.LIQUID | ||
| plld_foam_detection_drop: int = 30, | ||
| plld_foam_detection_edge_tolerance: int = 30, | ||
| plld_foam_ad_values: int = 30, |
There was a problem hiding this comment.
how about two dataclasses, one for all plld and one for all clld parameters? this will be nicer to pass around, and also we can use it in STARBackend.aspirate and stuff in the future without having to repeat this whole list
There was a problem hiding this comment.
The problem I see with packaging everything up into dataclasses is that developers then need to perform an ever-growing list of new imports: just to perform an aspiration with pre-mix e.g. requires import Mix, import plld_parameters/import clld_parameters.
This repeats for error handling where plld/clld might switch depending on error handling logic (of only the subset of channels that have failed)
...it's not impossible, but it does add a lot of extra cognitive load on the developer and increases code written, even though, in most cases, I only want to switch 1-3 arguments between different commands
There was a problem hiding this comment.
but this change should either way be its own PR, because this PR's responsibility is creating the new probing logic for complex well - channel mappings
(and acts as a test for how to scale this to all liquid handler actions across PLR)
There was a problem hiding this comment.
so you mean moving parameters out of this method?
it is the topic for a separate PR, but for parameters something like plld=False or plld=True (use default values) or plld=PLLDParams(x=1) (y=2 default) makes sense. these parameters naturally go together (you either use clld or plld or both), so you don't want to allow specifying some parameters which are not even used. That is in addition to this then being easily shared across function calls because there will be more than one function using pLLD with this set of parameters. even in this function, it would really simplify things because extra_kwargs can largely be deleted.
There was a problem hiding this comment.
I mean, this PR does not modify any liquid level detection behaviour - this PR is about action management:
it focuses only on fixing the current issue of being restricted to probing in only one x dimension, and the bug of not being able to automatically decide between sequential probing inside a container vs parallel probing inside a container based on the container's size_y and the total pipette spacing available.
(and the wider goal of this PR is to test this new "smart"/automatic batching algorithm, because it is applicable to all multi-pipette commands but needs a safe test case -> probing, as opposed to aspirate/dispense)
Happy to discuss modifying the liquid level detection parameter bundling in a separate PR
There was a problem hiding this comment.
why is this deleted? unrelated change and also breaking
There was a problem hiding this comment.
thank you for catching this! that is a bug, returned it now
| # Raise channels before moving X carriage (tips may be lowered from previous group) | ||
| if not is_first_x_group: | ||
| assert prev_indices is not None | ||
| if min_traverse_height_during_command is None: | ||
| await self.move_all_channels_in_z_safety() | ||
| else: | ||
| prev_channels = [use_channels[i] for i in prev_indices] | ||
| await self.position_channels_in_z_direction( | ||
| {ch: min_traverse_height_during_command for ch in prev_channels} | ||
| ) | ||
| await self.move_channel_x(0, group_x) |
There was a problem hiding this comment.
why is is_first_x_group needed? the exact same code is executed above before entering this loop. this can be simplified by just putting it in the loop. (the only difference would be when x_groups is empty, but I would say the correct behavior in that case is to simply do nothing which is what this refactor would do.
There was a problem hiding this comment.
I don't see where the exact same code is executed above?
is_first_x_group is needed to set min_traverse_height_during_command - this is a powerful parameter to save time in between movements of the same batch (i.e. usually above the same plate; no need to move all the way to safe z height and waste time).
|
One of the toughest parts of this pipette/container "orchestration" is to encapsulate it so it can be applied to the main features I am going to work on next:
Please let me know if you see a way that doesn't require re-implenentation of this behaviour for these commands, keeping their specific requirements in mind. |
The Problem
probe_liquid_heightshad several limitations that prevented it from handling real-world channel/container configurations:NotImplementedError, even though the method could handle them sequentially.channels like
[0,1,2,5,6,7]were spaced as if they were[0,1,2,3,4,5], ignoring the physical gap required by phantom intermediate channels (3 and 4). If the container was too small to fit allchannels, this raised an unhandled
ValueErrorinstead of falling back gracefully.members) would raise errors mid-command instead of being automatically partitioned into compatible batches.
This PR
Makes
probe_liquid_heights"smart" 🤓, i.e. automatically handle any channel/container configuration:channel index difference. Phantom channels between non-consecutive batch members are explicitly positioned at minimum spacing to prevent firmware errors. Each sub-batch is probed in parallel.
get_wide_single_resource_liquid_op_offsets, then only the actual channel offsets are kept. Channels like[0,1,2,5,6,7]now correctly fit in one batch. If the container is too small, offsets fallback to center and Y sub-batching automatically serializes the channels that can't coexist.
move_to_z_safety_afterflag allows skipping the final Z raiseentirely. On any error, all channels return to full Z safety regardless of these settings.
min_traverse_height_at_beginning_of_commandNone→ full Z safetymin_traverse_height_during_commandNone→ full Z safetyz_position_at_end_of_commandNone→ full Z safetymove_to_z_safety_afterTrueFalseallow_duplicate_channels— defaults toFalsewith a clear error message;aspirate/dispensepassTruesince they may legitimately probe the same container multiple times.No breaking changes — single-X, consecutive-channel usage produces identical behavior.
Pseudocode: full method flow