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,204 @@
|
|
|
1
|
+
"""Scattering-matrix Bloch backend: the batched, differentiable form of ``bloch``.
|
|
2
|
+
|
|
3
|
+
Replaces the per-direction dynamical matrix with batched solves of the
|
|
4
|
+
scattering matrix, much faster for dense direction grids and equal to
|
|
5
|
+
:class:`~de_ebsd.diffraction.backends.BlochWaveBackend` in double precision.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
import de_ebsd.diffraction.reflections as _R
|
|
14
|
+
from de_ebsd.diffraction.backends import BlochWaveBackend, SimulationInputs
|
|
15
|
+
from de_ebsd.diffraction.reflections import excitation_errors, master_reflection_list
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BlochPrismBackend(BlochWaveBackend):
|
|
19
|
+
"""Scattering-matrix (PRISM) Bloch backend: one shared eigen-decomposition.
|
|
20
|
+
|
|
21
|
+
``cdtype`` selects the working precision: ``torch.complex128`` (default,
|
|
22
|
+
exact against the reference) or ``torch.complex64`` (faster, ~1e-4).
|
|
23
|
+
|
|
24
|
+
The whole intensity path is torch (``matrix_exp`` + a depth recurrence), so
|
|
25
|
+
it is autograd-differentiable: with a ``requires_grad`` structure-factor LUT
|
|
26
|
+
(``u_lut``) gradients flow through the Bloch solve back to ``U_g`` (matching
|
|
27
|
+
finite differences; M4). Connecting ``U_g`` to crystal parameters needs torch
|
|
28
|
+
structure factors -- the remaining M4 piece.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
name = "bloch_prism"
|
|
32
|
+
|
|
33
|
+
def __init__(self, cdtype: torch.dtype = torch.complex128):
|
|
34
|
+
self.cdtype = cdtype
|
|
35
|
+
self.rdtype = torch.float64 if cdtype == torch.complex128 else torch.float32
|
|
36
|
+
# class_chunk bounds the [chunk, Cmax, Cmax] candidate-Bethe tensor; block
|
|
37
|
+
# is classified up front so buckets span it (large, GPU-efficient batches);
|
|
38
|
+
# matmul_batch caps the [B, S, S] matrix_exp batch.
|
|
39
|
+
self.class_chunk = 1024
|
|
40
|
+
self.block = 65536
|
|
41
|
+
self.matmul_batch = 4096
|
|
42
|
+
|
|
43
|
+
# ---- setup ---------------------------------------------------------- #
|
|
44
|
+
def prepare(self, inputs: SimulationInputs) -> None:
|
|
45
|
+
super().prepare(
|
|
46
|
+
inputs
|
|
47
|
+
) # structure_factors, bethe, depth profile, Sgh LUT, kn, gzero, n_atoms, hmax
|
|
48
|
+
dev = torch.device(self.device if self.device is not None else "cpu")
|
|
49
|
+
self.tdev = dev
|
|
50
|
+
self.off = 2 * self.hmax
|
|
51
|
+
self.u_lut = torch.as_tensor(self.structure_factors.ucg_lut, device=dev, dtype=self.cdtype)
|
|
52
|
+
self.sgh_t = torch.as_tensor(self.sgh_lut, device=dev, dtype=self.cdtype)
|
|
53
|
+
self.lam = float(self.structure_factors.wavelength)
|
|
54
|
+
self.upz0 = float(self.structure_factors.upz0)
|
|
55
|
+
self.lambda_t = torch.as_tensor(self.lambda_e, device=dev, dtype=self.rdtype)
|
|
56
|
+
self.n_depths = self.lambda_t.shape[0]
|
|
57
|
+
|
|
58
|
+
# fixed, direction-independent tables (g = 0 prepended at index 0)
|
|
59
|
+
master = master_reflection_list(self.structure_factors)
|
|
60
|
+
z1 = np.zeros((1, 3), np.int64)
|
|
61
|
+
self.hkl_aug = torch.as_tensor(np.concatenate([z1, master.hkl]), device=dev)
|
|
62
|
+
self.n_beams = self.hkl_aug.shape[0]
|
|
63
|
+
u0 = float(np.abs(self.structure_factors.lookup(np.zeros(3, np.int64))))
|
|
64
|
+
self.u_mod = torch.as_tensor(
|
|
65
|
+
np.concatenate([[u0], master.u_mod]), device=dev, dtype=self.rdtype
|
|
66
|
+
)
|
|
67
|
+
self.forbidden = torch.as_tensor(np.concatenate([[False], master.forbidden]), device=dev)
|
|
68
|
+
self.g_cart = torch.as_tensor(
|
|
69
|
+
self.structure_factors.crystal.g_cartesian(master.hkl), device=dev, dtype=self.rdtype
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _lookup(self, lut: torch.Tensor, hkl: torch.Tensor) -> torch.Tensor:
|
|
73
|
+
i = hkl + self.off
|
|
74
|
+
return lut[i[..., 0], i[..., 1], i[..., 2]]
|
|
75
|
+
|
|
76
|
+
# ---- depth integration via the scattering matrix -------------------- #
|
|
77
|
+
def _calc_lgh(self, a: torch.Tensor) -> torch.Tensor:
|
|
78
|
+
phase = 2.0 * np.pi * self.depthstep / (2.0 * self.kn)
|
|
79
|
+
step = torch.complex(
|
|
80
|
+
torch.zeros((), device=self.tdev, dtype=self.rdtype),
|
|
81
|
+
torch.as_tensor(phase, device=self.tdev, dtype=self.rdtype),
|
|
82
|
+
)
|
|
83
|
+
sigma = torch.linalg.matrix_exp(step * a) # exp(i phase A)
|
|
84
|
+
psi = sigma[:, :, self.gzero].clone() # psi_1 = Sigma e_gzero
|
|
85
|
+
lgh = torch.zeros_like(a)
|
|
86
|
+
for n in range(self.n_depths):
|
|
87
|
+
lgh = lgh + self.lambda_t[n] * (psi.conj()[:, :, None] * psi[:, None, :])
|
|
88
|
+
psi = torch.matmul(sigma, psi[:, :, None]).squeeze(-1)
|
|
89
|
+
return lgh * (self.depthstep / self.thick)
|
|
90
|
+
|
|
91
|
+
# ---- one intensity per incident direction --------------------------- #
|
|
92
|
+
def intensities(self, directions: torch.Tensor) -> torch.Tensor:
|
|
93
|
+
k_all = directions.to(self.tdev, self.rdtype)
|
|
94
|
+
total = k_all.shape[0]
|
|
95
|
+
out = torch.empty(total, device=self.tdev, dtype=self.out_dtype)
|
|
96
|
+
for m0 in range(0, total, self.block):
|
|
97
|
+
k = k_all[m0 : m0 + self.block]
|
|
98
|
+
strong, weak, sg = self._classify(k)
|
|
99
|
+
counts = strong.sum(1)
|
|
100
|
+
for s in torch.unique(counts).tolist():
|
|
101
|
+
sub = torch.nonzero(counts == s, as_tuple=False).squeeze(1)
|
|
102
|
+
for b0 in range(0, sub.shape[0], self.matmul_batch):
|
|
103
|
+
self._bucket(
|
|
104
|
+
int(s), sub[b0 : b0 + self.matmul_batch], strong, weak, sg, out, m0
|
|
105
|
+
)
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
def _classify(self, k: torch.Tensor):
|
|
109
|
+
bethe = self.bethe
|
|
110
|
+
d = k.shape[0]
|
|
111
|
+
strong = torch.zeros(d, self.n_beams, dtype=torch.bool, device=self.tdev)
|
|
112
|
+
weak = torch.zeros_like(strong)
|
|
113
|
+
sg_all = torch.empty(d, self.n_beams, device=self.tdev, dtype=self.rdtype)
|
|
114
|
+
for c0 in range(0, d, self.class_chunk):
|
|
115
|
+
kc = k[c0 : c0 + self.class_chunk]
|
|
116
|
+
c = kc.shape[0]
|
|
117
|
+
sg = excitation_errors(self.g_cart, kc, self.lam)
|
|
118
|
+
sg = torch.cat([torch.zeros(c, 1, device=self.tdev, dtype=self.rdtype), sg], dim=1)
|
|
119
|
+
absg = sg.abs()
|
|
120
|
+
cand = (absg / (self.lam * self.u_mod.clamp_min(1e-300))) <= bethe.cutoff
|
|
121
|
+
cand[:, self.forbidden] = absg[:, self.forbidden] <= bethe.sg_double
|
|
122
|
+
cand[:, 0] = True
|
|
123
|
+
|
|
124
|
+
# gather the candidate beams to a padded [c, Cmax] index list
|
|
125
|
+
cc = cand.sum(1)
|
|
126
|
+
cmax = int(cc.max().item())
|
|
127
|
+
nz = cand.nonzero(as_tuple=False) # [T, 2], row-sorted
|
|
128
|
+
rows = nz[:, 0]
|
|
129
|
+
starts = torch.zeros(c, dtype=torch.long, device=self.tdev)
|
|
130
|
+
starts[1:] = cc.cumsum(0)[:-1]
|
|
131
|
+
within = torch.arange(rows.shape[0], device=self.tdev) - starts[rows]
|
|
132
|
+
cidx = torch.zeros(c, cmax, dtype=torch.long, device=self.tdev)
|
|
133
|
+
creal = torch.zeros(c, cmax, dtype=torch.bool, device=self.tdev)
|
|
134
|
+
cidx[rows, within] = nz[:, 1]
|
|
135
|
+
creal[rows, within] = True
|
|
136
|
+
|
|
137
|
+
# Bethe min over candidate h only
|
|
138
|
+
absg_c = absg.gather(1, cidx) # [c, Cmax]
|
|
139
|
+
hkl_c = self.hkl_aug[cidx] # [c, Cmax, 3]
|
|
140
|
+
u = (
|
|
141
|
+
self._lookup(self.u_lut, hkl_c[:, :, None, :] - hkl_c[:, None, :, :])
|
|
142
|
+
.abs()
|
|
143
|
+
.to(self.rdtype)
|
|
144
|
+
)
|
|
145
|
+
good = (u > _R._FORBIDDEN_TOL) & creal[:, None, :]
|
|
146
|
+
rh = torch.where(
|
|
147
|
+
good,
|
|
148
|
+
(absg_c[:, :, None] / self.lam) / u.clamp_min(1e-300),
|
|
149
|
+
torch.full_like(u, 1.0e4),
|
|
150
|
+
)
|
|
151
|
+
m_c = rh.min(dim=2).values # [c, Cmax]
|
|
152
|
+
st_c = (m_c <= bethe.strong) & creal
|
|
153
|
+
wk_c = (m_c > bethe.strong) & (m_c <= bethe.weak) & creal
|
|
154
|
+
|
|
155
|
+
st = torch.zeros(c, self.n_beams, dtype=torch.bool, device=self.tdev)
|
|
156
|
+
wk = torch.zeros_like(st)
|
|
157
|
+
st.scatter_(1, cidx, st_c)
|
|
158
|
+
wk.scatter_(1, cidx, wk_c)
|
|
159
|
+
st[:, 0] = True
|
|
160
|
+
st[:, self.forbidden] = False
|
|
161
|
+
wk[:, self.forbidden] = False
|
|
162
|
+
wk[:, 0] = False
|
|
163
|
+
strong[c0 : c0 + c] = st
|
|
164
|
+
weak[c0 : c0 + c] = wk
|
|
165
|
+
sg_all[c0 : c0 + c] = sg
|
|
166
|
+
return strong, weak, sg_all
|
|
167
|
+
|
|
168
|
+
def _bucket(self, s, sub, strong, weak, sg, out, c0):
|
|
169
|
+
b = sub.shape[0]
|
|
170
|
+
st, wk, sgs = strong[sub], weak[sub], sg[sub]
|
|
171
|
+
s_idx = st.nonzero(as_tuple=False)[:, 1].reshape(b, s) # ascending -> g0 first
|
|
172
|
+
hkl_s = self.hkl_aug[s_idx] # [b, s, 3]
|
|
173
|
+
sg_s = sgs.gather(1, s_idx) # [b, s]
|
|
174
|
+
|
|
175
|
+
wc = wk.sum(1)
|
|
176
|
+
w_max = int(wc.max().item()) if b else 0
|
|
177
|
+
a = self._lookup(self.u_lut, hkl_s[:, :, None, :] - hkl_s[:, None, :, :]).clone()
|
|
178
|
+
weak_diag = torch.zeros(b, s, device=self.tdev, dtype=self.rdtype)
|
|
179
|
+
if w_max > 0:
|
|
180
|
+
nz = wk.nonzero(as_tuple=False) # [T, 2], row-sorted
|
|
181
|
+
rows, cols = nz[:, 0], nz[:, 1]
|
|
182
|
+
starts = torch.zeros(b, dtype=torch.long, device=self.tdev)
|
|
183
|
+
starts[1:] = wc.cumsum(0)[:-1]
|
|
184
|
+
within = torch.arange(rows.shape[0], device=self.tdev) - starts[rows]
|
|
185
|
+
w_idx = torch.zeros(b, w_max, dtype=torch.long, device=self.tdev)
|
|
186
|
+
w_ok = torch.zeros(b, w_max, dtype=torch.bool, device=self.tdev)
|
|
187
|
+
w_idx[rows, within] = cols
|
|
188
|
+
w_ok[rows, within] = True
|
|
189
|
+
hkl_w = self.hkl_aug[w_idx]
|
|
190
|
+
sg_w = sgs.gather(1, w_idx)
|
|
191
|
+
inv_sgw = torch.where(w_ok, 1.0 / sg_w, torch.zeros_like(sg_w))
|
|
192
|
+
u_gw = self._lookup(self.u_lut, hkl_s[:, :, None, :] - hkl_w[:, None, :, :])
|
|
193
|
+
u_wh = self._lookup(self.u_lut, hkl_w[:, :, None, :] - hkl_s[:, None, :, :])
|
|
194
|
+
a = a - 0.5 * self.lam * torch.matmul(u_gw * inv_sgw[:, None, :], u_wh)
|
|
195
|
+
weak_diag = 0.5 * self.lam * (u_gw.abs() ** 2 * inv_sgw[:, None, :]).sum(2)
|
|
196
|
+
|
|
197
|
+
di = torch.arange(s, device=self.tdev)
|
|
198
|
+
diag = (2.0 * sg_s / self.lam - weak_diag).to(self.cdtype) + 1j * self.upz0
|
|
199
|
+
a[:, di, di] = diag
|
|
200
|
+
|
|
201
|
+
lgh = self._calc_lgh(a)
|
|
202
|
+
sgh = self._lookup(self.sgh_t, hkl_s[:, None, :, :] - hkl_s[:, :, None, :]) # [g,h]=h-g
|
|
203
|
+
inten = (lgh * sgh).sum((1, 2)).real / self.n_atoms
|
|
204
|
+
out[sub + c0] = inten.to(self.out_dtype)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Reflection lists, excitation errors, and the strong/weak Bethe partition.
|
|
2
|
+
|
|
3
|
+
Ports of EMsoft's ``Initialize_ReflectionList``, ``CalcsgDouble`` and
|
|
4
|
+
``Apply_BethePotentials``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import torch
|
|
14
|
+
|
|
15
|
+
from de_ebsd.crystal.structure_factors import StructureFactors
|
|
16
|
+
from de_ebsd.diffraction.bethe import BetheParameters
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from anyplotlib import Figure
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"STRONG",
|
|
23
|
+
"WEAK",
|
|
24
|
+
"IGNORED",
|
|
25
|
+
"ReflectionList",
|
|
26
|
+
"master_reflection_list",
|
|
27
|
+
"excitation_errors",
|
|
28
|
+
"candidate_reflections",
|
|
29
|
+
"partition_strong_weak",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# status codes
|
|
33
|
+
STRONG = 0
|
|
34
|
+
WEAK = 1
|
|
35
|
+
IGNORED = 2
|
|
36
|
+
|
|
37
|
+
_FORBIDDEN_TOL = 1e-4 # |U_g| in nm^-2 below which a reflection counts as forbidden
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ReflectionList:
|
|
42
|
+
"""A list of reflections, optionally classified for one beam direction.
|
|
43
|
+
|
|
44
|
+
Attributes
|
|
45
|
+
----------
|
|
46
|
+
hkl, g, d, u_mod
|
|
47
|
+
Miller indices (N, 3), lengths ``|g|`` (nm^-1), spacings d (nm), and
|
|
48
|
+
structure-factor moduli ``|U_g|`` (nm^-2).
|
|
49
|
+
forbidden
|
|
50
|
+
True where ``|U_g|`` is (numerically) zero; such reflections only matter
|
|
51
|
+
as double-diffraction channels.
|
|
52
|
+
k
|
|
53
|
+
The beam direction (unit vector, Cartesian crystal frame) used for
|
|
54
|
+
the classification, or None for the master list.
|
|
55
|
+
sg
|
|
56
|
+
Excitation errors (nm^-1) for ``k``, or None.
|
|
57
|
+
status
|
|
58
|
+
Per-reflection STRONG / WEAK / IGNORED for ``k``, or None.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
hkl: np.ndarray
|
|
62
|
+
g: np.ndarray
|
|
63
|
+
d: np.ndarray = field(repr=False)
|
|
64
|
+
u_mod: np.ndarray = field(repr=False)
|
|
65
|
+
forbidden: np.ndarray = field(repr=False)
|
|
66
|
+
k: np.ndarray | None = None
|
|
67
|
+
sg: np.ndarray | None = field(default=None, repr=False)
|
|
68
|
+
status: np.ndarray | None = field(default=None, repr=False)
|
|
69
|
+
|
|
70
|
+
def __len__(self) -> int:
|
|
71
|
+
return len(self.hkl)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def n_strong(self) -> int:
|
|
75
|
+
if self.status is None:
|
|
76
|
+
raise ValueError("no classification; call simulator.reflections(k=...)")
|
|
77
|
+
return int(np.sum(self.status == STRONG))
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def n_weak(self) -> int:
|
|
81
|
+
if self.status is None:
|
|
82
|
+
raise ValueError("no classification; call simulator.reflections(k=...)")
|
|
83
|
+
return int(np.sum(self.status == WEAK))
|
|
84
|
+
|
|
85
|
+
def strong(self) -> np.ndarray:
|
|
86
|
+
"""Miller indices of the strong reflections."""
|
|
87
|
+
if self.status is None:
|
|
88
|
+
raise ValueError("no classification; call simulator.reflections(k=...)")
|
|
89
|
+
return self.hkl[self.status == STRONG]
|
|
90
|
+
|
|
91
|
+
def weak(self) -> np.ndarray:
|
|
92
|
+
"""Miller indices of the weak reflections."""
|
|
93
|
+
if self.status is None:
|
|
94
|
+
raise ValueError("no classification; call simulator.reflections(k=...)")
|
|
95
|
+
return self.hkl[self.status == WEAK]
|
|
96
|
+
|
|
97
|
+
def show(self, n: int = 40) -> Figure:
|
|
98
|
+
"""Bar chart of ``|U_g|`` for the first ``n`` reflections.
|
|
99
|
+
|
|
100
|
+
With a classification, bars are colored by status (strong / weak /
|
|
101
|
+
ignored); without one, all bars share one color.
|
|
102
|
+
"""
|
|
103
|
+
from de_ebsd.viz.figures import bar_figure
|
|
104
|
+
|
|
105
|
+
take = slice(0, n)
|
|
106
|
+
labels = [f"{h} {k} {l_}" for h, k, l_ in self.hkl[take]]
|
|
107
|
+
values = self.u_mod[take]
|
|
108
|
+
colors = None
|
|
109
|
+
title = f"Reflections (of {len(self)})"
|
|
110
|
+
if self.status is not None:
|
|
111
|
+
palette = {STRONG: "#4fc3f7", WEAK: "#ffb74d", IGNORED: "#9e9e9e"}
|
|
112
|
+
colors = [palette[s] for s in self.status[take]]
|
|
113
|
+
title = (
|
|
114
|
+
f"Reflections for k = {np.round(self.k, 4)} "
|
|
115
|
+
f"({self.n_strong} strong / {self.n_weak} weak of {len(self)})"
|
|
116
|
+
)
|
|
117
|
+
return bar_figure(values, labels, title=title, colors=colors)
|
|
118
|
+
|
|
119
|
+
def __repr__(self) -> str:
|
|
120
|
+
if self.status is None:
|
|
121
|
+
return f"ReflectionList({len(self)} reflections, master list)"
|
|
122
|
+
return (
|
|
123
|
+
f"ReflectionList({len(self)} candidates for k={np.round(self.k, 4)}: "
|
|
124
|
+
f"{self.n_strong} strong, {self.n_weak} weak)"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def master_reflection_list(structure_factors: StructureFactors) -> ReflectionList:
|
|
129
|
+
"""All centering-allowed reflections with d >= dmin, sorted by ``|g|``."""
|
|
130
|
+
u_mod = np.abs(structure_factors.ucg)
|
|
131
|
+
return ReflectionList(
|
|
132
|
+
hkl=structure_factors.hkl,
|
|
133
|
+
g=structure_factors.g,
|
|
134
|
+
d=structure_factors.d,
|
|
135
|
+
u_mod=u_mod,
|
|
136
|
+
forbidden=u_mod < _FORBIDDEN_TOL,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def excitation_errors(
|
|
141
|
+
g_cart: torch.Tensor,
|
|
142
|
+
k_hat: torch.Tensor,
|
|
143
|
+
wavelength: float,
|
|
144
|
+
) -> torch.Tensor:
|
|
145
|
+
"""Excitation errors s_g in nm^-1 (EMsoft ``Calcsg``), batched over beams.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
g_cart
|
|
150
|
+
Reflections in the Cartesian crystal frame, shape (G, 3), nm^-1.
|
|
151
|
+
k_hat
|
|
152
|
+
Unit incident-beam directions, shape (..., 3). The foil normal is
|
|
153
|
+
taken along the beam (master pattern geometry).
|
|
154
|
+
wavelength
|
|
155
|
+
Electron wavelength in nm (refraction corrected).
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
torch.Tensor
|
|
160
|
+
s_g of shape (..., G).
|
|
161
|
+
"""
|
|
162
|
+
k = k_hat / wavelength # (..., 3), nm^-1
|
|
163
|
+
kg = k.unsqueeze(-2) + g_cart # (..., G, 3)
|
|
164
|
+
numerator = -(g_cart * (2.0 * k.unsqueeze(-2) + g_cart)).sum(-1) # -g.(2k+g)
|
|
165
|
+
denominator = 2.0 * (kg * k_hat.unsqueeze(-2)).sum(-1) # 2 (k+g).n
|
|
166
|
+
return numerator / denominator
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def candidate_reflections(
|
|
170
|
+
refl: ReflectionList,
|
|
171
|
+
sg: np.ndarray,
|
|
172
|
+
wavelength: float,
|
|
173
|
+
bethe: BetheParameters,
|
|
174
|
+
) -> np.ndarray:
|
|
175
|
+
"""Mask of reflections kept for one beam direction.
|
|
176
|
+
|
|
177
|
+
EMsoft ``Initialize_ReflectionList``: keep when
|
|
178
|
+
``|s_g| / (lambda |U_g|) <= cutoff``; forbidden reflections are kept as
|
|
179
|
+
double-diffraction channels when ``|s_g| <= sg_double``.
|
|
180
|
+
"""
|
|
181
|
+
with np.errstate(divide="ignore"):
|
|
182
|
+
r_g = np.abs(sg) / (wavelength * np.maximum(refl.u_mod, 1e-300))
|
|
183
|
+
keep = r_g <= bethe.cutoff
|
|
184
|
+
keep[refl.forbidden] = np.abs(sg[refl.forbidden]) <= bethe.sg_double
|
|
185
|
+
return keep
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def partition_strong_weak(
|
|
189
|
+
hkl: np.ndarray,
|
|
190
|
+
sg: np.ndarray,
|
|
191
|
+
forbidden: np.ndarray,
|
|
192
|
+
structure_factors: StructureFactors,
|
|
193
|
+
bethe: BetheParameters,
|
|
194
|
+
) -> np.ndarray:
|
|
195
|
+
"""Classify candidate reflections as STRONG / WEAK / IGNORED.
|
|
196
|
+
|
|
197
|
+
Port of EMsoft ``Apply_BethePotentials``: for each reflection g the
|
|
198
|
+
perturbation strength is ``min_h |s_g| / (lambda |U_{g-h}|)`` over all
|
|
199
|
+
candidates h; thresholds ``bethe.strong`` and ``bethe.weak`` split the
|
|
200
|
+
classes. The incident beam (first entry, g = 0) is always strong.
|
|
201
|
+
|
|
202
|
+
Systematically absent (double-diffraction) reflections carry no structure
|
|
203
|
+
factor (``|U_g| ~ 0``) and so cannot be Bloch beams: they are never strong
|
|
204
|
+
or weak. They stay in the candidate list only as difference partners for
|
|
205
|
+
genuine double-diffraction paths ``U_{g-h}`` of the *allowed* beams. Without
|
|
206
|
+
this exclusion the degenerate self difference ``g - g = 0`` (whose
|
|
207
|
+
coefficient ``U_0`` is the largest in the table) would drive ``m`` down to
|
|
208
|
+
``|s_g|/(lambda |U_0|)`` and spuriously promote any near-Bragg forbidden
|
|
209
|
+
reflection to a strong beam -- adding beams EMsoft never includes and
|
|
210
|
+
corrupting the dynamical matrix.
|
|
211
|
+
|
|
212
|
+
Parameters are the candidate subset (index 0 must be the g = 0 beam).
|
|
213
|
+
"""
|
|
214
|
+
n = len(hkl)
|
|
215
|
+
status = np.full(n, IGNORED, dtype=np.int64)
|
|
216
|
+
status[0] = STRONG
|
|
217
|
+
|
|
218
|
+
diff = hkl[:, None, :] - hkl[None, :, :] # (N, N, 3)
|
|
219
|
+
u_diff = np.abs(structure_factors.lookup(diff)) # (N, N)
|
|
220
|
+
inv_lambda = 1.0 / structure_factors.wavelength
|
|
221
|
+
|
|
222
|
+
# rh(i, h) = |s_i| / lambda / |U_{g_i - g_h}|; forbidden difference
|
|
223
|
+
# vectors are excluded from the minimum (EMsoft sets rh = 10000 there).
|
|
224
|
+
with np.errstate(divide="ignore"):
|
|
225
|
+
rh = (inv_lambda * np.abs(sg))[:, None] / u_diff
|
|
226
|
+
rh = np.where(u_diff > _FORBIDDEN_TOL, rh, 1.0e4)
|
|
227
|
+
m = rh.min(axis=1)
|
|
228
|
+
|
|
229
|
+
status[1:][m[1:] <= bethe.strong] = STRONG
|
|
230
|
+
between = (m[1:] > bethe.strong) & (m[1:] <= bethe.weak)
|
|
231
|
+
status[1:][between] = WEAK
|
|
232
|
+
# forbidden reflections are difference partners only, never beams themselves
|
|
233
|
+
status[forbidden] = IGNORED
|
|
234
|
+
return status
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Orientation indexing: the matching counterpart to :mod:`de_ebsd.sim`.
|
|
2
|
+
|
|
3
|
+
:class:`SphericalIndexer` back-projects an experimental pattern to the sphere and
|
|
4
|
+
cross-correlates it against the master over all of SO(3) via spherical harmonics
|
|
5
|
+
and the Wigner-D transform (after EMSphInx; Lenthe, Singh & De Graef, 2019).
|
|
6
|
+
:class:`DictionaryIndexer` is the brute-force template-matching alternative.
|
|
7
|
+
Both emit orix objects for kikuchipy.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DictionaryIndexer",
|
|
16
|
+
"HREBSDResult",
|
|
17
|
+
"LiveIndexer",
|
|
18
|
+
"SphericalIndexer",
|
|
19
|
+
"determine_pattern_center",
|
|
20
|
+
"hrebsd",
|
|
21
|
+
"master_from_emsoft",
|
|
22
|
+
"phase_from_crystal",
|
|
23
|
+
"refine_hr",
|
|
24
|
+
"refine_orientation",
|
|
25
|
+
"remap_reference",
|
|
26
|
+
"remove_background",
|
|
27
|
+
"remove_dynamic_background",
|
|
28
|
+
"remove_static_background",
|
|
29
|
+
"to_crystal_map",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
_ORIGIN = {
|
|
33
|
+
"DictionaryIndexer": "de_ebsd.index.dictionary",
|
|
34
|
+
"SphericalIndexer": "de_ebsd.index.spherical",
|
|
35
|
+
"determine_pattern_center": "de_ebsd.index.refine",
|
|
36
|
+
"HREBSDResult": "de_ebsd.index.crosscorr",
|
|
37
|
+
"LiveIndexer": "de_ebsd.index.live",
|
|
38
|
+
"hrebsd": "de_ebsd.index.crosscorr",
|
|
39
|
+
"remap_reference": "de_ebsd.index.crosscorr",
|
|
40
|
+
"refine_hr": "de_ebsd.index.refine",
|
|
41
|
+
"refine_orientation": "de_ebsd.index.refine",
|
|
42
|
+
"master_from_emsoft": "de_ebsd.index.io",
|
|
43
|
+
"phase_from_crystal": "de_ebsd.index.io",
|
|
44
|
+
"to_crystal_map": "de_ebsd.index.io",
|
|
45
|
+
"remove_background": "de_ebsd.index.preprocess",
|
|
46
|
+
"remove_dynamic_background": "de_ebsd.index.preprocess",
|
|
47
|
+
"remove_static_background": "de_ebsd.index.preprocess",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def __getattr__(name: str):
|
|
52
|
+
# lazy so ``import de_ebsd.index`` stays light: orix, diffpy and
|
|
53
|
+
# anyplotlib are only imported when actually used
|
|
54
|
+
module = _ORIGIN.get(name)
|
|
55
|
+
if module is None:
|
|
56
|
+
raise AttributeError(f"module 'de_ebsd.index' has no attribute {name!r}")
|
|
57
|
+
return getattr(importlib.import_module(module), name)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def __dir__() -> list[str]:
|
|
61
|
+
return sorted(__all__)
|