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,356 @@
|
|
|
1
|
+
"""Calibrate a single pattern center shared across many patterns.
|
|
2
|
+
|
|
3
|
+
Alternates per-pattern orientation refinement with a global Levenberg-Marquardt
|
|
4
|
+
step on the shared PC, using the band-edge samples of
|
|
5
|
+
:mod:`de_ebsd.index.refine.highres`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
from de_ebsd.core.device import resolve_device
|
|
14
|
+
from de_ebsd.core.rotation import quaternion_to_matrix, rotation_vector_to_matrix
|
|
15
|
+
from de_ebsd.index.refine.highres import (
|
|
16
|
+
direction_cosine_derivative,
|
|
17
|
+
direction_cosines_from_pattern_center,
|
|
18
|
+
edge_importance_samples,
|
|
19
|
+
levenberg_marquardt_refine,
|
|
20
|
+
normalized_residual_jacobian,
|
|
21
|
+
standardize_tiled,
|
|
22
|
+
)
|
|
23
|
+
from de_ebsd.index.refine.levenberg import adapt_shared_damping
|
|
24
|
+
|
|
25
|
+
__all__ = ["determine_pattern_center"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
#: Blur of the master, in radians on the sphere, from the first round to the last.
|
|
29
|
+
#: A real dynamical master is sharp: its band-edge NCC falls from 1 to ~0 within
|
|
30
|
+
#: 0.02 of PC, so a start 0.03 off sees no slope at all. Blurring widens that basin;
|
|
31
|
+
#: the schedule ends unblurred, so the answer is the sharp master's optimum.
|
|
32
|
+
_DEFAULT_BLUR = (0.05, 0.025, 0.012, 0.0)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _blurred(master, sigma: float):
|
|
36
|
+
"""*master* blurred by a Gaussian of *sigma* radians on the sphere.
|
|
37
|
+
|
|
38
|
+
Blurred in its square-Lambert grid, which is equal-area, so one pixel is
|
|
39
|
+
about ``sqrt(2 pi) / n`` radians everywhere; the grid's shape distortion
|
|
40
|
+
and the seam at each hemisphere's edge only matter in the coarse stages,
|
|
41
|
+
which the schedule's final unblurred stage corrects. One convolution per
|
|
42
|
+
stage, so a blurred stage samples as fast as a sharp one.
|
|
43
|
+
"""
|
|
44
|
+
from de_ebsd.sim.masterpattern import MasterPattern
|
|
45
|
+
|
|
46
|
+
data = master.data
|
|
47
|
+
n = data.shape[-1]
|
|
48
|
+
sigma_px = sigma * (n - 1) / np.sqrt(2.0 * np.pi)
|
|
49
|
+
radius = min(int(np.ceil(3.0 * sigma_px)), n - 1)
|
|
50
|
+
x = torch.arange(-radius, radius + 1, device=data.device, dtype=data.dtype)
|
|
51
|
+
kernel = torch.exp(-0.5 * (x / sigma_px) ** 2)
|
|
52
|
+
kernel = kernel / kernel.sum()
|
|
53
|
+
flat = data.reshape(-1, 1, n, n)
|
|
54
|
+
flat = torch.nn.functional.pad(flat, (radius, radius, 0, 0), mode="reflect")
|
|
55
|
+
flat = torch.nn.functional.conv2d(flat, kernel.view(1, 1, 1, -1))
|
|
56
|
+
flat = torch.nn.functional.pad(flat, (0, 0, radius, radius), mode="reflect")
|
|
57
|
+
flat = torch.nn.functional.conv2d(flat, kernel.view(1, 1, -1, 1))
|
|
58
|
+
return MasterPattern(flat.reshape(data.shape), master.energies_kev, dict(master.metadata))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def determine_pattern_center(
|
|
62
|
+
patterns,
|
|
63
|
+
master,
|
|
64
|
+
detector,
|
|
65
|
+
rotations,
|
|
66
|
+
pattern_center=None,
|
|
67
|
+
n_rounds: int = 5,
|
|
68
|
+
n_tiles: int = 16,
|
|
69
|
+
samples_per_tile: int = 8,
|
|
70
|
+
refine_orientations: bool = True,
|
|
71
|
+
n_pattern_center_steps: int = 10,
|
|
72
|
+
damping: float = 1e-3,
|
|
73
|
+
keep_best: bool = True,
|
|
74
|
+
device=None,
|
|
75
|
+
return_orientations: bool = False,
|
|
76
|
+
n_orientation_steps: int = 15,
|
|
77
|
+
blur=_DEFAULT_BLUR,
|
|
78
|
+
):
|
|
79
|
+
"""Calibrate one shared pattern center from many EBSD patterns.
|
|
80
|
+
|
|
81
|
+
Fits a single pattern center shared by the whole calibration set by alternating
|
|
82
|
+
an orientation-only refinement of every pattern at the current PC with a
|
|
83
|
+
Levenberg-Marquardt step on the 3-parameter PC whose normal equations are
|
|
84
|
+
accumulated over all patterns. That ensemble accumulation is what breaks the
|
|
85
|
+
single-pattern R--PC degeneracy. Calibrate once, then refine the full scan at the
|
|
86
|
+
fixed PC with ``refine_hr(refine_pattern_center=False, pattern_center=<calibrated
|
|
87
|
+
pattern_center>)``.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
patterns
|
|
92
|
+
Calibration patterns ``(N, H, W)``, or a single ``(H, W)`` image. A
|
|
93
|
+
well-distributed few dozen to few hundred are plenty to fit three
|
|
94
|
+
parameters. The raw sampled values are used.
|
|
95
|
+
master, detector
|
|
96
|
+
The continuous :class:`de_ebsd.sim.MasterPattern` and the
|
|
97
|
+
:class:`~de_ebsd.sim.detector.EBSDDetector` geometry. ``detector.pattern_center`` is
|
|
98
|
+
used only when ``pattern_center`` is ``None``.
|
|
99
|
+
rotations
|
|
100
|
+
Per-pattern seed orientations, e.g. from coarse indexing.
|
|
101
|
+
pattern_center
|
|
102
|
+
Starting shared pattern center ``(3,)``; ``None`` uses ``detector.pattern_center``.
|
|
103
|
+
n_rounds
|
|
104
|
+
Alternating (orientation, shared-PC) rounds.
|
|
105
|
+
n_tiles, samples_per_tile
|
|
106
|
+
Tiles per detector side and band-edge samples kept per tile; the defaults
|
|
107
|
+
give 2048 samples per pattern.
|
|
108
|
+
refine_orientations
|
|
109
|
+
Re-refine each orientation at the current PC every round (default), or hold
|
|
110
|
+
the input ``rotations`` fixed and fit only the PC.
|
|
111
|
+
n_pattern_center_steps
|
|
112
|
+
Levenberg-Marquardt steps on the shared PC per round.
|
|
113
|
+
damping
|
|
114
|
+
Initial LM damping, adapted each step (x0.5 accepted, x3 rejected) and
|
|
115
|
+
carried across rounds. Also seeds the inner orientation refinement.
|
|
116
|
+
keep_best
|
|
117
|
+
Never return a PC / orientation set scoring below the best seen, making the
|
|
118
|
+
calibration monotone in the total per-tile NCC.
|
|
119
|
+
device
|
|
120
|
+
Defaults to ``master.data.device``; otherwise everything is moved there.
|
|
121
|
+
return_orientations
|
|
122
|
+
Also return the refined orientations, before ``info``.
|
|
123
|
+
n_orientation_steps
|
|
124
|
+
LM steps for the inner orientation refinement each round.
|
|
125
|
+
blur
|
|
126
|
+
Gaussian blur of the master (radians on the sphere) per stage, coarse to
|
|
127
|
+
fine, spread evenly over the rounds. A sharp dynamical master only has a
|
|
128
|
+
slope within ~0.01 of the true PC; the blurred stages pull a further-off
|
|
129
|
+
seed into that basin. ``None`` or ``(0,)`` fits the sharp master only.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
pattern_center : torch.Tensor
|
|
134
|
+
The calibrated shared pattern center ``(3,)``.
|
|
135
|
+
rotations : orix.quaternion.Rotation
|
|
136
|
+
Only when ``return_orientations``: the orientations at the calibrated PC.
|
|
137
|
+
info : dict
|
|
138
|
+
``"ncc"`` and ``"ncc_seed"`` (mean per-tile NCC at the calibrated and seed
|
|
139
|
+
geometry), ``"pc_step"`` (L2 magnitude of the final round's PC move -- small
|
|
140
|
+
means converged), ``"pc_step_history"``, and ``"n_rounds"``.
|
|
141
|
+
"""
|
|
142
|
+
device = resolve_device(device, master.data.device)
|
|
143
|
+
dtype = master.data.dtype
|
|
144
|
+
if master.data.device != device:
|
|
145
|
+
master = master.to(device)
|
|
146
|
+
|
|
147
|
+
patt = torch.as_tensor(patterns, device=device, dtype=dtype)
|
|
148
|
+
if patt.ndim == 2:
|
|
149
|
+
patt = patt[None]
|
|
150
|
+
if patt.ndim != 3:
|
|
151
|
+
raise ValueError(f"patterns must be (H, W) or (N, H, W), got shape {tuple(patt.shape)}")
|
|
152
|
+
n_p = patt.shape[0]
|
|
153
|
+
|
|
154
|
+
q0 = torch.as_tensor(np.asarray(rotations.data), device=device, dtype=dtype).reshape(-1, 4)
|
|
155
|
+
if q0.shape[0] != n_p:
|
|
156
|
+
raise ValueError(f"rotations length {q0.shape[0]} != number of patterns {n_p}")
|
|
157
|
+
r_mat = quaternion_to_matrix(q0).detach() # (N, 3, 3)
|
|
158
|
+
|
|
159
|
+
if pattern_center is None:
|
|
160
|
+
pattern_center = detector.pattern_center
|
|
161
|
+
pattern_center = torch.as_tensor(pattern_center, device=device, dtype=dtype)
|
|
162
|
+
if tuple(pattern_center.shape) != (3,):
|
|
163
|
+
raise ValueError(
|
|
164
|
+
"pattern_center must be a shared 3-vector (3,), "
|
|
165
|
+
f"got shape {tuple(pattern_center.shape)}"
|
|
166
|
+
)
|
|
167
|
+
pattern_center = pattern_center.clone()
|
|
168
|
+
|
|
169
|
+
# fixed band-edge samples + per-tile normalization, computed once (patterns never move)
|
|
170
|
+
rows, cols, flat, tile_ids, counts = edge_importance_samples(patt, n_tiles, samples_per_tile)
|
|
171
|
+
e_raw = patt.reshape(n_p, -1).gather(1, flat.long()) # (N, K) raw experimental
|
|
172
|
+
n_total = int(counts.shape[0])
|
|
173
|
+
n_active = int((counts > 0).sum())
|
|
174
|
+
counts_ps = counts.index_select(0, tile_ids).to(dtype) # (K,)
|
|
175
|
+
_, _, e_hat = standardize_tiled(e_raw, tile_ids, counts_ps, n_total) # (N, K) fixed
|
|
176
|
+
|
|
177
|
+
def _ncc_per_pattern(r_cur: torch.Tensor, pc_vec: torch.Tensor, seen=master) -> torch.Tensor:
|
|
178
|
+
pc_bc = pc_vec[None].expand(n_p, 3)
|
|
179
|
+
dc = direction_cosines_from_pattern_center(detector, pc_bc, rows, cols)
|
|
180
|
+
m = seen.sample(dc @ r_cur.transpose(-1, -2)).to(device=device, dtype=dtype)
|
|
181
|
+
_, _, m_hat = standardize_tiled(m, tile_ids, counts_ps, n_total)
|
|
182
|
+
return (e_hat * m_hat).sum(-1) / n_active
|
|
183
|
+
|
|
184
|
+
def _total(r_cur: torch.Tensor, pc_vec: torch.Tensor) -> torch.Tensor:
|
|
185
|
+
return _ncc_per_pattern(r_cur, pc_vec).sum()
|
|
186
|
+
|
|
187
|
+
ncc_seed = float(_ncc_per_pattern(r_mat, pattern_center).mean())
|
|
188
|
+
|
|
189
|
+
best_pc = pattern_center.clone()
|
|
190
|
+
best_r = r_mat.clone()
|
|
191
|
+
best_total = _total(r_mat, pattern_center)
|
|
192
|
+
damping = float(damping)
|
|
193
|
+
pc_step_history: list[float] = []
|
|
194
|
+
blur = tuple(blur) if blur else (0.0,)
|
|
195
|
+
|
|
196
|
+
for round_index in range(n_rounds):
|
|
197
|
+
# spread the blur stages over the rounds, ending on the last (sharpest)
|
|
198
|
+
sigma = blur[round_index * len(blur) // n_rounds]
|
|
199
|
+
seen = _blurred(master, sigma) if sigma > 0 else master
|
|
200
|
+
|
|
201
|
+
def _seen_total(r_cur, pc_vec, seen=seen):
|
|
202
|
+
return _ncc_per_pattern(r_cur, pc_vec, seen).sum()
|
|
203
|
+
|
|
204
|
+
# 1. orientation block -- refine each pattern at the current shared PC (well-posed)
|
|
205
|
+
if refine_orientations:
|
|
206
|
+
pc_bc = pattern_center[None].expand(n_p, 3).contiguous()
|
|
207
|
+
r_mat, _, _ = levenberg_marquardt_refine(
|
|
208
|
+
seen,
|
|
209
|
+
detector,
|
|
210
|
+
rows,
|
|
211
|
+
cols,
|
|
212
|
+
e_raw,
|
|
213
|
+
tile_ids,
|
|
214
|
+
counts,
|
|
215
|
+
r_mat,
|
|
216
|
+
pc_bc,
|
|
217
|
+
False,
|
|
218
|
+
n_orientation_steps,
|
|
219
|
+
damping,
|
|
220
|
+
keep_best,
|
|
221
|
+
device,
|
|
222
|
+
dtype,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# 2. shared-PC block -- LM on the 3-vector, normal equations summed over all
|
|
226
|
+
# patterns; jointly with the orientations when those are refined, so the PC
|
|
227
|
+
# step already accounts for how each orientation would compensate it
|
|
228
|
+
pc_before = pattern_center.clone()
|
|
229
|
+
pattern_center, r_mat, damping = _shared_pc_lm(
|
|
230
|
+
seen,
|
|
231
|
+
detector,
|
|
232
|
+
rows,
|
|
233
|
+
cols,
|
|
234
|
+
r_mat,
|
|
235
|
+
pattern_center,
|
|
236
|
+
e_hat,
|
|
237
|
+
tile_ids,
|
|
238
|
+
counts_ps,
|
|
239
|
+
n_total,
|
|
240
|
+
damping,
|
|
241
|
+
n_pattern_center_steps,
|
|
242
|
+
_seen_total,
|
|
243
|
+
device,
|
|
244
|
+
dtype,
|
|
245
|
+
joint=refine_orientations,
|
|
246
|
+
)
|
|
247
|
+
pc_step_history.append(float((pattern_center - pc_before).norm()))
|
|
248
|
+
|
|
249
|
+
# judged on the sharp master, so a blurred round cannot claim "best"
|
|
250
|
+
total = _total(r_mat, pattern_center)
|
|
251
|
+
if bool(total > best_total):
|
|
252
|
+
best_total, best_pc, best_r = total, pattern_center.clone(), r_mat.clone()
|
|
253
|
+
|
|
254
|
+
if keep_best:
|
|
255
|
+
pattern_center, r_mat = best_pc, best_r
|
|
256
|
+
|
|
257
|
+
pattern_center = pattern_center.detach()
|
|
258
|
+
info = {
|
|
259
|
+
"ncc": float(_ncc_per_pattern(r_mat, pattern_center).mean()),
|
|
260
|
+
"ncc_seed": ncc_seed,
|
|
261
|
+
"pc_step": pc_step_history[-1] if pc_step_history else 0.0,
|
|
262
|
+
"pc_step_history": pc_step_history,
|
|
263
|
+
"n_rounds": n_rounds,
|
|
264
|
+
}
|
|
265
|
+
if return_orientations:
|
|
266
|
+
from orix.quaternion import Rotation
|
|
267
|
+
|
|
268
|
+
rotation = Rotation.from_matrix(r_mat.cpu().numpy())
|
|
269
|
+
return pattern_center, rotation, info
|
|
270
|
+
return pattern_center, info
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _shared_pc_lm(
|
|
274
|
+
master,
|
|
275
|
+
detector,
|
|
276
|
+
rows,
|
|
277
|
+
cols,
|
|
278
|
+
r_mat,
|
|
279
|
+
pattern_center,
|
|
280
|
+
e_hat,
|
|
281
|
+
tile_ids,
|
|
282
|
+
counts_ps,
|
|
283
|
+
n_total,
|
|
284
|
+
damping,
|
|
285
|
+
n_steps,
|
|
286
|
+
total_fn,
|
|
287
|
+
device,
|
|
288
|
+
dtype,
|
|
289
|
+
joint=False,
|
|
290
|
+
):
|
|
291
|
+
"""LM on the shared PC; with ``joint``, on the orientations alongside it.
|
|
292
|
+
|
|
293
|
+
The joint system is block-arrow: each pattern's 3x3 orientation block couples
|
|
294
|
+
only to the shared PC. Eliminating the orientation blocks (a Schur complement)
|
|
295
|
+
leaves a 3x3 system for the PC that already allows for every orientation's
|
|
296
|
+
compensating move -- so the PC is not dragged back along the R--PC valley the
|
|
297
|
+
way it is when the two blocks are refined in turn.
|
|
298
|
+
"""
|
|
299
|
+
n_p = r_mat.shape[0]
|
|
300
|
+
cur_total = total_fn(r_mat, pattern_center)
|
|
301
|
+
|
|
302
|
+
for _ in range(n_steps):
|
|
303
|
+
pc_bc = pattern_center[None].expand(n_p, 3).contiguous()
|
|
304
|
+
dc = direction_cosines_from_pattern_center(
|
|
305
|
+
detector, pc_bc, rows, cols
|
|
306
|
+
).detach() # (N, K, 3)
|
|
307
|
+
# one reverse-mode grid_sample pass -> per-point master gradient dM/dv
|
|
308
|
+
with torch.enable_grad():
|
|
309
|
+
v = (dc @ r_mat.transpose(-1, -2)).detach().requires_grad_(True)
|
|
310
|
+
m_raw = master.sample(v)
|
|
311
|
+
(g_m,) = torch.autograd.grad(m_raw.sum(), v)
|
|
312
|
+
m = m_raw.detach().to(device=device, dtype=dtype)
|
|
313
|
+
g_m = g_m.to(device=device, dtype=dtype)
|
|
314
|
+
|
|
315
|
+
q = torch.einsum("pka,pab->pkb", g_m, r_mat) # gM @ R -> (N, K, 3)
|
|
316
|
+
ddc_dpc = direction_cosine_derivative(detector, pc_bc, rows, cols) # (N, K, 3, 3)
|
|
317
|
+
dm_dpc = torch.einsum("pkb,pkbc->pkc", q, ddc_dpc) # raw dM/dpc, (N, K, 3)
|
|
318
|
+
if joint:
|
|
319
|
+
dm_dw = torch.linalg.cross(dc, q, dim=-1) # raw dM/dw, (N, K, 3)
|
|
320
|
+
g_raw = torch.cat([dm_dw, dm_dpc], dim=-1) # (N, K, 6)
|
|
321
|
+
else:
|
|
322
|
+
g_raw = dm_dpc
|
|
323
|
+
|
|
324
|
+
res, j_r = normalized_residual_jacobian(m, e_hat, g_raw, tile_ids, counts_ps, n_total)
|
|
325
|
+
with torch.no_grad():
|
|
326
|
+
jtr = torch.einsum("pk,pki->pi", res, j_r) # per pattern, (N, P)
|
|
327
|
+
jtj = torch.einsum("pki,pkj->pij", j_r, j_r) # per pattern, (N, P, P)
|
|
328
|
+
if joint:
|
|
329
|
+
a = jtj[:, :3, :3]
|
|
330
|
+
a = a + damping * torch.diag_embed(
|
|
331
|
+
torch.diagonal(a, dim1=-2, dim2=-1).clamp_min(1e-9)
|
|
332
|
+
)
|
|
333
|
+
b = jtj[:, :3, 3:] # (N, 3, 3) orientation-PC coupling
|
|
334
|
+
c = jtj[:, 3:, 3:].sum(0)
|
|
335
|
+
c = c + damping * torch.diag_embed(torch.diagonal(c).clamp_min(1e-9))
|
|
336
|
+
a_inv_b = torch.linalg.solve(a, b)
|
|
337
|
+
a_inv_g = torch.linalg.solve(a, jtr[:, :3, None])[..., 0]
|
|
338
|
+
schur = c - torch.einsum("pji,pjk->ik", b, a_inv_b)
|
|
339
|
+
rhs = jtr[:, 3:].sum(0) - torch.einsum("pji,pj->i", b, a_inv_g)
|
|
340
|
+
delta_pc = -torch.linalg.solve(schur, rhs) # (3,)
|
|
341
|
+
delta_w = -(a_inv_g + (a_inv_b @ delta_pc[:, None])[..., 0]) # (N, 3)
|
|
342
|
+
r_new = r_mat @ rotation_vector_to_matrix(delta_w)
|
|
343
|
+
else:
|
|
344
|
+
jtr, jtj = jtr.sum(0), jtj.sum(0) # summed over patterns
|
|
345
|
+
diag = torch.diagonal(jtj).clamp_min(1e-9)
|
|
346
|
+
a = jtj + damping * torch.diag_embed(diag) # Marquardt (scale-invariant) damping
|
|
347
|
+
delta_pc = -torch.linalg.solve(a, jtr) # (3,)
|
|
348
|
+
r_new = r_mat
|
|
349
|
+
pc_new = pattern_center + delta_pc
|
|
350
|
+
new_total = total_fn(r_new, pc_new)
|
|
351
|
+
accepted = bool(new_total > cur_total)
|
|
352
|
+
if accepted:
|
|
353
|
+
pattern_center, r_mat = pc_new, r_new
|
|
354
|
+
cur_total = new_total
|
|
355
|
+
damping = adapt_shared_damping(damping, accepted)
|
|
356
|
+
return pattern_center, r_mat, damping
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Spherical-harmonic and SO(3) machinery behind the spherical indexer."""
|
|
2
|
+
|
|
3
|
+
from de_ebsd.index.spherical.correlation import so3_cross_correlation
|
|
4
|
+
from de_ebsd.index.spherical.harmonics import (
|
|
5
|
+
SphericalHarmonicTransform,
|
|
6
|
+
forward_sht,
|
|
7
|
+
gauss_legendre_grid,
|
|
8
|
+
inverse_sht,
|
|
9
|
+
lm_indices,
|
|
10
|
+
n_coefficients,
|
|
11
|
+
spherical_harmonics,
|
|
12
|
+
)
|
|
13
|
+
from de_ebsd.index.spherical.indexer import SphericalIndexer
|
|
14
|
+
from de_ebsd.index.spherical.wigner import (
|
|
15
|
+
euler_zyz_to_orix,
|
|
16
|
+
grid_to_rotation,
|
|
17
|
+
n_orientations,
|
|
18
|
+
so3_grid,
|
|
19
|
+
wigner_D,
|
|
20
|
+
wigner_d,
|
|
21
|
+
wigner_small_d,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"SphericalHarmonicTransform",
|
|
26
|
+
"SphericalIndexer",
|
|
27
|
+
"euler_zyz_to_orix",
|
|
28
|
+
"forward_sht",
|
|
29
|
+
"gauss_legendre_grid",
|
|
30
|
+
"grid_to_rotation",
|
|
31
|
+
"inverse_sht",
|
|
32
|
+
"lm_indices",
|
|
33
|
+
"n_coefficients",
|
|
34
|
+
"n_orientations",
|
|
35
|
+
"so3_cross_correlation",
|
|
36
|
+
"so3_grid",
|
|
37
|
+
"spherical_harmonics",
|
|
38
|
+
"wigner_D",
|
|
39
|
+
"wigner_d",
|
|
40
|
+
"wigner_small_d",
|
|
41
|
+
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""SO(3) cross-correlation of two spherical-harmonic spectra.
|
|
2
|
+
|
|
3
|
+
Combines the two spectra with the Wigner-d matrices into a coefficient cube,
|
|
4
|
+
then one 3-D inverse FFT gives the correlation over all of SO(3) on the SOFT
|
|
5
|
+
grid.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
|
|
12
|
+
from de_ebsd.index.spherical.harmonics import lm_indices, n_coefficients
|
|
13
|
+
from de_ebsd.index.spherical.wigner import so3_grid, wigner_d
|
|
14
|
+
|
|
15
|
+
__all__ = ["so3_cross_correlation"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _pack_to_grid(coeffs: torch.Tensor, bandwidth: int) -> torch.Tensor:
|
|
19
|
+
L = int(bandwidth)
|
|
20
|
+
degrees, orders = lm_indices(L, device=coeffs.device)
|
|
21
|
+
pos = degrees * (2 * L + 1) + (orders + L) # flat index into (L+1)*(2L+1)
|
|
22
|
+
base = coeffs.new_zeros((*coeffs.shape[:-1], (L + 1) * (2 * L + 1)))
|
|
23
|
+
packed = base.index_add(-1, pos, coeffs)
|
|
24
|
+
return packed.reshape(*coeffs.shape[:-1], L + 1, 2 * L + 1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def so3_cross_correlation(
|
|
28
|
+
master_coeffs: torch.Tensor,
|
|
29
|
+
pattern_coeffs: torch.Tensor,
|
|
30
|
+
bandwidth: int,
|
|
31
|
+
wigner_d_cached: torch.Tensor | None = None,
|
|
32
|
+
) -> torch.Tensor:
|
|
33
|
+
"""Cross-correlation of two harmonic spectra over all of SO(3), via SOFT.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
master_coeffs : torch.Tensor
|
|
38
|
+
Master spectrum ``a_lm``, shape ``(..., (L+1)**2)`` (packed
|
|
39
|
+
``c = l*l + l + m``, the :mod:`.harmonics` convention). May carry leading
|
|
40
|
+
batch dims that broadcast against ``pattern_coeffs``.
|
|
41
|
+
pattern_coeffs : torch.Tensor
|
|
42
|
+
Pattern spectrum ``b_lm``, shape ``(..., (L+1)**2)``. The leading dims are
|
|
43
|
+
the batch of patterns (many indexed at once).
|
|
44
|
+
bandwidth : int
|
|
45
|
+
The band limit ``L``; both spectra must have ``(L+1)**2`` slots.
|
|
46
|
+
wigner_d_cached : torch.Tensor, optional
|
|
47
|
+
A precomputed Wigner-d block ``wigner_d(L, beta)`` for this bandwidth's SOFT
|
|
48
|
+
``beta`` grid, shape ``(N, L+1, 2L+1, 2L+1)`` with ``N = 2L + 2`` (see
|
|
49
|
+
:func:`de_ebsd.index.spherical.wigner.wigner_d`). This ``O(L^4)`` block is otherwise
|
|
50
|
+
rebuilt on every call; pass it to reuse one block across a mini-batched scan
|
|
51
|
+
(:class:`~de_ebsd.index.spherical.indexer.SphericalIndexer` caches it in
|
|
52
|
+
``__init__``). The result is byte-identical to omitting it, provided the
|
|
53
|
+
cached block matches this bandwidth's ``beta`` grid and the real dtype used
|
|
54
|
+
here (``float64`` for complex128 spectra, else ``float32``).
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
torch.Tensor
|
|
59
|
+
Real tensor ``C`` of shape ``(*batch, N, N, N)`` with ``N = 2L + 2``, sampled
|
|
60
|
+
on the :func:`.so3_grid` ``(alpha, beta, gamma)`` grid (axes in that order),
|
|
61
|
+
where ``batch`` is the broadcast of the two inputs' leading dims. ``argmax``
|
|
62
|
+
gives the best-matching orientation index; feed it to
|
|
63
|
+
:func:`.grid_to_rotation`. Differentiable in both spectra.
|
|
64
|
+
"""
|
|
65
|
+
L = int(bandwidth)
|
|
66
|
+
n_coeff = n_coefficients(L)
|
|
67
|
+
if master_coeffs.shape[-1] != n_coeff or pattern_coeffs.shape[-1] != n_coeff:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"expected {n_coeff} coefficients for bandwidth {L}, got "
|
|
70
|
+
f"{master_coeffs.shape[-1]} (master) and {pattern_coeffs.shape[-1]} (pattern)"
|
|
71
|
+
)
|
|
72
|
+
a = master_coeffs
|
|
73
|
+
b = pattern_coeffs
|
|
74
|
+
cdtype = torch.result_type(a, b)
|
|
75
|
+
if not cdtype.is_complex:
|
|
76
|
+
cdtype = torch.complex128 if cdtype == torch.float64 else torch.complex64
|
|
77
|
+
a = a.to(cdtype)
|
|
78
|
+
b = b.to(cdtype)
|
|
79
|
+
|
|
80
|
+
a_grid = _pack_to_grid(a, L) # (..., L+1, 2L+1) over m'
|
|
81
|
+
b_grid = _pack_to_grid(b, L) # (..., L+1, 2L+1) over m
|
|
82
|
+
# cross-spectrum f_{l, m, m'} = b_lm conj(a_lm')
|
|
83
|
+
f = b_grid[..., :, :, None] * a_grid.conj()[..., :, None, :] # (..., L+1, 2L+1, 2L+1)
|
|
84
|
+
|
|
85
|
+
n = 2 * L + 2
|
|
86
|
+
real_dtype = torch.float64 if cdtype == torch.complex128 else torch.float32
|
|
87
|
+
alpha, beta, gamma = so3_grid(L, device=a.device, dtype=real_dtype)
|
|
88
|
+
if wigner_d_cached is not None:
|
|
89
|
+
d = wigner_d_cached.to(cdtype) # reuse the precomputed O(L^4) block
|
|
90
|
+
else:
|
|
91
|
+
d = wigner_d(L, beta).to(cdtype) # (N, L+1, 2L+1, 2L+1)
|
|
92
|
+
|
|
93
|
+
# Wigner-d contraction over beta: S_{k, m, m'} = sum_l f_{l, m, m'} d^l_{m m'}(beta_k)
|
|
94
|
+
s = torch.einsum("...lmn,klmn->...kmn", f, d) # (..., N, 2L+1, 2L+1)
|
|
95
|
+
|
|
96
|
+
# inverse FFT over alpha (m) and gamma (m'): pad the (2L+1) order axes to N,
|
|
97
|
+
# then C = N^2 ifft2(s) with the linear phase that shifts m,m' back to -L..L
|
|
98
|
+
s = torch.nn.functional.pad(s, (0, 1, 0, 1)) # (..., N, N, N)
|
|
99
|
+
c = (n * n) * torch.fft.ifft2(s, dim=(-2, -1)) # axis -2 -> alpha, -1 -> gamma
|
|
100
|
+
phase_a = torch.exp(-1j * L * alpha).to(cdtype).reshape(n, 1)
|
|
101
|
+
phase_g = torch.exp(-1j * L * gamma).to(cdtype).reshape(1, n)
|
|
102
|
+
c = c * phase_a * phase_g # (..., N=beta, N=alpha, N=gamma)
|
|
103
|
+
|
|
104
|
+
# reorder (beta, alpha, gamma) -> (alpha, beta, gamma) and take the real part
|
|
105
|
+
c = c.transpose(-3, -2)
|
|
106
|
+
return c.real.contiguous()
|