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.
Files changed (69) hide show
  1. de_ebsd/__init__.py +76 -0
  2. de_ebsd/core/__init__.py +23 -0
  3. de_ebsd/core/constants.py +81 -0
  4. de_ebsd/core/device.py +12 -0
  5. de_ebsd/core/geometry.py +67 -0
  6. de_ebsd/core/lambert.py +148 -0
  7. de_ebsd/core/rotation.py +139 -0
  8. de_ebsd/crystal/__init__.py +22 -0
  9. de_ebsd/crystal/data/__init__.py +0 -0
  10. de_ebsd/crystal/data/element_properties.py +29 -0
  11. de_ebsd/crystal/data/scattering_tables.py +414 -0
  12. de_ebsd/crystal/lattice.py +517 -0
  13. de_ebsd/crystal/scattering.py +339 -0
  14. de_ebsd/crystal/scattering_torch.py +233 -0
  15. de_ebsd/crystal/structure_factors.py +291 -0
  16. de_ebsd/diffraction/__init__.py +45 -0
  17. de_ebsd/diffraction/backends.py +318 -0
  18. de_ebsd/diffraction/bethe.py +28 -0
  19. de_ebsd/diffraction/kinematic.py +389 -0
  20. de_ebsd/diffraction/kvectors.py +120 -0
  21. de_ebsd/diffraction/prism.py +204 -0
  22. de_ebsd/diffraction/reflections.py +234 -0
  23. de_ebsd/index/__init__.py +61 -0
  24. de_ebsd/index/_kernels.py +222 -0
  25. de_ebsd/index/backproject.py +171 -0
  26. de_ebsd/index/crosscorr.py +654 -0
  27. de_ebsd/index/dictionary.py +362 -0
  28. de_ebsd/index/io.py +324 -0
  29. de_ebsd/index/live/__init__.py +5 -0
  30. de_ebsd/index/live/background.py +103 -0
  31. de_ebsd/index/live/calibration.py +70 -0
  32. de_ebsd/index/live/common.py +44 -0
  33. de_ebsd/index/live/deformation.py +289 -0
  34. de_ebsd/index/live/indexer.py +386 -0
  35. de_ebsd/index/preprocess.py +195 -0
  36. de_ebsd/index/refine/__init__.py +7 -0
  37. de_ebsd/index/refine/highres.py +479 -0
  38. de_ebsd/index/refine/levenberg.py +33 -0
  39. de_ebsd/index/refine/orientation.py +367 -0
  40. de_ebsd/index/refine/pattern_center.py +356 -0
  41. de_ebsd/index/spherical/__init__.py +41 -0
  42. de_ebsd/index/spherical/correlation.py +106 -0
  43. de_ebsd/index/spherical/harmonics.py +311 -0
  44. de_ebsd/index/spherical/indexer.py +345 -0
  45. de_ebsd/index/spherical/wigner.py +190 -0
  46. de_ebsd/sim/__init__.py +20 -0
  47. de_ebsd/sim/detector.py +145 -0
  48. de_ebsd/sim/masterpattern.py +231 -0
  49. de_ebsd/sim/microscope.py +93 -0
  50. de_ebsd/sim/montecarlo/__init__.py +19 -0
  51. de_ebsd/sim/montecarlo/depth.py +167 -0
  52. de_ebsd/sim/montecarlo/transport.py +532 -0
  53. de_ebsd/sim/simulator.py +291 -0
  54. de_ebsd/viz/__init__.py +53 -0
  55. de_ebsd/viz/camera.py +56 -0
  56. de_ebsd/viz/depth_widget.js +76 -0
  57. de_ebsd/viz/explorer.py +647 -0
  58. de_ebsd/viz/figures.py +67 -0
  59. de_ebsd/viz/maps.py +277 -0
  60. de_ebsd/viz/patterns.py +164 -0
  61. de_ebsd/viz/setup_widget.js +171 -0
  62. de_ebsd/viz/views.py +125 -0
  63. de_ebsd/viz/widgets.py +191 -0
  64. de_ebsd-0.2.0.dist-info/METADATA +251 -0
  65. de_ebsd-0.2.0.dist-info/RECORD +69 -0
  66. de_ebsd-0.2.0.dist-info/WHEEL +5 -0
  67. de_ebsd-0.2.0.dist-info/licenses/LICENSE +674 -0
  68. de_ebsd-0.2.0.dist-info/licenses/NOTICE +44 -0
  69. 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)