feat: per-region laser-AF offset (focus-map constant-z + laser AF)#562
Open
Alpaca233 wants to merge 14 commits into
Open
feat: per-region laser-AF offset (focus-map constant-z + laser AF)#562Alpaca233 wants to merge 14 commits into
Alpaca233 wants to merge 14 commits into
Conversation
…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>
Contributor
There was a problem hiding this comment.
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]throughMultiPointController → AcquisitionParameters → MultiPointWorker, and use it as the laser-AF target inperform_autofocus. - Extend
FocusMapWidgetto capture/clear/sync per-region offsets, gate the UI with a new checkbox, and persist offsets via anOffset_umCSV 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 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 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) |
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 hardcodedmove_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.py—perform_autofocustargetsregion_laser_af_offsets.get(region_id, 0.0)instead of0; new attribute unpacked from params.control/core/multi_point_controller.py—region_laser_af_offsetsfield +set_region_laser_af_offsets(); threaded throughbuild_params; cleared per-run afterbuild_paramsso 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.py—AcquisitionParameters.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-compatibleOffset_umcolumn. 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 intoFocusMapWidget(toleratesNonewhen laser AF is unsupported).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
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.black --checkclean.Not yet done
--simulationGUI 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_maphas the same pre-existing cross-entry-point staleness that the offsets had; only the newregion_laser_af_offsetsis reset per run here.🤖 Generated with Claude Code