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,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__)