Skip to content

Conversation

@K4rishma
Copy link
Contributor

@K4rishma K4rishma commented Jan 7, 2026

Move to safe z value when moving between postures

To be Tested on Hardware - Opening as a draft PR

@K4rishma K4rishma requested a review from pieleric January 7, 2026 23:08
@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

Adds a new CALIB parameter z_low to the TFS3 simulator YAML and introduces module-level Z constants (Z_LOW, DELTA_Z) in src/odemis/acq/move.py. MeteorTFS1PostureManager now validates stage metadata and required axes, forces sample-stage conversion with a pre-tilt, and restricts postures to SEM_IMAGING/FM_IMAGING; its target-position logic includes TFS1-specific handling for cryo posture switches. MeteorTFS3PostureManager uses a derived z_low (from MD_CALIB or Z_LOW) and z_low_fm (z_low - DELTA_Z) to sequence multi-step, Z-thresholded moves (lowering, rotations rx/rz, lateral moves, final Z) and to store/restore FM/SEM sample-position metadata during transitions.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant PostureManager
    participant StageController
    participant MetadataStore

    Caller->>PostureManager: request cryo-switch(targetPosture)
    PostureManager->>MetadataStore: read MD_CALIB / FM/SEM positions / z_low
    alt TFS3: SEM_IMAGING -> FM_IMAGING
        PostureManager->>StageController: move Z -> z_low_fm
        StageController-->>PostureManager: ack
        PostureManager->>StageController: rotate rx,rz
        StageController-->>PostureManager: ack
        PostureManager->>StageController: move X,Y toward FM sample pos
        StageController-->>PostureManager: ack
        PostureManager->>StageController: move Z -> final FM Z
        StageController-->>PostureManager: ack
        PostureManager->>MetadataStore: store FM deactive/recovery positions
    else TFS3: FM_IMAGING -> SEM_IMAGING/GRID
        PostureManager->>StageController: optionally move Z -> z_low_fm/z_low
        StageController-->>PostureManager: ack
        PostureManager->>StageController: rotate rx,rz and XY to SEM pos
        StageController-->>PostureManager: ack
        PostureManager->>MetadataStore: restore/store SEM/FM metadata
    else TFS1: SEM<->FM transition
        PostureManager->>StageController: validate axes/metadata, enforce Z constraints
        StageController-->>PostureManager: ack
        PostureManager->>StageController: perform staged moves (Z threshold, rotations, lateral)
        StageController-->>PostureManager: ack
        PostureManager->>MetadataStore: update/use MD_CALIB as needed
    end
    PostureManager-->>Caller: completed
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature: safe stage switching between postures in TFS microscope systems, which aligns with the changeset that adds Z-threshold sequencing and safe positioning logic.
Description check ✅ Passed The description accurately relates to the changeset, explaining the core purpose of moving to a safe Z value during posture transitions, matching the changes in the move.py file and configuration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

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

🤖 Fix all issues with AI agents
In @src/odemis/acq/move.py:
- Around line 838-840: Fix the typo in the comment above the constants Z_LOW and
DELTA_Z: change "Miniumum stare bare z height required to switch between FM and
SEM postures" to "Minimum stage bare z height required to switch between FM and
SEM postures" so the comment correctly describes the constants Z_LOW and
DELTA_Z.
- Around line 1120-1126: Fix the typo in the inline comment inside the move
handling block where current_label is compared to FM_IMAGING (and target in
[SEM_IMAGING, MILLING, FIB_IMAGING]); change "assesment" to "assessment" in the
comment that precedes the sub_moves appends (the block using self.stage, z_low,
target_pos, and filter_dict).
- Around line 1095-1097: The code in the MeteorTFS3PostureManager branch
incorrectly computes z_low_fm using the global Z_LOW instead of the calibrated
z_low from md_calib and also uses type(self) == MeteorTFS3PostureManager for
type checking; change the type check to use "is" (type(self) is
MeteorTFS3PostureManager) and compute z_low_fm from the calibrated value
(z_low_fm = z_low - DELTA_Z) after obtaining z_low = md_calib.get("z_low",
Z_LOW) so the FM transition uses the calibration-aware height.
🧹 Nitpick comments (1)
src/odemis/acq/move.py (1)

1082-1093: Use is for type comparison instead of ==.

Per Python style guidelines, use is for type identity checks rather than ==. Since MeteorTFS3PostureManager inherits from MeteorTFS1PostureManager, using isinstance() would match both, so direct type comparison is appropriate here, but should use is.

♻️ Suggested fix
-                if (type(self) == MeteorTFS1PostureManager):
+                if type(self) is MeteorTFS1PostureManager:
                     if current_label == SEM_IMAGING and target == FM_IMAGING:
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e44fd47 and fc6988e.

📒 Files selected for processing (2)
  • install/linux/usr/share/odemis/sim/meteor-tfs3-sim.odm.yaml
  • src/odemis/acq/move.py
🧰 Additional context used
🧠 Learnings (2)
📓 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-10-20T10:01:17.192Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/cont/features.py:367-369
Timestamp: 2025-10-20T10:01:17.192Z
Learning: In src/odemis/gui/cont/features.py, when constructing Target objects for sample coordinates, it is intentional to use x/y from feature_sample_stage (sample-stage frame) and z from feature_focus (focus-actuator frame), even though this mixes coordinate frames. This is the expected way sample coordinates are defined in the codebase.

Applied to files:

  • src/odemis/acq/move.py
🧬 Code graph analysis (1)
src/odemis/acq/move.py (1)
src/odemis/driver/actuator.py (5)
  • updateMetadata (446-450)
  • updateMetadata (749-754)
  • updateMetadata (2215-2223)
  • updateMetadata (2819-2837)
  • updateMetadata (3318-3327)
🪛 Ruff (0.14.10)
src/odemis/acq/move.py

1082-1082: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)


1095-1095: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)

⏰ 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 (3)
install/linux/usr/share/odemis/sim/meteor-tfs3-sim.odm.yaml (1)

41-48: LGTM!

The new z_low calibration parameter is properly placed under CALIB and the value (28.e-3 m) aligns with the Z_LOW constant default in move.py. This allows per-system configuration of the safe Z height for posture switching.

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

1037-1043: LGTM!

The metadata retrieval is properly structured with md_calib accessed from stage metadata, which is validated during initialization for TFS3 systems.


1099-1118: Verify behavior when current Z is already below z_low_fm.

The multi-step move sequence looks correct for the standard case. However, if the stage is already at a Z position below z_low_fm, the first sub-move would move Z upward, which could be unexpected. Consider whether this edge case needs handling or if it's acceptable behavior.

@K4rishma K4rishma force-pushed the safer_stage_switching_tfs3 branch 2 times, most recently from d54f44e to 182249d Compare January 13, 2026 13:38
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

🤖 Fix all issues with AI agents
In @src/odemis/acq/move.py:
- Around line 838-840: Fix the typo in the comment above Z_LOW and DELTA_Z:
change "stare" to "stage" in the explanatory comment ("Minimum stage bare z
height required to switch between FM and SEM postures") so the comment correctly
references the microscope stage; ensure the symbols Z_LOW and DELTA_Z remain
unchanged.
- Around line 1037-1043: The stage metadata lookup unconditionally accesses
model.MD_CALIB which can raise KeyError for TFS1 systems; change the retrieval
of md_calib to use stage_md.get(model.MD_CALIB) with a sensible default (e.g.,
None) or move the md_calib lookup inside the TFS3-specific branch where it is
used, and ensure MeteorTFS1PostureManager (which only adds model.MD_POS_COR) is
not assumed to provide MD_CALIB.
🧹 Nitpick comments (3)
src/odemis/acq/move.py (3)

1082-1094: Use is instead of == for type comparisons.

Per Python convention (and static analysis hint E721), use is for type identity checks:

Proposed fix
-                if (type(self) == MeteorTFS1PostureManager):
+                if type(self) is MeteorTFS1PostureManager:

Additionally, consider whether this type-dispatching logic would be cleaner as an overridden method in MeteorTFS3PostureManager rather than branching by type in the base class. This would better follow the Open/Closed principle.


1095-1098: Use is instead of == for type comparisons.

Same issue as the TFS1 branch:

Proposed fix
-                elif (type(self) == MeteorTFS3PostureManager):
+                elif type(self) is MeteorTFS3PostureManager:

1099-1118: Verify multi-line condition formatting and move sequence.

The condition at lines 1099-1101 has unusual indentation. Consider reformatting for clarity:

Suggested formatting
-                    if (current_label in [SEM_IMAGING, MILLING, FIB_IMAGING]
-                      and target == FM_IMAGING
-                     ):
+                    if (
+                        current_label in [SEM_IMAGING, MILLING, FIB_IMAGING]
+                        and target == FM_IMAGING
+                    ):

The move sequence (z → rx/rz → x/y → final) aligns with the PR objective of safe stage switching. The intermediate move to z_low_fm before rotation ensures vertical clearance.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc6988e and d54f44e.

📒 Files selected for processing (2)
  • install/linux/usr/share/odemis/sim/meteor-tfs3-sim.odm.yaml
  • src/odemis/acq/move.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • install/linux/usr/share/odemis/sim/meteor-tfs3-sim.odm.yaml
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-20T10:01:17.192Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/cont/features.py:367-369
Timestamp: 2025-10-20T10:01:17.192Z
Learning: In src/odemis/gui/cont/features.py, when constructing Target objects for sample coordinates, it is intentional to use x/y from feature_sample_stage (sample-stage frame) and z from feature_focus (focus-actuator frame), even though this mixes coordinate frames. This is the expected way sample coordinates are defined in the codebase.

Applied to files:

  • src/odemis/acq/move.py
📚 Learning: 2026-01-12T12:37:35.155Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/acq/align/z_localization.py:29-40
Timestamp: 2026-01-12T12:37:35.155Z
Learning: Maintain the debugging pattern of importing odemis.gui.conf and exporting TIFF files within acquisition/localization loops across all odemis Python sources. Do not remove or restructure this behavior in refactors if it serves debugging consistency; document the debugging purpose with comments and ensure the behavior remains consistent across modules (e.g., acquisition/localization loops such as src/odemis/acq/align/z_localization.py).

Applied to files:

  • src/odemis/acq/move.py
🪛 Ruff (0.14.10)
src/odemis/acq/move.py

1082-1082: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)


1095-1095: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)

⏰ 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 (1)
src/odemis/acq/move.py (1)

1120-1129: Add explicit rotation sequencing to FM→SEM transition, or document why it differs from SEM→FM.

The FM→SEM path (lines 1120-1129) doesn't explicitly sequence rotation before x/y movement like the SEM→FM path does (lines 1115-1118). The SEM→FM comments specify: z → rx/rz → x/y → final, while FM→SEM only does: (optional z) → x/y → final with rotation deferred to the last combined move. Either sequence rotations separately before lateral moves (matching SEM→FM), or add a comment explaining why the FM→SEM direction has different safety requirements.

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 838-840: Fix the comment above the constants Z_LOW and DELTA_Z:
change the word "stare" to "stage" so the comment reads "Minimum stage bare z
height required to switch between FM and SEM postures" (leave the constants
Z_LOW and DELTA_Z unchanged).
- Around line 1041-1042: Direct indexing of stage_md with model.MD_CALIB can
raise KeyError on TFS1 systems; change the access to use
stage_md.get(model.MD_CALIB) (or otherwise guard with an 'if model.MD_CALIB in
stage_md' check) and handle the None/missing case similarly to the pattern in
MeteorPostureManager.__init__; ensure this aligns with how required_keys are set
across MeteorTFS3PostureManager and MeteorTFS1PostureManager so TFS1 systems do
not crash when MD_CALIB is absent.
- Around line 1095-1134: The MeteorTFS3PostureManager branch omits handling
transitions involving LOADING so when target==LOADING (or when leaving LOADING
toward SEM/MILLING/FIB) no stage moves are appended; update the TFS3 branch in
the block guarded by (type(self) == MeteorTFS3PostureManager) to add symmetric
handling for LOADING: compute z_low and z_low_fm as done now, and for
transitions where target == LOADING append appropriate sub_moves (move to safe z
z_low_fm if needed, move r/t via filter_dict({'rx','rz'}, target_pos), move x/y,
then final target_pos) and for transitions where current_label == LOADING and
target in [SEM_IMAGING, MILLING, FIB_IMAGING] ensure you move z to z_low if
current_pos["z"] > z_low, then append filter_dict({'x','y'}, target_pos) and
target_pos; reference MeteorTFS3PostureManager, z_low/z_low_fm, current_label,
target, current_pos, target_pos, sub_moves, and filter_dict to locate insertion
points.
🧹 Nitpick comments (1)
src/odemis/acq/move.py (1)

1082-1094: Use isinstance() for type checks instead of type() ==.

Per static analysis (E721) and Python best practices, use isinstance() for type comparisons. Since MeteorTFS3PostureManager inherits from MeteorTFS1PostureManager, you need to check the subclass first to ensure correct branching.

♻️ Suggested fix
-                if (type(self) == MeteorTFS1PostureManager):
+                if isinstance(self, MeteorTFS3PostureManager):
+                    # TFS3 logic handled below
+                    pass
+                elif isinstance(self, MeteorTFS1PostureManager):
                     if current_label == SEM_IMAGING and target == FM_IMAGING:
                         # NOTE: with TFS1, no distinction was made between SEM and MILL positions...
                         self.stage.updateMetadata({model.MD_FAV_SEM_POS_ACTIVE: {'rx': current_pos['rx'],
                                                                                  'rz': current_pos['rz']}})
                     # Move translation axes, then rotational ones
                     sub_moves.append((self.stage, filter_dict({'x', 'y', 'z'}, target_pos)))
                     sub_moves.append((self.stage, filter_dict({'rx', 'rz'}, target_pos)))
-
-                elif (type(self) == MeteorTFS3PostureManager):

Alternatively, consider overriding _doCryoSwitchSamplePosition in MeteorTFS3PostureManager to avoid runtime type checking altogether, which would be a cleaner OOP approach.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d54f44e and 182249d.

📒 Files selected for processing (2)
  • install/linux/usr/share/odemis/sim/meteor-tfs3-sim.odm.yaml
  • src/odemis/acq/move.py
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-08T12:16:19.734Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/cont/acquisition/cryo_acq.py:789-807
Timestamp: 2026-01-08T12:16:19.734Z
Learning: In the cryo workflow, `feature.get_posture_position(FM_IMAGING)` always returns a valid position dictionary and never returns None, so no guard is required when calling this method.

Applied to files:

  • src/odemis/acq/move.py
📚 Learning: 2025-10-20T10:01:17.192Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/cont/features.py:367-369
Timestamp: 2025-10-20T10:01:17.192Z
Learning: In src/odemis/gui/cont/features.py, when constructing Target objects for sample coordinates, it is intentional to use x/y from feature_sample_stage (sample-stage frame) and z from feature_focus (focus-actuator frame), even though this mixes coordinate frames. This is the expected way sample coordinates are defined in the codebase.

Applied to files:

  • src/odemis/acq/move.py
📚 Learning: 2026-01-12T12:37:35.155Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/acq/align/z_localization.py:29-40
Timestamp: 2026-01-12T12:37:35.155Z
Learning: Maintain the debugging pattern of importing odemis.gui.conf and exporting TIFF files within acquisition/localization loops across all odemis Python sources. Do not remove or restructure this behavior in refactors if it serves debugging consistency; document the debugging purpose with comments and ensure the behavior remains consistent across modules (e.g., acquisition/localization loops such as src/odemis/acq/align/z_localization.py).

Applied to files:

  • src/odemis/acq/move.py
🧬 Code graph analysis (1)
src/odemis/acq/move.py (2)
src/odemis/model/_components.py (1)
  • model (570-571)
src/odemis/driver/actuator.py (5)
  • updateMetadata (446-450)
  • updateMetadata (749-754)
  • updateMetadata (2215-2223)
  • updateMetadata (2819-2837)
  • updateMetadata (3318-3327)
🪛 Ruff (0.14.10)
src/odemis/acq/move.py

1082-1082: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)


1095-1095: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)

⏰ 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 (3)
install/linux/usr/share/odemis/sim/meteor-tfs3-sim.odm.yaml (1)

41-48: LGTM! New z_low calibration parameter for safe posture switching.

The z_low value of 28mm aligns with the default Z_LOW constant in move.py and provides a system-specific override for the minimum safe Z height during posture transitions.

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

842-856: LGTM! TFS1-specific posture manager initialization.

The explicit metadata validation, axis requirements check, and transformation initialization for TFS1 are well-structured. The postures list [SEM_IMAGING, FM_IMAGING] correctly defines the supported postures for this system variant.


1115-1123: Clear and well-documented multi-step movement sequence for TFS3 SEM→FM transition.

The staged movement order (z_low_fm → rx/rz → x/y → final position) follows the TFS safety assessment for Hydra Bio systems. The inline comments explain the rationale clearly.

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/acq/move.py:
- Around line 1037-1042: The code directly indexes stage_md[model.MD_CALIB] when
building md_calib which can raise KeyError for TFS1 systems because
MeteorTFS1PostureManager does not require MD_CALIB; change to safely access the
key (e.g., md_calib = stage_md.get(model.MD_CALIB)) and ensure subsequent code
that reads md_calib handles a None/default value so TFS1 branches proceed
without raising an exception.
🧹 Nitpick comments (1)
src/odemis/acq/move.py (1)

1082-1094: Use isinstance() for type checks and consider refactoring the class hierarchy.

The static analysis correctly flags the use of type(self) == Class comparisons. While the exact type check works here to distinguish TFS1 from TFS3, this pattern indicates a design smell where subclass-specific logic is embedded in the parent class.

Consider either:

  1. Override _doCryoSwitchSamplePosition in MeteorTFS3PostureManager to handle TFS3-specific logic
  2. Or use isinstance() if exact type matching is truly needed: isinstance(self, MeteorTFS1PostureManager) and not isinstance(self, MeteorTFS3PostureManager)
♻️ Suggested approach using isinstance()
-                if (type(self) == MeteorTFS1PostureManager):
+                if isinstance(self, MeteorTFS1PostureManager) and not isinstance(self, MeteorTFS3PostureManager):

However, the cleaner solution would be to move the TFS3-specific logic (lines 1095-1134) into an overridden _doCryoSwitchSamplePosition method in MeteorTFS3PostureManager.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 182249d and f0d0d27.

📒 Files selected for processing (1)
  • src/odemis/acq/move.py
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2026-01-08T09:01:44.840Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/main_xrc.py:11968-11974
Timestamp: 2026-01-08T09:01:44.840Z
Learning: In the file `src/odemis/gui/main_xrc.py`, the `ctrl_target_z` UnitFloatCtrl uses meters (m) as its unit for Z-axis positioning.

Applied to files:

  • src/odemis/acq/move.py
📚 Learning: 2025-10-20T10:01:17.192Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/cont/features.py:367-369
Timestamp: 2025-10-20T10:01:17.192Z
Learning: In src/odemis/gui/cont/features.py, when constructing Target objects for sample coordinates, it is intentional to use x/y from feature_sample_stage (sample-stage frame) and z from feature_focus (focus-actuator frame), even though this mixes coordinate frames. This is the expected way sample coordinates are defined in the codebase.

Applied to files:

  • src/odemis/acq/move.py
📚 Learning: 2026-01-09T15:17:06.048Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/cont/acquisition/cryo_z_localization.py:164-198
Timestamp: 2026-01-09T15:17:06.048Z
Learning: In src/odemis/gui/cont/acquisition/cryo_z_localization.py, the cmb_targets combobox intentionally only lists TargetType.Fiducial targets, not POIs. POI and feature share values and are managed separately from the fiducial list. The currentTarget may be set to a POI without it appearing in the combobox, which is the intended design.

Applied to files:

  • src/odemis/acq/move.py
📚 Learning: 2025-10-20T10:02:02.606Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/cont/features.py:360-379
Timestamp: 2025-10-20T10:02:02.606Z
Learning: In the cryo workflow, POIs (Points of Interest) are linked to features and should always be derived from the latest feature position and focus values when loading correlation data, rather than using any saved POI values from correlation_data.fm_pois. This ensures POIs reflect the current feature state.

Applied to files:

  • src/odemis/acq/move.py
📚 Learning: 2026-01-12T12:37:35.155Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/acq/align/z_localization.py:29-40
Timestamp: 2026-01-12T12:37:35.155Z
Learning: Maintain the debugging pattern of importing odemis.gui.conf and exporting TIFF files within acquisition/localization loops across all odemis Python sources. Do not remove or restructure this behavior in refactors if it serves debugging consistency; document the debugging purpose with comments and ensure the behavior remains consistent across modules (e.g., acquisition/localization loops such as src/odemis/acq/align/z_localization.py).

Applied to files:

  • src/odemis/acq/move.py
🧬 Code graph analysis (1)
src/odemis/acq/move.py (1)
src/odemis/driver/actuator.py (5)
  • updateMetadata (446-450)
  • updateMetadata (749-754)
  • updateMetadata (2215-2223)
  • updateMetadata (2819-2837)
  • updateMetadata (3318-3327)
🪛 Ruff (0.14.10)
src/odemis/acq/move.py

1082-1082: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)


1095-1095: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)

⏰ 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-24.04)
  • GitHub Check: build (ubuntu-22.04)
🔇 Additional comments (5)
src/odemis/acq/move.py (5)

838-840: LGTM! Well-documented safety constants.

The constants are appropriately defined with a clear comment explaining the purpose and origin (TFS Hydra Bio system). The fallback pattern md_calib.get("z_low", Z_LOW) used later allows system-specific overrides while maintaining safe defaults.


842-855: LGTM! Proper initialization with validation.

The initialization correctly validates required metadata and axes, retrieves the pre_tilt from the linked component, and initializes the transformation. The error message clearly indicates which axes are missing.


1107-1114: Z position recovery logic is well-designed.

The approach of storing the Z position in sample-stage coordinates (via MD_FM_POS_SAMPLE_DEACTIVE) for later recovery when returning to SEM is sound. Using fix_fm_plane=False ensures the raw transformed position is stored without the fixed plane correction.

Note: This relies on the TFS3-specific fix_fm_plane parameter in _transformFromSEMToMeteor. The type check at line 1095 ensures this parameter is only used when self is exactly MeteorTFS3PostureManager.


1160-1182: TFS3 initialization looks correct.

The initialization properly:

  1. Calls MeteorPostureManager.__init__ directly (bypassing TFS1's init)
  2. Validates TFS3-specific calibration requirements
  3. Sets up the sample stage and FM position defaults

The class inherits from TFS1 for code reuse but configures itself independently, which is appropriate for this architecture.


1095-1134: The Z-safety logic is correct, no changes needed.

The z_low boundary condition at line 1131 correctly uses > (not >=) to check if the current Z is higher than the safe value. If z equals z_low exactly, no preliminary Z move occurs, which is intentional since the stage is already at a safe height. This matches the documented requirement: "If current z is more than safe value, move it to safe z value else skip it."

The z_low_fm calculation properly handles the edge case where z_low is 0, applying an additional DELTA_Z safety margin only when z_low is non-zero. The staged movement sequence (z → rotate → translate → final for SEM→FM, and conditional z → translate → final for FM→SEM) is well-designed and safe.

@K4rishma K4rishma force-pushed the safer_stage_switching_tfs3 branch from f0d0d27 to 583588c Compare January 13, 2026 13:51
Move to safe z value when moving between postures
@K4rishma K4rishma force-pushed the safer_stage_switching_tfs3 branch from 583588c to 0255e7a Compare January 13, 2026 14:11
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/move.py (1)

1080-1092: Consider using method override instead of type checking.

The type(self) == ClassName pattern is intentional to differentiate TFS1 from TFS3, but it creates a design smell where the base class has explicit knowledge of its derived class. This violates the Open/Closed Principle.

Consider having MeteorTFS3PostureManager override _doCryoSwitchSamplePosition instead, or extract the posture-specific move sequencing into a protected method that each subclass can override:

# In base class (TFS1)
def _get_posture_switch_moves(self, current_label, target, current_pos, target_pos):
    """Returns sub_moves list for posture switching. Override in subclasses."""
    sub_moves = []
    # TFS1 specific logic...
    return sub_moves

This would also address the static analysis hint (E721) about using isinstance() for type comparisons.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0d0d27 and 0255e7a.

📒 Files selected for processing (2)
  • install/linux/usr/share/odemis/sim/meteor-tfs3-sim.odm.yaml
  • src/odemis/acq/move.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • install/linux/usr/share/odemis/sim/meteor-tfs3-sim.odm.yaml
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/cont/features.py:367-369
Timestamp: 2025-10-20T10:01:17.192Z
Learning: In src/odemis/gui/cont/features.py, when constructing Target objects for sample coordinates, it is intentional to use x/y from feature_sample_stage (sample-stage frame) and z from feature_focus (focus-actuator frame), even though this mixes coordinate frames. This is the expected way sample coordinates are defined in the codebase.
📚 Learning: 2025-10-20T10:01:17.192Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/cont/features.py:367-369
Timestamp: 2025-10-20T10:01:17.192Z
Learning: In src/odemis/gui/cont/features.py, when constructing Target objects for sample coordinates, it is intentional to use x/y from feature_sample_stage (sample-stage frame) and z from feature_focus (focus-actuator frame), even though this mixes coordinate frames. This is the expected way sample coordinates are defined in the codebase.

Applied to files:

  • src/odemis/acq/move.py
📚 Learning: 2026-01-09T15:17:06.048Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/cont/acquisition/cryo_z_localization.py:164-198
Timestamp: 2026-01-09T15:17:06.048Z
Learning: In src/odemis/gui/cont/acquisition/cryo_z_localization.py, the cmb_targets combobox intentionally only lists TargetType.Fiducial targets, not POIs. POI and feature share values and are managed separately from the fiducial list. The currentTarget may be set to a POI without it appearing in the combobox, which is the intended design.

Applied to files:

  • src/odemis/acq/move.py
📚 Learning: 2026-01-08T09:01:44.840Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/main_xrc.py:11968-11974
Timestamp: 2026-01-08T09:01:44.840Z
Learning: In the file `src/odemis/gui/main_xrc.py`, the `ctrl_target_z` UnitFloatCtrl uses meters (m) as its unit for Z-axis positioning.

Applied to files:

  • src/odemis/acq/move.py
📚 Learning: 2025-10-20T10:02:02.606Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/cont/features.py:360-379
Timestamp: 2025-10-20T10:02:02.606Z
Learning: In the cryo workflow, POIs (Points of Interest) are linked to features and should always be derived from the latest feature position and focus values when loading correlation data, rather than using any saved POI values from correlation_data.fm_pois. This ensures POIs reflect the current feature state.

Applied to files:

  • src/odemis/acq/move.py
📚 Learning: 2026-01-12T12:37:35.155Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/acq/align/z_localization.py:29-40
Timestamp: 2026-01-12T12:37:35.155Z
Learning: Maintain the debugging pattern of importing odemis.gui.conf and exporting TIFF files within acquisition/localization loops across all odemis Python sources. Do not remove or restructure this behavior in refactors if it serves debugging consistency; document the debugging purpose with comments and ensure the behavior remains consistent across modules (e.g., acquisition/localization loops such as src/odemis/acq/align/z_localization.py).

Applied to files:

  • src/odemis/acq/move.py
📚 Learning: 2026-01-08T12:16:19.734Z
Learnt from: K4rishma
Repo: delmic/odemis PR: 3245
File: src/odemis/gui/cont/acquisition/cryo_acq.py:789-807
Timestamp: 2026-01-08T12:16:19.734Z
Learning: In the cryo workflow, `feature.get_posture_position(FM_IMAGING)` always returns a valid position dictionary and never returns None, so no guard is required when calling this method.

Applied to files:

  • src/odemis/acq/move.py
🪛 Ruff (0.14.10)
src/odemis/acq/move.py

1080-1080: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)


1093-1093: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)

⏰ 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-24.04)
  • GitHub Check: build (ubuntu-22.04)
🔇 Additional comments (2)
src/odemis/acq/move.py (2)

838-840: LGTM!

Clear documentation and sensible default values for the safe Z thresholds. The constants follow the existing naming conventions and unit standards (meters) used in the codebase.


1093-1134: Z-threshold sequencing logic is correct for safe posture switching.

The multi-step movement sequence with Z-threshold checks correctly implements the safety requirements:

  1. SEM→FM: Lower to z_low_fm (z_low - 1mm = 27mm) first, then rotate, then translate
  2. FM→SEM: Conditionally lower to z_low (28mm), translate, then final position

The asymmetry between thresholds (z_low_fm for SEM→FM vs z_low for FM→SEM) is intentional—SEM→FM applies an extra safety margin per Hydra Bio TFS assessment, while FM→SEM follows Hydra Bio xt assessment. This differentiation is properly documented in the comments and matches the staged movement strategy for each direction.

sub_moves.append((self.stage, {"z": z_low_fm}))
sub_moves.append((self.stage, filter_dict({'rx', 'rz'}, target_pos)))
sub_moves.append((self.stage, filter_dict({'x', 'y'}, target_pos)))
sub_moves.append((self.stage, filter_dict({'x', 'y', 'z'}, target_pos)))
Copy link
Member

Choose a reason for hiding this comment

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

Any reason you repeat x & y axes? Assuming the previous move has succeeded, it should be fine to move only z. Or do you have in mind some way to handle imprecision or incorrect move? In such case, please add a comment to explain.

Also, the TFS recommendation is different:

  • move rx rz
  • move x y
  • move everything (again) at once


elif (current_label == FM_IMAGING
and target in [SEM_IMAGING, MILLING, FIB_IMAGING]):
# Stage switched based on Hydra Bio xt assesment
Copy link
Member

Choose a reason for hiding this comment

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

As it's based on the same assessment as the code just above, it'd be clearer to write it down just once. I'd suggest to place this remark just above the z_low computation.

sub_moves.append((self.stage, {"z": z_low}))
sub_moves.append((self.stage, filter_dict({'x', 'y'}, target_pos)))
sub_moves.append((self.stage, filter_dict({'rx', 'rz'}, target_pos)))
sub_moves.append((self.stage, filter_dict({'x', 'y', 'z'}, target_pos)))
Copy link
Member

Choose a reason for hiding this comment

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

Same comment here: why moving x & y twice to the same position?



# Minimum stage bare z height required to switch between FM and SEM postures
Z_LOW = 0.028 # m, safe value provided by TFS on a Hydra Bio system
Copy link
Member

Choose a reason for hiding this comment

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

Hydra Bio -> Hydra Bio CX system, which should be also compatible on other systems

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.

2 participants