Skip to content

Commit ecd2ca2

Browse files
committed
[test] PostureManager: add test case for the TescanPostureManager on Tescan FIBSEM
Use the same test cases as for the TFS3 FIBSEM to test the TescanPostureManager with the Meteor FIBSEM on Tescan.
1 parent 2b8930f commit ecd2ca2

File tree

3 files changed

+116
-21
lines changed

3 files changed

+116
-21
lines changed

install/linux/usr/share/odemis/sim/meteor-tescan-fibsem-full-sim.odm.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
role: e-beam,
3737
init: {},
3838
affects: ["EBeam Detector"],
39+
properties: {
40+
# Set scan rotation to 180°, which is the standard, to ensure the camera
41+
# rotation is matching.
42+
rotation: 3.141592653589, # rad, 180°
43+
},
3944
}
4045

4146
"EBeam Detector": {
@@ -54,6 +59,11 @@
5459
role: ion-beam,
5560
init: {},
5661
affects: ["Ion Detector"],
62+
properties: {
63+
# Set scan rotation to 180°, which is the standard, to ensure the camera
64+
# rotation is matching.
65+
rotation: 3.141592653589, # rad, 180°
66+
},
5767
}
5868

5969
"Ion Detector": {

src/odemis/acq/test/move_tescan_test.py

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22
"""
3-
Copyright © 2020 Delmic
3+
Copyright © 2023-2025 Delmic
44
55
This file is part of Odemis.
66
@@ -18,20 +18,22 @@
1818
import logging
1919
import math
2020
import os
21+
import time
2122
import unittest
2223

23-
from odemis.util import testing
24-
2524
import odemis
2625
from odemis import model
27-
from odemis.acq.move import (FM_IMAGING, SEM_IMAGING, UNKNOWN)
26+
from odemis.acq.move import (FM_IMAGING, SEM_IMAGING, UNKNOWN, MicroscopePostureManager, LOADING)
2827
from odemis.acq.test.move_tfs1_test import TestMeteorTFS1Move
28+
from odemis.acq.test.move_tfs3_test import TestMeteorTFS3Move
29+
from odemis.util import testing
2930

3031
logging.getLogger().setLevel(logging.DEBUG)
3132
logging.basicConfig(format="%(asctime)s %(levelname)-7s %(module)s:%(lineno)d %(message)s")
3233

3334
CONFIG_PATH = os.path.dirname(odemis.__file__) + "/../../install/linux/usr/share/odemis/"
3435
METEOR_TESCAN1_CONFIG = CONFIG_PATH + "sim/meteor-tescan-sim.odm.yaml"
36+
METEOR_TESCAN1_FIBSEM_CONFIG = CONFIG_PATH + "sim/meteor-tescan-fibsem-full-sim.odm.yaml"
3537

3638

3739
class TestMeteorTescan1Move(TestMeteorTFS1Move):
@@ -165,5 +167,76 @@ def test_stage_to_chamber(self):
165167
self.assertAlmostEqual(zshift["z"], shift["z"], places=5)
166168

167169

170+
class TestMeteorFibsemTescan1Move(TestMeteorTFS3Move):
171+
MIC_CONFIG = METEOR_TESCAN1_FIBSEM_CONFIG
172+
ROTATION_AXES = {'rx', 'rz'}
173+
174+
@classmethod
175+
def setUpClass(cls):
176+
testing.start_backend(cls.MIC_CONFIG)
177+
cls.microscope = model.getMicroscope()
178+
cls.pm = MicroscopePostureManager(microscope=cls.microscope)
179+
180+
# get the stage components
181+
cls.stage_bare = model.getComponent(role="stage-bare")
182+
cls.stage = cls.pm.sample_stage
183+
184+
# get the metadata
185+
cls.stage_md = cls.stage_bare.getMetadata()
186+
cls.stage_grid_centers = cls.stage_md[model.MD_SAMPLE_CENTERS]
187+
cls.stage_loading = cls.stage_md[model.MD_FAV_POS_DEACTIVE]
188+
189+
# Reset to loading position (in case the backend was already running and in a different posture)
190+
f = cls.pm.cryoSwitchSamplePosition(LOADING)
191+
f.result()
192+
193+
# Test not meaningful for Tescan
194+
def test_fixed_fm_z(self):
195+
pass
196+
197+
# Test not meaningful for Tescan
198+
def test_revert_from_fixed_fm_z(self):
199+
pass
200+
201+
def test_stage_to_chamber(self):
202+
# Override, as Tescan has different behaviour: the Z axis is directly connected the chamber Z
203+
# go to sem imaging
204+
f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING)
205+
f.result()
206+
time.sleep(0.1)
207+
208+
# calculate the vertical shift in chamber coordinates
209+
shift = {"x": 100e-6, "z": 50e-6}
210+
zshift = self.pm._transformFromChamberToStage(shift)
211+
# Should return the same movement
212+
testing.assert_pos_almost_equal(zshift, shift)
213+
214+
def test_rel_move_fm_posture(self):
215+
f = self.pm.cryoSwitchSamplePosition(FM_IMAGING)
216+
f.result()
217+
current_imaging_mode = self.pm.getCurrentPostureLabel()
218+
self.assertEqual(FM_IMAGING, current_imaging_mode)
219+
220+
# relative moves in sample stage coordinates
221+
sample_stage_moves = [
222+
{"x": 10e-6, "y": 0},
223+
{"x": 0, "y": 10e-6},
224+
]
225+
# Corresponding stage-bare relative moves (based on "ground truth" tested on hardware)
226+
# The system is configured with a scan rotation of 180°, so all the moves are inverted.
227+
stage_bare_moves = [
228+
{"x": -10e-6, "y": 0, "z": 0},
229+
{"x": 0, "y": -5.9e-6, "z": -6.7e-6}, # 40° pre-tilt
230+
]
231+
for m_sample, m_bare in zip(sample_stage_moves, stage_bare_moves):
232+
old_bare_pos = self.stage_bare.position.value
233+
self.stage.moveRel(m_sample).result()
234+
new_bare_pos = self.stage_bare.position.value
235+
236+
exp_bare_pos = old_bare_pos.copy()
237+
for axis in m_bare.keys():
238+
exp_bare_pos[axis] += m_bare[axis]
239+
testing.assert_pos_almost_equal(new_bare_pos, exp_bare_pos, atol=1e-6)
240+
168241
if __name__ == "__main__":
169242
unittest.main()

src/odemis/acq/test/move_tfs3_test.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525

2626
import odemis
2727
from odemis import model
28-
from odemis.acq.move import (FM_IMAGING, GRID_1,MILLING, SEM_IMAGING, UNKNOWN, POSITION_NAMES,
29-
MeteorTFS3PostureManager)
28+
from odemis.acq.move import (FM_IMAGING, GRID_1, MILLING, SEM_IMAGING, UNKNOWN, POSITION_NAMES,
29+
MeteorTFS3PostureManager, LOADING)
3030
from odemis.acq.move import MicroscopePostureManager
3131
from odemis.util import testing
3232
from odemis.util.driver import isNearPosition
@@ -62,12 +62,23 @@ def setUpClass(cls):
6262
cls.stage_grid_centers = cls.stage_md[model.MD_SAMPLE_CENTERS]
6363
cls.stage_loading = cls.stage_md[model.MD_FAV_POS_DEACTIVE]
6464

65-
def test_switching_movements(self):
66-
"""Test switching between different postures and check that the 3D transformations work as expected"""
65+
def setUp(self):
66+
# reset stage-bare metadata, so that even if a test modifies it, the next test starts fresh
6767
if self.pm.current_posture.value == UNKNOWN:
68-
f = self.stage_bare.moveAbs(self.stage_grid_centers[POSITION_NAMES[GRID_1]])
68+
logging.warning("Test setup: posture is UNKNOWN, resetting to SEM_IMAGING")
69+
# Reset to loading position before each test
70+
f = self.pm.cryoSwitchSamplePosition(LOADING)
71+
f.result()
72+
# From loading, going to SEM IMAGING will use GRID 1 as base position
73+
f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING)
6974
f.result()
7075

76+
def test_switching_movements(self):
77+
"""Test switching between different postures and check that the 3D transformations work as expected"""
78+
# if self.pm.current_posture.value == UNKNOWN:
79+
# f = self.stage_bare.moveAbs(self.stage_grid_centers[POSITION_NAMES[GRID_1]])
80+
# f.result()
81+
7182
f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING)
7283
f.result()
7384

@@ -86,14 +97,14 @@ def test_switching_movements(self):
8697
def test_to_posture(self):
8798
"""Test that posture projection is the same as moving to the posture"""
8899

89-
# first move back to grid-1 to make sure we are in a known position
90-
f = self.stage_bare.moveAbs(self.stage_grid_centers[POSITION_NAMES[GRID_1]])
91-
f.result()
92-
93100
# move to SEM imaging posture
94101
f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING)
95102
f.result()
96103

104+
# first move back to grid-1 to make sure we are in a known position
105+
f = self.stage_bare.moveAbs(self.stage_grid_centers[POSITION_NAMES[GRID_1]])
106+
f.result()
107+
97108
# Check that getCurrentPostureLabel() with a given stage-bare position returns the expected posture
98109
pos = self.stage_bare.position.value
99110
self.assertEqual(self.pm.getCurrentPostureLabel(pos), SEM_IMAGING)
@@ -121,29 +132,30 @@ def test_to_posture(self):
121132

122133
def test_sample_stage_movement(self):
123134
"""Test sample stage movements in different postures match the expected movements"""
124-
135+
# move to SEM/GRID 1
136+
f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING)
137+
f.result()
125138
f = self.stage_bare.moveAbs(self.stage_grid_centers[POSITION_NAMES[GRID_1]])
126139
f.result()
127140

128141
dx, dy = 50e-6, 50e-6
129-
self.pm.use_3d_transforms = True
130-
for posture in [FM_IMAGING, SEM_IMAGING]:
142+
for posture in [SEM_IMAGING, FM_IMAGING]:
131143

132144
if self.pm.current_posture.value is not posture:
133145
f = self.pm.cryoSwitchSamplePosition(posture)
134146
f.result()
135147

136148
f = self.pm.cryoSwitchSamplePosition(GRID_1)
137149
f.result()
138-
time.sleep(2) # simulated stage moves too fast, needs time to update
150+
time.sleep(0.1) # simulated stage moves too fast, needs time to update
139151

140152
# test relative movement
141153
init_ss_pos = self.stage.position.value
142154
init_sb_pos = self.stage_bare.position.value
143155

144156
f = self.stage.moveRel({"x": dx, "y": dy})
145157
f.result()
146-
time.sleep(2)
158+
time.sleep(0.1)
147159

148160
new_pos = self.stage.position.value
149161
new_sb_pos = self.stage_bare.position.value
@@ -155,15 +167,15 @@ def test_sample_stage_movement(self):
155167
# test absolute movement
156168
f = self.pm.cryoSwitchSamplePosition(GRID_1)
157169
f.result()
158-
time.sleep(2) # simulated stage moves too fast, needs time to update
170+
time.sleep(0.1) # simulated stage moves too fast, needs time to update
159171

160172
abs_pos = init_ss_pos.copy()
161173
abs_pos["x"] += dx
162174
abs_pos["y"] += dy
163175

164176
f = self.stage.moveAbs(abs_pos)
165177
f.result()
166-
time.sleep(2)
178+
time.sleep(0.1)
167179

168180
new_pos = self.stage.position.value
169181
new_sb_pos = self.stage_bare.position.value
@@ -216,7 +228,7 @@ def test_stage_to_chamber(self):
216228
# go to sem imaging
217229
f = self.pm.cryoSwitchSamplePosition(SEM_IMAGING)
218230
f.result()
219-
time.sleep(2)
231+
time.sleep(0.1)
220232

221233
# calculate the vertical shift in chamber coordinates
222234
shift = {"x": 100e-6, "z": 50e-6}

0 commit comments

Comments
 (0)