Skip to content

Commit

Permalink
Unify stage coordinates' units across instamatic (#109)
Browse files Browse the repository at this point in the history
* Make stage coordinates uniform across instamatic: `int_nm` stage positions and `float_deg` stage tilts

* Add `typing.Optional` to `setStagePosition` methods that can accept some `None`s

* Add `typing.Optional` and `int_nm`, `float_deg` to stage backlash methods

* Bugfix: fix new `Stage.set` TypeError if supplied partial arguments

* Make microscope interfaces `getStagePosition` return instance of `StagePositionTuple`

* Fix miscellaneous issues found by `mypy`

* Since `stage.x`, `y`, `z` are `int`egers, do not waste space formatting them as floats `:.1f`

* Rename `typing.py` to `utils.py` since `StagePositionTuple` affects more than typing

* Make `int_nm`, `float_deg` just `int`, `float` on Python3.8- that don't have `Annotated`

* Fix `FEIMicroscope`'s `getStagePosition` docstring that suggested alpha, beta are not numbers

* In `stage.py`, use `Tuple` instead of `tuple` for typing to remain compatible with python3.7, 3.8

* Minor bugfixes, revert unwanted accidental changes

* Fix `tests/test_ctrl.py:test_stage` wrongly expected `stage.x/y/z` of `float` type, not `int`
  • Loading branch information
Baharis authored Feb 12, 2025
1 parent e689b97 commit 0a3febd
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 145 deletions.
2 changes: 1 addition & 1 deletion src/instamatic/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ def one_cycle(tilt: float = 5, sign=1) -> list:
# self.stage.z = 0 # for testing

zc = self.stage.z
print(f'Current z = {zc:.1f} nm')
print(f'Current z = {zc} nm')

zs = zc + np.linspace(-dz, dz, steps)
shifts = []
Expand Down
17 changes: 14 additions & 3 deletions src/instamatic/microscope/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Tuple
from typing import Optional, Tuple

from instamatic.microscope.utils import StagePositionTuple
from instamatic.typing import float_deg, int_nm


class MicroscopeBase(ABC):
Expand Down Expand Up @@ -90,7 +93,7 @@ def getSpotSize(self) -> int:
pass

@abstractmethod
def getStagePosition(self) -> Tuple[int, int, int, int, int]:
def getStagePosition(self) -> StagePositionTuple:
pass

@abstractmethod
Expand Down Expand Up @@ -178,7 +181,15 @@ def setSpotSize(self, value: int) -> None:
pass

@abstractmethod
def setStagePosition(self, x: int, y: int, z: int, a: int, b: int, wait: bool) -> None:
def setStagePosition(
self,
x: Optional[int_nm],
y: Optional[int_nm],
z: Optional[int_nm],
a: Optional[float_deg],
b: Optional[float_deg],
wait: bool,
) -> None:
pass

@abstractmethod
Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/microscope/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import threading
import time
from functools import wraps
from typing import Any, Callable
from typing import Any, Callable, Dict

from instamatic import config
from instamatic.exceptions import TEMCommunicationError, exception_list
Expand Down Expand Up @@ -92,7 +92,7 @@ def wrapper(*args, **kwargs):

return wrapper

def _eval_dct(self, dct: dict[str, Any]) -> Any:
def _eval_dct(self, dct: Dict[str, Any]) -> Any:
"""Takes approximately 0.2-0.3 ms per call if HOST=='localhost'."""
self.s.send(dumper(dct))

Expand Down
132 changes: 76 additions & 56 deletions src/instamatic/microscope/components/stage.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
from __future__ import annotations

import time
from collections import namedtuple
from contextlib import contextmanager
from typing import Tuple
from typing import Optional, Tuple

import numpy as np

from instamatic.microscope.base import MicroscopeBase

# namedtuples to store results from .get()
StagePositionTuple = namedtuple('StagePositionTuple', ['x', 'y', 'z', 'a', 'b'])
from instamatic.microscope.utils import StagePositionTuple
from instamatic.typing import float_deg, int_nm


class Stage:
Expand All @@ -25,7 +23,7 @@ def __init__(self, tem: MicroscopeBase):

def __repr__(self):
x, y, z, a, b = self.get()
return f'{self.name}(x={x:.1f}, y={y:.1f}, z={z:.1f}, a={a:.1f}, b={b:.1f})'
return f'{self.name}(x={x}, y={y}, z={z}, a={a:.1f}, b={b:.1f})'

@property
def name(self) -> str:
Expand All @@ -34,23 +32,30 @@ def name(self) -> str:

def set(
self,
x: int = None,
y: int = None,
z: int = None,
a: int = None,
b: int = None,
x: Optional[int_nm] = None,
y: Optional[int_nm] = None,
z: Optional[int_nm] = None,
a: Optional[float_deg] = None,
b: Optional[float_deg] = None,
wait: bool = True,
) -> None:
"""Wait: bool, block until stage movement is complete (JEOL only)"""
self._setter(x, y, z, a, b, wait=wait)
self._setter(
round(x) if x is not None else None,
round(y) if y is not None else None,
round(z) if z is not None else None,
float(a) if a is not None else None,
float(b) if b is not None else None,
wait=wait,
)

def set_with_speed(
self,
x: int = None,
y: int = None,
z: int = None,
a: int = None,
b: int = None,
x: Optional[int_nm] = None,
y: Optional[int_nm] = None,
z: Optional[int_nm] = None,
a: Optional[float_deg] = None,
b: Optional[float_deg] = None,
wait: bool = True,
speed: float = 1.0,
) -> None:
Expand All @@ -59,7 +64,15 @@ def set_with_speed(
wait: ignored, but necessary for compatibility with JEOL API
speed: float, set stage rotation with specified speed (FEI only)
"""
self._setter(x, y, z, a, b, wait=wait, speed=speed)
self._setter(
round(x) if x is not None else None,
round(y) if y is not None else None,
round(z) if z is not None else None,
float(a) if a is not None else None,
float(b) if b is not None else None,
wait=wait,
speed=speed,
)

def set_rotation_speed(self, speed=1) -> None:
"""Sets the stage (rotation) movement speed on the TEM."""
Expand Down Expand Up @@ -97,46 +110,45 @@ def rotating_speed(self, speed: int):
else:
yield # if requested speed is the same as current

def get(self) -> Tuple[int, int, int, int, int]:
"""Get stage positions; x, y, z, and status of the rotation axes; a,
b."""
def get(self) -> StagePositionTuple:
"""Get stage positions x, y, z in nm and rotation axes a, b in deg."""
return StagePositionTuple(*self._getter())

@property
def x(self) -> int:
"""X position."""
def x(self) -> int_nm:
"""Stage position X expressed in nm."""
x, y, z, a, b = self.get()
return x

@x.setter
def x(self, value: int):
def x(self, value: int_nm) -> None:
self.set(x=value, wait=self._wait)

@property
def y(self) -> int:
"""Y position."""
def y(self) -> int_nm:
"""Stage position Y expressed in nm."""
x, y, z, a, b = self.get()
return y

@y.setter
def y(self, value: int):
def y(self, value: int_nm) -> None:
self.set(y=value, wait=self._wait)

@property
def xy(self) -> Tuple[int, int]:
"""XY position as a tuple."""
def xy(self) -> Tuple[int_nm, int_nm]:
"""Stage position XY expressed as a tuple in nm."""
x, y, z, a, b = self.get()
return x, y

@xy.setter
def xy(self, values: Tuple[int, int]):
def xy(self, values: Tuple[int_nm, int_nm]) -> None:
x, y = values
self.set(x=x, y=y, wait=self._wait)

def move_in_projection(self, delta_x: int, delta_y: int) -> None:
def move_in_projection(self, delta_x: int_nm, delta_y: int_nm) -> None:
r"""Y and z are always perpendicular to the sample stage. To achieve the
movement in the projection, x and yshould be broken down into the
components z' and y'.
movement in the projection plane instead, x and y should be broken down
into the components z' and y'.
y = y' * cos(a)
z = y' * sin(a)
Expand All @@ -155,7 +167,7 @@ def move_in_projection(self, delta_x: int, delta_y: int) -> None:
z = z - delta_y * np.sin(a)
self.set(x=x, y=y, z=z)

def move_along_optical_axis(self, delta_z: int):
def move_along_optical_axis(self, delta_z: int_nm) -> None:
"""See `Stage.move_in_projection`"""
x, y, z, a, b = self.get()
a = np.radians(a)
Expand All @@ -164,38 +176,38 @@ def move_along_optical_axis(self, delta_z: int):
self.set(y=y, z=z)

@property
def z(self) -> int:
"""Stage height Z."""
def z(self) -> int_nm:
"""Stage height Z expressed in nm."""
x, y, z, a, b = self.get()
return z

@z.setter
def z(self, value: int):
def z(self, value: int_nm) -> None:
self.set(z=value, wait=self._wait)

@property
def a(self) -> int:
"""Rotation angle."""
def a(self) -> float_deg:
"""Primary rotation angle alpha expressed in degrees."""
x, y, z, a, b = self.get()
return a

@a.setter
def a(self, value: int):
def a(self, value: float_deg) -> None:
self.set(a=value, wait=self._wait)

@property
def b(self) -> int:
"""Secondary rotation angle."""
def b(self) -> float_deg:
"""Secondary rotation angle beta expressed in degrees."""
x, y, z, a, b = self.get()
return b

@b.setter
def b(self, value: int):
def b(self, value: float_deg) -> None:
self.set(b=value, wait=self._wait)

def neutral(self) -> None:
"""Reset the position of the stage to the 0-position."""
self.set(x=0, y=0, z=0, a=0, b=0)
self.set(x=0, y=0, z=0, a=0.0, b=0.0)

def is_moving(self) -> bool:
"""Return 'True' if the stage is moving."""
Expand Down Expand Up @@ -224,7 +236,7 @@ def stop(self) -> None:
Stage.set."""
self._tem.stopStage()

def alpha_wobbler(self, delta: float = 5.0, event=None) -> None:
def alpha_wobbler(self, delta: float_deg = 5.0, event=None) -> None:
"""Tilt the stage by plus/minus the value of delta (degrees) If event
is not set, press Ctrl-C to interrupt."""

Expand All @@ -246,15 +258,19 @@ def alpha_wobbler(self, delta: float = 5.0, event=None) -> None:

print(f"Restoring 'alpha': {a_center:.2f}")
self.a = a_center
print(f'Print z={self.z:.2f}')
print(f'Print z={self.z}')

def relax_xy(self, step: int = 100) -> None:
"""Relax the stage by moving it in the opposite direction from the last
movement."""
pass

def set_xy_with_backlash_correction(
self, x: int = None, y: int = None, step: float = 10000, settle_delay: float = 0.200
self,
x: Optional[int_nm] = None,
y: Optional[int_nm] = None,
step: int_nm = 10000,
settle_delay: float = 0.200,
) -> None:
"""Move to new x/y position with backlash correction. This is done by
approaching the target x/y position always from the same direction.
Expand All @@ -277,9 +293,9 @@ def set_xy_with_backlash_correction(

def move_xy_with_backlash_correction(
self,
shift_x: int = None,
shift_y: int = None,
step: float = 5000,
shift_x: Optional[int_nm] = None,
shift_y: Optional[int_nm] = None,
step: int_nm = 5000,
settle_delay: float = 0.200,
wait=True,
) -> None:
Expand All @@ -305,7 +321,7 @@ def move_xy_with_backlash_correction(
target_x = stage.x + shift_x
if target_x > stage.x:
pre_x = stage.x - step
elif target_x < stage.x:
else: # if target_x < stage.x:
pre_x = stage.x + step
else:
pre_x = None
Expand All @@ -315,7 +331,7 @@ def move_xy_with_backlash_correction(
target_y = stage.y + shift_y
if target_y > stage.y:
pre_y = stage.y - step
elif target_y < stage.y:
else: # if target_y < stage.y:
pre_y = stage.y + step
else:
pre_y = None
Expand All @@ -329,12 +345,16 @@ def move_xy_with_backlash_correction(
if settle_delay:
time.sleep(settle_delay)

def eliminate_backlash_xy(self, step: float = 10000, settle_delay: float = 0.200) -> None:
def eliminate_backlash_xy(
self,
step: int_nm = 10000,
settle_delay: float = 0.200,
) -> None:
"""Eliminate backlash by in XY by moving the stage away from the
current position, and approaching it from the common direction. Uses
`set_xy_with_backlash_correction` internally.
step: float,
step: int,
stepsize in nm
settle_delay: float,
delay between movements in seconds to allow the stage to settle
Expand All @@ -346,8 +366,8 @@ def eliminate_backlash_xy(self, step: float = 10000, settle_delay: float = 0.200

def eliminate_backlash_a(
self,
target_angle: float = 0.0,
step: float = 1.0,
target_angle: float_deg = 0.0,
step: float_deg = 1.0,
n_steps: int = 3,
settle_delay: float = 0.200,
) -> None:
Expand Down
Loading

0 comments on commit 0a3febd

Please sign in to comment.