-
Notifications
You must be signed in to change notification settings - Fork 40
[MSD-40][refactor] Milling angle as VA #3313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughMILLING_RANGE was converted from degrees to radians. Posture managers now expose a milling_angle FloatContinuous initialized from stage metadata (MD_FAV_MILL_POS_ACTIVE.rx) and persist changes back to that metadata via a new _set_milling_angle. Transformation initialization/update APIs were simplified to _initialise_transformation(angle) and _update_conversion(pre_tilt); milling_angle changes subscribe to initialise transformation. to_posture fast-path logic was adjusted to avoid shortcutting when target posture is MILLING. FibsemTab was updated to set milling_angle via the posture manager and perform asynchronous posture switches with tracked futures; tests were added/updated for milling behavior. Sequence Diagram(s)sequenceDiagram
rect rgba(220,235,255,0.5)
participant UI as FibsemTab UI
participant PM as PostureManager
participant Stage as Stage Metadata
participant Transform as Transformation System
end
UI->>PM: set milling_angle.value (rad)
activate PM
PM->>Stage: write MD_FAV_MILL_POS_ACTIVE (update rx, keep rz)
PM->>PM: publish milling_angle change
PM->>Transform: _initialise_transformation(angle)
activate Transform
Transform->>Transform: recompute axes/tilt using angle
deactivate Transform
PM-->>UI: milling_angle change notification
deactivate PM
UI->>UI: refresh features / UI
sequenceDiagram
rect rgba(235,245,225,0.5)
participant User as User
participant UI as FibsemTab UI
participant Driver as Stage Driver
participant PM as PostureManager
end
User->>UI: click move-to-milling-posture
UI->>Driver: cryoSwitchSamplePosition(MILLING) (returns Future)
UI->>UI: store future in _posture_switch_future
UI->>UI: attach completion callback (_on_milling_move_complete)
activate Driver
Driver->>Driver: perform async posture switch
Driver-->>UI: future completes
deactivate Driver
UI->>UI: _on_milling_move_complete(future)
alt success
UI->>Driver: refresh stage position
UI->>UI: update buttons / layout
else cancelled / error
UI->>UI: log error / refresh as needed
end
Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (3)
src/odemis/acq/test/move_tescan_test.py (1)
126-156: Clarify the commented-out code or remove it.The test methodology is sound and properly validates milling angle stability. However, lines 129-132 contain commented-out code for setting a default milling angle.
Either uncomment this code if it's necessary for the test setup, or remove it entirely to keep the test clean.
🔎 Suggested cleanup
If the commented code is not needed:
def test_milling_angle_stable_pos(self): sample_stage = self.posture_manager.sample_stage - # Set default milling angle - # milling_angle = math.radians(15) - # current_md = self.stage.getMetadata() - # self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: {'rx': milling_angle, - # "rz": current_md[model.MD_FAV_MILL_POS_ACTIVE]["rz"]}}) # Transform to milling posture self.posture_manager.cryoSwitchSamplePosition(MILLING).result()src/odemis/gui/cont/tabs/fibsem_tab.py (1)
416-454: Remove commented-out code, otherwise LGTM.The logic correctly:
- Uses the
milling_angleVA setter to trigger transformation updates- Updates existing feature positions with the new milling angle
- Automatically re-applies the milling posture when the angle changes
However, lines 432-439 contain commented-out code about user confirmation for updating features. Since the decision was made to always update (line 440), this commented code should be removed for clarity.
🔎 Suggested cleanup
- # # changing milling angle, causes previously defined features at milling angle to be "seen" as SEM_IMAGING - # # QUERY: should we update the features to the new milling angle? - # box = wx.MessageDialog(self.main_frame, - # message=f"Do you want to update existing feature positions with the updated milling angle ({math.degrees(milling_angle):.2f}°)?", - # caption="Update existing feature positions?", style=wx.YES_NO | wx.ICON_QUESTION | wx.CENTER) - # - # ans = box.ShowModal() # Waits for the window to be closed - # if ans == wx.ID_YES: logging.debug(f"Updating existing feature positions with the updated milling angle ({math.degrees(milling_angle):.2f}°)")src/odemis/acq/move.py (1)
1640-1654: Clarify theangleparameter usage or refactor the subscription.The
milling_angleVA subscribes to_initialise_transformationwithinit=True(line 1642), passing the angle value as a parameter. However, the_initialise_transformationmethod (line 1648) receives but doesn't use thisangleparameter, instead relying onself.pre_tilt(line 1653) and metadata lookups in_update_conversion(lines 1688-1692).This works because
_set_milling_anglealready updated the metadata before the subscription fires, but the flow is confusing.Options:
- Remove the parameter and make the subscription trigger without arguments
- Use the parameter by passing it through to
_update_conversioninstead of reading from metadata🔎 Option 1: Remove unused parameter
- def _initialise_transformation(self, angle): + def _initialise_transformation(self, _angle=None): """ Initializes the transformation parameters that allows conversion between stage-bare and sample plane. """🔎 Option 2: Use the angle parameter
Refactor to pass the angle through to
_update_conversionand avoid the metadata lookup for rx_mill. This would require more substantial changes to track the milling angle explicitly rather than reading from metadata each time.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/odemis/acq/move.pysrc/odemis/acq/test/move_tescan_test.pysrc/odemis/gui/cont/tabs/fibsem_tab.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-19T08:30:13.018Z
Learnt from: tmoerkerken
Repo: delmic/odemis PR: 3200
File: src/odemis/gui/cont/tabs/fibsem_tab.py:357-369
Timestamp: 2025-08-19T08:30:13.018Z
Learning: In the FIBSEM tab GUI event handling system, warning dialogs for stage movement are informational only and should not prevent event propagation. The actual movement prevention is handled through canvas abilities (CAN_MOVE_STAGE) rather than stopping events at the handler level, as dragging and double-clicking are processed by lower-level handlers that need to receive the events.
Applied to files:
src/odemis/gui/cont/tabs/fibsem_tab.py
📚 Learning: 2025-09-25T16:47:08.868Z
Learnt from: pieleric
Repo: delmic/odemis PR: 3202
File: src/odemis/acq/move.py:1158-1159
Timestamp: 2025-09-25T16:47:08.868Z
Learning: In TFS3 PostureManager coordinate transformations, using `pos` instead of `transformed_pos` for the input coordinates improves readability. The original use of `transformed_pos` was defensive programming from when `pos` could be modified mid-way through transformations.
Applied to files:
src/odemis/acq/move.py
🧬 Code graph analysis (3)
src/odemis/acq/test/move_tescan_test.py (2)
src/odemis/acq/move.py (1)
cryoSwitchSamplePosition(136-151)src/odemis/util/testing.py (1)
assert_pos_almost_equal(172-183)
src/odemis/gui/cont/tabs/fibsem_tab.py (5)
src/odemis/driver/tescan.py (1)
CancelledError(69-71)src/odemis/model/_futures.py (1)
InstantaneousFuture(235-282)src/odemis/gui/comp/text.py (1)
SetValueRange(837-839)src/odemis/gui/util/__init__.py (1)
call_in_wx_main(41-60)src/odemis/acq/move.py (3)
getCurrentPostureLabel(127-134)getCurrentPostureLabel(390-415)cryoSwitchSamplePosition(136-151)
src/odemis/acq/move.py (1)
src/odemis/model/_vattributes.py (1)
FloatContinuous(1072-1082)
🪛 Ruff (0.14.10)
src/odemis/gui/cont/tabs/fibsem_tab.py
455-455: Unused method argument: evt
(ARG002)
461-461: Unused method argument: evt
(ARG002)
477-477: Unused method argument: future
(ARG002)
src/odemis/acq/move.py
1648-1648: Unused method argument: angle
(ARG002)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build (ubuntu-22.04)
- GitHub Check: build (ubuntu-24.04)
🔇 Additional comments (10)
src/odemis/acq/test/move_tescan_test.py (2)
25-25: LGTM! Import additions support the new milling angle test.The MILLING constant and testing utilities are appropriately imported for the new test method.
Also applies to: 27-27
33-33: LGTM! Config path updated for FIBSEM simulation.The config file name change clarifies this is for FIBSEM testing with milling support.
src/odemis/gui/cont/tabs/fibsem_tab.py (5)
25-25: LGTM! Imports support async posture switching.CancelledError and BTN_TOGGLE_PROGRESS are properly used in the new asynchronous posture management flow.
Also applies to: 46-46
188-188: LGTM! Proper initialization of async state and UI range.The
_posture_switch_futureinitialization withInstantaneousFuture()ensures initial state checks work correctly (always.done()). The milling angle range conversion from radians to integer degrees is appropriate for UI display.Also applies to: 194-195
198-199: LGTM! Improved naming and thread safety.The rename from "position" to "posture" better reflects the semantic meaning. The
@call_in_wx_maindecorator ensures_on_viewupdates the UI safely from any thread.Also applies to: 202-202
400-414: LGTM! UI updates properly deferred during posture transitions.The check
if self._posture_switch_future.done()prevents UI state updates while a posture switch is in progress, avoiding flicker and displaying intermediate states. The button enable/disable logic correctly restricts controls to SEM_IMAGING and MILLING postures.
455-464: LGTM! Async posture switching methods properly implemented.The methods correctly:
- Initiate non-blocking posture switches via
cryoSwitchSamplePosition- Store the future for state tracking
- Register completion callbacks
The
evtparameter warnings from static analysis are false positives—these parameters are required by the wx.EVT_BUTTON binding signature.src/odemis/acq/move.py (3)
86-86: LGTM! MILLING_RANGE correctly converted to radians.The conversion from degrees to radians aligns with internal angle handling conventions and the FloatContinuous VA unit specification.
522-529: LGTM! Milling angle setter correctly updates metadata.The setter properly:
- Preserves the existing
rzvalue from metadata- Updates
MD_FAV_MILL_POS_ACTIVEwith the new milling angle- Returns the angle value as required by the VA setter contract
742-743: LGTM! Special MILLING handling enables dynamic angle updates.The condition
if posture != MILLING and position_posture == posturebypasses the early exit when the target is MILLING, even if already at that posture. This is necessary to recalculate positions when the milling angle changes (supporting the MILLING→MILLING transformation at line 371).
| # Initialize the milling angle. If not specified in the config, set it to 0 | ||
| milling_angle = stage_md.get(model.MD_FAV_MILL_POS_ACTIVE, None) | ||
| milling_angle = milling_angle["rx"] if milling_angle else 0 | ||
| self.milling_angle = model.FloatContinuous( | ||
| milling_angle, (MILLING_RANGE[0], MILLING_RANGE[1]), unit="rad", setter=self._set_milling_angle | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, check the file structure and context around lines 335-340
head -n 350 src/odemis/acq/move.py | tail -n 30Repository: delmic/odemis
Length of output: 1928
🏁 Script executed:
# Search for milling_angle usage and any guards/conditions
rg -n "milling_angle" src/odemis/acq/move.py -A 2 -B 2Repository: delmic/odemis
Length of output: 4042
🏁 Script executed:
# Check if there's any capability detection for milling support
rg -n "MD_FAV_MILL_POS_ACTIVE|hasattr.*milling|milling.*support" src/odemis/acq/move.py -B 3 -A 3Repository: delmic/odemis
Length of output: 5668
🏁 Script executed:
# Look at the broader class context - what class is this code in?
sed -n '300,350p' src/odemis/acq/move.pyRepository: delmic/odemis
Length of output: 2886
The milling_angle setter needs a guard for non-milling systems.
The VA initialization with default 0 is safe, but the setter at line 527 will crash on systems without milling support. The setter directly accesses MD_FAV_MILL_POS_ACTIVE["rz"] without checking if the metadata key exists. While MILLING posture operations are guarded elsewhere, direct VA modification (e.g., via automation) on non-milling systems will raise a KeyError. Add a check in _set_milling_angle() to verify MD_FAV_MILL_POS_ACTIVE exists before accessing it, or document that this VA should not be accessible on non-milling systems.
🤖 Prompt for AI Agents
In src/odemis/acq/move.py around lines 335-340 (and the setter at ~line 527),
the milling_angle VA is initialized but the _set_milling_angle() setter assumes
MD_FAV_MILL_POS_ACTIVE exists and directly reads ["rz"], which will KeyError on
non-milling systems; update _set_milling_angle() to first check for the presence
of model.MD_FAV_MILL_POS_ACTIVE in stage_md (and that it contains the "rz" key)
before accessing it, and if missing either (a) no-op and log a debug/warning
that milling is unsupported, or (b) raise a clear ValueError indicating the VA
is not available on non-milling systems — choose one behavior and implement it
consistently so the setter cannot crash on systems without milling support.
| def _on_milling_move_complete(self, future): | ||
| try: | ||
| future.result() | ||
| except CancelledError: | ||
| logging.info("Posture switch was cancelled") | ||
| except Exception: | ||
| logging.exception("Failed to switch posture") | ||
|
|
||
| def _move_to_sem(self, evt: wx.Event): | ||
| f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING) | ||
| f.result() | ||
| self._on_stage_pos(self.pm.stage.position.value) | ||
|
|
||
| @call_in_wx_main | ||
| def _on_sem_move_complete(self, future): | ||
| self._on_stage_pos(self.pm.stage.position.value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add exception handling to _on_sem_move_complete.
The milling completion handler properly handles CancelledError and general exceptions (lines 466-474), but the SEM completion handler (lines 477-478) lacks any exception handling. This inconsistency could allow unhandled exceptions from the SEM posture switch to propagate and cause issues.
🔎 Proposed fix for consistent exception handling
@call_in_wx_main
def _on_sem_move_complete(self, future):
+ try:
+ future.result()
+ except CancelledError:
+ logging.info("Posture switch was cancelled")
+ except Exception:
+ logging.exception("Failed to switch posture")
+
self._on_stage_pos(self.pm.stage.position.value)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _on_milling_move_complete(self, future): | |
| try: | |
| future.result() | |
| except CancelledError: | |
| logging.info("Posture switch was cancelled") | |
| except Exception: | |
| logging.exception("Failed to switch posture") | |
| def _move_to_sem(self, evt: wx.Event): | |
| f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING) | |
| f.result() | |
| self._on_stage_pos(self.pm.stage.position.value) | |
| @call_in_wx_main | |
| def _on_sem_move_complete(self, future): | |
| self._on_stage_pos(self.pm.stage.position.value) | |
| def _on_milling_move_complete(self, future): | |
| try: | |
| future.result() | |
| except CancelledError: | |
| logging.info("Posture switch was cancelled") | |
| except Exception: | |
| logging.exception("Failed to switch posture") | |
| self._on_stage_pos(self.pm.stage.position.value) | |
| @call_in_wx_main | |
| def _on_sem_move_complete(self, future): | |
| try: | |
| future.result() | |
| except CancelledError: | |
| logging.info("Posture switch was cancelled") | |
| except Exception: | |
| logging.exception("Failed to switch posture") | |
| self._on_stage_pos(self.pm.stage.position.value) |
🧰 Tools
🪛 Ruff (0.14.10)
477-477: Unused method argument: future
(ARG002)
🤖 Prompt for AI Agents
In src/odemis/gui/cont/tabs/fibsem_tab.py around lines 466 to 478, the
_on_sem_move_complete handler lacks exception handling causing exceptions from
the SEM posture switch to propagate; wrap the future.result() call in a
try/except like _on_milling_move_complete: call future.result() inside try,
catch CancelledError and log an info message, catch Exception and log.exception
with a descriptive message, and finally call
self._on_stage_pos(self.pm.stage.position.value); keep the @call_in_wx_main
decorator and mirror the existing logging behavior for consistency.
59e4c65 to
27d508e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Fix all issues with AI Agents 🤖
In @src/odemis/gui/cont/tabs/fibsem_tab.py:
- Around line 447-455: Existing posture-switch futures are replaced without
cancellation, leaving orphaned background tasks; before assigning a new future
in _move_to_milling_posture and _move_to_sem_posture, check for an existing
self._posture_switch_future and call its cancel() (and optionally only if it’s
not done/cancelled) to stop the previous operation before creating the new
future, then proceed to assign the new future and add_done_callback as before.
♻️ Duplicate comments (1)
src/odemis/gui/cont/tabs/fibsem_tab.py (1)
468-470: Add exception handling to_on_sem_move_complete.The SEM posture completion handler lacks exception handling, unlike
_on_milling_move_complete(lines 458-466). This inconsistency means errors or cancellations during SEM posture switches are silently ignored without logging or user feedback.🔎 Proposed fix for consistent exception handling
@call_in_wx_main def _on_sem_move_complete(self, future): + try: + future.result() + except CancelledError: + logging.info("Posture switch was cancelled") + except Exception: + logging.exception("Failed to switch posture") + self._on_stage_pos(self.pm.stage.position.value)Based on past review comments, this issue was previously identified but remains unaddressed.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/odemis/gui/cont/tabs/fibsem_tab.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-19T08:30:13.018Z
Learnt from: tmoerkerken
Repo: delmic/odemis PR: 3200
File: src/odemis/gui/cont/tabs/fibsem_tab.py:357-369
Timestamp: 2025-08-19T08:30:13.018Z
Learning: In the FIBSEM tab GUI event handling system, warning dialogs for stage movement are informational only and should not prevent event propagation. The actual movement prevention is handled through canvas abilities (CAN_MOVE_STAGE) rather than stopping events at the handler level, as dragging and double-clicking are processed by lower-level handlers that need to receive the events.
Applied to files:
src/odemis/gui/cont/tabs/fibsem_tab.py
🧬 Code graph analysis (1)
src/odemis/gui/cont/tabs/fibsem_tab.py (3)
src/odemis/driver/tescan.py (1)
CancelledError(69-71)src/odemis/model/_futures.py (1)
InstantaneousFuture(235-282)src/odemis/acq/move.py (1)
cryoSwitchSamplePosition(136-151)
🪛 Ruff (0.14.10)
src/odemis/gui/cont/tabs/fibsem_tab.py
447-447: Unused method argument: evt
(ARG002)
453-453: Unused method argument: evt
(ARG002)
469-469: Unused method argument: future
(ARG002)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build (ubuntu-22.04)
- GitHub Check: build (ubuntu-24.04)
🔇 Additional comments (7)
src/odemis/gui/cont/tabs/fibsem_tab.py (7)
25-26: LGTM! Import additions support async posture switching.The additions of
CancelledErrorandBTN_TOGGLE_PROGRESScorrectly support the new asynchronous posture switching logic with proper cancellation handling and UI state indication.Also applies to: 46-46
188-188: LGTM! Appropriate initial state for posture switch tracking.Initializing with
InstantaneousFuture()correctly represents "no switch in progress" since it's immediately done, allowing initial UI updates to proceed.
194-195: LGTM! Proper unit conversion for UI.The conversion from
MILLING_RANGE(radians) to integer degrees usingnumpy.rad2degand rounding is appropriate for the tilt angle UI control, preventing excessive decimal places.
198-199: LGTM! Method renames and thread-safety decorator.The renames to
_move_to_milling_postureand_move_to_sem_posturebetter reflect posture-based terminology. Adding@call_in_wx_mainto_on_viewensures thread-safe UI updates when this subscriber callback is invoked.Also applies to: 202-202
400-414: LGTM! Guarded UI updates prevent intermediate states.Gating button enable/disable and value updates on
_posture_switch_future.done()correctly prevents UI flickering or incorrect states during ongoing posture transitions. Completion callbacks will trigger a final refresh.
416-445: LGTM! Milling angle update logic correctly handles posture coordination.The refactored flow properly:
- Updates
milling_anglevia the posture manager VA (triggering internal MD updates)- Propagates the resulting
stage_tiltto existing feature positions- Triggers a posture switch when already at MILLING to apply the new angle immediately
The check for
evt is Noneappropriately skips UI interactions during initialization.
447-455: LGTM! Correct asynchronous posture switch pattern.Both methods properly initiate non-blocking posture switches via
cryoSwitchSamplePosition, store the future, and attach completion callbacks. Theevtparameter is unused but required by thewx.EVT_BUTTONhandler signature.
4b461e1 to
92f477c
Compare
92f477c to
b8966e1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI Agents
In @src/odemis/acq/move.py:
- Around line 522-529: The setter _set_milling_angle accesses
model.MD_FAV_MILL_POS_ACTIVE["rz"] without checking presence, causing KeyError
on non-milling systems; fix by reading metadata = self.stage.getMetadata(),
retrieving fav = metadata.get(model.MD_FAV_MILL_POS_ACTIVE) and use
fav.get("rz") if fav and "rz" in fav else a safe default (e.g., 0.0) before
building rotations = {'rx': angle, 'rz': rz_value}, then call
self.stage.updateMetadata(...).
In @src/odemis/gui/cont/tabs/fibsem_tab.py:
- Around line 446-454: The handlers _move_to_milling_posture and
_move_to_sem_posture replace self._posture_switch_future without cancelling any
previous task; before assigning a new future, check if
self._posture_switch_future exists and is not done, call its cancel() (and
optionally ignore CancellationError in its callback), then assign the new future
from pm.cryoSwitchSamplePosition(MILLING) /
pm.cryoSwitchSamplePosition(SEM_IMAGING) and attach the appropriate
add_done_callback (_on_milling_move_complete or _on_sem_move_complete) after
assignment to avoid orphaned async tasks.
- Around line 468-469: The SEM move completion handler _on_sem_move_complete
currently calls _on_stage_pos without any error handling; wrap its body in a
try/except similar to the milling handler: catch asyncio.CancelledError (or
concurrent.futures.CancelledError) and return quietly, and catch Exception to
log the error (use the same logger used elsewhere, e.g., self._logger.exception
or self._logger.error) so unexpected exceptions from the SEM posture switch
don’t propagate; keep the existing call to
self._on_stage_pos(self.pm.stage.position.value) inside the try block.
🧹 Nitpick comments (1)
src/odemis/acq/move.py (1)
1649-1655: Consider passing the angle parameter through for clarity.The
angleparameter from themilling_anglesubscriber is unused. Instead,_update_conversionreads the updated value from stage metadata (line 1691). While functional, passing the angle directly would make the data flow clearer and reduce metadata access.🔎 Optional refactor for clarity
-def _initialise_transformation(self, angle): +def _initialise_transformation(self, angle: float): """ Initializes the transformation parameters that allows conversion between stage-bare and sample plane. + :param angle: milling angle in radians (from the VA subscriber) """ self._axes_dep = {"x": self.linked_axes[0], "y": self.linked_axes[1]} - self._update_conversion(self.pre_tilt) + # Note: _update_conversion will read the milling angle from stage metadata, + # which was already updated by the VA setter + self._update_conversion(self.pre_tilt) # angle is implicitly used via metadata self._initialise_offset()Alternatively, refactor to pass the angle to
_update_conversionand avoid the metadata re-read, but this would require broader changes.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/odemis/acq/move.pysrc/odemis/acq/test/move_tescan_test.pysrc/odemis/gui/cont/tabs/fibsem_tab.py
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: pieleric
Repo: delmic/odemis PR: 3202
File: src/odemis/acq/move.py:1158-1159
Timestamp: 2025-09-25T16:47:08.868Z
Learning: In TFS3 PostureManager coordinate transformations, using `pos` instead of `transformed_pos` for the input coordinates improves readability. The original use of `transformed_pos` was defensive programming from when `pos` could be modified mid-way through transformations.
📚 Learning: 2025-08-19T08:30:13.018Z
Learnt from: tmoerkerken
Repo: delmic/odemis PR: 3200
File: src/odemis/gui/cont/tabs/fibsem_tab.py:357-369
Timestamp: 2025-08-19T08:30:13.018Z
Learning: In the FIBSEM tab GUI event handling system, warning dialogs for stage movement are informational only and should not prevent event propagation. The actual movement prevention is handled through canvas abilities (CAN_MOVE_STAGE) rather than stopping events at the handler level, as dragging and double-clicking are processed by lower-level handlers that need to receive the events.
Applied to files:
src/odemis/gui/cont/tabs/fibsem_tab.py
📚 Learning: 2025-09-25T16:47:08.868Z
Learnt from: pieleric
Repo: delmic/odemis PR: 3202
File: src/odemis/acq/move.py:1158-1159
Timestamp: 2025-09-25T16:47:08.868Z
Learning: In TFS3 PostureManager coordinate transformations, using `pos` instead of `transformed_pos` for the input coordinates improves readability. The original use of `transformed_pos` was defensive programming from when `pos` could be modified mid-way through transformations.
Applied to files:
src/odemis/acq/move.py
🧬 Code graph analysis (3)
src/odemis/gui/cont/tabs/fibsem_tab.py (5)
src/odemis/driver/tescan.py (1)
CancelledError(69-71)src/odemis/model/_futures.py (1)
InstantaneousFuture(235-282)src/odemis/gui/comp/text.py (1)
SetValueRange(837-839)src/odemis/gui/util/__init__.py (1)
call_in_wx_main(41-60)src/odemis/acq/move.py (1)
cryoSwitchSamplePosition(136-151)
src/odemis/acq/test/move_tescan_test.py (4)
src/odemis/acq/test/move_tfs1_test.py (1)
TestMeteorTFS1Move(37-292)src/odemis/acq/move.py (1)
cryoSwitchSamplePosition(136-151)src/odemis/util/testing.py (1)
assert_pos_almost_equal(172-183)src/odemis/model/_components.py (1)
model(570-571)
src/odemis/acq/move.py (1)
src/odemis/model/_vattributes.py (1)
FloatContinuous(1072-1082)
🪛 Ruff (0.14.10)
src/odemis/gui/cont/tabs/fibsem_tab.py
446-446: Unused method argument: evt
(ARG002)
452-452: Unused method argument: evt
(ARG002)
468-468: Unused method argument: future
(ARG002)
src/odemis/acq/move.py
1649-1649: Unused method argument: angle
(ARG002)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build (ubuntu-22.04)
- GitHub Check: build (ubuntu-24.04)
🔇 Additional comments (6)
src/odemis/acq/test/move_tescan_test.py (1)
126-154: LGTM! Well-structured test for milling angle stability.The test correctly verifies that:
- Sample stage position remains stable across SEM ↔ MILLING transitions
- Updating the milling angle doesn't unexpectedly shift the sample position
- Position consistency is maintained after the angle change
The test structure is clear and uses appropriate tolerances.
src/odemis/gui/cont/tabs/fibsem_tab.py (2)
188-188: LGTM! Good addition of posture switch tracking.Initializing
_posture_switch_futurewithInstantaneousFuture()provides a clean way to track asynchronous posture switches and check completion status before updating UI state.
194-195: LGTM! Correct conversion for UI display.The conversion from radians to integer degrees is appropriate for the UI control, preventing excessive decimal places while maintaining the correct range.
src/odemis/acq/move.py (3)
86-86: LGTM! Correct conversion to radians.Converting
MILLING_RANGEto radians aligns with the internal representation used throughout the codebase for angular values.
335-340: Safe initialization, but verify setter guard.The VA initialization safely defaults to 0 for non-milling systems. However, ensure the setter (line 527) has appropriate guards—see separate comment on that method.
742-744: LGTM! Correct handling of dynamic milling angle.Skipping the fast-path when the target posture is
MILLINGcorrectly handles the case where the milling angle has changed, requiring re-transformation even when already at the milling posture.
| def _set_milling_angle(self, angle: float): | ||
| """ | ||
| Set the milling angle of the stage | ||
| :param angle: (float) milling angle in radians | ||
| """ | ||
| rotations = {'rx': angle, 'rz': self.stage.getMetadata()[model.MD_FAV_MILL_POS_ACTIVE]["rz"]} | ||
| self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: rotations}) | ||
| return angle |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add guard for non-milling systems in the setter.
Line 527 directly accesses MD_FAV_MILL_POS_ACTIVE["rz"] without checking if the metadata key exists. On systems without milling support, this will raise a KeyError if the VA is modified directly (e.g., via automation).
🔎 Proposed fix
def _set_milling_angle(self, angle: float):
"""
Set the milling angle of the stage
:param angle: (float) milling angle in radians
"""
+ stage_md = self.stage.getMetadata()
+ if model.MD_FAV_MILL_POS_ACTIVE not in stage_md:
+ raise ValueError("Milling angle is not supported on this system (MD_FAV_MILL_POS_ACTIVE not available)")
+
- rotations = {'rx': angle, 'rz': self.stage.getMetadata()[model.MD_FAV_MILL_POS_ACTIVE]["rz"]}
+ rotations = {'rx': angle, 'rz': stage_md[model.MD_FAV_MILL_POS_ACTIVE]["rz"]}
self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: rotations})
return angle📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _set_milling_angle(self, angle: float): | |
| """ | |
| Set the milling angle of the stage | |
| :param angle: (float) milling angle in radians | |
| """ | |
| rotations = {'rx': angle, 'rz': self.stage.getMetadata()[model.MD_FAV_MILL_POS_ACTIVE]["rz"]} | |
| self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: rotations}) | |
| return angle | |
| def _set_milling_angle(self, angle: float): | |
| """ | |
| Set the milling angle of the stage | |
| :param angle: (float) milling angle in radians | |
| """ | |
| stage_md = self.stage.getMetadata() | |
| if model.MD_FAV_MILL_POS_ACTIVE not in stage_md: | |
| raise ValueError("Milling angle is not supported on this system (MD_FAV_MILL_POS_ACTIVE not available)") | |
| rotations = {'rx': angle, 'rz': stage_md[model.MD_FAV_MILL_POS_ACTIVE]["rz"]} | |
| self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: rotations}) | |
| return angle |
🤖 Prompt for AI Agents
In @src/odemis/acq/move.py around lines 522 - 529, The setter _set_milling_angle
accesses model.MD_FAV_MILL_POS_ACTIVE["rz"] without checking presence, causing
KeyError on non-milling systems; fix by reading metadata =
self.stage.getMetadata(), retrieving fav =
metadata.get(model.MD_FAV_MILL_POS_ACTIVE) and use fav.get("rz") if fav and "rz"
in fav else a safe default (e.g., 0.0) before building rotations = {'rx': angle,
'rz': rz_value}, then call self.stage.updateMetadata(...).
| def _move_to_milling_posture(self, evt: wx.Event): | ||
| self._posture_switch_future = self.pm.cryoSwitchSamplePosition(MILLING) | ||
|
|
||
| # Do NOT call f.result(). Instead, add a callback: | ||
| self._posture_switch_future.add_done_callback(self._on_milling_move_complete) | ||
|
|
||
| def _move_to_sem_posture(self, evt: wx.Event): | ||
| self._posture_switch_future = self.pm.cryoSwitchSamplePosition(SEM_IMAGING) | ||
| self._posture_switch_future.add_done_callback(self._on_sem_move_complete) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cancel the previous posture switch before initiating a new one.
If a user rapidly clicks between posture buttons, the previous _posture_switch_future is replaced without cancellation, leaving an orphaned async task running. Add cancellation logic before assigning a new future.
🔎 Proposed fix
def _move_to_milling_posture(self, evt: wx.Event):
+ if self._posture_switch_future and not self._posture_switch_future.done():
+ self._posture_switch_future.cancel()
self._posture_switch_future = self.pm.cryoSwitchSamplePosition(MILLING)
# Do NOT call f.result(). Instead, add a callback:
self._posture_switch_future.add_done_callback(self._on_milling_move_complete)
def _move_to_sem_posture(self, evt: wx.Event):
+ if self._posture_switch_future and not self._posture_switch_future.done():
+ self._posture_switch_future.cancel()
self._posture_switch_future = self.pm.cryoSwitchSamplePosition(SEM_IMAGING)
self._posture_switch_future.add_done_callback(self._on_sem_move_complete)🧰 Tools
🪛 Ruff (0.14.10)
446-446: Unused method argument: evt
(ARG002)
452-452: Unused method argument: evt
(ARG002)
🤖 Prompt for AI Agents
In @src/odemis/gui/cont/tabs/fibsem_tab.py around lines 446 - 454, The handlers
_move_to_milling_posture and _move_to_sem_posture replace
self._posture_switch_future without cancelling any previous task; before
assigning a new future, check if self._posture_switch_future exists and is not
done, call its cancel() (and optionally ignore CancellationError in its
callback), then assign the new future from pm.cryoSwitchSamplePosition(MILLING)
/ pm.cryoSwitchSamplePosition(SEM_IMAGING) and attach the appropriate
add_done_callback (_on_milling_move_complete or _on_sem_move_complete) after
assignment to avoid orphaned async tasks.
| def _on_sem_move_complete(self, future): | ||
| self._on_stage_pos(self.pm.stage.position.value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add exception handling to _on_sem_move_complete.
The milling completion handler properly catches CancelledError and general exceptions (lines 458-463), but the SEM completion handler lacks any exception handling. This inconsistency could allow unhandled exceptions from the SEM posture switch to propagate.
🔎 Proposed fix
@call_in_wx_main
def _on_sem_move_complete(self, future):
+ try:
+ future.result()
+ except CancelledError:
+ logging.info("Posture switch was cancelled")
+ except Exception:
+ logging.exception("Failed to switch posture")
+
self._on_stage_pos(self.pm.stage.position.value)🧰 Tools
🪛 Ruff (0.14.10)
468-468: Unused method argument: future
(ARG002)
🤖 Prompt for AI Agents
In @src/odemis/gui/cont/tabs/fibsem_tab.py around lines 468 - 469, The SEM move
completion handler _on_sem_move_complete currently calls _on_stage_pos without
any error handling; wrap its body in a try/except similar to the milling
handler: catch asyncio.CancelledError (or concurrent.futures.CancelledError) and
return quietly, and catch Exception to log the error (use the same logger used
elsewhere, e.g., self._logger.exception or self._logger.error) so unexpected
exceptions from the SEM posture switch don’t propagate; keep the existing call
to self._on_stage_pos(self.pm.stage.position.value) inside the try block.
Uh oh!
There was an error while loading. Please reload this page.