Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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": {
Expand Down
79 changes: 75 additions & 4 deletions src/odemis/acq/test/move_tescan_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Copyright © 2020 Delmic
Copyright © 2023-2025 Delmic

This file is part of Odemis.

Expand All @@ -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):
Expand Down Expand Up @@ -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()
44 changes: 26 additions & 18 deletions src/odemis/acq/test/move_tfs3_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -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)
Expand Down Expand Up @@ -121,29 +128,30 @@ 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
init_sb_pos = self.stage_bare.position.value

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
Expand All @@ -155,15 +163,15 @@ 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
abs_pos["y"] += dy

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
Expand Down Expand Up @@ -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}
Expand Down