Skip to content

Commit f9cf3f7

Browse files
authored
Implement basic camera simulation (#105)
* Simple crystal for real- and reciprocal-space simulation * Add basic grid simulation * Add full simulation * Add tilting * Add warnings for tilting * Implement camera * Move to seperate folder, add docstrings and tests * Add warning
1 parent f72fdc8 commit f9cf3f7

File tree

13 files changed

+1102
-0
lines changed

13 files changed

+1102
-0
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ dependencies = [
5555
"tqdm >= 4.41.1",
5656
"virtualbox >= 2.0.0",
5757
"pyserialem >= 0.3.2",
58+
"diffpy.structure",
5859
]
5960

6061
[project.urls]

src/instamatic/simulation/__init__.py

Whitespace-only changes.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from __future__ import annotations
2+
3+
from typing import Tuple
4+
5+
from numpy import ndarray
6+
7+
from instamatic.camera.camera_base import CameraBase
8+
from instamatic.simulation.stage import Stage
9+
10+
11+
class CameraSimulation(CameraBase):
12+
streamable = True
13+
14+
def __init__(self, name: str = 'simulate'):
15+
super().__init__(name)
16+
17+
self.ready = False
18+
19+
# TODO put parameters into config
20+
self.stage = Stage()
21+
self.mag = None
22+
23+
def establish_connection(self):
24+
pass
25+
26+
def actually_establish_connection(self):
27+
if self.ready:
28+
return
29+
import time
30+
31+
time.sleep(2)
32+
from instamatic.controller import get_instance
33+
34+
ctrl = get_instance()
35+
self.tem = ctrl.tem
36+
37+
ctrl.stage.set(z=0, a=0, b=0)
38+
print(self.tem.getStagePosition())
39+
print(self.stage.samples[0].x, self.stage.samples[0].y)
40+
41+
self.ready = True
42+
43+
def release_connection(self):
44+
self.tem = None
45+
self.ready = False
46+
47+
def get_image(self, exposure: float = None, binsize: int = None, **kwargs) -> ndarray:
48+
self.actually_establish_connection()
49+
50+
if exposure is None:
51+
exposure = self.default_exposure
52+
if binsize is None:
53+
binsize = self.default_binsize
54+
55+
# TODO this has inconsistent units. Assume m, deg
56+
pos = self.tem.getStagePosition()
57+
if pos is not None and len(pos) == 5:
58+
x, y, z, alpha, beta = pos
59+
self.stage.set_position(x=x, y=y, z=z, alpha_tilt=alpha, beta_tilt=beta)
60+
61+
mode = self.tem.getFunctionMode()
62+
63+
# Get real-space extent
64+
if mode == 'diff':
65+
# TODO this has inconsistent units. Assume mm
66+
self.camera_length = self.tem.getMagnification()
67+
else:
68+
mag = self.tem.getMagnification()
69+
if isinstance(mag, (float, int)):
70+
self.mag = mag
71+
else:
72+
print(mag, type(mag))
73+
if self.mag is None:
74+
raise ValueError('Must start in image mode')
75+
76+
# TODO consider beam shift, tilt ect.
77+
x_min, x_max, y_min, y_max = self._mag_to_ranges(self.mag)
78+
x_min += self.stage.x
79+
x_max += self.stage.x
80+
y_min += self.stage.y
81+
y_max += self.stage.y
82+
83+
# TODO I mean properly considering them, this has no regard for units ect
84+
bx, by = self.tem.getBeamShift()
85+
x_min += bx
86+
x_max += bx
87+
y_min += by
88+
y_max += by
89+
90+
shape_x, shape_y = self.get_camera_dimensions()
91+
shape = (shape_x // binsize, shape_y // binsize)
92+
93+
if mode == 'diff':
94+
return self.stage.get_diffraction_pattern(
95+
shape=shape, x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max
96+
)
97+
else:
98+
return self.stage.get_image(
99+
shape=shape, x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max
100+
)
101+
102+
def _mag_to_ranges(self, mag: float) -> Tuple[float, float, float, float]:
103+
# assume 50x = 2mm full size
104+
half_width = 50 * 1e6 / mag # 2mm/2 in nm is 1e6
105+
return -half_width, half_width, -half_width, half_width
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
from __future__ import annotations
2+
3+
from typing import Type, TypeVar
4+
5+
import numpy as np
6+
from diffpy import structure as diffpy
7+
8+
Crystal_T = TypeVar('Crystal_T', bound='Crystal')
9+
10+
11+
class Crystal:
12+
def __init__(
13+
self, a: float, b: float, c: float, alpha: float, beta: float, gamma: float
14+
) -> None:
15+
"""Simulate a primitive crystal given the unit cell. No additional
16+
symmetry is imposed.
17+
18+
Standard orientation as defined in diffpy.
19+
20+
Parameters
21+
----------
22+
a : float
23+
Unit cell length a, in Å
24+
b : float
25+
Unit cell length b, in Å
26+
c : float
27+
Unit cell length c, in Å
28+
alpha : float
29+
Angle between b and c, in degrees
30+
beta : float
31+
Angle between a and c, in degrees
32+
gamma : float
33+
Angle between a and b, in degrees
34+
"""
35+
self.a = a
36+
self.b = b
37+
self.c = c
38+
self.alpha = alpha
39+
self.beta = beta
40+
self.gamma = gamma
41+
42+
self.lattice = diffpy.Lattice(self.a, self.b, self.c, self.alpha, self.beta, self.gamma)
43+
self.structure = diffpy.Structure(
44+
atoms=[diffpy.Atom(xyz=[0, 0, 0])],
45+
lattice=self.lattice,
46+
)
47+
48+
@property
49+
def a_vec(self) -> np.ndarray:
50+
return self.lattice.cartesian((1, 0, 0))
51+
52+
@property
53+
def b_vec(self) -> np.ndarray:
54+
return self.lattice.cartesian((0, 1, 0))
55+
56+
@property
57+
def c_vec(self) -> np.ndarray:
58+
return self.lattice.cartesian((0, 0, 1))
59+
60+
@property
61+
def a_star_vec(self) -> np.ndarray:
62+
return self.lattice.reciprocal().cartesian((1, 0, 0))
63+
64+
@property
65+
def b_star_vec(self) -> np.ndarray:
66+
return self.lattice.reciprocal().cartesian((0, 1, 0))
67+
68+
@property
69+
def c_star_vec(self) -> np.ndarray:
70+
return self.lattice.reciprocal().cartesian((0, 0, 1))
71+
72+
@classmethod
73+
def default(cls: Type[Crystal_T]) -> Crystal_T:
74+
return cls(1, 2, 3, 90, 100, 110)
75+
76+
def real_space_lattice(self, d_max: float) -> np.ndarray:
77+
"""Get the real space lattice as a (n, 3) shape array.
78+
79+
Parameters
80+
----------
81+
d_max: float
82+
The maximum d-spacing
83+
84+
Returns
85+
-------
86+
np.ndarray
87+
Shape (n, 3), lattice points
88+
"""
89+
max_h = int(d_max // self.a)
90+
max_k = int(d_max // self.b)
91+
max_l = int(d_max // self.c)
92+
hkls = np.array(
93+
[
94+
(h, k, l)
95+
for h in range(-max_h, max_h + 1) # noqa: E741
96+
for k in range(-max_k, max_k + 1) # noqa: E741
97+
for l in range(-max_l, max_l + 1) # noqa: E741
98+
]
99+
)
100+
vecs = self.lattice.cartesian(hkls)
101+
return vecs
102+
103+
def reciprocal_space_lattice(self, d_min: float) -> np.ndarray:
104+
"""Get the reciprocal space lattice as a (n, 3) shape array for input
105+
n.
106+
107+
Parameters
108+
----------
109+
d_min: float
110+
Minimum d-spacing included
111+
112+
Returns
113+
-------
114+
np.ndarray
115+
Shape (n, 3), lattice points
116+
"""
117+
max_h = int(d_min // self.lattice.ar)
118+
max_k = int(d_min // self.lattice.br)
119+
max_l = int(d_min // self.lattice.cr)
120+
hkls = np.array(
121+
[
122+
(h, k, l)
123+
for h in range(-max_h, max_h + 1) # noqa: E741
124+
for k in range(-max_k, max_k + 1) # noqa: E741
125+
for l in range(-max_l, max_l + 1) # noqa: E741
126+
]
127+
)
128+
vecs = self.lattice.reciprocal().cartesian(hkls)
129+
return vecs
130+
131+
def diffraction_pattern_mask(
132+
self,
133+
shape: tuple[int, int],
134+
d_min: float,
135+
rotation_matrix: np.ndarray,
136+
wavelength: float,
137+
excitation_error: float,
138+
) -> np.ndarray:
139+
"""Get a diffraction pattern with a given shape, up to a given
140+
resolution, in a given orientation and wavelength.
141+
142+
Parameters
143+
----------
144+
shape : tuple[int, int]
145+
Output shape
146+
d_min : float
147+
Minimum d-spacing, in Å
148+
rotation_matrix : np.ndarray
149+
Orientation
150+
wavelength : float
151+
Wavelength of incident beam, in Å
152+
excitation_error : float
153+
Excitation error used for intensity calculation, in reciprocal Å
154+
155+
Returns
156+
-------
157+
np.ndarray
158+
Diffraction pattern
159+
"""
160+
# TODO calibration
161+
out = np.zeros(shape, dtype=bool)
162+
163+
# TODO this depends on convergence angle
164+
spot_radius = 3 # pixels
165+
166+
vecs = self.reciprocal_space_lattice(d_min)
167+
d = np.sum(vecs**2, axis=1)
168+
vecs = vecs[d < d_min**2]
169+
170+
k = 2 * np.pi / wavelength
171+
k_vec = rotation_matrix @ np.array([0, 0, -k])
172+
173+
# Find intersect with Ewald's sphere
174+
q_squared = np.sum((vecs - k_vec) ** 2, axis=1)
175+
vecs = vecs[
176+
(q_squared > (k - excitation_error) ** 2)
177+
& (q_squared < (k + excitation_error) ** 2)
178+
]
179+
180+
# Project onto screen
181+
vecs_xy = (rotation_matrix.T @ vecs.T).T[:, :-1] # ignoring curvature
182+
183+
# Make image
184+
for vec in vecs_xy:
185+
x = int(vec[0] * d_min * shape[1] / 2) + shape[1] // 2
186+
y = int(vec[1] * d_min * shape[0] / 2) + shape[0] // 2
187+
min_x = max(0, x - spot_radius)
188+
max_x = min(shape[1], x + spot_radius)
189+
min_y = max(0, y - spot_radius)
190+
max_y = min(shape[0], y + spot_radius)
191+
out[min_y:max_y, min_x:max_x] = 1
192+
return out
193+
194+
def __str__(self) -> str:
195+
return f'{self.__class__.__name__}(a = {self.a}, b = {self.b}, c = {self.c}, alpha = {self.alpha}, beta = {self.beta}, gamma = {self.gamma})'
196+
197+
198+
class CubicCrystal(Crystal):
199+
def __init__(self, a: float) -> None:
200+
super().__init__(a, a, a, 90, 90, 90)
201+
202+
@classmethod
203+
def default(cls: Type[Crystal_T]) -> Crystal_T:
204+
return cls(1)
205+
206+
207+
class HexagonalCrystal(Crystal):
208+
def __init__(self, a: float, c: float) -> None:
209+
super().__init__(a, a, c, 90, 90, 120)
210+
211+
@classmethod
212+
def default(cls: Type[Crystal_T]) -> Crystal_T:
213+
return cls(1, 2)
214+
215+
216+
class TrigonalCrystal(Crystal):
217+
def __init__(self, a: float, alpha: float) -> None:
218+
super().__init__(a, a, a, alpha, alpha, alpha)
219+
220+
@classmethod
221+
def default(cls: Type[Crystal_T]) -> Crystal_T:
222+
return cls(1, 100)
223+
224+
225+
class TetragonalCrystal(Crystal):
226+
def __init__(self, a: float, c: float) -> None:
227+
super().__init__(a, a, c, 90, 90, 90)
228+
229+
@classmethod
230+
def default(cls: Type[Crystal_T]) -> Crystal_T:
231+
return cls(1, 2)
232+
233+
234+
class OrthorhombicCrystal(Crystal):
235+
def __init__(self, a: float, b: float, c: float) -> None:
236+
super().__init__(a, b, c, 90, 90, 90)
237+
238+
@classmethod
239+
def default(cls: Type[Crystal_T]) -> Crystal_T:
240+
return cls(1, 2, 3)
241+
242+
243+
class MonoclinicCrystal(Crystal):
244+
def __init__(self, a: float, b: float, c: float, beta: float) -> None:
245+
super().__init__(a, b, c, 90, beta, 90)
246+
247+
@classmethod
248+
def default(cls: Type[Crystal_T]) -> Crystal_T:
249+
return cls(1, 2, 3, 100)
250+
251+
252+
class TriclinicCrystal(Crystal):
253+
@classmethod
254+
def default(cls: Type[Crystal_T]) -> Crystal_T:
255+
return cls(1, 2, 3, 90, 100, 110)

0 commit comments

Comments
 (0)