Skip to content

Conversation

@tmoerkerken
Copy link
Contributor

@tmoerkerken tmoerkerken commented Dec 24, 2025

  • Add milling angle as VA in posture manager
  • Utilize VA callback for updating the sample stage matrices
  • Add functionality to change milling angle while already being at the milling posture
  • Add unit test to test sample stage position stability for different postures and milling angles

@coderabbitai
Copy link

coderabbitai bot commented Dec 24, 2025

📝 Walkthrough

Walkthrough

MILLING_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
Loading
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
Loading

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is empty and provides no information about the changeset. Add a meaningful description explaining the purpose, motivation, and impact of refactoring milling angle as a VA, including testing considerations.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title '[MSD-40][refactor] Milling angle as VA' clearly summarizes the main change: refactoring milling angle to be exposed as a Value Attribute (VA) in the module.
✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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:

  1. Uses the milling_angle VA setter to trigger transformation updates
  2. Updates existing feature positions with the new milling angle
  3. 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 the angle parameter usage or refactor the subscription.

The milling_angle VA subscribes to _initialise_transformation with init=True (line 1642), passing the angle value as a parameter. However, the _initialise_transformation method (line 1648) receives but doesn't use this angle parameter, instead relying on self.pre_tilt (line 1653) and metadata lookups in _update_conversion (lines 1688-1692).

This works because _set_milling_angle already updated the metadata before the subscription fires, but the flow is confusing.

Options:

  1. Remove the parameter and make the subscription trigger without arguments
  2. Use the parameter by passing it through to _update_conversion instead 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_conversion and 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8ad878c and 6148f22.

📒 Files selected for processing (3)
  • src/odemis/acq/move.py
  • src/odemis/acq/test/move_tescan_test.py
  • src/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_future initialization with InstantaneousFuture() 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_main decorator ensures _on_view updates 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:

  1. Initiate non-blocking posture switches via cryoSwitchSamplePosition
  2. Store the future for state tracking
  3. Register completion callbacks

The evt parameter 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:

  1. Preserves the existing rz value from metadata
  2. Updates MD_FAV_MILL_POS_ACTIVE with the new milling angle
  3. 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 == posture bypasses 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).

Comment on lines +335 to +340
# 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
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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 30

Repository: 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 2

Repository: 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 3

Repository: 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.py

Repository: 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.

Comment on lines 466 to 469
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)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

@tmoerkerken tmoerkerken changed the title Milling angle as VA [MSD-40] [refactor] Milling angle as VA Jan 5, 2026
@tmoerkerken tmoerkerken force-pushed the milling-angle-as-va branch 2 times, most recently from 59e4c65 to 27d508e Compare January 5, 2026 10:08
@tmoerkerken tmoerkerken changed the title [MSD-40] [refactor] Milling angle as VA [MSD-40][refactor] Milling angle as VA Jan 5, 2026
Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6148f22 and 59e4c65.

📒 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 CancelledError and BTN_TOGGLE_PROGRESS correctly 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 using numpy.rad2deg and 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_posture and _move_to_sem_posture better reflect posture-based terminology. Adding @call_in_wx_main to _on_view ensures 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_angle via the posture manager VA (triggering internal MD updates)
  • Propagates the resulting stage_tilt to existing feature positions
  • Triggers a posture switch when already at MILLING to apply the new angle immediately

The check for evt is None appropriately 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. The evt parameter is unused but required by the wx.EVT_BUTTON handler signature.

@tmoerkerken tmoerkerken force-pushed the milling-angle-as-va branch 2 times, most recently from 4b461e1 to 92f477c Compare January 5, 2026 10:12
Copy link

@coderabbitai coderabbitai bot left a 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 angle parameter from the milling_angle subscriber is unused. Instead, _update_conversion reads 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_conversion and avoid the metadata re-read, but this would require broader changes.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59e4c65 and b8966e1.

📒 Files selected for processing (3)
  • src/odemis/acq/move.py
  • src/odemis/acq/test/move_tescan_test.py
  • src/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:

  1. Sample stage position remains stable across SEM ↔ MILLING transitions
  2. Updating the milling angle doesn't unexpectedly shift the sample position
  3. 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_future with InstantaneousFuture() 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_RANGE to 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 MILLING correctly handles the case where the milling angle has changed, requiring re-transformation even when already at the milling posture.

Comment on lines +522 to +529
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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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(...).

Comment on lines +446 to +454
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)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +468 to 469
def _on_sem_move_complete(self, future):
self._on_stage_pos(self.pm.stage.position.value)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

@tmoerkerken tmoerkerken marked this pull request as ready for review January 6, 2026 13:14
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.

3 participants