Skip to content

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing#1067

Draft
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch
Draft

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing#1067
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch

Conversation

@BioCam
Copy link
Copy Markdown
Collaborator

@BioCam BioCam commented May 28, 2026

The Problem

PyLabRobot has a single-channel force-sensed Z probe (ztouch_probe_z_height_using_channel, firmware ZH) but no batched, parallel equivalent. Multi-channel workflows that need this measurement currently require careful and time-consuming planning.

Concrete needs that surface this gap:

  • Verifying that lids, adapters, or labware are seated at the expected Z before a workflow continues.
  • Calibrating the Z of unknown or replaceable deck features without manual teach-in.
  • Capturing per-channel Z offsets against a calibration block in one shot rather than N sequential probes.

Naively wrapping the primitive in asyncio.gather does not solve this, because of two parallelism hazards specific to force-sensed probing on the STAR head:

  • Carriage-force cross-talk. All channels share one rigid Z-drive carriage. When channels descend simultaneously, each channel's touchdown produces a force transient that perturbs the PWM-based detection threshold on the others, biasing every measurement after the first to land.

  • C0-command serialization. The single-channel primitive issues move_channel_z (firmware JZ via C0) when post_detection_dist != 0 (TODO: move to PX version), and request_tip_len_on_channel (also C0) when tip_len=None. Both route through the master controller and serialize the gather - defeating the parallelism the caller is trying to get.

A correct batched implementation has to address both of these explicitly.

PR Content / Solution

Adds channels_probe_z_using_ztouch to the STAR backend: a batched, parallel force-sensed Z probe that returns the absolute deck-frame Z of whatever solid surface each channel first touches.

Architecture

Reuses the same batched-channel orchestration skeleton that probe_liquid_heights already uses for cLLD/pLLD: plan_batches builds channel/X/Y batches, execute_batched handles inter-batch X/Y positioning and Z safety, and the per-batch callback fans out ztouch_probe_z_height_using_channel across the batch's channels under asyncio.gather. No reinvention of X/Y partitioning, no-go-zone handling, or inter-batch Z safety.

Carriage cross-talk: stagger the channel starts

Within each batch, channel starts are staggered by inter_channel_start_delay (default 0.3 s) so contact-force transients don't superpose. The stagger lives in a small _delayed(delay, factory) helper that takes a coroutine factory (not a coroutine) to avoid un-awaited-coroutine warnings if the gather is cancelled mid-sleep. Tunable to 0.0 for callers who've validated mechanically isolated geometry.

C0-avoidance: keep the gather truly parallel

The inner ZH call receives post_detection_dist=0 and an explicit tip_len so the primitive does not issue move_channel_z or request_tip_len_on_channel. Channel raising between batches and at the end is handled by the existing min_traverse_height_during_command and z_position_at_end_of_command kwargs.

Safety and aggregation

Outer try / except BaseException lifts all channels to Z safety on any exception (firmware error, KeyboardInterrupt, SystemExit, asyncio.CancelledError) before re-raising. Aggregated Z is rounded to the firmware z-drive quantum (0.01 mm) so users don't see float-averaging noise across replicates.

Design decisions

  • Returns absolute deck-frame Z, not relative to a container or any other reference. The method targets arbitrary surfaces; there's no "in this well" frame to subtract. Callers wanting relative values subtract themselves.
  • No public post_detection_dist kwarg. Using it inside the gather would issue a C0 JZ per channel and serialize the parallelism. Channel raising is already covered by min_traverse_height_during_command / z_position_at_end_of_command.
  • except BaseException, not except Exception. Catches KeyboardInterrupt, SystemExit, and asyncio.CancelledError too - all cases where channels might be pressing on a surface when control is lost. Matches the pattern already in execute_batched.
  • inter_channel_start_delay default 0.3 s. Empirical default covering a typical ~tens-of-ms contact transient with margin; tunable to 0.0 for callers who don't need the decoupling.
  • Round aggregated Z to 0.01 mm.

Batched ZH probing across multiple channels, modeled on probe_liquid_heights:
  plan_batches -> execute_batched -> asyncio.gather over `ztouch_probe_z_height_using_channel`.

  Channel starts within a batch are staggered by inter_channel_start_delay (default
   0.3 s) to decouple contact-force transients on the shared carriage -> creating a "cascading motion".

The inner ztouch_probe_z_height_using_channel call receives post_detection_dist=0
   and an explicit tip_len so no C0 commands (move_channel_z,
  request_tip_len_on_channel) serialize the gather. Channel raising is handled by
  the existing min_traverse_height_during_command / z_position_at_end_of_command
  kwargs.

  Outer try/except BaseException lifts all channels to Z safety on any exception
  (firmware error, KeyboardInterrupt, CancelledError) before re-raising. Aggregated
   Z is rounded to the firmware z-drive quantum (0.01 mm). Chatterbox stub
  included.
@BioCam BioCam marked this pull request as draft May 28, 2026 18:45
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant