feat: Add 'From Center' z-stack mode with autofocus restrictions#483
feat: Add 'From Center' z-stack mode with autofocus restrictions#483hongquanli wants to merge 9 commits intomasterfrom
Conversation
Adds a new z-stack acquisition mode that centers the z-stack around the current focus position, in addition to the existing "From Bottom" mode. Changes: - WellplateMultiPointWidget: Add "From Center" option to z-mode dropdown - FlexibleMultiPointWidget: Connect existing combobox_z_stack to controller - Both widgets: Calculate correct z_range for each mode (always [bottom, top]) - Worker: Remove redundant offset calculation in prepare_z_stack() - Fix: Sync z_stacking_config when loading from cache or YAML Design: z_range is always [bottom, top] in absolute coordinates. The UI calculates the correct range based on mode, and the worker just executes. For FROM_CENTER, the stage returns to the center (user's focus position) after acquisition completes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Autofocus options are now enabled/disabled based on z-stack mode: - Contrast AF: only enabled for "From Center" (focus plane is at center) - Laser AF: only enabled for "From Bottom" (can find surface first) Both AF options are disabled for "From Top" and "Set Range" modes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Warn when YAML has z_stacking_config='FROM TOP' in wellplate mode (not supported, falls back to 'From Bottom') - Warn when AF settings from YAML are disabled due to z-mode restrictions (e.g., contrast AF disabled when z-mode is not 'From Center') Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ZStackMode IntEnum with FROM_BOTTOM, FROM_CENTER, FROM_TOP, SET_RANGE - Enum properties: allows_contrast_af, allows_laser_af, worker_config_index - Extract shared helpers: calculate_z_range(), update_autofocus_checkboxes(), log_af_restriction_warnings() - Fix conflicting AF checkbox management in WellplateMultiPointWidget - Fix silent failures: add validation and logging for invalid z-modes - Replace print() with proper logging in set_z_stacking_config() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- TestZStackModeEnum: 11 tests for enum properties and values - TestCalculateZRange: 5 tests for z-range calculation - TestUpdateAutofocusCheckboxes: 7 tests for AF checkbox behavior - TestLogAfRestrictionWarnings: 5 tests for warning logging Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new “From Center” z-stack mode to the multipoint UI and enforces autofocus (AF) compatibility rules based on the selected z-stack mode, with additional YAML/cache load warnings and helper-unit tests.
Changes:
- Introduces a shared
ZStackModeenum plus helpers to compute z-range and enable/disable AF checkboxes. - Updates both multipoint widgets to expose “From Center” and synchronize AF checkbox state + YAML/cache loading behavior.
- Simplifies the worker’s
prepare_z_stack()and replaces aprintwith structured logging in the controller.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| software/control/widgets.py | Adds ZStackMode + z-range/AF helpers; wires “From Center” mode into UI, YAML/cache sync, and z-range calculation. |
| software/control/core/multi_point_worker.py | Removes FROM CENTER repositioning in prepare_z_stack() (now only stabilization sleep). |
| software/control/core/multi_point_controller.py | Uses logger instead of print, and warns on invalid z-stacking indices. |
| software/tests/control/test_z_stack_mode.py | Adds unit tests for ZStackMode and helper functions. |
Comments suppressed due to low confidence (1)
software/control/widgets.py:5883
- In FlexibleMultiPointWidget, enabling “Set Z-range” no longer updates autofocus checkbox states.
toggle_z_range_controls()used to disable reflection AF when the Z-range UI is shown, but now AF is only updated viaon_z_stack_mode_changed(), which is unrelated to the Set Z-range checkbox. Consider treating Set Z-range asZStackMode.SET_RANGEand callingupdate_autofocus_checkboxes(...)(and possiblyset_z_stacking_config(mode.worker_config_index)) when the checkbox toggles so AF restrictions stay consistent.
def toggle_z_range_controls(self, state):
is_visible = bool(state)
# Hide/show widgets in z_min_layout
for i in range(self.z_min_layout.count()):
widget = self.z_min_layout.itemAt(i).widget()
if widget is not None:
widget.setVisible(is_visible)
widget = self.z_max_layout.itemAt(i).widget()
if widget is not None:
widget.setVisible(is_visible)
# Enable/disable NZ entry based on the inverse of is_visible
self.entry_NZ.setEnabled(not is_visible)
current_z = self.stage.get_pos().z_mm * 1000
self.entry_minZ.setValue(current_z)
if is_visible:
self._reset_reflection_af_reference()
self.entry_maxZ.setValue(current_z)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -1282,10 +1282,7 @@ def perform_autofocus(self, region_id, fov): | |||
| return True | |||
|
|
|||
| def prepare_z_stack(self): | |||
There was a problem hiding this comment.
prepare_z_stack() no longer repositions the stage for FROM CENTER, but perform_autofocus() still runs before prepare_z_stack() and assumes the current Z is the stack center, and move_z_back_after_stack() still contains FROM CENTER-specific logic. With the new behavior, subsequent FOVs will start imaging from the AF plane (center) instead of the stack start, and the first FOV may autofocus at the stack start (z_range[0]) rather than the center. Either restore a FROM CENTER reposition here (preferably moving to z_range[0] relative to the post-AF center), or update the worker to consistently use z_range/midpoint for AF and stack start/end across FOVs.
| def prepare_z_stack(self): | |
| def prepare_z_stack(self): | |
| # For FROM CENTER stacking, autofocus leaves us at the stack center. | |
| # Move to the start of the z range (relative to this center) before imaging. | |
| if getattr(self, "NZ", 1) > 1 and getattr(self, "z_stacking_config", None) == "FROM CENTER": | |
| try: | |
| start_offset_um = self.z_range[0] | |
| except (AttributeError, IndexError, TypeError): | |
| start_offset_um = None | |
| if start_offset_um is not None: | |
| self._log.info( | |
| "Preparing z stack (FROM CENTER): moving to start offset %.3f µm from AF plane", | |
| start_offset_um, | |
| ) | |
| # z_range values are in µm; convert to mm for the stage.move_z API. | |
| self.stage.move_z(start_offset_um / 1000.0) | |
| self.wait_till_operation_is_completed() |
There was a problem hiding this comment.
[Claude Code] Fixed in commit 03f9890 - Restored FROM CENTER logic: initialize_z_stack() now moves to center (midpoint of z_range) so AF runs at correct Z level, and prepare_z_stack() moves from center to bottom before imaging. move_z_back_after_stack() was already correct.
| # Z mode - map YAML config to combobox text | ||
| z_mode_map = { | ||
| "FROM BOTTOM": "From Bottom", | ||
| "FROM CENTER": "From Center", | ||
| "SET RANGE": "Set Range", | ||
| } | ||
| z_mode = z_mode_map.get(yaml_data.z_stacking_config, "From Bottom") | ||
| if yaml_data.z_stacking_config == "FROM TOP": | ||
| self._log.warning( | ||
| f"YAML has z_stacking_config='FROM TOP' which is not supported in wellplate mode. " | ||
| f"Using 'From Bottom' instead." | ||
| ) | ||
| self.combobox_z_mode.setCurrentText(z_mode) |
There was a problem hiding this comment.
Wellplate YAML z-mode parsing only warns for FROM TOP, but any other unknown z_stacking_config value silently falls back to From Bottom via dict.get(..., "From Bottom"). If the intent is to warn on unsupported/unknown YAML values (per PR description and the flexible widget behavior), add a warning when yaml_data.z_stacking_config is not in z_mode_map (and not one of the explicitly handled unsupported values).
There was a problem hiding this comment.
[Claude Code] Fixed in commit 03f9890 - Now warns on any unknown z_stacking_config value (not just FROM TOP). Uses dict.get() without default and checks for None.
There was a problem hiding this comment.
Import of 'Mock' is not used.
| from unittest.mock import MagicMock, Mock | |
| from unittest.mock import MagicMock |
There was a problem hiding this comment.
[Claude Code] Fixed in commit 03f9890 - Removed unused Mock import.
- Restore FROM CENTER logic in worker: initialize_z_stack() moves to center (for correct AF), prepare_z_stack() moves from center to bottom - Fix wellplate YAML: warn on any unknown z_stacking_config, not just FROM TOP - Remove unused imports (logging, Mock) in test file Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -5738,8 +5874,6 @@ def toggle_z_range_controls(self, state): | |||
| if widget is not None: | |||
| widget.setVisible(is_visible) | |||
|
|
|||
There was a problem hiding this comment.
When toggling the "Set Z-range" checkbox, the autofocus checkbox states should be updated to reflect SET_RANGE mode restrictions (both AF types disabled). Currently, only the combobox_z_stack change triggers AF checkbox updates via on_z_stack_mode_changed(), but checking/unchecking checkbox_set_z_range doesn't update the AF checkboxes. This means users can have AF enabled in SET_RANGE mode, which shouldn't be allowed according to the ZStackMode.SET_RANGE.allows_contrast_af and allows_laser_af properties.
Add a call to update_autofocus_checkboxes() in toggle_z_range_controls() to disable both AF checkboxes when is_visible is True (SET_RANGE mode), and re-enable them based on the current combobox_z_stack mode when is_visible is False.
There was a problem hiding this comment.
[Claude Code] Fixed in commit ce81cfa - toggle_z_range_controls() now disables both AF checkboxes when Set Z-range is checked, and restores them based on current combobox_z_stack mode when unchecked.
software/control/widgets.py
Outdated
There was a problem hiding this comment.
The z_mode mapping only explicitly handles "From Center", falling back to FROM_BOTTOM for all other modes. While this works for the current combobox options ("From Bottom", "From Center", "Set Range"), it would silently use the wrong mode if "From Top" were added to the combobox in the future.
Consider using a mode_map dictionary similar to on_z_mode_changed() (lines 7916-7920) for consistency and maintainability. For example:
mode_map = {
"From Bottom": ZStackMode.FROM_BOTTOM,
"From Center": ZStackMode.FROM_CENTER,
}
mode = mode_map.get(z_mode, ZStackMode.FROM_BOTTOM)
| mode = ZStackMode.FROM_CENTER if z_mode == "From Center" else ZStackMode.FROM_BOTTOM | |
| mode_map = { | |
| "From Bottom": ZStackMode.FROM_BOTTOM, | |
| "From Center": ZStackMode.FROM_CENTER, | |
| } | |
| mode = mode_map.get(z_mode, ZStackMode.FROM_BOTTOM) |
There was a problem hiding this comment.
[Claude Code] Fixed in commit ce81cfa - Now uses a dict mapping for consistency with on_z_mode_changed().
- FlexibleMultiPointWidget: toggle_z_range_controls() now updates AF checkboxes (both disabled when Set Z-range checked, restored when unchecked) - WellplateMultiPointWidget: use dict mapping for z_mode in toggle_acquisition for consistency and future-proofing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Abort acquisition on invalid z-stack mode instead of silently falling back. Add try-except guards, combobox reset on fallback, validate z_stacking_config in worker, and reject SET_RANGE in calculate_z_range. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
software/control/core/multi_point_worker.py:1301
- The
prepare_z_stack()method has a double sleep that results in 2x the expected stabilization time for FROM CENTER mode. Line 1300 sleeps after moving Z, and line 1301 always sleeps again. For FROM CENTER mode, this causes 2 * SCAN_STABILIZATION_TIME_MS_Z sleep, while other modes only sleep once. Either remove the redundant sleep on line 1301, or restructure the logic to ensure consistent stabilization time across all modes.
def prepare_z_stack(self):
# For FROM CENTER: autofocus ran at center, now move to bottom before imaging
if self.z_stacking_config == "FROM CENTER":
self.stage.move_z(-self.deltaZ * round((self.NZ - 1) / 2.0))
self._sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000)
self._sleep(SCAN_STABILIZATION_TIME_MS_Z / 1000)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| """ | ||
| if mode == ZStackMode.SET_RANGE: | ||
| raise ValueError( | ||
| "calculate_z_range() should not be called with SET_RANGE mode. " "Use user-specified Z min/max directly." |
There was a problem hiding this comment.
String concatenation splits the error message across lines, which may cause issues with some linters and makes the code harder to read. Consider using a single string or proper string formatting with parentheses for multi-line strings.
| "calculate_z_range() should not be called with SET_RANGE mode. " "Use user-specified Z min/max directly." | |
| "calculate_z_range() should not be called with SET_RANGE mode. Use user-specified Z min/max directly." |
| mode = ZStackMode(self.combobox_z_stack.currentIndex()) | ||
| except ValueError: | ||
| self._log.error( | ||
| f"Invalid z-stack mode index: {self.combobox_z_stack.currentIndex()}. " f"Aborting acquisition." |
There was a problem hiding this comment.
String concatenation splits the error message across lines, which may cause issues with some linters and makes the code harder to read. Consider using a single string or proper string formatting with parentheses for multi-line strings.
| f"Invalid z-stack mode index: {self.combobox_z_stack.currentIndex()}. " f"Aborting acquisition." | |
| f"Invalid z-stack mode index: {self.combobox_z_stack.currentIndex()}. Aborting acquisition." |
Summary
ZStackModeIntEnum with properties (allows_contrast_af,allows_laser_af,worker_config_index)calculate_z_range(),update_autofocus_checkboxes(),log_af_restriction_warnings()initialize_z_stack()moves to center for correct AF,prepare_z_stack()moves to bottom before imagingprint()with proper logging inset_z_stacking_config()Test plan
🤖 Generated with Claude Code