-
Notifications
You must be signed in to change notification settings - Fork 41
[MSD-40][refactor] Milling angle as VA with automatic sample stage matrix recalculation #3313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,7 +83,7 @@ | |
|
|
||
| # These values might differ per system and would then require a configuration option per system. | ||
| # Hardcoded for now. Note that these values correspond to the milling angle, and not the actual stage tilt. | ||
| MILLING_RANGE = (5, 30) # degrees | ||
| MILLING_RANGE = (math.radians(5), math.radians(30)) | ||
|
|
||
| def filter_dict(keys: set, d: Dict[str, float]) -> Dict[str, float]: | ||
| """ | ||
|
|
@@ -332,7 +332,6 @@ def __init__(self, microscope): | |
| md_calib = stage_md.get(model.MD_CALIB, {}) | ||
| self.pre_tilt = md_calib.get(model.MD_SAMPLE_PRE_TILT, None) | ||
| self.fib_column_tilt = TFS_FIB_COLUMN_TILT | ||
|
|
||
| # use_linked_sem_focus_compensation: when True, the SEM focus is restored to the eucentric | ||
| # focus when moving the stage. This is done on TFS systems to compensate | ||
| # for the SEM focus changing when the stage is moved in Z (due to stage linking). | ||
|
|
@@ -356,6 +355,13 @@ def __init__(self, microscope): | |
| mill_md["mill_angle"] = mill_md.pop("rx") | ||
| self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: mill_md}) | ||
|
|
||
| # Initialize the milling angle. If not specified in the config, set it to 0 | ||
| milling_angle = stage_md.get(model.MD_FAV_MILL_POS_ACTIVE, None) | ||
| milling_angle = milling_angle["mill_angle"] if milling_angle else 0 | ||
| self.milling_angle = model.FloatContinuous( | ||
| milling_angle, (MILLING_RANGE[0], MILLING_RANGE[1]), unit="rad", setter=self._set_milling_angle | ||
| ) | ||
|
|
||
| # current posture va | ||
| self.current_posture = model.VigilantAttribute(UNKNOWN) | ||
| self.stage.position.subscribe(self._update_posture, init=True) | ||
|
|
@@ -530,6 +536,18 @@ def _transformFromSEMToMeteor(self, pos: Dict[str, float]) -> Dict[str, float]: | |
| """ | ||
| pass | ||
|
|
||
| def _set_milling_angle(self, angle: float) -> float: | ||
| """ | ||
| Set the milling angle of the stage | ||
| :param angle: (float) milling angle in radians | ||
| """ | ||
| if model.MD_FAV_MILL_POS_ACTIVE not in self.stage.getMetadata(): | ||
| logging.warning("Trying to set a milling angle on a system that was not configured for milling") | ||
| return angle | ||
| rotations = {'mill_angle': angle, 'rz': self.stage.getMetadata()[model.MD_FAV_MILL_POS_ACTIVE]["rz"]} | ||
| self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: rotations}) | ||
| return angle | ||
|
|
||
| def _transformFromMeteorToSEM(self, pos: Dict[str, float]) -> Dict[str, float]: | ||
| """ | ||
| Transforms the current stage position from the meteor/FM imaging area | ||
|
|
@@ -744,7 +762,8 @@ def to_posture(self, pos: Dict[str, float], posture: int) -> Dict[str, float]: | |
|
|
||
| logging.info(f"Position Posture: {POSITION_NAMES[position_posture]}, Target Posture: {POSITION_NAMES[posture]}") | ||
|
|
||
| if position_posture == posture: | ||
| # The milling angle can change, so we should handle the MILLING --> MILLING posture switch differently. | ||
| if posture != MILLING and position_posture == posture: | ||
| return pos | ||
|
|
||
| # validate the transformation | ||
|
|
@@ -1642,30 +1661,21 @@ def __init__(self, microscope): | |
| self.postures = [SEM_IMAGING, FM_IMAGING] | ||
| if model.MD_FAV_MILL_POS_ACTIVE in stage_md: | ||
| self.postures.append(MILLING) | ||
| self._initialise_transformation(axes=["y", "z"], rotation=self.pre_tilt) | ||
|
|
||
| self.linked_axes = ["y", "z"] | ||
| # The setter of self.milling_angle sets the milling angle metadata, which is used to update the transformation parameters. | ||
| self.milling_angle.subscribe(self._initialise_transformation, init=True) | ||
tmoerkerken marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self.create_sample_stage() | ||
|
|
||
| # Update the posture based on the actual metadata | ||
| self._update_posture(self.stage.position.value) | ||
|
|
||
| def _initialise_transformation( | ||
| self, | ||
| axes: Sequence[str], | ||
| rotation: float = 0, | ||
| scale: tuple = (1, 1), | ||
| translation: tuple = (0, 0), | ||
| shear: tuple = (0, 0), | ||
| ): | ||
| def _initialise_transformation(self, angle): | ||
| """ | ||
| Initializes the transformation parameters that allows conversion between stage-bare and sample plane. | ||
| :param axes: stage axes which are used to calculate transformation parameters | ||
| :param rotation: rotation in radians from sample plane to stage | ||
| :param scale: scale from sample to stage | ||
| :param translation: translation from sample to stage | ||
| :param shear: shear from sample to stage | ||
| """ | ||
| self._axes_dep = {"x": axes[0], "y": axes[1]} # TODO: Should be called y, z... or even better: also take x as first axis | ||
| self._update_conversion(rotation) | ||
| self._axes_dep = {"x": self.linked_axes[0], "y": self.linked_axes[1]} # TODO: Should be called y, z... or even better: also take x as first axis | ||
| self._update_conversion(self.pre_tilt) | ||
| self._initialise_offset() | ||
|
Comment on lines
+1673
to
1679
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, locate and read the file
head -n 1680 src/odemis/acq/move.py | tail -n 10Repository: delmic/odemis Length of output: 532 🏁 Script executed: # Get more context around the function
sed -n '1665,1690p' src/odemis/acq/move.pyRepository: delmic/odemis Length of output: 1363 🏁 Script executed: # Check if there are type hints elsewhere in the file to understand the style
rg "def.*\(.*\)" src/odemis/acq/move.py -A 2 | head -n 50Repository: delmic/odemis Length of output: 1705 🏁 Script executed: # Check if ruff/linting reports are available or if we can verify the unused parameter
rg "_initialise_transformation" src/odemis/acq/move.py -B 2 -A 10Repository: delmic/odemis Length of output: 4222 Add type hints, complete docstring, and mark unused callback parameter The Suggested fix- def _initialise_transformation(self, angle):
- """
- Initializes the transformation parameters that allows conversion between stage-bare and sample plane.
- """
+ def _initialise_transformation(self, _angle: float) -> None:
+ """
+ Initializes the transformation parameters that allows conversion between stage-bare and sample plane.
+ :param _angle: Current milling angle in radians (provided by VA callback).
+ """🧰 Tools🪛 Ruff (0.14.14)[warning] 1673-1673: Unused method argument: (ARG002) 🤖 Prompt for AI Agents |
||
|
|
||
| def _update_conversion(self, | ||
|
|
@@ -1704,7 +1714,6 @@ def _update_conversion(self, | |
| tf_sem = tf_reverse @ tf_tilt @ tf_sr | ||
| tf_sem_inv = numpy.linalg.inv(tf_sem) | ||
|
|
||
| # TODO: update MILLING transformations when changing milling angle | ||
| if model.MD_FAV_MILL_POS_ACTIVE in stage_md: | ||
| mill_pos_active = stage_md[model.MD_FAV_MILL_POS_ACTIVE] | ||
| rx_mill = calculate_stage_tilt_from_milling_angle(milling_angle=mill_pos_active["mill_angle"], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ | |
|
|
||
| import odemis | ||
| from odemis import model | ||
| from odemis.acq.move import (FM_IMAGING, SEM_IMAGING, UNKNOWN, MicroscopePostureManager, LOADING) | ||
| from odemis.acq.move import (FM_IMAGING, SEM_IMAGING, UNKNOWN, MeteorTescan1PostureManager, LOADING, MILLING) | ||
| from odemis.acq.test.move_tfs1_test import TestMeteorTFS1Move | ||
| from odemis.acq.test.move_tfs3_test import TestMeteorTFS3Move | ||
| from odemis.util import testing | ||
|
|
@@ -175,7 +175,7 @@ class TestMeteorTescan1FibsemMove(TestMeteorTFS3Move): | |
| def setUpClass(cls): | ||
| testing.start_backend(cls.MIC_CONFIG) | ||
| cls.microscope = model.getMicroscope() | ||
| cls.pm = MicroscopePostureManager(microscope=cls.microscope) | ||
| cls.pm = MeteorTescan1PostureManager(microscope=cls.microscope) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @pieleric was there a reason you decided not to use the Tescan posture manager? You foresee any problems changing it? I need it for the milling angle callback. |
||
|
|
||
| # get the stage components | ||
| cls.stage_bare = model.getComponent(role="stage-bare") | ||
|
|
@@ -236,5 +236,44 @@ def test_rel_move_fm_posture(self): | |
| exp_bare_pos[axis] += m_bare[axis] | ||
| testing.assert_pos_almost_equal(new_bare_pos, exp_bare_pos, atol=1e-6) | ||
|
|
||
| def test_milling_angle_callback(self): | ||
| initial_mill_pos_md = self.stage_md.get(model.MD_FAV_MILL_POS_ACTIVE) | ||
| self.assertIsNotNone(initial_mill_pos_md) | ||
| initial_mill_angle = math.radians(10) | ||
| self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: {"rx": initial_mill_angle, "rz": initial_mill_pos_md["rz"]}}) | ||
| # Now test that when altering the milling angle VA, the metadata is automatically updated. | ||
| self.pm.milling_angle.value = math.radians(22) | ||
| altered_mill_angle_md = self.pm.stage.getMetadata().get(model.MD_FAV_MILL_POS_ACTIVE)["mill_angle"] | ||
| self.assertNotAlmostEqual(initial_mill_angle, altered_mill_angle_md, places=3) | ||
|
|
||
| def test_milling_angle_stable_pos(self): | ||
| # Make sure to start from a valid position | ||
| self.pm.cryoSwitchSamplePosition(LOADING).result() | ||
| # Transform to milling posture | ||
| self.pm.cryoSwitchSamplePosition(MILLING).result() | ||
| # Store shorthand for sample stage | ||
| sample_stage = self.pm.sample_stage | ||
| # Take note of sample stage pos | ||
| initial_sample_stage_pos = sample_stage.position.value | ||
| # Switch to SEM posture | ||
| self.pm.cryoSwitchSamplePosition(SEM_IMAGING).result() | ||
| # Compare sample stage pos to previous pos | ||
| testing.assert_pos_almost_equal(sample_stage.position.value, initial_sample_stage_pos, atol=1e-6) | ||
| # Switch back to milling posture | ||
| self.pm.cryoSwitchSamplePosition(MILLING).result() | ||
| testing.assert_pos_almost_equal(sample_stage.position.value, initial_sample_stage_pos, atol=1e-6) | ||
|
Comment on lines
249
to
264
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test uses legacy At lines 187-188, the test directly updates self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: {'rx': milling_angle, ...}})However, based on the changes in 🔧 Proposed fix: Use the milling_angle VA or correct key # Now change milling angle to check stability; sample stage pos should remain the same.
milling_angle = math.radians(30)
- current_md = self.stage.getMetadata()
- self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: {'rx': milling_angle,
- "rz": current_md[model.MD_FAV_MILL_POS_ACTIVE]["rz"]}})
+ # Use the posture manager's milling_angle VA to properly trigger the update flow
+ self.posture_manager.milling_angle.value = milling_angleOr if directly updating metadata is intended for testing: milling_angle = math.radians(30)
current_md = self.stage.getMetadata()
- self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: {'rx': milling_angle,
+ self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: {'mill_angle': milling_angle,
"rz": current_md[model.MD_FAV_MILL_POS_ACTIVE]["rz"]}})
+ # Trigger transformation re-initialization
+ self.posture_manager._initialise_transformation(milling_angle)🤖 Prompt for AI Agents |
||
| # TODO: uncomment the following section once all the sample stage problems are resolved | ||
| # # Now change milling angle to check stability; sample stage pos should remain the same. | ||
| # milling_angle = math.radians(30) | ||
| # self.pm.milling_angle.value = milling_angle | ||
| # # Switch to SEM posture | ||
| # self.pm.cryoSwitchSamplePosition(SEM_IMAGING).result() | ||
| # # Compare sample stage pos to previous pos | ||
| # testing.assert_pos_almost_equal(sample_stage.position.value, initial_sample_stage_pos, atol=1e-6) | ||
| # # Switch back to milling posture | ||
| # self.pm.cryoSwitchSamplePosition(MILLING).result() | ||
| # testing.assert_pos_almost_equal(sample_stage.position.value, initial_sample_stage_pos, atol=1e-6) | ||
|
|
||
|
Comment on lines
+239
to
+276
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Check if the file exists and examine the test methods
find . -name "move_tescan_test.py" -type fRepository: delmic/odemis Length of output: 98 🏁 Script executed: # Read the relevant section of the file to verify the current state
if [ -f "src/odemis/acq/test/move_tescan_test.py" ]; then
wc -l src/odemis/acq/test/move_tescan_test.py
fiRepository: delmic/odemis Length of output: 100 🏁 Script executed: # Get a broader view of the test methods in the file
if [ -f "src/odemis/acq/test/move_tescan_test.py" ]; then
sed -n '230,290p' src/odemis/acq/test/move_tescan_test.py
fiRepository: delmic/odemis Length of output: 2863 🏁 Script executed: # Check other test methods in the file to see if they follow the docstring/type hint pattern
if [ -f "src/odemis/acq/test/move_tescan_test.py" ]; then
rg "def test_" src/odemis/acq/test/move_tescan_test.py -A 2 | head -50
fiRepository: delmic/odemis Length of output: 1731 Add docstrings and return type annotations for test methods Both 📝 Suggested update- def test_milling_angle_callback(self):
+ def test_milling_angle_callback(self) -> None:
+ """Verify the milling-angle VA updates metadata."""
initial_mill_pos_md = self.stage_md.get(model.MD_FAV_MILL_POS_ACTIVE)
self.assertIsNotNone(initial_mill_pos_md)
initial_mill_angle = math.radians(10)
self.stage.updateMetadata({model.MD_FAV_MILL_POS_ACTIVE: {"rx": initial_mill_angle, "rz": initial_mill_pos_md["rz"]}})
# Now test that when altering the milling angle VA, the metadata is automatically updated.
self.pm.milling_angle.value = math.radians(22)
altered_mill_angle_md = self.pm.stage.getMetadata().get(model.MD_FAV_MILL_POS_ACTIVE)["mill_angle"]
self.assertNotAlmostEqual(initial_mill_angle, altered_mill_angle_md, places=3)
- def test_milling_angle_stable_pos(self):
+ def test_milling_angle_stable_pos(self) -> None:
+ """Ensure sample-stage position is stable across posture switches."""
# Make sure to start from a valid position
self.pm.cryoSwitchSamplePosition(LOADING).result()🤖 Prompt for AI Agents |
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |
| import logging | ||
| import math | ||
| from typing import Dict | ||
| from concurrent.futures import CancelledError | ||
|
|
||
| import numpy | ||
| import wx | ||
|
|
@@ -42,7 +43,7 @@ | |
| StaticSEMStream, | ||
| ) | ||
| from odemis.gui import conf | ||
| from odemis.gui.comp.buttons import BTN_TOGGLE_COMPLETE, BTN_TOGGLE_OFF | ||
| from odemis.gui.comp.buttons import BTN_TOGGLE_COMPLETE, BTN_TOGGLE_OFF, BTN_TOGGLE_PROGRESS | ||
| from odemis.gui.conf.licences import LICENCE_FIBSEM_ENABLED, LICENCE_MILLING_ENABLED | ||
| from odemis.gui.cont import milling, settings | ||
| from odemis.gui.cont.features import CryoFeatureController | ||
|
|
@@ -188,15 +189,19 @@ def __init__(self, name, button, panel, main_frame, main_data): | |
| self.pm.stage.position.subscribe(self._on_stage_pos, init=True) | ||
| self.panel = panel | ||
|
|
||
| self._posture_switch_future = model.InstantaneousFuture() | ||
|
|
||
| rx = self.pm.stage.getMetadata()[model.MD_FAV_MILL_POS_ACTIVE]["mill_angle"] | ||
| self.panel.ctrl_milling_angle.SetValue(math.degrees(rx)) | ||
| self.panel.ctrl_milling_angle.SetValueRange(*MILLING_RANGE) | ||
| # Tilt field is in degrees, so convert from radians to integer degrees to prevent a lot of decimals from showing | ||
| self.panel.ctrl_milling_angle.SetValueRange(*numpy.round(numpy.rad2deg(MILLING_RANGE)).astype(int)) | ||
| self.panel.ctrl_milling_angle.Bind(wx.EVT_COMMAND_ENTER, self._update_milling_angle) | ||
| self._update_milling_angle(None) | ||
| self.panel.btn_switch_milling.Bind(wx.EVT_BUTTON, self._move_to_milling_position) | ||
| self.panel.btn_switch_sem_imaging.Bind(wx.EVT_BUTTON, self._move_to_sem) | ||
| self.panel.btn_switch_milling.Bind(wx.EVT_BUTTON, self._move_to_milling_posture) | ||
| self.panel.btn_switch_sem_imaging.Bind(wx.EVT_BUTTON, self._move_to_sem_posture) | ||
| self.tab_data_model.streams.subscribe(self._on_acquired_streams) | ||
|
|
||
| @call_in_wx_main | ||
| def _on_view(self, view): | ||
| """Hide/Disable milling controls when fib view is not selected""" | ||
| # is_fib_view = issubclass(view.stream_classes, FIBStream) | ||
|
|
@@ -394,68 +399,70 @@ def _on_stage_pos(self, pos: Dict[str, float]) -> None: | |
| logging.warning("Failed to update stage position label: %s", e) | ||
|
|
||
| # update the stage position buttons | ||
| self.panel.btn_switch_sem_imaging.Enable(posture in [SEM_IMAGING, MILLING]) | ||
| self.panel.btn_switch_milling.Enable(posture in [SEM_IMAGING, MILLING]) | ||
| self.panel.ctrl_milling_angle.Enable(posture in [SEM_IMAGING, MILLING]) | ||
| if self._posture_switch_future.done(): | ||
| self.panel.btn_switch_sem_imaging.Enable(posture in [SEM_IMAGING, MILLING]) | ||
| self.panel.btn_switch_milling.Enable(posture in [SEM_IMAGING, MILLING]) | ||
| self.panel.ctrl_milling_angle.Enable(posture in [SEM_IMAGING, MILLING]) | ||
|
|
||
| if posture == SEM_IMAGING: | ||
| self.panel.btn_switch_sem_imaging.SetValue(BTN_TOGGLE_COMPLETE) | ||
| else: | ||
| self.panel.btn_switch_sem_imaging.SetValue(BTN_TOGGLE_OFF) | ||
| if posture == MILLING: | ||
| self.panel.btn_switch_milling.SetValue(BTN_TOGGLE_COMPLETE) | ||
| else: | ||
| self.panel.btn_switch_milling.SetValue(BTN_TOGGLE_OFF) | ||
| if posture == SEM_IMAGING: | ||
| self.panel.btn_switch_sem_imaging.SetValue(BTN_TOGGLE_COMPLETE) | ||
| else: | ||
| self.panel.btn_switch_sem_imaging.SetValue(BTN_TOGGLE_OFF) | ||
| if posture == MILLING: | ||
| self.panel.btn_switch_milling.SetValue(BTN_TOGGLE_COMPLETE) | ||
| else: | ||
| self.panel.btn_switch_milling.SetValue(BTN_TOGGLE_OFF) | ||
|
|
||
| self.panel.Layout() | ||
| self.panel.Layout() | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def _update_milling_angle(self, evt: wx.Event): | ||
| # Check if already at milling posture | ||
| already_at_milling = self.pm.getCurrentPostureLabel() == MILLING | ||
|
|
||
| # update the metadata of the stage | ||
| milling_angle = math.radians(self.panel.ctrl_milling_angle.GetValue()) | ||
| current_md = self.pm.stage.getMetadata() | ||
| self.pm.stage.updateMetadata({ | ||
| model.MD_FAV_MILL_POS_ACTIVE: {"mill_angle": milling_angle, | ||
| "rz": current_md[model.MD_FAV_MILL_POS_ACTIVE]["rz"]} | ||
| }) | ||
|
|
||
| self.pm.milling_angle.value = milling_angle # this will call the setter in the move posture manager and handle md update | ||
| md = self.pm.get_posture_orientation(MILLING) | ||
| stage_tilt = md["rx"] | ||
| self.panel.ctrl_milling_angle.SetToolTip(f"A milling angle of {math.degrees(milling_angle):.2f}° " | ||
| f"corresponds to a stage tilt of {math.degrees(stage_tilt):.2f}°") | ||
|
|
||
| self._on_stage_pos(self.pm.stage.position.value) | ||
|
|
||
| # if the tab isn't shown, we don't want to ask the user | ||
| if evt is None: # if the event is None, it means this is the initial update, dont ask the user | ||
| # if the tab isn't shown, we don't want to perform a move or change features | ||
| if evt is None: | ||
| return | ||
|
|
||
| # changing milling angle, causes previously defined features at milling angle to be "seen" as SEM_IMAGING | ||
| # QUERY: should we update the features to the new milling angle? | ||
| box = wx.MessageDialog(self.main_frame, | ||
| message=f"Do you want to update existing feature positions with the updated milling angle ({math.degrees(milling_angle):.2f}°)?", | ||
| caption="Update existing feature positions?", | ||
| style=wx.YES_NO | wx.ICON_QUESTION | wx.CENTER) | ||
|
|
||
| ans = box.ShowModal() # Waits for the window to be closed | ||
| if ans == wx.ID_YES: | ||
| logging.debug( | ||
| f"Updating existing feature positions with the updated milling angle ({math.degrees(milling_angle):.2f}°)") | ||
| # NOTE: use stage_tilt, not milling_angle | ||
| for feature in self.main_data.features.value: | ||
| milling_position = feature.get_posture_position(MILLING) | ||
| if milling_position is not None: | ||
| milling_position["rx"] = stage_tilt | ||
| feature.set_posture_position(MILLING, milling_position) | ||
|
|
||
| def _move_to_milling_position(self, evt: wx.Event) -> None: | ||
| logging.info(f"MILLING ORIENTATION: {self.pm.get_posture_orientation(MILLING)}") | ||
| f = self.pm.cryoSwitchSamplePosition(MILLING) | ||
| f.result() | ||
|
|
||
| def _move_to_sem(self, evt: wx.Event) -> None: | ||
| f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING) | ||
| f.result() | ||
| logging.debug(f"Updating existing feature positions with the updated milling angle ({math.degrees(milling_angle):.2f}°)") | ||
| # NOTE: use stage_tilt, not milling_angle | ||
| for feature in self.main_data.features.value: | ||
| milling_position = feature.get_posture_position(MILLING) | ||
| if milling_position is not None: | ||
| milling_position["rx"] = stage_tilt | ||
| feature.set_posture_position(MILLING, milling_position) | ||
|
|
||
| if already_at_milling: | ||
| # Normally this happens automatically when clicking the ProgressRadioButton, but since we are now not | ||
| # clicking it, but still want to show progress, we set the progress state of the button manually. | ||
| self.panel.btn_switch_milling.SetValue(BTN_TOGGLE_PROGRESS) | ||
| self._move_to_milling_posture(None) | ||
|
|
||
| def _move_to_milling_posture(self, evt: wx.Event): | ||
| self._posture_switch_future = self.pm.cryoSwitchSamplePosition(MILLING) | ||
|
|
||
| # Do NOT call f.result(). Instead, add a callback: | ||
| self._posture_switch_future.add_done_callback(self._on_move_complete) | ||
|
|
||
| def _move_to_sem_posture(self, evt: wx.Event): | ||
| self._posture_switch_future = self.pm.cryoSwitchSamplePosition(SEM_IMAGING) | ||
| self._posture_switch_future.add_done_callback(self._on_move_complete) | ||
|
|
||
| @call_in_wx_main | ||
| def _on_move_complete(self, future): | ||
| try: | ||
| future.result() | ||
| except CancelledError: | ||
| logging.info("Posture switch was cancelled") | ||
| except Exception: | ||
| logging.exception("Failed to switch posture") | ||
|
|
||
|
Comment on lines
418
to
466
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Check if file exists and examine the specific lines
if [ -f "src/odemis/gui/cont/tabs/fibsem_tab.py" ]; then
echo "=== File exists. Examining lines 418-466 ==="
sed -n '418,466p' "src/odemis/gui/cont/tabs/fibsem_tab.py"
else
echo "File not found at src/odemis/gui/cont/tabs/fibsem_tab.py"
# Search for the file
find . -name "fibsem_tab.py" 2>/dev/null
fiRepository: delmic/odemis Length of output: 2575 🏁 Script executed: # Check file size to ensure we can read it
wc -l src/odemis/gui/cont/tabs/fibsem_tab.py 2>/dev/null || echo "File not found"Repository: delmic/odemis Length of output: 99 Add return type hints and docstrings All four methods lack return type annotations and reStructuredText docstrings as required by the coding guidelines. Additionally, the unused event parameters in 📝 Suggested update- def _update_milling_angle(self, evt: wx.Event):
+ def _update_milling_angle(self, evt: wx.Event) -> None:
+ """
+ Update milling angle metadata and associated feature positions.
+ :param evt: UI event triggering the update.
+ """
# Check if already at milling posture
already_at_milling = self.pm.getCurrentPostureLabel() == MILLING
@@
- def _move_to_milling_posture(self, evt: wx.Event):
+ def _move_to_milling_posture(self, _evt: wx.Event) -> None:
+ """
+ Start an asynchronous switch to milling posture.
+ :param _evt: UI event (unused).
+ """
self._posture_switch_future = self.pm.cryoSwitchSamplePosition(MILLING)
@@
- def _move_to_sem_posture(self, evt: wx.Event):
+ def _move_to_sem_posture(self, _evt: wx.Event) -> None:
+ """
+ Start an asynchronous switch to SEM posture.
+ :param _evt: UI event (unused).
+ """
self._posture_switch_future = self.pm.cryoSwitchSamplePosition(SEM_IMAGING)
self._posture_switch_future.add_done_callback(self._on_move_complete)
@@
- def _on_move_complete(self, future):
+ def _on_move_complete(self, future) -> None:
+ """
+ Handle completion of a posture switch future.
+ :param future: The completed future.
+ """
try:
future.result()🧰 Tools🪛 Ruff (0.14.14)[warning] 448-448: Unused method argument: (ARG002) [warning] 454-454: Unused method argument: (ARG002) 🤖 Prompt for AI Agents |
||
| def terminate(self): | ||
| self.main_data.stage.position.unsubscribe(self._on_stage_pos) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: delmic/odemis
Length of output: 4126
🏁 Script executed:
Repository: delmic/odemis
Length of output: 712
🏁 Script executed:
Repository: delmic/odemis
Length of output: 631
🏁 Script executed:
Repository: delmic/odemis
Length of output: 1351
🏁 Script executed:
Repository: delmic/odemis
Length of output: 39
🏁 Script executed:
Repository: delmic/odemis
Length of output: 717
🏁 Script executed:
Repository: delmic/odemis
Length of output: 1073
🏁 Script executed:
Repository: delmic/odemis
Length of output: 7491
🏁 Script executed:
Repository: delmic/odemis
Length of output: 7046
🏁 Script executed:
Repository: delmic/odemis
Length of output: 1909
🏁 Script executed:
Repository: delmic/odemis
Length of output: 464
🏁 Script executed:
Repository: delmic/odemis
Length of output: 1183
🏁 Script executed:
Repository: delmic/odemis
Length of output: 1706
🏁 Script executed:
Repository: delmic/odemis
Length of output: 3623
🏁 Script executed:
Repository: delmic/odemis
Length of output: 98
🏁 Script executed:
Repository: delmic/odemis
Length of output: 206
🏁 Script executed:
Repository: delmic/odemis
Length of output: 2890
🏁 Script executed:
Repository: delmic/odemis
Length of output: 5033
🏁 Script executed:
Repository: delmic/odemis
Length of output: 39
🏁 Script executed:
Repository: delmic/odemis
Length of output: 39
🏁 Script executed:
Repository: delmic/odemis
Length of output: 1077
🏁 Script executed:
Repository: delmic/odemis
Length of output: 1098
🏁 Script executed:
Repository: delmic/odemis
Length of output: 545
🏁 Script executed:
Repository: delmic/odemis
Length of output: 2030
🏁 Script executed:
Repository: delmic/odemis
Length of output: 1766
🏁 Script executed:
Repository: delmic/odemis
Length of output: 546
Default milling_angle of 0 violates the allowed range and will cause initialization to fail
The FloatContinuous validation during initialization checks that the value is within
MILLING_RANGE(5–30° in radians, or ~0.087–0.524 rad). With a default of 0, this is outside the configured range and will raiseIndexErroron systems lackingMD_FAV_MILL_POS_ACTIVEin metadata.🛠️ Proposed fix
🤖 Prompt for AI Agents