diff --git a/install/linux/usr/share/odemis/sim/meteor-tescan-fibsem-full-sim.odm.yaml b/install/linux/usr/share/odemis/sim/meteor-tescan-fibsem-full-sim.odm.yaml index 83e73a45ba..6d5d3eb54f 100644 --- a/install/linux/usr/share/odemis/sim/meteor-tescan-fibsem-full-sim.odm.yaml +++ b/install/linux/usr/share/odemis/sim/meteor-tescan-fibsem-full-sim.odm.yaml @@ -36,6 +36,11 @@ role: e-beam, init: {}, affects: ["EBeam Detector"], + properties: { + # Set scan rotation to 180°, which is the standard, to ensure the camera + # rotation is matching. + rotation: 3.141592653589, # rad, 180° + }, } "EBeam Detector": { @@ -54,6 +59,11 @@ role: ion-beam, init: {}, affects: ["Ion Detector"], + properties: { + # Set scan rotation to 180°, which is the standard, to ensure the camera + # rotation is matching. + rotation: 3.141592653589, # rad, 180° + }, } "Ion Detector": { diff --git a/src/odemis/acq/test/move_tescan_test.py b/src/odemis/acq/test/move_tescan_test.py index d539522c29..675cb36ff5 100644 --- a/src/odemis/acq/test/move_tescan_test.py +++ b/src/odemis/acq/test/move_tescan_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Copyright © 2020 Delmic +Copyright © 2023-2025 Delmic This file is part of Odemis. @@ -18,20 +18,22 @@ import logging import math import os +import time import unittest -from odemis.util import testing - import odemis from odemis import model -from odemis.acq.move import (FM_IMAGING, SEM_IMAGING, UNKNOWN) +from odemis.acq.move import (FM_IMAGING, SEM_IMAGING, UNKNOWN, MicroscopePostureManager, LOADING) from odemis.acq.test.move_tfs1_test import TestMeteorTFS1Move +from odemis.acq.test.move_tfs3_test import TestMeteorTFS3Move +from odemis.util import testing logging.getLogger().setLevel(logging.DEBUG) logging.basicConfig(format="%(asctime)s %(levelname)-7s %(module)s:%(lineno)d %(message)s") CONFIG_PATH = os.path.dirname(odemis.__file__) + "/../../install/linux/usr/share/odemis/" METEOR_TESCAN1_CONFIG = CONFIG_PATH + "sim/meteor-tescan-sim.odm.yaml" +METEOR_TESCAN1_FIBSEM_CONFIG = CONFIG_PATH + "sim/meteor-tescan-fibsem-full-sim.odm.yaml" class TestMeteorTescan1Move(TestMeteorTFS1Move): @@ -165,5 +167,74 @@ def test_stage_to_chamber(self): self.assertAlmostEqual(zshift["z"], shift["z"], places=5) +class TestMeteorTescan1FibsemMove(TestMeteorTFS3Move): + MIC_CONFIG = METEOR_TESCAN1_FIBSEM_CONFIG + ROTATION_AXES = {'rx', 'rz'} + + @classmethod + def setUpClass(cls): + testing.start_backend(cls.MIC_CONFIG) + cls.microscope = model.getMicroscope() + cls.pm = MicroscopePostureManager(microscope=cls.microscope) + + # get the stage components + cls.stage_bare = model.getComponent(role="stage-bare") + cls.stage = cls.pm.sample_stage + + # get the metadata + cls.stage_md = cls.stage_bare.getMetadata() + cls.stage_grid_centers = cls.stage_md[model.MD_SAMPLE_CENTERS] + cls.stage_loading = cls.stage_md[model.MD_FAV_POS_DEACTIVE] + + # Reset to loading position (in case the backend was already running and in a different posture) + f = cls.pm.cryoSwitchSamplePosition(LOADING) + f.result() + + def test_fixed_fm_z(self): + self.skipTest("Test not meaningful for Tescan") + + def test_revert_from_fixed_fm_z(self): + self.skipTest("Test not meaningful for Tescan") + + def test_stage_to_chamber(self): + # Override, as Tescan has different behaviour: the Z axis is directly connected the chamber Z + # go to sem imaging + f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING) + f.result() + time.sleep(0.1) + + # calculate the vertical shift in chamber coordinates + shift = {"x": 100e-6, "z": 50e-6} + zshift = self.pm._transformFromChamberToStage(shift) + # Should return the same movement + testing.assert_pos_almost_equal(zshift, shift) + + def test_rel_move_fm_posture(self): + f = self.pm.cryoSwitchSamplePosition(FM_IMAGING) + f.result() + current_imaging_mode = self.pm.getCurrentPostureLabel() + self.assertEqual(FM_IMAGING, current_imaging_mode) + + # relative moves in sample stage coordinates + sample_stage_moves = [ + {"x": 10e-6, "y": 0}, + {"x": 0, "y": 10e-6}, + ] + # Corresponding stage-bare relative moves (based on "ground truth" tested on hardware) + # The system is configured with a scan rotation of 180°, so all the moves are inverted. + stage_bare_moves = [ + {"x": -10e-6, "y": 0, "z": 0}, + {"x": 0, "y": -5.9e-6, "z": -6.7e-6}, # 40° pre-tilt + ] + for m_sample, m_bare in zip(sample_stage_moves, stage_bare_moves): + old_bare_pos = self.stage_bare.position.value + self.stage.moveRel(m_sample).result() + new_bare_pos = self.stage_bare.position.value + + exp_bare_pos = old_bare_pos.copy() + for axis in m_bare.keys(): + exp_bare_pos[axis] += m_bare[axis] + testing.assert_pos_almost_equal(new_bare_pos, exp_bare_pos, atol=1e-6) + if __name__ == "__main__": unittest.main() diff --git a/src/odemis/acq/test/move_tfs3_test.py b/src/odemis/acq/test/move_tfs3_test.py index 9478ff8fa9..14eaf48dbe 100644 --- a/src/odemis/acq/test/move_tfs3_test.py +++ b/src/odemis/acq/test/move_tfs3_test.py @@ -25,8 +25,8 @@ import odemis from odemis import model -from odemis.acq.move import (FM_IMAGING, GRID_1,MILLING, SEM_IMAGING, UNKNOWN, POSITION_NAMES, - MeteorTFS3PostureManager) +from odemis.acq.move import (FM_IMAGING, GRID_1, MILLING, SEM_IMAGING, UNKNOWN, POSITION_NAMES, + MeteorTFS3PostureManager, LOADING) from odemis.acq.move import MicroscopePostureManager from odemis.util import testing from odemis.util.driver import isNearPosition @@ -62,12 +62,19 @@ def setUpClass(cls): cls.stage_grid_centers = cls.stage_md[model.MD_SAMPLE_CENTERS] cls.stage_loading = cls.stage_md[model.MD_FAV_POS_DEACTIVE] - def test_switching_movements(self): - """Test switching between different postures and check that the 3D transformations work as expected""" + def setUp(self): + # reset to a known posture before each test if self.pm.current_posture.value == UNKNOWN: - f = self.stage_bare.moveAbs(self.stage_grid_centers[POSITION_NAMES[GRID_1]]) + logging.info("Test setup: posture is UNKNOWN, resetting to SEM_IMAGING") + # Reset to loading position before each test + f = self.pm.cryoSwitchSamplePosition(LOADING) + f.result() + # From loading, going to SEM IMAGING will use GRID 1 as base position + f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING) f.result() + def test_switching_movements(self): + """Test switching between different postures and check that the 3D transformations work as expected""" f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING) f.result() @@ -86,14 +93,14 @@ def test_switching_movements(self): def test_to_posture(self): """Test that posture projection is the same as moving to the posture""" - # first move back to grid-1 to make sure we are in a known position - f = self.stage_bare.moveAbs(self.stage_grid_centers[POSITION_NAMES[GRID_1]]) - f.result() - # move to SEM imaging posture f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING) f.result() + # first move back to grid-1 to make sure we are in a known position + f = self.stage_bare.moveAbs(self.stage_grid_centers[POSITION_NAMES[GRID_1]]) + f.result() + # Check that getCurrentPostureLabel() with a given stage-bare position returns the expected posture pos = self.stage_bare.position.value self.assertEqual(self.pm.getCurrentPostureLabel(pos), SEM_IMAGING) @@ -121,21 +128,22 @@ def test_to_posture(self): def test_sample_stage_movement(self): """Test sample stage movements in different postures match the expected movements""" - + # move to SEM/GRID 1 + f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING) + f.result() f = self.stage_bare.moveAbs(self.stage_grid_centers[POSITION_NAMES[GRID_1]]) f.result() dx, dy = 50e-6, 50e-6 - self.pm.use_3d_transforms = True - for posture in [FM_IMAGING, SEM_IMAGING]: + for posture in [SEM_IMAGING, FM_IMAGING]: - if self.pm.current_posture.value is not posture: + if self.pm.current_posture.value != posture: f = self.pm.cryoSwitchSamplePosition(posture) f.result() f = self.pm.cryoSwitchSamplePosition(GRID_1) f.result() - time.sleep(2) # simulated stage moves too fast, needs time to update + time.sleep(0.1) # simulated stage moves too fast, needs time to update # test relative movement init_ss_pos = self.stage.position.value @@ -143,7 +151,7 @@ def test_sample_stage_movement(self): f = self.stage.moveRel({"x": dx, "y": dy}) f.result() - time.sleep(2) + time.sleep(0.1) new_pos = self.stage.position.value new_sb_pos = self.stage_bare.position.value @@ -155,7 +163,7 @@ def test_sample_stage_movement(self): # test absolute movement f = self.pm.cryoSwitchSamplePosition(GRID_1) f.result() - time.sleep(2) # simulated stage moves too fast, needs time to update + time.sleep(0.1) # simulated stage moves too fast, needs time to update abs_pos = init_ss_pos.copy() abs_pos["x"] += dx @@ -163,7 +171,7 @@ def test_sample_stage_movement(self): f = self.stage.moveAbs(abs_pos) f.result() - time.sleep(2) + time.sleep(0.1) new_pos = self.stage.position.value new_sb_pos = self.stage_bare.position.value @@ -216,7 +224,7 @@ def test_stage_to_chamber(self): # go to sem imaging f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING) f.result() - time.sleep(2) + time.sleep(0.1) # calculate the vertical shift in chamber coordinates shift = {"x": 100e-6, "z": 50e-6}