Skip to content

[S30-130][fix] Store e-beam current and voltage as metadata#3490

Open
pieleric wants to merge 2 commits into
delmic:masterfrom
pieleric:feat-odemisd-md-updater-also-takes-care-of-beam_current-and-beam_voltage
Open

[S30-130][fix] Store e-beam current and voltage as metadata#3490
pieleric wants to merge 2 commits into
delmic:masterfrom
pieleric:feat-odemisd-md-updater-also-takes-care-of-beam_current-and-beam_voltage

Conversation

@pieleric

@pieleric pieleric commented Jun 12, 2026

Copy link
Copy Markdown
Member

The metadata of the e-beam when controlled via API (eg, current and accel voltage) is never stored on the data.

=> extend the mdupdater to listen to the standard VAs of the e-beam and update the meteadata of all affected detectors

This ensures that it’s be displayed in the GUI, and in the exported image databar.

image image

Copilot AI review requested due to automatic review settings June 12, 2026 13:56
@pieleric pieleric changed the title [feat] odemisd MD updater also takes care of BEAM_CURRENT and BEAM_VOLTAGE [S30-130][fix] Store e-beam current and voltage as metadata Jun 12, 2026
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

MetadataUpdater.observeEbeam now subscribes to the e-beam's probeCurrent and accelVoltage and propagates those values to affected components (rotation still forwarded only for multibeam). StreamController.display_metadata shows acceleration voltage and emission current only for EM or FIB acquisition types and reads MD_BEAM* keys. New integration tests (EbeamMDUpdaterTest) exercise the simulated SEM flow and assert metadata propagation.

Sequence Diagram(s)

sequenceDiagram
  participant EBeamScanner
  participant MetadataUpdater
  participant DetectorComponent
  participant StreamController
  EBeamScanner->>MetadataUpdater: VA update (probeCurrent / accelVoltage)
  MetadataUpdater->>DetectorComponent: set MD_BEAM_CURRENT / MD_BEAM_VOLTAGE
  StreamController->>DetectorComponent: read metadata for display (if acquisitionType EM/FIB)
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: storing e-beam current and voltage as metadata through the mdupdater.
Description check ✅ Passed The description is directly related to the changeset, explaining that e-beam metadata is not stored and the fix extends mdupdater to listen to e-beam VAs and update affected detectors.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (2)
src/odemis/odemisd/test/mdupdater_test.py (1)

36-37: 💤 Low value

Consider using pathlib.Path for file path construction.

Lines 36-37 construct file paths using string concatenation and os.path.dirname. As per coding guidelines, file paths should use pathlib.Path unless interfacing with existing code that requires strings. If testing.start_backend accepts path-like objects, refactor to use pathlib.Path for better cross-platform compatibility and readability.

♻️ Example refactor using pathlib.Path
+from pathlib import Path
+
 import odemis
 from odemis import model
 from odemis.driver import static
 from odemis.model import Microscope
 from odemis.odemisd.mdupdater import MetadataUpdater
 from odemis.util import mock, testing

-CONFIG_PATH = os.path.dirname(odemis.__file__) + "/../../install/linux/usr/share/odemis/"
-SPARC2_CONFIG = CONFIG_PATH + "sim/sparc2-sim-scanner.odm.yaml"
+CONFIG_PATH = Path(odemis.__file__).parent / "../../install/linux/usr/share/odemis/"
+SPARC2_CONFIG = str(CONFIG_PATH / "sim/sparc2-sim-scanner.odm.yaml")

Note: str() wrapper retained only if testing.start_backend requires a string path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/odemis/odemisd/test/mdupdater_test.py` around lines 36 - 37, Replace
string-based path construction with pathlib.Path: create CONFIG_PATH as
Path(odemis.__file__).resolve().parent.joinpath("..","..","install","linux","usr","share","odemis")
(or equivalent) and set SPARC2_CONFIG = CONFIG_PATH / "sim" /
"sparc2-sim-scanner.odm.yaml"; when calling testing.start_backend, pass the Path
object directly if it accepts path-like objects, otherwise wrap with str()
(references: CONFIG_PATH, SPARC2_CONFIG, odemis, testing.start_backend).

Source: Coding guidelines

src/odemis/gui/cont/stream.py (1)

287-291: ⚡ Quick win

Use consistent metadata keys: check and retrieve the same key name.

Lines 287-288 check for MD_BEAM_VOLTAGE but retrieve md[MD_EBEAM_VOLTAGE]. Lines 290-291 do the same for MD_BEAM_CURRENT vs MD_EBEAM_CURRENT. While these are aliases pointing to the same string, the inconsistency is confusing. Since MetadataUpdater.observeEbeam (in mdupdater.py) now populates MD_BEAM_VOLTAGE and MD_BEAM_CURRENT, the GUI should consistently check for and retrieve those same keys.

♻️ Proposed fix for consistency
         if self.stream.acquisitionType.value in (model.MD_AT_EM, model.MD_AT_FIB):
             if model.MD_BEAM_VOLTAGE in md:
-                self.add_metadata("Acceleration voltage", md[model.MD_EBEAM_VOLTAGE], 'V')
+                self.add_metadata("Acceleration voltage", md[model.MD_BEAM_VOLTAGE], 'V')

             if model.MD_BEAM_CURRENT in md:
-                self.add_metadata("Emission current", md[model.MD_EBEAM_CURRENT], 'A')
+                self.add_metadata("Emission current", md[model.MD_BEAM_CURRENT], 'A')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/odemis/gui/cont/stream.py` around lines 287 - 291, The GUI checks for
model.MD_BEAM_VOLTAGE and model.MD_BEAM_CURRENT but retrieves
md[model.MD_EBEAM_VOLTAGE] and md[model.MD_EBEAM_CURRENT], which is inconsistent
with MetadataUpdater.observeEbeam; update the retrieval to use the same keys you
check for (md[model.MD_BEAM_VOLTAGE] and md[model.MD_BEAM_CURRENT]) so
add_metadata calls use the identical metadata keys, leaving add_metadata and
MetadataUpdater.observeEbeam unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/odemis/odemisd/mdupdater.py`:
- Around line 622-629: The subscription to ebeam.rotation should be guarded so
we don't AttributeError when the VA is missing: before creating updateRotation
and calling ebeam.rotation.subscribe, check that the ebeam actually exposes the
rotation VA (e.g., use ebeam.hasVA('rotation') or equivalent) and only then
define updateRotation, subscribe with init=True, append to self._onTerminate
(unsubscribe tuple) and set observed=True; if the check fails, skip the
subscription logic for that ebeam.

In `@src/odemis/odemisd/test/mdupdater_test.py`:
- Around line 134-136: The test assumes probeCurrent.choices supports set
subtraction; update the assignment to coerce choices into a set before
subtracting the current value: convert self.ebeam.probeCurrent.choices into
set(...) and then subtract {self.ebeam.probeCurrent.value}, then pick an element
and assign it back to self.ebeam.probeCurrent.value; change the expression that
computes new_current in the mdupdater_test (the lines using
probeCurrent.choices, probeCurrent.value, and new_current) to use
set(self.ebeam.probeCurrent.choices) to avoid TypeError for non-set iterables.

---

Nitpick comments:
In `@src/odemis/gui/cont/stream.py`:
- Around line 287-291: The GUI checks for model.MD_BEAM_VOLTAGE and
model.MD_BEAM_CURRENT but retrieves md[model.MD_EBEAM_VOLTAGE] and
md[model.MD_EBEAM_CURRENT], which is inconsistent with
MetadataUpdater.observeEbeam; update the retrieval to use the same keys you
check for (md[model.MD_BEAM_VOLTAGE] and md[model.MD_BEAM_CURRENT]) so
add_metadata calls use the identical metadata keys, leaving add_metadata and
MetadataUpdater.observeEbeam unchanged.

In `@src/odemis/odemisd/test/mdupdater_test.py`:
- Around line 36-37: Replace string-based path construction with pathlib.Path:
create CONFIG_PATH as
Path(odemis.__file__).resolve().parent.joinpath("..","..","install","linux","usr","share","odemis")
(or equivalent) and set SPARC2_CONFIG = CONFIG_PATH / "sim" /
"sparc2-sim-scanner.odm.yaml"; when calling testing.start_backend, pass the Path
object directly if it accepts path-like objects, otherwise wrap with str()
(references: CONFIG_PATH, SPARC2_CONFIG, odemis, testing.start_backend).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d4fdd0c4-a88f-4aca-8b09-2d356139b7a9

📥 Commits

Reviewing files that changed from the base of the PR and between f74a181 and b6f8e11.

📒 Files selected for processing (3)
  • src/odemis/gui/cont/stream.py
  • src/odemis/odemisd/mdupdater.py
  • src/odemis/odemisd/test/mdupdater_test.py

Comment thread src/odemis/odemisd/mdupdater.py
Comment thread src/odemis/odemisd/test/mdupdater_test.py

Copilot AI left a comment

Copy link
Copy Markdown

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 extends MetadataUpdater’s e-beam observer so that e-beam probe current and acceleration voltage are propagated as MD_BEAM_CURRENT and MD_BEAM_VOLTAGE to all affected components (and still propagates rotation for multibeam). It also updates the GUI metadata display logic and adds an integration test using the simulated SPARC2 scanner config.

Changes:

  • Propagate probeCurrentMD_BEAM_CURRENT and accelVoltageMD_BEAM_VOLTAGE for components affected by the e-beam.
  • Adjust GUI metadata display to avoid showing beam metadata on non-EM/FIB streams.
  • Add an integration test validating detector metadata updates when e-beam parameters change (simulated backend).

Reviewed changes

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

File Description
src/odemis/odemisd/mdupdater.py Enhances e-beam metadata propagation to include beam current/voltage (plus multibeam rotation).
src/odemis/odemisd/test/mdupdater_test.py Adds an integration test verifying metadata propagation for beam current/voltage via the simulated SPARC2 backend.
src/odemis/gui/cont/stream.py Updates which streams display beam voltage/current metadata in the GUI.

Comment thread src/odemis/odemisd/test/mdupdater_test.py
Comment thread src/odemis/odemisd/test/mdupdater_test.py
Comment thread src/odemis/gui/cont/stream.py Outdated
@pieleric pieleric force-pushed the feat-odemisd-md-updater-also-takes-care-of-beam_current-and-beam_voltage branch from b6f8e11 to c3513fb Compare June 12, 2026 14:03
pieleric added 2 commits June 12, 2026 16:27
…LTAGE

So far, we assumed that if the e-beam driver changes the current of
voltage, it would update the metadata. However, there are two issues:
* if it's a CompositedScanner, the component in charge of the column is
  not the same as the one acquiring the data, and so it cannot update
  the metadata.
* The metadata would be only on the e-beam data. However, on the SPARC,
  in case of CL, the optical image (spectrometer) also depends on the
  current & voltage, so it's worthy to store the metadata on it too.
…FIB streams

We used to show the metadata for all the streams, but until now only SEM
& FIB streams had this information (in some rare cases). Now, SEM
streams have this info almost all the time, and CL streams too, on the
SPARC. However, on the CL streams, it's always the same as the
concurrent SEM stream, so no need to display it too.
@pieleric pieleric force-pushed the feat-odemisd-md-updater-also-takes-care-of-beam_current-and-beam_voltage branch from c3513fb to bf4a4b1 Compare June 12, 2026 14:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/odemis/odemisd/test/mdupdater_test.py`:
- Around line 97-137: Add missing type hints and docstrings for the listed test
methods and helper: annotate setUpClass(cls) -> None, tearDownClass(cls) ->
None, test_ebeam_beam_params_on_detector(self) -> None, and
fake_get_component(name: str) -> ComponentType (or appropriate model.Component
return type), and add short reStructuredText docstrings for setUpClass,
tearDownClass, and fake_get_component describing their purpose; ensure
docstrings follow repo style and import any typing names needed (e.g., from
typing import Any or the model Component type) so the test file compiles with
type checking.
- Around line 116-117: The test sets cls.ebeam.affects to include the real
target "se-detector" but only asserts metadata on the fake CCD (cls.ccd); update
the test(s) around the cls.ebeam.affects usage (including the block covering
lines ~139-173) to also locate the actual "se-detector" target (e.g., by
querying the registry/manager by name or by resolving the path used in the code
under test) and assert the same propagated metadata values on that object as you
do for cls.ccd, ensuring you reference cls.ebeam.affects, cls.ccd.name and the
literal "se-detector" to find and validate the real detector.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8eaea8ad-909e-4a63-8ab8-651525bb2a4d

📥 Commits

Reviewing files that changed from the base of the PR and between c3513fb and bf4a4b1.

📒 Files selected for processing (3)
  • src/odemis/gui/cont/stream.py
  • src/odemis/odemisd/mdupdater.py
  • src/odemis/odemisd/test/mdupdater_test.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/odemis/gui/cont/stream.py
  • src/odemis/odemisd/mdupdater.py

Comment on lines +97 to +137
def setUpClass(cls):
# Create a minimal sparc2 microscope
cls.mic = Microscope("Fake SPARC2", "sparc2")

# Create a SimSEM with an e-beam scanner and an se-detector child
cls.sem = simsem.SimSEM(
"SEM",
"sem",
children={
"scanner": {"name": "e-beam scanner", "role": "e-beam"},
"detector0": {"name": "se-detector", "role": "se-detector"},
}
)
cls.ebeam = next(c for c in cls.sem.children.value if c.role == "e-beam")

# Create a fake CCD that will be affected by the e-beam
img = model.DataArray(numpy.empty((512, 768), dtype=numpy.uint16))
cls.ccd = mock.FakeCCD(img)

cls.ebeam.affects.value = [cls.ccd.name, "se-detector"]

# Mock model.getComponent() so MetadataUpdater can resolve component names
all_comps = list(cls.sem.children.value) + [cls.sem, cls.ccd]

def fake_get_component(name):
for c in all_comps:
if c.name == name:
return c
raise LookupError(f"no component {name}")

cls._patch_get_component = unittest.mock.patch.object(model, "getComponent", fake_get_component)
cls._patch_get_component.start()

cls.mdup = MetadataUpdater("MDUpdater", cls.mic)
cls.mic.alive.value = set(cls.sem.children.value) | {cls.ccd}

@classmethod
def tearDownClass(cls):
cls.mdup.terminate()
cls.sem.terminate()
cls._patch_get_component.stop()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add required type hints and function docstrings on new test methods/helpers.

setUpClass, tearDownClass, test_ebeam_beam_params_on_detector, and fake_get_component are missing required type hints, and setUpClass / tearDownClass / fake_get_component are missing function docstrings mandated by repo rules.

Suggested patch
 class EbeamMDUpdaterTest(unittest.TestCase):
@@
     `@classmethod`
-    def setUpClass(cls):
+    def setUpClass(cls) -> None:
+        """
+        Build a simulated microscope setup and start metadata updater wiring.
+        """
@@
-        def fake_get_component(name):
+        def fake_get_component(name: str):
+            """
+            Resolve a component by its name from the local simulated component list.
+            """
             for c in all_comps:
                 if c.name == name:
                     return c
             raise LookupError(f"no component {name}")
@@
     `@classmethod`
-    def tearDownClass(cls):
+    def tearDownClass(cls) -> None:
+        """
+        Tear down updater, simulated hardware, and patches created for this test class.
+        """
         cls.mdup.terminate()
         cls.sem.terminate()
         cls._patch_get_component.stop()
@@
-    def test_ebeam_beam_params_on_detector(self):
+    def test_ebeam_beam_params_on_detector(self) -> None:

As per coding guidelines: **/*.py: Always use type hints for function parameters and return types in Python code; include docstrings for all functions and classes (reStructuredText style).

Also applies to: 139-139

🧰 Tools
🪛 Ruff (0.15.15)

[warning] 119-119: Consider [*list(cls.sem.children.value), cls.sem, cls.ccd] instead of concatenation

Replace with [*list(cls.sem.children.value), cls.sem, cls.ccd]

(RUF005)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/odemis/odemisd/test/mdupdater_test.py` around lines 97 - 137, Add missing
type hints and docstrings for the listed test methods and helper: annotate
setUpClass(cls) -> None, tearDownClass(cls) -> None,
test_ebeam_beam_params_on_detector(self) -> None, and fake_get_component(name:
str) -> ComponentType (or appropriate model.Component return type), and add
short reStructuredText docstrings for setUpClass, tearDownClass, and
fake_get_component describing their purpose; ensure docstrings follow repo style
and import any typing names needed (e.g., from typing import Any or the model
Component type) so the test file compiles with type checking.

Source: Coding guidelines

Comment on lines +116 to +117
cls.ebeam.affects.value = [cls.ccd.name, "se-detector"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate metadata propagation on the actual se-detector too, not only the fake CCD.

The test configures two affected targets (cls.ccd.name and "se-detector"), but assertions only check cls.ccd. This misses regressions where propagation fails for the real detector path.

Suggested patch
         cls.ebeam = next(c for c in cls.sem.children.value if c.role == "e-beam")
+        cls.se_detector = next(c for c in cls.sem.children.value if c.role == "se-detector")
@@
         md = self.ccd.getMetadata()
+        md_se = self.se_detector.getMetadata()
@@
         self.assertAlmostEqual(md[model.MD_BEAM_CURRENT], self.ebeam.probeCurrent.value)
         self.assertAlmostEqual(md[model.MD_BEAM_VOLTAGE], self.ebeam.accelVoltage.value)
+        self.assertAlmostEqual(md_se[model.MD_BEAM_CURRENT], self.ebeam.probeCurrent.value)
+        self.assertAlmostEqual(md_se[model.MD_BEAM_VOLTAGE], self.ebeam.accelVoltage.value)
@@
         md = self.ccd.getMetadata()
+        md_se = self.se_detector.getMetadata()
         self.assertAlmostEqual(md[model.MD_BEAM_CURRENT], new_current)
+        self.assertAlmostEqual(md_se[model.MD_BEAM_CURRENT], new_current)
@@
         md = self.ccd.getMetadata()
+        md_se = self.se_detector.getMetadata()
         self.assertAlmostEqual(md[model.MD_BEAM_VOLTAGE], new_voltage)
+        self.assertAlmostEqual(md_se[model.MD_BEAM_VOLTAGE], new_voltage)

Also applies to: 139-173

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/odemis/odemisd/test/mdupdater_test.py` around lines 116 - 117, The test
sets cls.ebeam.affects to include the real target "se-detector" but only asserts
metadata on the fake CCD (cls.ccd); update the test(s) around the
cls.ebeam.affects usage (including the block covering lines ~139-173) to also
locate the actual "se-detector" target (e.g., by querying the registry/manager
by name or by resolving the path used in the code under test) and assert the
same propagated metadata values on that object as you do for cls.ccd, ensuring
you reference cls.ebeam.affects, cls.ccd.name and the literal "se-detector" to
find and validate the real detector.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants