Skip to content

[feat] Add correction of optical focus during ROA acquisition based o…#3258

Draft
tepals wants to merge 2 commits intodelmic:masterfrom
tepals:FVEM-26-optical-focus-mapping
Draft

[feat] Add correction of optical focus during ROA acquisition based o…#3258
tepals wants to merge 2 commits intodelmic:masterfrom
tepals:FVEM-26-optical-focus-mapping

Conversation

@tepals
Copy link
Contributor

@tepals tepals commented Nov 3, 2025

…n focus plane

AutofocusMultiprobe in fastem-calibrations has been updated to run in multiple positions and fit a plane through those positions. Based on this plane the focus is corrected during ROA acquisition

Related to: https://bitbucket.org/delmic/fastem-calibrations/pull-requests/184

@tepals tepals requested a review from nandishjpatel November 3, 2025 16:38
@coderabbitai
Copy link

coderabbitai bot commented Nov 3, 2025

📝 Walkthrough

Walkthrough

Adds optional focus-mapping to FASTEM acquisition (new acquire(..., focus_mapping) parameter, AcquisitionTask focus_mapping constructor param, linalg import, runtime validation, and 3D stage target computation when enabled). Updates alignment run() to use the first entry of provided stage_pos for stage movement and to pass focus_pos into run_calibration. GUI: introduces a colour-blind-friendly yellow constant and adds nine focus_map calibrations per scintillator plus visibility toggles for those regions. Tests updated to pass the new focus_mapping argument where applicable.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant acquire
    participant AcquisitionTask
    participant move_stage

    Caller->>acquire: acquire(..., focus_mapping=True)
    activate acquire
    acquire->>AcquisitionTask: __init__(..., focus_mapping=True)
    activate AcquisitionTask
    AcquisitionTask->>AcquisitionTask: _correct_focus = True
    AcquisitionTask->>AcquisitionTask: _focus_plane = stage.metadata.MD_CALIB.focus_plane
    deactivate AcquisitionTask

    rect rgba(240,248,255,0.5)
      Note over acquire,move_stage: per-tile acquisition run
      acquire->>AcquisitionTask: run()
      activate AcquisitionTask
      AcquisitionTask->>AcquisitionTask: validate _focus_plane present
      AcquisitionTask->>move_stage: move_stage_to_next_tile(tile_xy)
      activate move_stage
      alt focus mapping enabled
        move_stage->>move_stage: z = linalg.get_z_pos_on_plane(plane_normal, gamma, x,y)
        move_stage->>move_stage: target = (x,y,z)
      else focus mapping disabled
        move_stage->>move_stage: target = (x,y)
      end
      move_stage->>move_stage: stage.moveTo(target)
      deactivate move_stage
      deactivate AcquisitionTask
    end
    deactivate acquire
Loading
sequenceDiagram
    participant Caller
    participant AlignRun
    participant Calibration

    Caller->>AlignRun: run(stage_pos=[(x1,y1),...])
    activate AlignRun
    note right of AlignRun: when stage_pos provided, use stage_pos[0] as (x,y)
    AlignRun->>AlignRun: stage_move = {'x': stage_pos[0][0], 'y': stage_pos[0][1]}
    AlignRun->>Calibration: run_calibration(..., focus_pos=stage_move)
    activate Calibration
    Calibration-->>AlignRun: result
    deactivate Calibration
    deactivate AlignRun
Loading
sequenceDiagram
    participant GUI
    participant Model

    GUI->>Model: initialize scintillator calibrations
    activate Model
    Model->>Model: add 3 base calibrations
    Model->>Model: add focus_map_0..focus_map_8 (colour FG_COLOUR_BLIND_YELLOW)
    deactivate Model

    GUI->>GUI: on_visibility_btn toggle CALIBRATION_1
    GUI->>GUI: also toggle visibility for focus_map_0..focus_map_8
Loading
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is truncated but clearly describes the main change: adding optical focus correction during ROA acquisition based on a focus plane.
Description check ✅ Passed The description is fully related to the changeset, explaining how AutofocusMultiprobe was updated to fit a plane and how focus correction is now applied during acquisition.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 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: 2

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

163-164: Consider adding type annotation for consistency.

The focus_mapping parameter lacks a type annotation while other parameters like acq_dwell_time have explicit type hints. Adding : bool would improve consistency.

 def acquire(roa, path, username, scanner, multibeam, descanner, detector, stage, scan_stage, ccd, beamshift, lens,
             se_detector, ebeam_focus, pre_calibrations=None, save_full_cells=False, settings_obs=None,
-            spot_grid_thresh=0.5, blank_beam=True, stop_acq_on_failure=True, acq_dwell_time: Optional[float] = None,
-            focus_mapping=False):
+            spot_grid_thresh=0.5, blank_beam=True, stop_acq_on_failure=True, acq_dwell_time: Optional[float] = None,
+            focus_mapping: bool = False):
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0714e5 and 8fb3593.

📒 Files selected for processing (1)
  • src/odemis/acq/fastem.py (9 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 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.
🧬 Code graph analysis (1)
src/odemis/acq/fastem.py (3)
src/odemis/util/__init__.py (1)
  • TimeoutError (666-667)
src/odemis/model/_components.py (2)
  • model (570-571)
  • moveAbsSync (852-859)
src/odemis/util/linalg.py (1)
  • get_z_pos_on_plane (180-194)
🪛 Ruff (0.14.3)
src/odemis/acq/fastem.py

377-377: Avoid specifying long messages outside the exception class

(TRY003)

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

48-48: LGTM!

The linalg import is correctly added to support the focus plane calculations introduced in this PR.


303-304: LGTM!

Instance variables are properly initialized. The _focus_plane is correctly set to None and populated later in run() when focus mapping is enabled.


700-700: LGTM!

The moveAbsSync() call correctly uses the new_pos dictionary that contains either 2D (x, y) or 3D (x, y, z) coordinates depending on whether focus mapping is enabled. Assuming the validation concerns raised in previous comments are addressed, this implementation is sound.


694-694: LGTM!

The logging statements appropriately distinguish between 3D positioning (with focus correction) and 2D positioning (without), which will be helpful for debugging and verifying the focus mapping feature is working as expected.

Also applies to: 697-697


217-217: LGTM!

The focus_mapping parameter is correctly propagated from the acquire() function through to the AcquisitionTask constructor, and the documentation is clear and consistent throughout.

Also applies to: 236-236

Comment on lines +372 to +377
if self._correct_focus:
# The focus is corrected based on a plane fitted through focus points, during calibration 1
# The metadata should look like: {'gamma': -4.29e-05, 'normal': (1.7e-16, -7.99e-17, -1)}
self._focus_plane = self._stage.getMetadata().get(model.MD_CALIB, {}).get("focus_plane", None)
if not self._focus_plane:
raise ValueError("Focus plane not found in stage MD_CALIB metadata, run Calibration 1.")
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

Validate the structure of focus_plane to prevent KeyErrors downstream.

The code validates the presence of focus_plane but not its structure. Lines 690 and 692 expect focus_plane to contain "gamma" and "normal" keys. If these keys are missing or malformed, a KeyError will occur during stage movement, which is harder to debug than an early validation error.

Apply this diff to add structure validation:

 if self._correct_focus:
     # The focus is corrected based on a plane fitted through focus points, during calibration 1
     # The metadata should look like: {'gamma': -4.29e-05, 'normal': (1.7e-16, -7.99e-17, -1)}
     self._focus_plane = self._stage.getMetadata().get(model.MD_CALIB, {}).get("focus_plane", None)
     if not self._focus_plane:
         raise ValueError("Focus plane not found in stage MD_CALIB metadata, run Calibration 1.")
+    if "gamma" not in self._focus_plane or "normal" not in self._focus_plane:
+        raise ValueError(f"Focus plane metadata is incomplete. Expected 'gamma' and 'normal' keys, "
+                         f"but got: {self._focus_plane.keys()}")
📝 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
if self._correct_focus:
# The focus is corrected based on a plane fitted through focus points, during calibration 1
# The metadata should look like: {'gamma': -4.29e-05, 'normal': (1.7e-16, -7.99e-17, -1)}
self._focus_plane = self._stage.getMetadata().get(model.MD_CALIB, {}).get("focus_plane", None)
if not self._focus_plane:
raise ValueError("Focus plane not found in stage MD_CALIB metadata, run Calibration 1.")
if self._correct_focus:
# The focus is corrected based on a plane fitted through focus points, during calibration 1
# The metadata should look like: {'gamma': -4.29e-05, 'normal': (1.7e-16, -7.99e-17, -1)}
self._focus_plane = self._stage.getMetadata().get(model.MD_CALIB, {}).get("focus_plane", None)
if not self._focus_plane:
raise ValueError("Focus plane not found in stage MD_CALIB metadata, run Calibration 1.")
if "gamma" not in self._focus_plane or "normal" not in self._focus_plane:
raise ValueError(f"Focus plane metadata is incomplete. Expected 'gamma' and 'normal' keys, "
f"but got: {self._focus_plane.keys()}")
🧰 Tools
🪛 Ruff (0.14.3)

377-377: Avoid specifying long messages outside the exception class

(TRY003)

@tepals tepals force-pushed the FVEM-26-optical-focus-mapping branch from 8fb3593 to 72590e9 Compare November 4, 2025 08:32
@nandishjpatel
Copy link
Contributor

@tepals the changes LGTM! A question: how will the focus_pos be passed from the GUI to acq.align.fastem, this is still need right?

@tepals
Copy link
Contributor Author

tepals commented Nov 4, 2025

@tepals the changes LGTM! A question: how will the focus_pos be passed from the GUI to acq.align.fastem, this is still need right?

yes that still needs to be implemented. I want to do some testing first, to see whether it makes sense to have this always enabled or have a checkbox in the GUI.

@tepals tepals force-pushed the FVEM-26-optical-focus-mapping branch from 6e9b5e0 to 1991832 Compare November 5, 2025 16: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: 1

♻️ Duplicate comments (1)
src/odemis/acq/fastem.py (1)

372-377: Add validation for focus_plane metadata structure.

The code retrieves focus_plane from metadata but doesn't validate that it contains the required "gamma" and "normal" keys before they're accessed at lines 690 and 692. If these keys are missing, a KeyError will occur during stage movement.

Apply this diff:

 if self._correct_focus:
     # The focus is corrected based on a plane fitted through focus points, during calibration 1
     # The metadata should look like: {'gamma': -4.29e-05, 'normal': (1.7e-16, -7.99e-17, -1)}
     self._focus_plane = self._stage.getMetadata().get(model.MD_CALIB, {}).get("focus_plane", None)
     if not self._focus_plane:
         raise ValueError("Focus plane not found in stage MD_CALIB metadata, run Calibration 1.")
+    if "gamma" not in self._focus_plane or "normal" not in self._focus_plane:
+        raise ValueError(f"Focus plane metadata is incomplete. Expected 'gamma' and 'normal' keys, "
+                         f"but got: {self._focus_plane.keys()}")
🧹 Nitpick comments (1)
src/odemis/gui/model/main_gui_data.py (1)

39-39: Fix import order.

The import of FG_COLOUR_BLIND_YELLOW should come before conf to maintain alphabetical order within the odemis.gui imports.

Apply this diff:

 from odemis.gui import (
     FG_COLOUR_BLIND_BLUE,
     FG_COLOUR_BLIND_ORANGE,
     FG_COLOUR_BLIND_PINK,
-    conf, FG_COLOUR_BLIND_YELLOW,
+    FG_COLOUR_BLIND_YELLOW,
+    conf,
 )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72590e9 and 1991832.

📒 Files selected for processing (6)
  • src/odemis/acq/align/fastem.py (1 hunks)
  • src/odemis/acq/fastem.py (9 hunks)
  • src/odemis/acq/test/fastem_test.py (12 hunks)
  • src/odemis/gui/__init__.py (1 hunks)
  • src/odemis/gui/cont/acquisition/fastem_acq.py (2 hunks)
  • src/odemis/gui/model/main_gui_data.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/odemis/acq/align/fastem.py
  • src/odemis/acq/test/fastem_test.py
🧰 Additional context used
🧠 Learnings (1)
📚 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/gui/cont/acquisition/fastem_acq.py
🧬 Code graph analysis (2)
src/odemis/gui/model/main_gui_data.py (1)
src/odemis/acq/fastem.py (2)
  • FastEMCalibration (93-111)
  • FastEMROC (114-139)
src/odemis/acq/fastem.py (3)
src/odemis/util/__init__.py (1)
  • TimeoutError (666-667)
src/odemis/model/_components.py (2)
  • model (570-571)
  • moveAbsSync (852-859)
src/odemis/util/linalg.py (1)
  • get_z_pos_on_plane (180-194)
🪛 Ruff (0.14.3)
src/odemis/acq/fastem.py

377-377: Avoid specifying long messages outside the exception class

(TRY003)

⏰ 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 (3)
src/odemis/gui/__init__.py (1)

51-51: LGTM!

The new color constant follows the established naming pattern and is consistent with the other color-blind-friendly color definitions.

src/odemis/gui/cont/acquisition/fastem_acq.py (2)

1693-1695: Verify the visibility toggle behavior is consistent with user expectations.

When toggling CALIBRATION_1 visibility, the code also toggles visibility for all 9 focus_map regions. Ensure this behavior is intuitive for users—they might expect to control the focus_map regions independently, or they should be informed that these are linked to CALIBRATION_1.


1952-1960: The verification confirms the code transformation is correct—no changes needed.

The align() function expects stage_pos to be a list and uses the first position stage_pos[0] for stage movement via moveAbsSync(), while passing the entire stage_pos list as focus_pos to calibration routines. The code at lines 1952-1960 that converts stage_pos from a tuple to a list and appends 9 focus_map positions aligns correctly with this interface. The comment in the code confirms the first position is always the region of calibration (ROC) position, establishing that order is preserved as expected.

Comment on lines +788 to +811
for i, focus_pos in enumerate((
(-100, 10),
(-100, 100),
(-100, -100),
(10, -100),
(10, 100),
(10, 10),
(100, 10),
(100, 100),
(100, -100),
)):
calibration_name = f"focus_map_{i}"
calibration = FastEMCalibration(name=calibration_name)
number = 1
colour = FG_COLOUR_BLIND_YELLOW
calibration.sequence.value = calib_1_calibrations
xmin = position[0] - 0.5 * sz[0] + focus_pos[0] * sz[0]
xmax = position[0] + 0.5 * sz[0] + focus_pos[0] * sz[0]
ymin = position[1] + 0.5 * sz[1] + focus_pos[1] * sz[1]
ymax = position[1] - 0.5 * sz[1] + focus_pos[1] * sz[1]
calibration.region = FastEMROC(name=str(number),
scintillator_number=int(scintillator_number),
coordinates=(xmin, ymin, xmax, ymax), colour=colour)
scintillator.calibrations[calibration_name] = calibration
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Document the focus_pos offsets and clarify the spatial pattern.

The 9 focus_pos tuples use hardcoded values like (-100, 10), (100, -100), etc., but there's no explanation of:

  1. What these values represent (they appear to be multipliers of the field size, e.g., -100 means -100× the field width).
  2. Why these specific positions were chosen.
  3. The spatial pattern they form (is it a 3×3 grid? The values don't suggest a regular grid).

Additionally, line 807 has inconsistent spacing: ymax = (extra space before =).

Apply this diff to add documentation and fix spacing:

+                # Create 9 focus_map calibrations distributed around the scintillator
+                # focus_pos values are multipliers of the field size (sz), forming a spatial grid
+                # for multi-point optical focus mapping during Calibration 1
                 for i, focus_pos in enumerate((
                         (-100, 10),
                         (-100, 100),
                         (-100, -100),
                         (10, -100),
                         (10, 100),
                         (10, 10),
                         (100, 10),
                         (100, 100),
                         (100, -100),
                 )):
                     calibration_name = f"focus_map_{i}"
                     calibration = FastEMCalibration(name=calibration_name)
                     number = 1
                     colour = FG_COLOUR_BLIND_YELLOW
                     calibration.sequence.value = calib_1_calibrations
                     xmin = position[0] - 0.5 * sz[0] + focus_pos[0] * sz[0]
                     xmax = position[0] + 0.5 * sz[0] + focus_pos[0] * sz[0]
                     ymin = position[1] + 0.5 * sz[1] + focus_pos[1] * sz[1]
-                    ymax =  position[1] - 0.5 * sz[1] + focus_pos[1] * sz[1]
+                    ymax = position[1] - 0.5 * sz[1] + focus_pos[1] * sz[1]
                     calibration.region = FastEMROC(name=str(number),
                                                    scintillator_number=int(scintillator_number),
                                                    coordinates=(xmin, ymin, xmax, ymax), colour=colour)
                     scintillator.calibrations[calibration_name] = calibration

Consider also defining the focus_pos pattern more explicitly or using a comprehension to generate a regular grid if that's the intent:

# Example for a regular 3x3 grid:
focus_positions = [(x, y) for x in (-100, 0, 100) for y in (-100, 0, 100)]

…n focus plane

AutofocusMultiprobe in fastem-calibrations has been updated to run in multiple positions and fit a plane through those positions.
Based on this plane the focus is corrected during ROA acquisition
@tepals tepals force-pushed the FVEM-26-optical-focus-mapping branch from 1991832 to 0bc4c51 Compare February 24, 2026 15:00
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

♻️ Duplicate comments (2)
src/odemis/gui/model/main_gui_data.py (1)

788-811: Remaining issues from previous review: undocumented offsets and extra space on line 807.

Line 807 has ymax = position[1]... (two spaces before position). Additionally, the focus_pos multipliers (-100, 10), (10, -100), etc. still lack a comment explaining what the values represent (multipliers of field size sz?) and why these 9 specific positions were chosen.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/odemis/gui/model/main_gui_data.py` around lines 788 - 811, Fix the stray
extra space in the ymax assignment (remove the double space before position) and
add a concise comment above the focus_pos list explaining what the tuple values
represent (they are multipliers applied to the field size variable sz to produce
offset centers around the central position) and why these nine positions are
used (e.g., a 3x3 grid of offsets: left/center/right and top/center/bottom for
focus mapping). Reference the variables and symbols in the block—focus_pos, sz,
position, calibration_name, FastEMCalibration, and FastEMROC—so the maintainer
can locate and update the code accordingly.
src/odemis/acq/fastem.py (1)

373-378: focus_plane structure is still not validated after the existence check.

if not self._focus_plane only guards against a missing/empty value. Accesses to self._focus_plane["gamma"] and self._focus_plane["normal"] at lines 691–693 will raise an unguarded KeyError if the metadata is present but malformed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/odemis/acq/fastem.py` around lines 373 - 378, The existence check for
self._focus_plane is insufficient — after assigning self._focus_plane from
self._stage.getMetadata().get(model.MD_CALIB, {}).get("focus_plane") add robust
validation that the dict contains the expected keys and types (e.g., "gamma"
present and is numeric, "normal" present and is a sequence/tuple of length 3
with numeric components) before any later access (used by the focus correction
logic in this class); if validation fails, raise a ValueError with a clear
message like "focus_plane metadata malformed: expected {'gamma': number,
'normal': (x,y,z)}; run Calibration 1." Ensure this validation is executed only
when self._correct_focus is true and immediately after the current assignment to
self._focus_plane.
🧹 Nitpick comments (1)
src/odemis/gui/cont/acquisition/fastem_acq.py (1)

1757-1759: Hardcoded 9 appears in multiple locations — consider a shared constant.

The literal range(9) here and again at line 2019 (on_run) both depend on the 9 positions defined in FastEMMainGUIData.__init__(). If the count changes it must be updated in three places.

♻️ Suggested refactor (module-level constant)
+# Number of focus map calibration positions per scintillator, must match
+# the number of entries in FastEMMainGUIData's focus_pos tuple.
+NUM_FOCUS_MAP_POSITIONS = 9

Then replace both range(9) occurrences:

-            for i in range(9):
+            for i in range(NUM_FOCUS_MAP_POSITIONS):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/odemis/gui/cont/acquisition/fastem_acq.py` around lines 1757 - 1759, The
code uses the literal 9 in multiple places (e.g., in the if block checking
calibration_key == CALIBRATION_1 where you call self._show_calibration_region
and again in on_run) which duplicates the number of focus positions defined in
FastEMMainGUIData.__init__; introduce a single module-level constant (e.g.,
NUM_FOCUS_POSITIONS) and replace all occurrences of range(9) with
range(NUM_FOCUS_POSITIONS), updating both the block that calls
self._show_calibration_region and the on_run logic so they derive the count from
that constant rather than a hardcoded literal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/odemis/acq/fastem.py`:
- Around line 164-165: The parameter focus_mapping in the acquire() function and
in AcquisitionTask.__init__ lacks a type annotation; update both signatures to
include an explicit type hint (e.g., focus_mapping: bool = False) to comply with
the project typing guideline, adjust any related usages or callers if necessary,
and run the type checker to ensure no downstream type errors are introduced
(look for the acquire function and the AcquisitionTask.__init__ constructor to
make the changes).

In `@src/odemis/gui/cont/acquisition/fastem_acq.py`:
- Around line 2017-2025: The code wraps a single (x,y) tuple into a list only
for CALIBRATION_1 (in the block using CALIBRATION_1, focus_map_calibration and
current_sample.scintillators) but other callers expect a plain tuple, causing a
TypeError when later code (in the align logic that indexes stage_pos as
stage_pos[0][0]) treats stage_pos uniformly as a list of tuples; fix by
normalizing stage_pos right after it is received/constructed: detect whether
stage_pos is a tuple (e.g., isinstance(stage_pos, tuple)) and if so wrap it into
a single-item list, or conversely if it is a list with one tuple allow it, so
downstream code (the align function that does stage_pos[0][0]) always sees a
list of (float,float) pairs; update the CALIBRATION_1 block that currently does
stage_pos = [stage_pos] to use the same normalization helper or logic so both
plain tuple and list-of-tuples formats are supported consistently.

---

Duplicate comments:
In `@src/odemis/acq/fastem.py`:
- Around line 373-378: The existence check for self._focus_plane is insufficient
— after assigning self._focus_plane from
self._stage.getMetadata().get(model.MD_CALIB, {}).get("focus_plane") add robust
validation that the dict contains the expected keys and types (e.g., "gamma"
present and is numeric, "normal" present and is a sequence/tuple of length 3
with numeric components) before any later access (used by the focus correction
logic in this class); if validation fails, raise a ValueError with a clear
message like "focus_plane metadata malformed: expected {'gamma': number,
'normal': (x,y,z)}; run Calibration 1." Ensure this validation is executed only
when self._correct_focus is true and immediately after the current assignment to
self._focus_plane.

In `@src/odemis/gui/model/main_gui_data.py`:
- Around line 788-811: Fix the stray extra space in the ymax assignment (remove
the double space before position) and add a concise comment above the focus_pos
list explaining what the tuple values represent (they are multipliers applied to
the field size variable sz to produce offset centers around the central
position) and why these nine positions are used (e.g., a 3x3 grid of offsets:
left/center/right and top/center/bottom for focus mapping). Reference the
variables and symbols in the block—focus_pos, sz, position, calibration_name,
FastEMCalibration, and FastEMROC—so the maintainer can locate and update the
code accordingly.

---

Nitpick comments:
In `@src/odemis/gui/cont/acquisition/fastem_acq.py`:
- Around line 1757-1759: The code uses the literal 9 in multiple places (e.g.,
in the if block checking calibration_key == CALIBRATION_1 where you call
self._show_calibration_region and again in on_run) which duplicates the number
of focus positions defined in FastEMMainGUIData.__init__; introduce a single
module-level constant (e.g., NUM_FOCUS_POSITIONS) and replace all occurrences of
range(9) with range(NUM_FOCUS_POSITIONS), updating both the block that calls
self._show_calibration_region and the on_run logic so they derive the count from
that constant rather than a hardcoded literal.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1991832 and 0bc4c51.

📒 Files selected for processing (6)
  • src/odemis/acq/align/fastem.py
  • src/odemis/acq/fastem.py
  • src/odemis/acq/test/fastem_test.py
  • src/odemis/gui/__init__.py
  • src/odemis/gui/cont/acquisition/fastem_acq.py
  • src/odemis/gui/model/main_gui_data.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/odemis/acq/test/fastem_test.py
  • src/odemis/gui/init.py
  • src/odemis/acq/align/fastem.py

Comment on lines +164 to +165
spot_grid_thresh=0.5, blank_beam=True, stop_acq_on_failure=True, acq_dwell_time: Optional[float] = None,
focus_mapping=False):
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

Missing type hint on focus_mapping in acquire() — coding guideline violation.

focus_mapping=False lacks a type annotation while the adjacent parameter acq_dwell_time: Optional[float] = None has one.

🔧 Proposed fix
-            spot_grid_thresh=0.5, blank_beam=True, stop_acq_on_failure=True, acq_dwell_time: Optional[float] = None,
-            focus_mapping=False):
+            spot_grid_thresh=0.5, blank_beam=True, stop_acq_on_failure=True, acq_dwell_time: Optional[float] = None,
+            focus_mapping: bool = False):

The same applies to AcquisitionTask.__init__() at line 237:

-                 blank_beam, stop_acq_on_failure, focus_mapping, future):
+                 blank_beam, stop_acq_on_failure, focus_mapping: bool, future):

As per coding guidelines: "Always use type hints for function parameters and return types in Python code."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/odemis/acq/fastem.py` around lines 164 - 165, The parameter focus_mapping
in the acquire() function and in AcquisitionTask.__init__ lacks a type
annotation; update both signatures to include an explicit type hint (e.g.,
focus_mapping: bool = False) to comply with the project typing guideline, adjust
any related usages or callers if necessary, and run the type checker to ensure
no downstream type errors are introduced (look for the acquire function and the
AcquisitionTask.__init__ constructor to make the changes).

Comment on lines +2017 to +2025
if calib_name == CALIBRATION_1:
stage_pos = [stage_pos]
for i in range(9):
focus_map_calibration = current_sample.scintillators[
scintillator_num
].calibrations[f"focus_map_{i}"]
fxmin, fymin, fxmax, fymax = focus_map_calibration.region.coordinates.value
fstage_pos = ((fxmin + fxmax) / 2, (fymin + fymax) / 2)
stage_pos.append(fstage_pos)
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the align module and fastem-related files
find . -type f -name "*.py" | grep -E "(align|fastem)" | head -20

Repository: delmic/odemis

Length of output: 763


🏁 Script executed:

#!/bin/bash
# Search for stage_pos usage in the align module
rg -n "stage_pos" --type py src/odemis/acq/align/ -A 3 -B 3

Repository: delmic/odemis

Length of output: 5405


🏁 Script executed:

#!/bin/bash
# Look for the run() method in align module that handles stage_pos
rg -n "def run" --type py src/odemis/acq/align/ -A 5

Repository: delmic/odemis

Length of output: 1478


🏁 Script executed:

#!/bin/bash
# Check what run_calibration does with focus_pos
rg -n "focus_pos" --type py src/odemis/acq/align/fastem.py -A 5 -B 2

Repository: delmic/odemis

Length of output: 582


🏁 Script executed:

#!/bin/bash
# Check the full context around line 259 and 267 in fastem.py
sed -n '250,280p' src/odemis/acq/align/fastem.py

Repository: delmic/odemis

Length of output: 1646


🏁 Script executed:

#!/bin/bash
# Check the fastem_acq.py file to see how stage_pos is passed
sed -n '2010,2035p' src/odemis/gui/cont/acquisition/fastem_acq.py

Repository: delmic/odemis

Length of output: 1358


🏁 Script executed:

#!/bin/bash
# Get the full run() method to see if there's conditional handling
sed -n '204,290p' src/odemis/acq/align/fastem.py

Repository: delmic/odemis

Length of output: 4845


Fix stage_pos handling to support both plain tuple and list of tuples formats.

At line 259 in src/odemis/acq/align/fastem.py, the code does stage_pos[0][0] assuming stage_pos is always a list of tuples. However, the API contract (documented at line 112-113) specifies stage_pos as a (float, float) plain tuple. When CALIBRATION_2 or CALIBRATION_3 is called with a plain tuple, stage_pos[0] returns the float x-coordinate, and attempting [0] on a float causes a TypeError: 'float' object is not subscriptable. Only CALIBRATION_1 (wrapping the tuple in a list) avoids this crash. The code must detect the format and handle both cases, or the API contract must be updated consistently across both the GUI and align module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/odemis/gui/cont/acquisition/fastem_acq.py` around lines 2017 - 2025, The
code wraps a single (x,y) tuple into a list only for CALIBRATION_1 (in the block
using CALIBRATION_1, focus_map_calibration and current_sample.scintillators) but
other callers expect a plain tuple, causing a TypeError when later code (in the
align logic that indexes stage_pos as stage_pos[0][0]) treats stage_pos
uniformly as a list of tuples; fix by normalizing stage_pos right after it is
received/constructed: detect whether stage_pos is a tuple (e.g.,
isinstance(stage_pos, tuple)) and if so wrap it into a single-item list, or
conversely if it is a list with one tuple allow it, so downstream code (the
align function that does stage_pos[0][0]) always sees a list of (float,float)
pairs; update the CALIBRATION_1 block that currently does stage_pos =
[stage_pos] to use the same normalization helper or logic so both plain tuple
and list-of-tuples formats are supported consistently.

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