Skip to content

feat: per-region laser-AF offset (focus-map constant-z + laser AF)#562

Open
Alpaca233 wants to merge 14 commits into
masterfrom
per-region-laser-af-offset
Open

feat: per-region laser-AF offset (focus-map constant-z + laser AF)#562
Alpaca233 wants to merge 14 commits into
masterfrom
per-region-laser-af-offset

Conversation

@Alpaca233

Copy link
Copy Markdown
Collaborator

Summary

Lets each well/region be focused at its own offset from the single global laser-AF reference plane when laser autofocus (Reflection AF) and a constant-z focus map are used together — instead of every well being driven to that one shared reference plane.

Workflow: set the laser-AF reference once, define one focus point per well (the existing focus-map constant + Fit by Region case), and at each well record z. At capture, the system stores that region's laser-AF displacement (measure_displacement(), µm from the reference). During acquisition, laser AF at each FOV in a well drives to its stored offset (move_to_target(offset)) instead of the previously hardcoded move_to_target(0). This pins each well to its capture-time relationship with the reference plane, so laser AF maintains per-well focus against drift across a time-lapse.

Gated behind an explicit "Per-region laser AF offset" checkbox that is only active when Reflection AF is on and the focus map is constant + Fit by Region. The focus map's absolute z stays as the coarse pre-position; the existing per-channel z-offset composes additively (both unchanged).

What changed

  • control/core/multi_point_worker.pyperform_autofocus targets region_laser_af_offsets.get(region_id, 0.0) instead of 0; new attribute unpacked from params.
  • control/core/multi_point_controller.pyregion_laser_af_offsets field + set_region_laser_af_offsets(); threaded through build_params; cleared per-run after build_params so stale offsets can't leak into a later acquisition from entry points that don't push them (fluidics / control server).
  • control/core/multi_point_utils.pyAcquisitionParameters.region_laser_af_offsets: Dict[str, float] (defaults {} → current behavior).
  • control/widgets.py (FocusMapWidget) — captures the offset on Add/Update-Z; the new checkbox + enable gating (Reflection AF + constant + Fit-by-Region); clears offsets when the laser-AF reference changes or the focus-point set changes; CSV export/import gains a back-compatible Offset_um column. Both multipoint widgets wire and push it at acquisition start (gated three ways); the third (fluidics) widget has no focus map and is untouched.
  • control/gui_hcs.py — passes the laser-AF controller into FocusMapWidget (tolerates None when laser AF is unsupported).
  • Docs — design spec + implementation plan under docs/superpowers/.

Backward compatibility

Empty region_laser_af_offsets (the default, and whenever the mode is off) reproduces the exact pre-feature behavior: every FOV targets displacement 0. move_to_target, the focus-map z-baking, and the per-channel z-offset logic are unchanged.

Testing

  • New unit tests in tests/control/test_per_region_laser_af_offset.py (28): backend plumbing/apply, capture edge cases (no controller / no reference / NaN / out-of-range / disabled), reference-change invalidation, the constant-mode enable gating, and CSV round-trip + back-compat.
  • Full suite: 1433 passed, 8 skipped, 1 xfailed; black --check clean.

Not yet done

  • Interactive --simulation GUI smoke test is still pending (no display in the build env): live checkbox enable/disable, on-stage offset capture, reference-reset clearing, and CSV round-trip through the actual dialogs. The automated tests exercise the method logic via stubs but not the live Qt signal connections / acquisition-gate blocks.

Out of scope (noted)

MultiPointController.focus_map has the same pre-existing cross-entry-point staleness that the offsets had; only the new region_laser_af_offsets is reset per run here.

🤖 Generated with Claude Code

Alpaca233 and others added 10 commits June 18, 2026 20:57
…z + laser AF)

Capture a per-well laser-AF displacement offset at each focus point and
drive laser AF to that per-region target during acquisition, instead of the
single global reference plane. Active only when Reflection AF + Use Focus Map
+ the new mode checkbox are all on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ush)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Task 1 change made perform_autofocus read self.region_laser_af_offsets;
the pre-existing _af_stub in test_MultiPointWorker_offsets.py did not define
it, so two perform_autofocus tests regressed. The real worker sets it in
__init__; this fixes only the stale test stub.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…le logic

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fsets per run

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds support for per-region (well-level) laser reflection autofocus target offsets when using a constant-z focus map with “Fit by Region”, so each region can be maintained at its capture-time displacement relative to the global laser-AF reference plane.

Changes:

  • Thread region_laser_af_offsets: Dict[str, float] through MultiPointController → AcquisitionParameters → MultiPointWorker, and use it as the laser-AF target in perform_autofocus.
  • Extend FocusMapWidget to capture/clear/sync per-region offsets, gate the UI with a new checkbox, and persist offsets via an Offset_um CSV column (back-compatible read).
  • Add unit tests covering backend apply path, capture edge cases, gating, invalidation, and CSV round-trip.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
software/control/core/multi_point_utils.py Adds AcquisitionParameters.region_laser_af_offsets with default {}.
software/control/core/multi_point_controller.py Stores/sets offsets, passes them into build_params, and clears controller state post-build to avoid stale reuse.
software/control/core/multi_point_worker.py Uses per-region target offset in reflection AF path (move_to_target(target_um)).
software/control/widgets.py Captures offsets in FocusMapWidget, adds UI gating + CSV persistence, and pushes offsets at acquisition start from both multipoint widgets.
software/control/gui_hcs.py Passes the laser-AF controller into FocusMapWidget (supports None).
software/tests/control/test_per_region_laser_af_offset.py New test suite for per-region offset behavior.
software/tests/control/test_MultiPointWorker_offsets.py Updates AF stub to include region_laser_af_offsets for perform_autofocus.
software/docs/superpowers/specs/2026-06-18-per-region-laser-af-offset-design.md Design spec documenting behavior and rationale.
software/docs/superpowers/plans/2026-06-18-per-region-laser-af-offset.md Implementation plan and verification steps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread software/control/widgets.py Outdated
Comment on lines +6155 to +6156
# FocusMapWidget is shared by both multipoint tabs; enable-state is "last toggle wins" — harmless because each acquisition-start re-reads its own checkbox_withReflectionAutofocus.isChecked().
self.checkbox_withReflectionAutofocus.toggled.connect(self.focusMapWidget.set_reflection_af_available)
Comment thread software/control/widgets.py Outdated
Comment on lines +7705 to +7706
# FocusMapWidget is shared by both multipoint tabs; enable-state is "last toggle wins" — harmless because each acquisition-start re-reads its own checkbox_withReflectionAutofocus.isChecked().
self.checkbox_withReflectionAutofocus.toggled.connect(self.focusMapWidget.set_reflection_af_available)
Alpaca233 and others added 4 commits June 18, 2026 22:21
- guard offset capture with isfinite (matches capture_current_z_offset) instead of isnan
- extract FocusMapWidget.get_offsets_for_acquisition() to DRY the gating across both
  multipoint widgets (one source of truth for the reflection-AF + checkbox condition)
- _sync_offsets_to_focus_points via dict comprehension; dict(offsets or {}) in the setter
- only log the per-FOV laser-AF target when non-zero (no new noise on the default path)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…F (PR #562 review)

Copilot review: the FocusMapWidget is shared by all three multipoint tabs, so
wiring each tab's Reflection AF checkbox into a shared set_reflection_af_available
was 'last toggle wins' — an inactive tab could disable/uncheck the per-region
checkbox (clearing captured offsets) and the acquisition gate reads the shared
checkbox, silently dropping the feature.

- Enable the per-region checkbox purely on the shared focus-map controls
  (constant + Fit by Region); remove set_reflection_af_available and its per-tab
  wiring. The 'requires Reflection AF' rule is now enforced only at acquisition by
  get_offsets_for_acquisition, using the RUNNING tab's own checkbox.
- Stop clearing captured offsets on uncheck (data-loss vector); offsets are cleared
  only on reference change or focus-point edits, and the gate returns {} while
  unchecked, so retaining them is safe.
- Update tests accordingly.

Also add docs/per-region-laser-af-offset.md (user instructions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n status line

- Focus Map checkbox label 'Per-region laser AF offset' -> 'Laser AF Offset'
  (internal symbols unchanged).
- On a successful in-range capture, the focus-map status line now shows the
  recorded offset (e.g. 'Region A1: Laser AF offset +2.30 µm') when Add/Update Z runs.
- Doc + test updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Capturing a per-region offset calls measure_displacement(), which toggles the AF
laser over the microcontroller serial link and waits; a running main-camera live
stream queues triggers on the same link, contends, times out, and returns NaN
("spot not detected"). Mirror LiveControlWidget.capture_current_z_offset: stop the
main live around the measurement and restart it after (no signal, so the user is
not yanked to the Live tab). FocusMapWidget now receives the main liveController.

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.

2 participants