Skip to content

Conversation

@pieleric
Copy link
Member

The SEM scan rotation has an indirect effect on the FM movement: the FM
image is always transposed to match the SEM (and FIB) image, by
adjusting the microscope file. So if there is 180° scan rotation, the
FM relative moves need to be inverted.

This also ensure that old systems (where there is no scan rotation) do
still move in the right direction when using the new sample stage code.

@pieleric pieleric changed the title [fix] Tescan Posture Manager: FM sample stage move should depend on SEM rotation [MSD-237][fix] Tescan Posture Manager: FM sample stage move should depend on SEM rotation Dec 19, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 19, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Scan-rotation compensation was added to the Tescan posture manager: _get_scan_rotation now returns 0 with info-level logging when beams are unavailable, and MeteorTescan1PostureManager._update_conversion computes a scan-rotation (sr), sets scanner rotation metadata via _set_scanner_rotation_cor(sr), and composes tf_sr with existing tilt transforms so fm/sem/milling transforms incorporate scan-rotation compensation (tf_fm becomes tf_tilt @ tf_sr). Redundant recomputation of sr was removed. Logging for unavailable e-/ion-beams was downgraded from warning to info. Tests were extended with setup scaffolding and a new test validating relative FM posture movements.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding SEM rotation dependency to FM sample stage movement in the Tescan Posture Manager.
Description check ✅ Passed The description is directly related to the changeset, explaining the rationale for scan rotation compensation and its effects on FM movements.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 1

🧹 Nitpick comments (1)
src/odemis/acq/test/move_tescan_test.py (1)

123-131: Consider adding strict=True to zip() for safer iteration.

The static analysis tool flagged this zip() call without an explicit strict= parameter. While both lists currently have the same length, adding strict=True would catch accidental mismatches if the test data is modified in the future.

🔎 Proposed fix
-        for m_sample, m_bare in zip(sample_stage_moves, stage_bare_moves):
+        for m_sample, m_bare in zip(sample_stage_moves, stage_bare_moves, strict=True):
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed4659c and b20c904.

📒 Files selected for processing (2)
  • src/odemis/acq/move.py (2 hunks)
  • src/odemis/acq/test/move_tescan_test.py (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/odemis/acq/test/move_tescan_test.py (5)
src/odemis/acq/test/move_jeol_test.py (1)
  • setUpClass (49-58)
src/odemis/acq/test/move_tfs1_test.py (1)
  • setUpClass (45-57)
src/odemis/acq/test/move_util_test.py (1)
  • setUpClass (48-52)
src/odemis/acq/move.py (2)
  • getCurrentPostureLabel (127-134)
  • getCurrentPostureLabel (384-409)
src/odemis/util/testing.py (1)
  • assert_pos_almost_equal (172-183)
src/odemis/acq/move.py (1)
src/odemis/util/transform.py (1)
  • get_rotation_transforms (1302-1325)
🪛 Ruff (0.14.8)
src/odemis/acq/test/move_tescan_test.py

123-123: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

⏰ 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). (4)
  • GitHub Check: Agent
  • GitHub Check: CodeQL analysis (python)
  • GitHub Check: build (ubuntu-22.04)
  • GitHub Check: build (ubuntu-24.04)
🔇 Additional comments (3)
src/odemis/acq/test/move_tescan_test.py (1)

46-55: Good test isolation pattern.

The setUpClass captures initial metadata and setUp restores it before each test, ensuring test isolation. This follows the established pattern in other test files.

src/odemis/acq/move.py (2)

639-640: Appropriate logging level for expected configuration.

Downgrading from warning to info is correct here since missing e-beam/ion-beam is an expected configuration on older systems, not an error condition. This aligns with the PR objective of ensuring older systems continue to work correctly.


1664-1673: Scan rotation compensation looks correct.

The implementation correctly:

  1. Retrieves scan rotation from the beams (falling back to 0° if unavailable)
  2. Propagates the rotation correction to scanners via _set_scanner_rotation_cor
  3. Creates a Z-axis rotation transform with negated angle (rz=-sr) to compensate
  4. Composes with tilt transform for FM imaging (tf_fm = tf_tilt @ tf_sr)

This aligns with the PR objective that FM relative moves must be inverted when a 180° scan rotation is present.

Comment on lines 36 to 37
METEOR_TESCAN1_FIBSEM_CONFIG = CONFIG_PATH + "sim/meteor-tescan-fibsem-full-sim.odm.yaml"

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

🧩 Analysis chain

🏁 Script executed:

rg -n "METEOR_TESCAN1_FIBSEM_CONFIG" --type py

Repository: delmic/odemis

Length of output: 189


Remove unused constant or use it in a test.

METEOR_TESCAN1_FIBSEM_CONFIG is defined on line 36 but never used in this file or elsewhere in the codebase. Either remove it or incorporate it into an actual test case.

🤖 Prompt for AI Agents
In src/odemis/acq/test/move_tescan_test.py around lines 36-37, the constant
METEOR_TESCAN1_FIBSEM_CONFIG is defined but unused; either remove the constant
to eliminate dead code, or update/add a test to reference it (for example pass
it into the test fixture/setup that loads CONFIG_PATH files or use it in a
parametrized test that validates loading/parsing of that YAML). If you choose to
use it, ensure the test asserts expected behavior after loading the file and
imports CONFIG_PATH or uses the same path-building logic so the constant is
actually exercised.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes an issue where the FM (Fluorescence Microscope) sample stage movement didn't properly account for SEM (Scanning Electron Microscope) scan rotation. The FM image is transposed to match the SEM/FIB images, so when there's a 180° scan rotation, FM relative moves need to be inverted accordingly. This fix ensures correct movement behavior on both new systems (with scan rotation) and old systems (without scan rotation).

Key changes:

  • Modified the transformation matrix calculation order to apply scan rotation compensation to FM imaging mode
  • Changed FM transformation from tf_reverse @ tf_tilt to tf_tilt @ tf_sr to include scan rotation
  • Added comprehensive tests to verify FM posture relative movements match expected hardware behavior

Reviewed changes

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

File Description
src/odemis/acq/move.py Updated _update_conversion to compute scan rotation earlier and apply it to FM transformation matrix; changed logging level from warning to info for missing beam components
src/odemis/acq/test/move_tescan_test.py Added test infrastructure (setUp/setUpClass) to manage metadata state between tests; added new test test_rel_move_fm_posture to validate FM relative movements with scan rotation

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

CONFIG_PATH = os.path.dirname(odemis.__file__) + "/../../install/linux/usr/share/odemis/"
METEOR_TESCAN1_CONFIG = CONFIG_PATH + "sim/meteor-tescan-sim.odm.yaml"

METEOR_TESCAN1_FIBSEM_CONFIG = CONFIG_PATH + "sim/meteor-tescan-fibsem-full-sim.odm.yaml"
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

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

The constant METEOR_TESCAN1_FIBSEM_CONFIG is defined but never used in this file. Consider removing it if it's not needed, or add a comment explaining why it's defined for future use.

Suggested change
METEOR_TESCAN1_FIBSEM_CONFIG = CONFIG_PATH + "sim/meteor-tescan-fibsem-full-sim.odm.yaml"

Copilot uses AI. Check for mistakes.
rx_fm = stage_md[model.MD_FAV_FM_POS_ACTIVE]["rx"]
tf_tilt = self._get_tilt_transformation(pre_tilt, rx_fm)
tf_fm = tf_reverse @ tf_tilt
tf_fm = tf_tilt @ tf_sr # No reverse... why?!
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

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

The comment "No reverse... why?!" is unclear and appears to be a question about the implementation rather than documentation. This should be either removed if the implementation is correct, or expanded to explain why tf_reverse is not applied to the FM transformation (unlike SEM and MILLING which both use tf_reverse). Based on the JEOL implementation, this appears intentional but should be documented clearly.

Suggested change
tf_fm = tf_tilt @ tf_sr # No reverse... why?!
# For FM, the stage axes follow the Odemis convention directly, so no tf_reverse is applied here,
# unlike SEM and MILLING where the Tescan stage convention is opposite and requires tf_reverse.
tf_fm = tf_tilt @ tf_sr

Copilot uses AI. Check for mistakes.
…EM rotation

The SEM scan rotation has an indirect effect on the FM movement: the FM
image is always transposed to match the SEM (and FIB) image, by
adjusting the microscope file. So if there is 180° scan rotation, the
FM relative moves need to be inverted.

This also ensure that old systems (where there is no scan rotation) do
still move in the right direction when using the new sample stage code.
@pieleric pieleric force-pushed the fix-tescan-posture-manager-fm-sample-stage-move-should-depend-on-sem-rotation branch from b20c904 to 87ff61d Compare December 19, 2025 13:15
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: 0

🧹 Nitpick comments (1)
src/odemis/acq/test/move_tescan_test.py (1)

105-129: Consider adding strict=True to zip() for safety.

The test correctly validates relative FM posture movements and uses hardware-verified expected values. However, for robustness, consider adding strict=True to the zip() call on line 121 (Python 3.10+) to ensure both lists have the same length.

🔎 Proposed refactor
-        for m_sample, m_bare in zip(sample_stage_moves, stage_bare_moves):
+        for m_sample, m_bare in zip(sample_stage_moves, stage_bare_moves, strict=True):
             old_bare_pos = self.stage.position.value
             self.linked_stage.moveRel(m_sample).result()
             new_bare_pos = self.stage.position.value
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b20c904 and 87ff61d.

📒 Files selected for processing (2)
  • src/odemis/acq/move.py (2 hunks)
  • src/odemis/acq/test/move_tescan_test.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/odemis/acq/test/move_tescan_test.py (5)
src/odemis/acq/test/move_jeol_test.py (1)
  • setUpClass (49-58)
src/odemis/acq/test/move_tfs1_test.py (1)
  • setUpClass (45-57)
src/odemis/acq/test/move_util_test.py (1)
  • setUpClass (48-52)
src/odemis/acq/move.py (4)
  • cryoSwitchSamplePosition (136-151)
  • getCurrentPostureLabel (127-134)
  • getCurrentPostureLabel (384-409)
  • moveRel (2473-2483)
src/odemis/util/testing.py (1)
  • assert_pos_almost_equal (172-183)
src/odemis/acq/move.py (2)
src/odemis/util/transform.py (1)
  • get_rotation_transforms (1302-1325)
src/odemis/model/_components.py (1)
  • model (570-571)
🪛 Ruff (0.14.8)
src/odemis/acq/test/move_tescan_test.py

121-121: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

⏰ 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 (3)

23-24: LGTM!

The testing module import is properly added to support the new test's position comparison logic.


44-53: LGTM!

The setup methods properly implement test isolation by capturing and restoring stage metadata. This ensures each test starts with a clean state, even if previous tests modified the metadata.


57-60: LGTM!

The clarifying comments explain important behavioral differences between posture switching (which re-reads metadata) and sample stage moves (which use cached transformations). This context is valuable for understanding test setup requirements.

src/odemis/acq/move.py (3)

639-640: LGTM!

The logging level change from warning to info is appropriate. When e-beam/ion-beam components are unavailable (e.g., in simulation or older systems), this is an expected condition, not a problem. Returning 0° scan rotation as default ensures backward compatibility with systems that don't have scan rotation.


1664-1678: LGTM! Scan rotation compensation properly implemented.

The changes correctly introduce scan rotation compensation for FM imaging:

  1. Metadata sync (line 1666): Sets MD_ROTATION_COR on scanners to ensure total image rotation is 0°
  2. FM transform (line 1678): Now uses tf_tilt @ tf_sr instead of tf_reverse @ tf_tilt
  3. Rationale (lines 1673-1677): Clear explanation that although scan rotation doesn't directly affect FM optics, the camera image is transposed to match SEM orientation, so movements must account for it

The negative rotation -sr in line 1667 correctly compensates for the scan rotation. Systems without scan rotation (sr=0°) get an identity transform, ensuring backward compatibility.


1684-1695: LGTM! Consistent scan rotation compensation across postures.

The scan rotation transform tf_sr is correctly applied to both SEM (line 1684) and MILLING (line 1695) postures, ensuring consistent movement behavior across all imaging modes. The transform composition order (tf_reverse @ tf_tilt @ tf_sr) properly accounts for:

  • Tescan stage convention being opposite to Odemis (tf_reverse)
  • Stage tilt geometry (tf_tilt)
  • Scan rotation compensation (tf_sr)

for axis in sem_position.keys():
self.assertAlmostEqual(sem_position[axis], current_stage_position[axis], places=4)

def test_rel_move_fm_posture(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these tests for checking the sample stage movement with scan rotation?
If I am correct, the default the scan rotation is zero if not mentioned.

In these test cases, is it for scan rotation 0. If yes, maybe we can make test cases for 180 degree?

# Only the other hand, although the scan rotation has no direct effect on the FM image, in
# practice, the camera image is also "transposed" to match the SEM image orientation, in the
# microscope file. So also need to apply the scan rotation here.
tf_fm = tf_tilt @ tf_sr
Copy link
Contributor

Choose a reason for hiding this comment

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

This change is applicable for both TFS and Tescan, right?

The commit message shows it applicability to only Tescan.


@classmethod
def setUpClass(cls):
"""Set up the test case with Tescan 1 configuration"""
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe similar test cases to check sample stage movements with and without scan rotation with 0 and 180 can be added for TFS, if applicable.

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