de-ebsd 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- de_ebsd/__init__.py +76 -0
- de_ebsd/core/__init__.py +23 -0
- de_ebsd/core/constants.py +81 -0
- de_ebsd/core/device.py +12 -0
- de_ebsd/core/geometry.py +67 -0
- de_ebsd/core/lambert.py +148 -0
- de_ebsd/core/rotation.py +139 -0
- de_ebsd/crystal/__init__.py +22 -0
- de_ebsd/crystal/data/__init__.py +0 -0
- de_ebsd/crystal/data/element_properties.py +29 -0
- de_ebsd/crystal/data/scattering_tables.py +414 -0
- de_ebsd/crystal/lattice.py +517 -0
- de_ebsd/crystal/scattering.py +339 -0
- de_ebsd/crystal/scattering_torch.py +233 -0
- de_ebsd/crystal/structure_factors.py +291 -0
- de_ebsd/diffraction/__init__.py +45 -0
- de_ebsd/diffraction/backends.py +318 -0
- de_ebsd/diffraction/bethe.py +28 -0
- de_ebsd/diffraction/kinematic.py +389 -0
- de_ebsd/diffraction/kvectors.py +120 -0
- de_ebsd/diffraction/prism.py +204 -0
- de_ebsd/diffraction/reflections.py +234 -0
- de_ebsd/index/__init__.py +61 -0
- de_ebsd/index/_kernels.py +222 -0
- de_ebsd/index/backproject.py +171 -0
- de_ebsd/index/crosscorr.py +654 -0
- de_ebsd/index/dictionary.py +362 -0
- de_ebsd/index/io.py +324 -0
- de_ebsd/index/live/__init__.py +5 -0
- de_ebsd/index/live/background.py +103 -0
- de_ebsd/index/live/calibration.py +70 -0
- de_ebsd/index/live/common.py +44 -0
- de_ebsd/index/live/deformation.py +289 -0
- de_ebsd/index/live/indexer.py +386 -0
- de_ebsd/index/preprocess.py +195 -0
- de_ebsd/index/refine/__init__.py +7 -0
- de_ebsd/index/refine/highres.py +479 -0
- de_ebsd/index/refine/levenberg.py +33 -0
- de_ebsd/index/refine/orientation.py +367 -0
- de_ebsd/index/refine/pattern_center.py +356 -0
- de_ebsd/index/spherical/__init__.py +41 -0
- de_ebsd/index/spherical/correlation.py +106 -0
- de_ebsd/index/spherical/harmonics.py +311 -0
- de_ebsd/index/spherical/indexer.py +345 -0
- de_ebsd/index/spherical/wigner.py +190 -0
- de_ebsd/sim/__init__.py +20 -0
- de_ebsd/sim/detector.py +145 -0
- de_ebsd/sim/masterpattern.py +231 -0
- de_ebsd/sim/microscope.py +93 -0
- de_ebsd/sim/montecarlo/__init__.py +19 -0
- de_ebsd/sim/montecarlo/depth.py +167 -0
- de_ebsd/sim/montecarlo/transport.py +532 -0
- de_ebsd/sim/simulator.py +291 -0
- de_ebsd/viz/__init__.py +53 -0
- de_ebsd/viz/camera.py +56 -0
- de_ebsd/viz/depth_widget.js +76 -0
- de_ebsd/viz/explorer.py +647 -0
- de_ebsd/viz/figures.py +67 -0
- de_ebsd/viz/maps.py +277 -0
- de_ebsd/viz/patterns.py +164 -0
- de_ebsd/viz/setup_widget.js +171 -0
- de_ebsd/viz/views.py +125 -0
- de_ebsd/viz/widgets.py +191 -0
- de_ebsd-0.2.0.dist-info/METADATA +251 -0
- de_ebsd-0.2.0.dist-info/RECORD +69 -0
- de_ebsd-0.2.0.dist-info/WHEEL +5 -0
- de_ebsd-0.2.0.dist-info/licenses/LICENSE +674 -0
- de_ebsd-0.2.0.dist-info/licenses/NOTICE +44 -0
- de_ebsd-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Weickenmeier-Kohl atomic scattering factors (numpy).
|
|
2
|
+
|
|
3
|
+
Complex form factors including absorption: the elastic part from the WK
|
|
4
|
+
parameterization, plus thermal-diffuse (``_fphon``) and core-loss (``_fcore``)
|
|
5
|
+
absorptive parts. Port of EMsoft ``CalcUcg``/``WKscatter``; see
|
|
6
|
+
:mod:`de_ebsd.crystal.scattering_torch` for the differentiable version.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from de_ebsd.crystal.data.element_properties import ATOMIC_WEIGHTS
|
|
14
|
+
from de_ebsd.crystal.data.scattering_tables import ELEMENT_SYMBOLS, WK_A, WK_B
|
|
15
|
+
|
|
16
|
+
# Exact constants from the Fortran source (single-precision literals).
|
|
17
|
+
_FOURPI = 12.56637062 # FSCATT
|
|
18
|
+
_FOURPI_PHON = 12.56636 # FPHON uses a slightly different 4*pi
|
|
19
|
+
_PI = 3.1415927
|
|
20
|
+
_C_EULER = 0.5772157
|
|
21
|
+
|
|
22
|
+
_ATOMIC_NUMBER = {symbol: i + 1 for i, symbol in enumerate(ELEMENT_SYMBOLS)}
|
|
23
|
+
|
|
24
|
+
# Abramowitz & Stegun table used by RIH2 for x * exp(-x) * Ei(x) at large x.
|
|
25
|
+
_RIH2_TABLE = np.array(
|
|
26
|
+
[
|
|
27
|
+
1.000000,
|
|
28
|
+
1.005051,
|
|
29
|
+
1.010206,
|
|
30
|
+
1.015472,
|
|
31
|
+
1.020852,
|
|
32
|
+
1.026355,
|
|
33
|
+
1.031985,
|
|
34
|
+
1.037751,
|
|
35
|
+
1.043662,
|
|
36
|
+
1.049726,
|
|
37
|
+
1.055956,
|
|
38
|
+
1.062364,
|
|
39
|
+
1.068965,
|
|
40
|
+
1.075780,
|
|
41
|
+
1.082830,
|
|
42
|
+
1.090140,
|
|
43
|
+
1.097737,
|
|
44
|
+
1.105647,
|
|
45
|
+
1.113894,
|
|
46
|
+
1.122497,
|
|
47
|
+
1.131470,
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def atomic_number(element: str) -> int:
|
|
53
|
+
"""Atomic number Z for an element symbol (case sensitive, e.g. ``"Ni"``)."""
|
|
54
|
+
try:
|
|
55
|
+
return _ATOMIC_NUMBER[element]
|
|
56
|
+
except KeyError:
|
|
57
|
+
raise ValueError(f"Unknown element symbol {element!r}") from None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def atomic_weight(element: str) -> float:
|
|
61
|
+
"""Standard atomic weight (g/mol) for an element symbol (e.g. ``"Ni"``)."""
|
|
62
|
+
return ATOMIC_WEIGHTS[atomic_number(element) - 1]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _ei(x: np.ndarray) -> np.ndarray:
|
|
66
|
+
x = np.asarray(x, dtype=np.float64)
|
|
67
|
+
if np.any(x > 60.0):
|
|
68
|
+
raise ValueError("Ei(x) untested for x > 60 (matches EMsoft behavior)")
|
|
69
|
+
out = np.zeros_like(x)
|
|
70
|
+
|
|
71
|
+
rational = (x >= -60.0) & (x < -1.0)
|
|
72
|
+
if np.any(rational):
|
|
73
|
+
# Abramowitz & Stegun (5.1.56)
|
|
74
|
+
a1, a2, a3, a4 = 8.57332, 18.05901, 8.63476, 0.26777
|
|
75
|
+
b1, b2, b3, b4 = 9.57332, 25.63295, 21.09965, 3.95849
|
|
76
|
+
xp = np.abs(x[rational])
|
|
77
|
+
num = a4 + xp * (a3 + xp * (a2 + xp * (a1 + xp)))
|
|
78
|
+
den = b4 + xp * (b3 + xp * (b2 + xp * (b1 + xp)))
|
|
79
|
+
out[rational] = -(num / den) * np.exp(-xp) / xp
|
|
80
|
+
|
|
81
|
+
series = x >= -1.0
|
|
82
|
+
if np.any(series):
|
|
83
|
+
xs = x[series]
|
|
84
|
+
result = _C_EULER + np.log(np.abs(xs))
|
|
85
|
+
term = xs.copy()
|
|
86
|
+
total = term.copy()
|
|
87
|
+
for i in range(1, 500):
|
|
88
|
+
ri = float(i)
|
|
89
|
+
term = term * xs * ri / ((ri + 1.0) ** 2)
|
|
90
|
+
total += term
|
|
91
|
+
with np.errstate(invalid="ignore"):
|
|
92
|
+
if np.all(np.abs(term) <= 1.0e-6 * np.abs(xs)):
|
|
93
|
+
break
|
|
94
|
+
out[series] = result + total
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _rih2(x: np.ndarray) -> np.ndarray:
|
|
99
|
+
x = np.asarray(x, dtype=np.float64)
|
|
100
|
+
x1 = 1.0 / x
|
|
101
|
+
idx = (200.0 * x1).astype(np.int64)
|
|
102
|
+
idx = np.clip(idx, 0, len(_RIH2_TABLE) - 2)
|
|
103
|
+
f0 = _RIH2_TABLE[idx]
|
|
104
|
+
f1 = _RIH2_TABLE[idx + 1]
|
|
105
|
+
return f0 + 200.0 * (f1 - f0) * (x1 - 5.0e-4 * idx)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _rih1(x1: np.ndarray, x2: np.ndarray, x3: np.ndarray) -> np.ndarray:
|
|
109
|
+
x1, x2, x3 = np.broadcast_arrays(
|
|
110
|
+
np.asarray(x1, dtype=np.float64),
|
|
111
|
+
np.asarray(x2, dtype=np.float64),
|
|
112
|
+
np.asarray(x3, dtype=np.float64),
|
|
113
|
+
)
|
|
114
|
+
out = np.zeros(x1.shape, dtype=np.float64)
|
|
115
|
+
|
|
116
|
+
big2 = x2 > 20.0
|
|
117
|
+
small2 = ~big2
|
|
118
|
+
if np.any(big2):
|
|
119
|
+
out[big2] += np.exp(x2[big2] - x1[big2]) * _rih2(x2[big2]) / x2[big2]
|
|
120
|
+
if np.any(small2):
|
|
121
|
+
out[small2] += np.exp(-x1[small2]) * _ei(x2[small2])
|
|
122
|
+
|
|
123
|
+
big3 = x3 > 20.0
|
|
124
|
+
small3 = ~big3
|
|
125
|
+
if np.any(big3):
|
|
126
|
+
out[big3] -= np.exp(x3[big3] - x1[big3]) * _rih2(x3[big3]) / x3[big3]
|
|
127
|
+
if np.any(small3):
|
|
128
|
+
out[small3] -= np.exp(-x1[small3]) * _ei(x3[small3])
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _ri1(bi: float, bj: float, g: np.ndarray) -> np.ndarray:
|
|
133
|
+
g = np.asarray(g, dtype=np.float64)
|
|
134
|
+
g2 = g * g
|
|
135
|
+
out = np.zeros_like(g)
|
|
136
|
+
eps = max(bi, bj) * g2
|
|
137
|
+
|
|
138
|
+
small = eps <= 0.1
|
|
139
|
+
if np.any(small):
|
|
140
|
+
base = _PI * (bi * np.log((bi + bj) / bi) + bj * np.log((bi + bj) / bj))
|
|
141
|
+
bi2 = bi * bi
|
|
142
|
+
bj2 = bj * bj
|
|
143
|
+
temp = 0.5 * bi2 * np.log(bi / (bi + bj)) + 0.5 * bj2 * np.log(bj / (bi + bj))
|
|
144
|
+
temp += 0.75 * (bi2 + bj2) - 0.25 * (bi + bj) ** 2
|
|
145
|
+
temp -= 0.5 * (bi - bj) ** 2
|
|
146
|
+
out[small] = base + _PI * g2[small] * temp
|
|
147
|
+
|
|
148
|
+
asym = ~small
|
|
149
|
+
if np.any(asym):
|
|
150
|
+
g2a = g2[asym]
|
|
151
|
+
big2 = bi * g2a
|
|
152
|
+
bjg2 = bj * g2a
|
|
153
|
+
val = 2.0 * _C_EULER + np.log(big2) + np.log(bjg2)
|
|
154
|
+
val -= 2.0 * _ei(-bi * bj * g2a / (bi + bj))
|
|
155
|
+
val += _rih1(big2, big2 * bi / (bi + bj), big2)
|
|
156
|
+
val += _rih1(bjg2, bjg2 * bj / (bi + bj), bjg2)
|
|
157
|
+
out[asym] = val * _PI / g2a
|
|
158
|
+
return out
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _ri2(bi: float, bj: float, g: np.ndarray, u: float) -> np.ndarray:
|
|
162
|
+
g = np.asarray(g, dtype=np.float64)
|
|
163
|
+
g2 = g * g
|
|
164
|
+
out = np.zeros_like(g)
|
|
165
|
+
|
|
166
|
+
u2 = u * u
|
|
167
|
+
u22 = 0.5 * u2
|
|
168
|
+
biuh = bi + 0.5 * u2
|
|
169
|
+
bjuh = bj + 0.5 * u2
|
|
170
|
+
biu = bi + u2
|
|
171
|
+
bju = bj + u2
|
|
172
|
+
|
|
173
|
+
eps = max(bi, bj, u2) * g2
|
|
174
|
+
small = eps <= 0.1
|
|
175
|
+
if np.any(small):
|
|
176
|
+
base = (bi + u2) * np.log((bi + bj + u2) / (bi + u2))
|
|
177
|
+
base += bj * np.log((bi + bj + u2) / (bj + u2))
|
|
178
|
+
base += u2 * np.log(u2 / (bj + u2))
|
|
179
|
+
base *= _PI
|
|
180
|
+
temp = 0.5 * u22 * u22 * np.log(biu * bju / (u2 * u2))
|
|
181
|
+
temp += 0.5 * biuh * biuh * np.log(biu / (biuh + bjuh))
|
|
182
|
+
temp += 0.5 * bjuh * bjuh * np.log(bju / (biuh + bjuh))
|
|
183
|
+
temp += 0.25 * biu * biu + 0.5 * bi * bi
|
|
184
|
+
temp += 0.25 * bju * bju + 0.5 * bj * bj
|
|
185
|
+
temp -= 0.25 * (biuh + bjuh) ** 2
|
|
186
|
+
temp -= 0.5 * ((bi * biu - bj * bju) / (biuh + bjuh)) ** 2
|
|
187
|
+
temp -= u22 * u22
|
|
188
|
+
out[small] = base + _PI * g2[small] * temp
|
|
189
|
+
|
|
190
|
+
asym = ~small
|
|
191
|
+
if np.any(asym):
|
|
192
|
+
g2a = g2[asym]
|
|
193
|
+
val = _ei(-0.5 * u2 * g2a * biuh / biu) + _ei(-0.5 * u2 * g2a * bjuh / bju)
|
|
194
|
+
val -= _ei(-biuh * bjuh * g2a / (biuh + bjuh)) + _ei(-0.25 * u2 * g2a)
|
|
195
|
+
val *= 2.0
|
|
196
|
+
val += _rih1(0.5 * u2 * g2a, 0.25 * u2 * g2a, 0.25 * u2 * u2 * g2a / biu)
|
|
197
|
+
val += _rih1(0.5 * u2 * g2a, 0.25 * u2 * g2a, 0.25 * u2 * u2 * g2a / bju)
|
|
198
|
+
val += _rih1(biuh * g2a, biuh * biuh * g2a / (biuh + bjuh), biuh * biuh * g2a / biu)
|
|
199
|
+
val += _rih1(bjuh * g2a, bjuh * bjuh * g2a / (biuh + bjuh), bjuh * bjuh * g2a / bju)
|
|
200
|
+
out[asym] = val * _PI / g2a
|
|
201
|
+
return out
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _weko(a: np.ndarray, b: np.ndarray, s: np.ndarray) -> np.ndarray:
|
|
205
|
+
s = np.asarray(s, dtype=np.float64)
|
|
206
|
+
s2 = s * s
|
|
207
|
+
with np.errstate(divide="ignore"):
|
|
208
|
+
inv_s2 = np.where(s2 > 0.0, 1.0 / np.maximum(s2, 1e-300), 0.0)
|
|
209
|
+
total = np.zeros_like(s)
|
|
210
|
+
for ai, bi in zip(a, b, strict=True):
|
|
211
|
+
argu = bi * s2
|
|
212
|
+
small = argu < 0.1
|
|
213
|
+
large = argu > 20.0
|
|
214
|
+
mid = ~small & ~large
|
|
215
|
+
total += np.where(small, ai * bi * (1.0 - 0.5 * argu), 0.0)
|
|
216
|
+
total += np.where(large, ai * inv_s2, 0.0)
|
|
217
|
+
# only evaluate exp where needed to stay in range
|
|
218
|
+
total += np.where(mid, ai * (1.0 - np.exp(-np.where(mid, argu, 1.0))) * inv_s2, 0.0)
|
|
219
|
+
return total
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _fphon(g: np.ndarray, ul: float, a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
223
|
+
g = np.asarray(g, dtype=np.float64)
|
|
224
|
+
fp2 = _FOURPI_PHON * _FOURPI_PHON
|
|
225
|
+
a1 = np.asarray(a, dtype=np.float64) * fp2
|
|
226
|
+
b1 = np.asarray(b, dtype=np.float64) / fp2
|
|
227
|
+
u2 = ul * ul
|
|
228
|
+
dewa = np.exp(-0.5 * u2 * g * g)
|
|
229
|
+
|
|
230
|
+
out = np.zeros_like(g)
|
|
231
|
+
for j in range(4):
|
|
232
|
+
out += a1[j] * a1[j] * (dewa * _ri1(b1[j], b1[j], g) - _ri2(b1[j], b1[j], g, ul))
|
|
233
|
+
for i in range(j):
|
|
234
|
+
out += 2.0 * a1[j] * a1[i] * (dewa * _ri1(b1[i], b1[j], g) - _ri2(b1[i], b1[j], g, ul))
|
|
235
|
+
return out
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _fcore(g: np.ndarray, z: int, voltage_kv: float) -> np.ndarray:
|
|
239
|
+
g = np.asarray(g, dtype=np.float64)
|
|
240
|
+
a0 = 0.5289
|
|
241
|
+
|
|
242
|
+
k0 = 0.5068 * np.sqrt(1022.0 * voltage_kv + voltage_kv * voltage_kv)
|
|
243
|
+
de = (6 * z) * 1.0e-3
|
|
244
|
+
thetae = de / (2.0 * voltage_kv) * (2.0 * voltage_kv + 1022.0) / (voltage_kv + 1022.0)
|
|
245
|
+
r = 0.885 * a0 / float(z) ** 0.333333
|
|
246
|
+
ta = 1.0 / (k0 * r)
|
|
247
|
+
tb = g / (2.0 * k0)
|
|
248
|
+
|
|
249
|
+
omega = 2.0 * tb / ta
|
|
250
|
+
kappa = thetae / ta
|
|
251
|
+
k2 = kappa * kappa
|
|
252
|
+
o2 = omega * omega
|
|
253
|
+
|
|
254
|
+
x1 = (
|
|
255
|
+
omega
|
|
256
|
+
/ ((1.0 + o2) * np.sqrt(o2 + 4.0 * k2))
|
|
257
|
+
* np.log((omega + np.sqrt(o2 + 4.0 * k2)) / (2.0 * kappa))
|
|
258
|
+
)
|
|
259
|
+
x2 = (
|
|
260
|
+
1.0
|
|
261
|
+
/ np.sqrt((1.0 + o2) ** 2 + 4.0 * k2 * o2)
|
|
262
|
+
* np.log(
|
|
263
|
+
(1.0 + 2.0 * k2 + o2 + np.sqrt((1.0 + o2) ** 2 + 4.0 * k2 * o2))
|
|
264
|
+
/ (2.0 * kappa * np.sqrt(1.0 + k2))
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
x3_small = 1.0 / (4.0 * (1.0 + k2))
|
|
268
|
+
big = omega > 1.0e-2
|
|
269
|
+
omega_safe = np.where(big, omega, 1.0)
|
|
270
|
+
x3_big = (
|
|
271
|
+
1.0
|
|
272
|
+
/ (omega_safe * np.sqrt(o2 + 4.0 * (1.0 + k2)))
|
|
273
|
+
* np.log((omega_safe + np.sqrt(o2 + 4.0 * (1.0 + k2))) / (2.0 * np.sqrt(1.0 + k2)))
|
|
274
|
+
)
|
|
275
|
+
x3 = np.where(big, x3_big, x3_small)
|
|
276
|
+
|
|
277
|
+
hi = (2 * z) / (ta * ta) * (-x1 + x2 - x3)
|
|
278
|
+
return 4.0 / (a0 * a0) * 2.0 * _PI / (k0 * k0) * hi
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def wk_scattering_factor(
|
|
282
|
+
z: int,
|
|
283
|
+
g_wk: np.ndarray,
|
|
284
|
+
ul: float,
|
|
285
|
+
voltage_kv: float,
|
|
286
|
+
absorption: bool = True,
|
|
287
|
+
apply_debye_waller: bool = True,
|
|
288
|
+
) -> np.ndarray:
|
|
289
|
+
"""Complex WK scattering amplitude, EMsoft ``FSCATT`` with ``accflg=.TRUE.``.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
z
|
|
294
|
+
Atomic number (1-98).
|
|
295
|
+
g_wk
|
|
296
|
+
Scattering vector lengths in WK units, ``|g| [nm^-1] * 2 pi / 10``.
|
|
297
|
+
ul
|
|
298
|
+
RMS thermal displacement in Angstrom, ``sqrt(B [nm^2] * 100/(8 pi^2))``.
|
|
299
|
+
voltage_kv
|
|
300
|
+
Accelerating voltage in kV.
|
|
301
|
+
absorption
|
|
302
|
+
Include the phonon and core absorptive parts (EMsoft ``absflg = 3``).
|
|
303
|
+
apply_debye_waller
|
|
304
|
+
Multiply the elastic part with the Debye-Waller factor.
|
|
305
|
+
|
|
306
|
+
Returns
|
|
307
|
+
-------
|
|
308
|
+
numpy.ndarray
|
|
309
|
+
Complex amplitude, same shape as ``g_wk``. Real part is the elastic
|
|
310
|
+
amplitude times gamma; imaginary part is the absorptive amplitude
|
|
311
|
+
times gamma^2 / k0. Units: Angstrom times 4 pi.
|
|
312
|
+
"""
|
|
313
|
+
if not 1 <= z <= 98:
|
|
314
|
+
raise ValueError(f"Z = {z} is out of range 1..98")
|
|
315
|
+
if ul < 0:
|
|
316
|
+
raise ValueError("ul must be non-negative")
|
|
317
|
+
g_wk = np.asarray(g_wk, dtype=np.float64)
|
|
318
|
+
if np.any(g_wk < 0):
|
|
319
|
+
raise ValueError("g_wk must be non-negative")
|
|
320
|
+
|
|
321
|
+
a = np.asarray(WK_A[z - 1], dtype=np.float64)
|
|
322
|
+
b = np.asarray(WK_B[z - 1], dtype=np.float64)
|
|
323
|
+
|
|
324
|
+
dewa = np.exp(-0.5 * ul * ul * g_wk * g_wk) if apply_debye_waller else np.ones_like(g_wk)
|
|
325
|
+
s = g_wk / _FOURPI
|
|
326
|
+
f_real = _FOURPI * dewa * _weko(a, b, s)
|
|
327
|
+
|
|
328
|
+
if absorption:
|
|
329
|
+
f_imag = _fcore(g_wk, z, voltage_kv) * dewa
|
|
330
|
+
if ul > 0: # no thermal displacement -> no phonon absorption
|
|
331
|
+
f_imag = f_imag + _fphon(g_wk, ul, a, b)
|
|
332
|
+
else:
|
|
333
|
+
f_imag = np.zeros_like(g_wk)
|
|
334
|
+
|
|
335
|
+
k0 = 0.5068 * np.sqrt(1022.0 * voltage_kv + voltage_kv * voltage_kv)
|
|
336
|
+
gamma = (voltage_kv + 511.0) / 511.0
|
|
337
|
+
f_real = f_real * gamma
|
|
338
|
+
f_imag = f_imag * gamma * gamma / k0
|
|
339
|
+
return f_real + 1j * f_imag
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Differentiable torch port of :mod:`de_ebsd.crystal.scattering`.
|
|
2
|
+
|
|
3
|
+
Same Weickenmeier-Kohl form factors, with the Debye-Waller factor ``u`` as a
|
|
4
|
+
tensor so structure factors carry gradients back to the crystal parameters.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import torch
|
|
11
|
+
|
|
12
|
+
from de_ebsd.crystal.data.scattering_tables import WK_A, WK_B
|
|
13
|
+
from de_ebsd.crystal.scattering import (
|
|
14
|
+
_C_EULER,
|
|
15
|
+
_FOURPI,
|
|
16
|
+
_FOURPI_PHON,
|
|
17
|
+
_PI,
|
|
18
|
+
_RIH2_TABLE,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = ["wk_scattering_factor_torch"]
|
|
22
|
+
|
|
23
|
+
_EI_TERMS = 400 # fixed series length (numpy stops adaptively; this over-covers)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _ei(x: torch.Tensor) -> torch.Tensor:
|
|
27
|
+
out = torch.zeros_like(x)
|
|
28
|
+
|
|
29
|
+
rational = (x >= -60.0) & (x < -1.0)
|
|
30
|
+
xp = torch.where(rational, x.abs(), torch.ones_like(x))
|
|
31
|
+
a1, a2, a3, a4 = 8.57332, 18.05901, 8.63476, 0.26777
|
|
32
|
+
b1, b2, b3, b4 = 9.57332, 25.63295, 21.09965, 3.95849
|
|
33
|
+
num = a4 + xp * (a3 + xp * (a2 + xp * (a1 + xp)))
|
|
34
|
+
den = b4 + xp * (b3 + xp * (b2 + xp * (b1 + xp)))
|
|
35
|
+
out = torch.where(rational, -(num / den) * torch.exp(-xp) / xp, out)
|
|
36
|
+
|
|
37
|
+
series = x >= -1.0
|
|
38
|
+
xs = torch.where(series, x, torch.full_like(x, -0.5))
|
|
39
|
+
xs = torch.where(xs.abs() < 1e-300, torch.full_like(xs, 1e-300), xs)
|
|
40
|
+
total = xs.clone()
|
|
41
|
+
term = xs.clone()
|
|
42
|
+
for i in range(1, _EI_TERMS):
|
|
43
|
+
ri = float(i)
|
|
44
|
+
term = term * xs * ri / ((ri + 1.0) ** 2)
|
|
45
|
+
total = total + term
|
|
46
|
+
out = torch.where(series, _C_EULER + torch.log(xs.abs()) + total, out)
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _rih2(x: torch.Tensor, table: torch.Tensor) -> torch.Tensor:
|
|
51
|
+
x1 = 1.0 / x
|
|
52
|
+
idx = (200.0 * x1).long().clamp(0, table.shape[0] - 2)
|
|
53
|
+
f0 = table[idx]
|
|
54
|
+
f1 = table[idx + 1]
|
|
55
|
+
return f0 + 200.0 * (f1 - f0) * (x1 - 5.0e-4 * idx.to(x.dtype))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _rih1(x1, x2, x3, table: torch.Tensor) -> torch.Tensor:
|
|
59
|
+
x1, x2, x3 = torch.broadcast_tensors(x1, x2, x3)
|
|
60
|
+
# The _ei series overflows for large arguments; the large-x branch uses _rih2
|
|
61
|
+
# instead, so feed _ei a safe (small) argument wherever its result is unused
|
|
62
|
+
# -- otherwise 0 * inf in the discarded branch would poison the gradient.
|
|
63
|
+
big2 = x2 > 20.0
|
|
64
|
+
x2s = torch.where(big2, x2, torch.ones_like(x2))
|
|
65
|
+
x2e = torch.where(big2, torch.zeros_like(x2), x2)
|
|
66
|
+
out = torch.where(big2, torch.exp(x2 - x1) * _rih2(x2s, table) / x2s, torch.exp(-x1) * _ei(x2e))
|
|
67
|
+
big3 = x3 > 20.0
|
|
68
|
+
x3s = torch.where(big3, x3, torch.ones_like(x3))
|
|
69
|
+
x3e = torch.where(big3, torch.zeros_like(x3), x3)
|
|
70
|
+
out = out - torch.where(
|
|
71
|
+
big3, torch.exp(x3 - x1) * _rih2(x3s, table) / x3s, torch.exp(-x1) * _ei(x3e)
|
|
72
|
+
)
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _ri1(bi: float, bj: float, g: torch.Tensor, table: torch.Tensor) -> torch.Tensor:
|
|
77
|
+
g2 = g * g
|
|
78
|
+
small = (max(bi, bj) * g2) <= 0.1
|
|
79
|
+
|
|
80
|
+
base = _PI * (bi * np.log((bi + bj) / bi) + bj * np.log((bi + bj) / bj))
|
|
81
|
+
bi2, bj2 = bi * bi, bj * bj
|
|
82
|
+
temp = 0.5 * bi2 * np.log(bi / (bi + bj)) + 0.5 * bj2 * np.log(bj / (bi + bj))
|
|
83
|
+
temp += 0.75 * (bi2 + bj2) - 0.25 * (bi + bj) ** 2 - 0.5 * (bi - bj) ** 2
|
|
84
|
+
small_val = base + _PI * g2 * temp
|
|
85
|
+
|
|
86
|
+
g2s = torch.where(small, torch.ones_like(g2), g2)
|
|
87
|
+
big2, bjg2 = bi * g2s, bj * g2s
|
|
88
|
+
val = 2.0 * _C_EULER + torch.log(big2) + torch.log(bjg2)
|
|
89
|
+
val = val - 2.0 * _ei(-bi * bj * g2s / (bi + bj))
|
|
90
|
+
val = val + _rih1(big2, big2 * bi / (bi + bj), big2, table)
|
|
91
|
+
val = val + _rih1(bjg2, bjg2 * bj / (bi + bj), bjg2, table)
|
|
92
|
+
return torch.where(small, small_val, val * _PI / g2s)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _ri2(bi: float, bj: float, g: torch.Tensor, u: torch.Tensor, table) -> torch.Tensor:
|
|
96
|
+
g2 = g * g
|
|
97
|
+
u2 = u * u
|
|
98
|
+
u22 = 0.5 * u2
|
|
99
|
+
biuh, bjuh = bi + 0.5 * u2, bj + 0.5 * u2
|
|
100
|
+
biu, bju = bi + u2, bj + u2
|
|
101
|
+
|
|
102
|
+
eps = torch.maximum(u2, torch.full_like(u2, max(bi, bj))) * g2
|
|
103
|
+
small = eps <= 0.1
|
|
104
|
+
|
|
105
|
+
base = (bi + u2) * torch.log((bi + bj + u2) / (bi + u2))
|
|
106
|
+
base = base + bj * torch.log((bi + bj + u2) / (bj + u2))
|
|
107
|
+
base = base + u2 * torch.log(u2 / (bj + u2))
|
|
108
|
+
base = base * _PI
|
|
109
|
+
temp = 0.5 * u22 * u22 * torch.log(biu * bju / (u2 * u2))
|
|
110
|
+
temp = temp + 0.5 * biuh * biuh * torch.log(biu / (biuh + bjuh))
|
|
111
|
+
temp = temp + 0.5 * bjuh * bjuh * torch.log(bju / (biuh + bjuh))
|
|
112
|
+
temp = temp + 0.25 * biu * biu + 0.5 * bi * bi
|
|
113
|
+
temp = temp + 0.25 * bju * bju + 0.5 * bj * bj
|
|
114
|
+
temp = temp - 0.25 * (biuh + bjuh) ** 2
|
|
115
|
+
temp = temp - 0.5 * ((bi * biu - bj * bju) / (biuh + bjuh)) ** 2
|
|
116
|
+
temp = temp - u22 * u22
|
|
117
|
+
small_val = base + _PI * g2 * temp
|
|
118
|
+
|
|
119
|
+
g2s = torch.where(small, torch.ones_like(g2), g2)
|
|
120
|
+
val = _ei(-0.5 * u2 * g2s * biuh / biu) + _ei(-0.5 * u2 * g2s * bjuh / bju)
|
|
121
|
+
val = val - _ei(-biuh * bjuh * g2s / (biuh + bjuh)) - _ei(-0.25 * u2 * g2s)
|
|
122
|
+
val = val * 2.0
|
|
123
|
+
val = val + _rih1(0.5 * u2 * g2s, 0.25 * u2 * g2s, 0.25 * u2 * u2 * g2s / biu, table)
|
|
124
|
+
val = val + _rih1(0.5 * u2 * g2s, 0.25 * u2 * g2s, 0.25 * u2 * u2 * g2s / bju, table)
|
|
125
|
+
val = val + _rih1(biuh * g2s, biuh * biuh * g2s / (biuh + bjuh), biuh * biuh * g2s / biu, table)
|
|
126
|
+
val = val + _rih1(bjuh * g2s, bjuh * bjuh * g2s / (biuh + bjuh), bjuh * bjuh * g2s / bju, table)
|
|
127
|
+
return torch.where(small, small_val, val * _PI / g2s)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _weko(a: np.ndarray, b: np.ndarray, s: torch.Tensor) -> torch.Tensor:
|
|
131
|
+
s2 = s * s
|
|
132
|
+
inv_s2 = torch.where(s2 > 0.0, 1.0 / s2.clamp_min(1e-300), torch.zeros_like(s2))
|
|
133
|
+
total = torch.zeros_like(s)
|
|
134
|
+
for ai, bi in zip(a, b, strict=True):
|
|
135
|
+
argu = bi * s2
|
|
136
|
+
small = argu < 0.1
|
|
137
|
+
large = argu > 20.0
|
|
138
|
+
mid = ~small & ~large
|
|
139
|
+
argu_safe = torch.where(mid, argu, torch.ones_like(argu))
|
|
140
|
+
total = total + torch.where(small, ai * bi * (1.0 - 0.5 * argu), torch.zeros_like(s))
|
|
141
|
+
total = total + torch.where(large, ai * inv_s2, torch.zeros_like(s))
|
|
142
|
+
total = total + torch.where(
|
|
143
|
+
mid, ai * (1.0 - torch.exp(-argu_safe)) * inv_s2, torch.zeros_like(s)
|
|
144
|
+
)
|
|
145
|
+
return total
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _fphon(
|
|
149
|
+
g: torch.Tensor, ul: torch.Tensor, a: np.ndarray, b: np.ndarray, table: torch.Tensor
|
|
150
|
+
) -> torch.Tensor:
|
|
151
|
+
fp2 = _FOURPI_PHON * _FOURPI_PHON
|
|
152
|
+
a1 = np.asarray(a) * fp2
|
|
153
|
+
b1 = np.asarray(b) / fp2
|
|
154
|
+
dewa = torch.exp(-0.5 * ul * ul * g * g)
|
|
155
|
+
out = torch.zeros_like(g)
|
|
156
|
+
for j in range(4):
|
|
157
|
+
out = out + a1[j] * a1[j] * (
|
|
158
|
+
dewa * _ri1(b1[j], b1[j], g, table) - _ri2(b1[j], b1[j], g, ul, table)
|
|
159
|
+
)
|
|
160
|
+
for i in range(j):
|
|
161
|
+
out = out + 2.0 * a1[j] * a1[i] * (
|
|
162
|
+
dewa * _ri1(b1[i], b1[j], g, table) - _ri2(b1[i], b1[j], g, ul, table)
|
|
163
|
+
)
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _fcore(g: torch.Tensor, z: int, voltage_kv: float) -> torch.Tensor:
|
|
168
|
+
a0 = 0.5289
|
|
169
|
+
k0 = 0.5068 * np.sqrt(1022.0 * voltage_kv + voltage_kv * voltage_kv)
|
|
170
|
+
de = (6 * z) * 1.0e-3
|
|
171
|
+
thetae = de / (2.0 * voltage_kv) * (2.0 * voltage_kv + 1022.0) / (voltage_kv + 1022.0)
|
|
172
|
+
r = 0.885 * a0 / float(z) ** 0.333333
|
|
173
|
+
ta = 1.0 / (k0 * r)
|
|
174
|
+
tb = g / (2.0 * k0)
|
|
175
|
+
omega = 2.0 * tb / ta
|
|
176
|
+
kappa = thetae / ta
|
|
177
|
+
k2, o2 = kappa * kappa, omega * omega
|
|
178
|
+
|
|
179
|
+
x1 = (
|
|
180
|
+
omega
|
|
181
|
+
/ ((1.0 + o2) * torch.sqrt(o2 + 4.0 * k2))
|
|
182
|
+
* torch.log((omega + torch.sqrt(o2 + 4.0 * k2)) / (2.0 * kappa))
|
|
183
|
+
)
|
|
184
|
+
x2 = (
|
|
185
|
+
1.0
|
|
186
|
+
/ torch.sqrt((1.0 + o2) ** 2 + 4.0 * k2 * o2)
|
|
187
|
+
* torch.log(
|
|
188
|
+
(1.0 + 2.0 * k2 + o2 + torch.sqrt((1.0 + o2) ** 2 + 4.0 * k2 * o2))
|
|
189
|
+
/ (2.0 * kappa * np.sqrt(1.0 + k2))
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
x3_small = 1.0 / (4.0 * (1.0 + k2))
|
|
193
|
+
big = omega > 1.0e-2
|
|
194
|
+
omega_safe = torch.where(big, omega, torch.ones_like(omega))
|
|
195
|
+
x3_big = (
|
|
196
|
+
1.0
|
|
197
|
+
/ (omega_safe * torch.sqrt(o2 + 4.0 * (1.0 + k2)))
|
|
198
|
+
* torch.log((omega_safe + torch.sqrt(o2 + 4.0 * (1.0 + k2))) / (2.0 * np.sqrt(1.0 + k2)))
|
|
199
|
+
)
|
|
200
|
+
x3 = torch.where(big, x3_big, torch.full_like(omega, x3_small))
|
|
201
|
+
|
|
202
|
+
hi = (2 * z) / (ta * ta) * (-x1 + x2 - x3)
|
|
203
|
+
return 4.0 / (a0 * a0) * 2.0 * _PI / (k0 * k0) * hi
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def wk_scattering_factor_torch(
|
|
207
|
+
z: int,
|
|
208
|
+
g_wk: torch.Tensor,
|
|
209
|
+
ul: torch.Tensor,
|
|
210
|
+
voltage_kv: float,
|
|
211
|
+
absorption: bool = True,
|
|
212
|
+
) -> torch.Tensor:
|
|
213
|
+
"""Complex WK scattering amplitude in torch, differentiable in ``ul``/``g_wk``.
|
|
214
|
+
|
|
215
|
+
Torch port of :func:`de_ebsd.crystal.scattering.wk_scattering_factor`; see that for
|
|
216
|
+
the parameters and units. ``ul`` and ``g_wk`` are tensors (pass ``ul`` as a
|
|
217
|
+
``requires_grad`` leaf for Debye-Waller gradients). Returns a complex tensor.
|
|
218
|
+
"""
|
|
219
|
+
a = np.asarray(WK_A[z - 1], dtype=np.float64)
|
|
220
|
+
b = np.asarray(WK_B[z - 1], dtype=np.float64)
|
|
221
|
+
table = torch.as_tensor(_RIH2_TABLE, device=g_wk.device, dtype=g_wk.dtype)
|
|
222
|
+
|
|
223
|
+
dewa = torch.exp(-0.5 * ul * ul * g_wk * g_wk)
|
|
224
|
+
f_real = _FOURPI * dewa * _weko(a, b, g_wk / _FOURPI)
|
|
225
|
+
|
|
226
|
+
if absorption:
|
|
227
|
+
f_imag = _fcore(g_wk, z, voltage_kv) * dewa + _fphon(g_wk, ul, a, b, table)
|
|
228
|
+
else:
|
|
229
|
+
f_imag = torch.zeros_like(g_wk)
|
|
230
|
+
|
|
231
|
+
k0 = 0.5068 * np.sqrt(1022.0 * voltage_kv + voltage_kv * voltage_kv)
|
|
232
|
+
gamma = (voltage_kv + 511.0) / 511.0
|
|
233
|
+
return torch.complex(f_real * gamma, f_imag * gamma * gamma / k0)
|