Skip to content

Commit 2576993

Browse files
authored
Add inheritance (#91)
* Add simple experiment base class * Use pytest's `tmp_path` for IO testing * Refactor format tests * Properly separate read/write tests * Add context manager for experiments * Refactor experiment tests * Microscope base class * Sort alphabethically * Use microscope base class; add type hints * Base camera class proposal * Add mock cameras for testing * Run ruff * Use the camera base class * Add rudimentary camera tests * Add type hints * PR feedback; 3.7 compatibility * Run pre-commit
1 parent 6c4ab42 commit 2576993

33 files changed

+708
-305
lines changed

src/instamatic/TEMController/TEMController.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import time
22
from collections import namedtuple
33
from concurrent.futures import ThreadPoolExecutor
4-
from typing import Tuple
4+
from typing import Optional, Tuple
55

66
import numpy as np
77

88
from instamatic import config
99
from instamatic.camera import Camera
10+
from instamatic.camera.camera_base import CameraBase
1011
from instamatic.exceptions import TEMControllerError
1112
from instamatic.formats import write_tiff
1213
from instamatic.image_utils import rotate_image
14+
from instamatic.TEMController.microscope_base import MicroscopeBase
1315

1416
from .deflectors import *
1517
from .lenses import *
@@ -89,7 +91,7 @@ class TEMController:
8991
cam: Camera control object (see instamatic.camera) [optional]
9092
"""
9193

92-
def __init__(self, tem, cam=None):
94+
def __init__(self, tem: MicroscopeBase, cam: Optional[CameraBase] = None):
9395
super().__init__()
9496

9597
self._executor = ThreadPoolExecutor(max_workers=1)
@@ -122,7 +124,7 @@ def __init__(self, tem, cam=None):
122124

123125
def __repr__(self):
124126
return (f'Mode: {self.tem.getFunctionMode()}\n'
125-
f'High tension: {self.high_tension/1000:.0f} kV\n'
127+
f'High tension: {self.high_tension / 1000:.0f} kV\n'
126128
f'Current density: {self.current_density:.2f} pA/cm2\n'
127129
f'{self.gunshift}\n'
128130
f'{self.guntilt}\n'
@@ -244,7 +246,7 @@ def run_script(self, script: str, verbose: bool = True) -> None:
244246
t1 = time.perf_counter()
245247

246248
if verbose:
247-
print(f'\nScript finished in {t1-t0:.4f} s')
249+
print(f'\nScript finished in {t1 - t0:.4f} s')
248250

249251
def get_stagematrix(self, binning: int = None, mag: int = None, mode: int = None):
250252
"""Helper function to get the stage matrix from the config file. The

src/instamatic/TEMController/fei_microscope.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from instamatic import config
99
from instamatic.exceptions import FEIValueError, TEMCommunicationError
10+
from instamatic.TEMController.microscope_base import MicroscopeBase
1011

1112
logger = logging.getLogger(__name__)
1213

@@ -45,7 +46,7 @@ def get_camera_length_mapping():
4546
CameraLengthMapping = get_camera_length_mapping()
4647

4748

48-
class FEIMicroscope:
49+
class FEIMicroscope(MicroscopeBase):
4950
"""Python bindings to the FEI microscope using the COM interface."""
5051

5152
def __init__(self, name='fei'):

src/instamatic/TEMController/fei_simu_microscope.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from instamatic import config
99
from instamatic.exceptions import FEIValueError, TEMCommunicationError
10+
from instamatic.TEMController.microscope_base import MicroscopeBase
1011

1112
logger = logging.getLogger(__name__)
1213

@@ -17,7 +18,7 @@
1718
MAX = 1.0
1819

1920

20-
class FEISimuMicroscope:
21+
class FEISimuMicroscope(MicroscopeBase):
2122
"""Python bindings to the FEI simulated microscope using the COM
2223
interface."""
2324

src/instamatic/TEMController/jeol_microscope.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from instamatic import config
99
from instamatic.exceptions import JEOLValueError, TEMCommunicationError, TEMValueError
10+
from instamatic.TEMController.microscope_base import MicroscopeBase
1011

1112
logger = logging.getLogger(__name__)
1213

@@ -45,7 +46,7 @@
4546
# Piezo stage seems to operate on a different level than standard XY
4647

4748

48-
class JeolMicroscope:
49+
class JeolMicroscope(MicroscopeBase):
4950
"""Python bindings to the JEOL microscope using the COM interface."""
5051

5152
def __init__(self, name: str = 'jeol'):

src/instamatic/TEMController/microscope.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from instamatic import config
2+
from instamatic.TEMController.microscope_base import MicroscopeBase
23

34
default_tem_interface = config.microscope.interface
45

56
__all__ = ['Microscope', 'get_tem']
67

78

8-
def get_tem(interface: str):
9+
def get_tem(interface: str) -> 'type[MicroscopeBase]':
910
"""Grab tem class with the specific 'interface'."""
1011
simulate = config.settings.simulate
1112

@@ -28,7 +29,7 @@ def get_tem(interface: str):
2829
return cls
2930

3031

31-
def Microscope(name: str = None, use_server: bool = False):
32+
def Microscope(name: str = None, use_server: bool = False) -> MicroscopeBase:
3233
"""Generic class to load microscope interface class.
3334
3435
name: str
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Tuple
3+
4+
5+
class MicroscopeBase(ABC):
6+
@abstractmethod
7+
def getBeamShift(self) -> Tuple[int, int]:
8+
pass
9+
10+
@abstractmethod
11+
def getBeamTilt(self) -> Tuple[int, int]:
12+
pass
13+
14+
@abstractmethod
15+
def getBrightness(self) -> int:
16+
pass
17+
18+
@abstractmethod
19+
def getCondensorLensStigmator(self) -> Tuple[int, int]:
20+
pass
21+
22+
@abstractmethod
23+
def getCurrentDensity(self) -> float:
24+
pass
25+
26+
@abstractmethod
27+
def getDiffFocus(self, confirm_mode: bool) -> int:
28+
pass
29+
30+
@abstractmethod
31+
def getDiffShift(self) -> Tuple[int, int]:
32+
pass
33+
34+
@abstractmethod
35+
def getFunctionMode(self) -> str:
36+
pass
37+
38+
@abstractmethod
39+
def getGunShift(self) -> Tuple[int, int]:
40+
pass
41+
42+
@abstractmethod
43+
def getGunTilt(self) -> Tuple[int, int]:
44+
pass
45+
46+
@abstractmethod
47+
def getHTValue(self) -> float:
48+
pass
49+
50+
@abstractmethod
51+
def getImageShift1(self) -> Tuple[int, int]:
52+
pass
53+
54+
@abstractmethod
55+
def getImageShift2(self) -> Tuple[int, int]:
56+
pass
57+
58+
@abstractmethod
59+
def getIntermediateLensStigmator(self) -> Tuple[int, int]:
60+
pass
61+
62+
@abstractmethod
63+
def getMagnification(self) -> int:
64+
pass
65+
66+
@abstractmethod
67+
def getMagnificationAbsoluteIndex(self) -> int:
68+
pass
69+
70+
@abstractmethod
71+
def getMagnificationIndex(self) -> int:
72+
pass
73+
74+
@abstractmethod
75+
def getMagnificationRanges(self) -> dict:
76+
pass
77+
78+
@abstractmethod
79+
def getObjectiveLensStigmator(self) -> Tuple[int, int]:
80+
pass
81+
82+
@abstractmethod
83+
def getSpotSize(self) -> int:
84+
pass
85+
86+
@abstractmethod
87+
def getStagePosition(self) -> Tuple[int, int, int, int, int]:
88+
pass
89+
90+
@abstractmethod
91+
def isBeamBlanked(self) -> bool:
92+
pass
93+
94+
@abstractmethod
95+
def isStageMoving(self) -> bool:
96+
pass
97+
98+
@abstractmethod
99+
def release_connection(self) -> None:
100+
pass
101+
102+
@abstractmethod
103+
def setBeamBlank(self, mode: bool) -> None:
104+
pass
105+
106+
@abstractmethod
107+
def setBeamShift(self, x: int, y: int) -> None:
108+
pass
109+
110+
@abstractmethod
111+
def setBeamTilt(self, x: int, y: int) -> None:
112+
pass
113+
114+
@abstractmethod
115+
def setBrightness(self, value: int) -> None:
116+
pass
117+
118+
@abstractmethod
119+
def setCondensorLensStigmator(self, x: int, y: int) -> None:
120+
pass
121+
122+
@abstractmethod
123+
def setDiffFocus(self, value: int, confirm_mode: bool) -> None:
124+
pass
125+
126+
@abstractmethod
127+
def setDiffShift(self, x: int, y: int) -> None:
128+
pass
129+
130+
@abstractmethod
131+
def setFunctionMode(self, value: int) -> None:
132+
pass
133+
134+
@abstractmethod
135+
def setGunShift(self, x: int, y: int) -> None:
136+
pass
137+
138+
@abstractmethod
139+
def setGunTilt(self, x: int, y: int) -> None:
140+
pass
141+
142+
@abstractmethod
143+
def setImageShift1(self, x: int, y: int) -> None:
144+
pass
145+
146+
@abstractmethod
147+
def setImageShift2(self, x: int, y: int) -> None:
148+
pass
149+
150+
@abstractmethod
151+
def setIntermediateLensStigmator(self, x: int, y: int) -> None:
152+
pass
153+
154+
@abstractmethod
155+
def setMagnification(self, value: int) -> None:
156+
pass
157+
158+
@abstractmethod
159+
def setMagnificationIndex(self, index: int) -> None:
160+
pass
161+
162+
@abstractmethod
163+
def setObjectiveLensStigmator(self, x: int, y: int) -> None:
164+
pass
165+
166+
@abstractmethod
167+
def setSpotSize(self, value: int) -> None:
168+
pass
169+
170+
@abstractmethod
171+
def setStagePosition(self, x: int, y: int, z: int, a: int, b: int, wait: bool) -> None:
172+
pass
173+
174+
@abstractmethod
175+
def stopStage(self) -> None:
176+
pass

src/instamatic/TEMController/simu_microscope.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from instamatic import config
66
from instamatic.exceptions import TEMValueError
7+
from instamatic.TEMController.microscope_base import MicroscopeBase
78

89
NTRLMAPPING = {
910
'GUN1': 0,
@@ -30,7 +31,7 @@
3031
MIN = 0
3132

3233

33-
class SimuMicroscope:
34+
class SimuMicroscope(MicroscopeBase):
3435
"""Simulates a microscope connection.
3536
3637
Has the same variables as the real JEOL/FEI equivalents, but does

src/instamatic/camera/camera.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def get_cam(interface: str = None):
2424
elif interface == 'gatansocket':
2525
from instamatic.camera.camera_gatan2 import CameraGatan2 as cam
2626
elif interface in ('timepix', 'pytimepix'):
27-
from instamatic.camera import camera_timepix as cam
27+
from instamatic.camera.camera_timepix import CameraTPX as cam
2828
elif interface in ('emmenu', 'tvips'):
2929
from instamatic.camera.camera_emmenu import CameraEMMENU as cam
3030
elif interface == 'serval':
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from abc import ABC, abstractmethod
2+
from typing import List, Tuple
3+
4+
from numpy import ndarray
5+
6+
from instamatic import config
7+
8+
9+
class CameraBase(ABC):
10+
11+
# Set manually
12+
name: str
13+
streamable: bool
14+
15+
# Set by `load_defaults`
16+
camera_rotation_vs_stage_xy: float
17+
default_binsize: int
18+
default_exposure: float
19+
dimensions: Tuple[int, int]
20+
interface: str
21+
possible_binsizes: List[int]
22+
stretch_amplitude: float
23+
stretch_azimuth: float
24+
25+
@abstractmethod
26+
def __init__(self, name: str):
27+
self.name = name
28+
self.load_defaults()
29+
30+
@abstractmethod
31+
def establish_connection(self):
32+
pass
33+
34+
@abstractmethod
35+
def release_connection(self):
36+
pass
37+
38+
@abstractmethod
39+
def get_image(
40+
self, exposure: float = None, binsize: int = None, **kwargs
41+
) -> ndarray:
42+
pass
43+
44+
def get_movie(
45+
self, n_frames: int, exposure: float = None, binsize: int = None, **kwargs
46+
) -> List[ndarray]:
47+
"""Basic implementation, subclasses should override with appropriate
48+
optimization."""
49+
return [
50+
self.get_image(exposure=exposure, binsize=binsize, **kwargs)
51+
for _ in range(n_frames)
52+
]
53+
54+
def __enter__(self):
55+
self.establish_connection()
56+
return self
57+
58+
def __exit__(self, kind, value, traceback):
59+
self.release_connection()
60+
61+
def get_camera_dimensions(self) -> Tuple[int, int]:
62+
return self.dimensions
63+
64+
def get_name(self) -> str:
65+
return self.name
66+
67+
def load_defaults(self):
68+
if self.name != config.settings.camera:
69+
config.load_camera_config(camera_name=self.name)
70+
for key, val in config.camera.mapping.items():
71+
setattr(self, key, val)

0 commit comments

Comments
 (0)