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,367 @@
1
+ """Gradient refinement of orientation and (optionally) pattern center.
2
+
3
+ Maximizes a masked spherical NCC between the back-projected pattern and the
4
+ master pattern sampled at trial-rotated directions -- both differentiable, so a
5
+ coarse seed sharpens to sub-grid-cell accuracy. ``method="adam"`` is first-order
6
+ and supports pattern-center refinement; ``method="lm"`` is a batched
7
+ Gauss-Newton with an analytic Jacobian, ~2x faster but orientation-only.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import numpy as np
13
+ import torch
14
+
15
+ from de_ebsd.core.constants import DIVISION_FLOOR
16
+ from de_ebsd.core.device import resolve_device
17
+ from de_ebsd.core.rotation import quaternion_to_matrix, rotation_vector_to_matrix, skew_matrix
18
+ from de_ebsd.index.backproject import pattern_to_sphere
19
+ from de_ebsd.index.refine.levenberg import adapt_damping
20
+
21
+ __all__ = ["refine_orientation"]
22
+
23
+
24
+ def _masked_ncc(a: torch.Tensor, b: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
25
+ denom = mask.sum(-1, keepdim=True).clamp_min(DIVISION_FLOOR)
26
+ ma = (a * mask).sum(-1, keepdim=True) / denom
27
+ mb = (b * mask).sum(-1, keepdim=True) / denom
28
+ a0 = (a - ma) * mask
29
+ b0 = (b - mb) * mask
30
+ num = (a0 * b0).sum(-1)
31
+ na = (a0 * a0).sum(-1).clamp_min(DIVISION_FLOOR).sqrt()
32
+ nb = (b0 * b0).sum(-1).clamp_min(DIVISION_FLOOR).sqrt()
33
+ return num / (na * nb)
34
+
35
+
36
+ def _fov_directions(detector, device, dtype, target: int = 1024) -> torch.Tensor:
37
+ pix = detector.direction_cosines(device=device, dtype=dtype).reshape(-1, 3)
38
+ step = max(1, pix.shape[0] // int(target))
39
+ return pix[::step][: int(target)].contiguous()
40
+
41
+
42
+ def _backproject(patt, detector, dirs, pattern_center, per_pattern):
43
+ if not per_pattern:
44
+ e, mask = pattern_to_sphere(patt, detector, dirs, pattern_center=pattern_center)
45
+ return e, mask
46
+ es, masks = [], []
47
+ for i in range(patt.shape[0]):
48
+ e_i, m_i = pattern_to_sphere(
49
+ patt[i : i + 1], detector, dirs, pattern_center=pattern_center[i]
50
+ )
51
+ es.append(e_i)
52
+ masks.append(m_i)
53
+ return torch.cat(es, dim=0), torch.stack(masks, dim=0)
54
+
55
+
56
+ def _standardize(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
57
+ c = x - x.mean(-1, keepdim=True)
58
+ return c, c.norm(dim=-1, keepdim=True).clamp_min(DIVISION_FLOOR)
59
+
60
+
61
+ def _lm_residual_jacobian(master, dirs, r_mat, e_hat, device, dtype):
62
+ n = dirs # (M, 3), all in the FOV
63
+ # grid_sample has no forward-mode AD, and being pointwise its Jacobian is
64
+ # diagonal in the points -- so one reverse-mode pass gives every dM/dv
65
+ with torch.enable_grad():
66
+ v = (n @ r_mat.transpose(-1, -2)).detach().requires_grad_(True) # (Np, M, 3)
67
+ m_raw = master.sample(v) # (Np, M)
68
+ (g_m,) = torch.autograd.grad(m_raw.sum(), v) # (Np, M, 3) = dM_i/dv_i
69
+ m = m_raw.detach().to(device=device, dtype=dtype)
70
+ g_m = g_m.to(device=device, dtype=dtype)
71
+ # tangent of v = R n wrt delta_k: R @ (-skew(n)), columns k -> (Np, M, 3, 3)
72
+ tang = r_mat[:, None] @ (-skew_matrix(n))[None]
73
+ g = torch.einsum("pma,pmak->pmk", g_m, tang) # raw dM/ddelta (Np, M, 3)
74
+ c, s = _standardize(m)
75
+ m_hat = c / s
76
+ mu_g = g.mean(1) # mean_k(g) over the directions (Np, 3)
77
+ nu = torch.einsum("pm,pmk->pk", c, g) / s # (Np, 3)
78
+ dm_hat = ((g - mu_g[:, None]) - m_hat[..., None] * nu[:, None]) / s[..., None]
79
+ return e_hat - m_hat, -dm_hat, m
80
+
81
+
82
+ def _lm_refine(
83
+ master, dirs, r0_mat, e, mask, n_steps, damping, keep_best, return_history, device, dtype
84
+ ):
85
+ e_c, e_s = _standardize(e)
86
+ e_hat = e_c / e_s # (Np, M) standardized experimental field
87
+ r_mat = r0_mat.clone()
88
+ n_p = r_mat.shape[0]
89
+ damping = torch.full((n_p,), float(damping), device=device, dtype=dtype)
90
+
91
+ with torch.no_grad():
92
+ m_seed = master.sample(dirs @ r0_mat.transpose(-1, -2)).to(device=device, dtype=dtype)
93
+ seed_score = _masked_ncc(e, m_seed, mask)
94
+ cur_score = seed_score
95
+ history = [] if return_history else None
96
+
97
+ for _ in range(n_steps):
98
+ if return_history:
99
+ history.append(cur_score)
100
+ res, j_r, _ = _lm_residual_jacobian(master, dirs, r_mat, e_hat, device, dtype)
101
+ with torch.no_grad():
102
+ jtr = torch.einsum("pm,pmk->pk", res, j_r) # (Np, 3)
103
+ jtj = torch.einsum("pmk,pml->pkl", j_r, j_r) # (Np, 3, 3)
104
+ # LM damping: add damping * diag(JtJ) to the Gauss-Newton normal matrix.
105
+ diag = torch.diagonal(jtj, dim1=-2, dim2=-1).clamp_min(1e-9)
106
+ a = jtj + damping[:, None, None] * torch.diag_embed(diag)
107
+ delta = -torch.linalg.solve(a, jtr[..., None])[..., 0] # (Np, 3)
108
+ r_new = r_mat @ rotation_vector_to_matrix(delta)
109
+ m_new = master.sample(dirs @ r_new.transpose(-1, -2)).to(device=device, dtype=dtype)
110
+ new_score = _masked_ncc(e, m_new, mask)
111
+ # per-pattern accept/reject: robust to the bilinear grid_sample kinks,
112
+ # and monotone by construction
113
+ better = new_score > cur_score
114
+ r_mat = torch.where(better[:, None, None], r_new, r_mat)
115
+ damping = adapt_damping(damping, better)
116
+ cur_score = torch.where(better, new_score, cur_score)
117
+
118
+ refined_score = cur_score
119
+ if keep_best:
120
+ # already guaranteed by the accept/reject; kept symmetric with the Adam path
121
+ keep = (refined_score >= seed_score).reshape(-1, 1, 1)
122
+ r_final = torch.where(keep, r_mat, r0_mat)
123
+ score = torch.maximum(refined_score, seed_score)
124
+ else:
125
+ r_final = r_mat
126
+ score = refined_score
127
+ if return_history:
128
+ hist = torch.stack(history, dim=0) if history else e.new_zeros((0, n_p))
129
+ else:
130
+ hist = None
131
+ return r_final, score, hist
132
+
133
+
134
+ def refine_orientation(
135
+ patterns,
136
+ master,
137
+ detector,
138
+ rotations,
139
+ pattern_center=None,
140
+ refine_pattern_center=False,
141
+ method="adam",
142
+ n_iterations=None,
143
+ learning_rate=0.02,
144
+ pattern_center_learning_rate=None,
145
+ damping=1e-3,
146
+ directions=None,
147
+ device=None,
148
+ keep_best=True,
149
+ return_history=False,
150
+ ):
151
+ """Refine ``rotations`` (and optionally the pattern center) by autograd.
152
+
153
+ Sharpens a coarse seed to sub-grid-cell accuracy by maximizing a spherical
154
+ masked NCC between the back-projected pattern ``E(n)`` and the master sampled at
155
+ the trial-rotated directions ``M(R n)``, with ``R = R0 . dR(w)`` for a
156
+ differentiable rotation-vector correction ``w``. The whole stack is refined in
157
+ one parallel optimization on the master's device.
158
+
159
+ Parameters
160
+ ----------
161
+ patterns
162
+ A detector image ``(H, W)`` or stack ``(Np, H, W)``, numpy or torch.
163
+ master, detector
164
+ The continuous :class:`de_ebsd.sim.MasterPattern` and the
165
+ :class:`~de_ebsd.sim.detector.EBSDDetector` whose pixel -> direction map is
166
+ inverted (differentiably in the pattern center) to build ``E(n)``.
167
+ rotations
168
+ Seed :class:`orix.quaternion.Rotation`, typically the coarse index output.
169
+ pattern_center
170
+ ``None`` uses ``detector.pattern_center``. A ``(3,)`` value shares one center across the
171
+ stack -- the common, fully-vectorized path; ``(Np, 3)`` gives a per-pattern
172
+ center and loops the back-projection.
173
+ refine_pattern_center
174
+ Refine the pattern center jointly with the orientation. Forces
175
+ ``method="adam"``, since the LM path is orientation-only.
176
+ method
177
+ ``"adam"`` (default) is first-order. ``"lm"`` is Levenberg-Marquardt with an
178
+ analytic Jacobian, reaching the same sub-degree accuracy in ~10 steps
179
+ instead of ~60 (~2.3x faster at equal accuracy on real data). LM is
180
+ orientation-only and assumes the default in-FOV ``directions``.
181
+ n_iterations
182
+ Step budget. ``None`` resolves per method: 60 for Adam, 12 for LM.
183
+ learning_rate
184
+ Adam step size for the rotation vector, in radians. The key stability knob:
185
+ too large a step overshoots the shallow, noisy basin of real patterns and
186
+ diverges. ``0.02`` is stable on real data; use ~0.01 for noisy scans.
187
+ pattern_center_learning_rate
188
+ Adam step size for the pattern center (default ``learning_rate / 5``, since its natural
189
+ scale is finer). Ignored unless ``refine_pattern_center``.
190
+ damping
191
+ Initial LM damping, adapted per pattern each step (x0.5 accepted, x3
192
+ rejected). LM only.
193
+ directions
194
+ Sphere directions ``(M, 3)`` the NCC is evaluated over. ``None`` uses the
195
+ detector's in-FOV pixel directions, where every point contributes -- ~3x
196
+ faster and more accurate than a whole-sphere set that is mostly masked away.
197
+ ``method="lm"`` requires an all-in-FOV set; the default satisfies this, but a
198
+ custom non-FOV set is unsafe for LM (use Adam, whose masked NCC handles it).
199
+ device
200
+ Defaults to ``master.data.device``; otherwise everything is moved there.
201
+ keep_best
202
+ Never return an orientation worse than the seed, making refinement monotone.
203
+ This removes the stray outliers gradient refinement can introduce on
204
+ low-quality real patterns.
205
+ return_history
206
+ Also return the per-step NCC trajectory.
207
+
208
+ Returns
209
+ -------
210
+ rotation : orix.quaternion.Rotation
211
+ The refined orientation(s), in the convention ``MasterPattern.project``
212
+ consumes.
213
+ score : torch.Tensor
214
+ Final NCC per pattern, in ``[-1, 1]``; ``>=`` the coarse score. This is what
215
+ ``index_scan(refine=True)`` stores as ``prop["quality"]``.
216
+ pattern_center : torch.Tensor
217
+ Only when ``refine_pattern_center``: the refined pattern center, shaped like the input.
218
+ history : torch.Tensor
219
+ Only when ``return_history``: the NCC at every step, ``(n_iterations, Np)``, with
220
+ ``history[0]`` the seed score.
221
+ """
222
+ # LM is orientation-only, so refine_pattern_center forces Adam
223
+ method = str(method).lower()
224
+ if method not in ("adam", "lm"):
225
+ raise ValueError(f"method must be 'adam' or 'lm', got {method!r}")
226
+ if method == "lm" and refine_pattern_center:
227
+ method = "adam" # joint pattern-center + orientation LM is future work
228
+ if n_iterations is None:
229
+ n_iterations = 12 if method == "lm" else 60
230
+
231
+ device = resolve_device(device, master.data.device)
232
+ dtype = master.data.dtype
233
+ # master.data.device drives sampling, so move the master to honor `device`
234
+ if master.data.device != device:
235
+ master = master.to(device)
236
+
237
+ patt = torch.as_tensor(patterns, device=device, dtype=dtype)
238
+ single = patt.ndim == 2
239
+ if single:
240
+ patt = patt[None]
241
+ if patt.ndim != 3:
242
+ raise ValueError(f"patterns must be (H, W) or (Np, H, W), got shape {tuple(patt.shape)}")
243
+ n_p = patt.shape[0]
244
+
245
+ # the detached anchor: exactly the matrix MasterPattern.project builds
246
+ q0 = torch.as_tensor(np.asarray(rotations.data), device=device, dtype=dtype).reshape(-1, 4)
247
+ if q0.shape[0] != n_p:
248
+ raise ValueError(f"rotations length {q0.shape[0]} != number of patterns {n_p}")
249
+ r0_mat = quaternion_to_matrix(q0).detach() # (Np, 3, 3)
250
+
251
+ if directions is None:
252
+ dirs = _fov_directions(detector, device, dtype)
253
+ else:
254
+ dirs = torch.as_tensor(directions, device=device, dtype=dtype).reshape(-1, 3)
255
+
256
+ # pattern center: shared (3,) or per-pattern (Np, 3)
257
+ if pattern_center is None:
258
+ pc_init = torch.as_tensor(detector.pattern_center, device=device, dtype=dtype)
259
+ else:
260
+ pc_init = torch.as_tensor(pattern_center, device=device, dtype=dtype)
261
+ if pc_init.ndim == 1 and pc_init.shape[0] == 3:
262
+ per_pattern = False
263
+ elif pc_init.ndim == 2 and pc_init.shape == (n_p, 3):
264
+ per_pattern = True
265
+ else:
266
+ raise ValueError(
267
+ f"pattern_center must be (3,) shared or (Np, 3) per-pattern (Np={n_p}), got "
268
+ f"{tuple(pc_init.shape)}"
269
+ )
270
+
271
+ # orientation-only, so the pattern center is fixed and E is back-projected once
272
+ if method == "lm":
273
+ with torch.no_grad():
274
+ e_lm, mask_lm = _backproject(patt, detector, dirs, pc_init, per_pattern)
275
+ r_final, score, hist = _lm_refine(
276
+ master,
277
+ dirs,
278
+ r0_mat,
279
+ e_lm,
280
+ mask_lm,
281
+ n_iterations,
282
+ damping,
283
+ keep_best,
284
+ return_history,
285
+ device,
286
+ dtype,
287
+ )
288
+ from orix.quaternion import Rotation
289
+
290
+ rotation = Rotation.from_matrix(r_final.cpu().numpy())
291
+ out = [rotation, score]
292
+ if return_history:
293
+ out.append(hist)
294
+ return tuple(out)
295
+
296
+ # the FOV mask is held fixed at the seed geometry so the objective stays smooth
297
+ # as the pattern center moves
298
+ with torch.no_grad():
299
+ _, mask = _backproject(patt, detector, dirs, pc_init, per_pattern)
300
+
301
+ w = torch.zeros(n_p, 3, device=device, dtype=dtype, requires_grad=True)
302
+ params = [{"params": [w], "lr": learning_rate}]
303
+ if refine_pattern_center:
304
+ pc_param = pc_init.clone().requires_grad_(True)
305
+ params.append(
306
+ {
307
+ "params": [pc_param],
308
+ "lr": pattern_center_learning_rate
309
+ if pattern_center_learning_rate is not None
310
+ else learning_rate / 5.0,
311
+ }
312
+ )
313
+ else:
314
+ pc_param = pc_init
315
+ # pattern center is fixed -> E does not change; back-project it once.
316
+ e_fixed, _ = _backproject(patt, detector, dirs, pc_param, per_pattern)
317
+
318
+ opt = torch.optim.Adam(params)
319
+ history = [] if return_history else None
320
+
321
+ def _score():
322
+ if refine_pattern_center:
323
+ e = _backproject(patt, detector, dirs, pc_param, per_pattern)[0]
324
+ else:
325
+ e = e_fixed
326
+ d_r = rotation_vector_to_matrix(w) # (Np, 3, 3)
327
+ r = r0_mat @ d_r # right-compose: R = R0 . dR(w)
328
+ v = dirs @ r.transpose(-1, -2) # (Np, M, 3) = R n, matching project
329
+ m_r = master.sample(v).to(device=device, dtype=dtype) # (Np, M)
330
+ return _masked_ncc(e, m_r, mask)
331
+
332
+ # seed NCC (w = 0), for the keep_best gate below
333
+ with torch.no_grad():
334
+ seed_score = _score().detach()
335
+
336
+ for _ in range(n_iterations):
337
+ opt.zero_grad()
338
+ s = _score()
339
+ if return_history:
340
+ history.append(s.detach())
341
+ (-s.sum()).backward()
342
+ opt.step()
343
+
344
+ with torch.no_grad():
345
+ refined_score = _score().detach()
346
+ r_ref = r0_mat @ rotation_vector_to_matrix(w) # (Np, 3, 3)
347
+ if keep_best:
348
+ # keep the coarse seed where refinement did not raise the NCC. Under
349
+ # refine_pattern_center the gate reverts only the orientation, so seed_score (taken
350
+ # at the seed pattern_center) is an approximation there -- a safe one.
351
+ keep = (refined_score >= seed_score).reshape(-1, 1, 1)
352
+ r_final = torch.where(keep, r_ref, r0_mat).cpu().numpy()
353
+ score = torch.maximum(refined_score, seed_score)
354
+ else:
355
+ r_final = r_ref.cpu().numpy()
356
+ score = refined_score
357
+
358
+ from orix.quaternion import Rotation
359
+
360
+ rotation = Rotation.from_matrix(r_final)
361
+
362
+ out = [rotation, score]
363
+ if refine_pattern_center:
364
+ out.append(pc_param.detach())
365
+ if return_history:
366
+ out.append(torch.stack(history, dim=0))
367
+ return tuple(out)