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,479 @@
|
|
|
1
|
+
"""High-resolution refinement on band-edge samples.
|
|
2
|
+
|
|
3
|
+
Instead of a whole back-projected direction set, ``refine_hr`` importance-samples
|
|
4
|
+
a few thousand high-gradient band-edge pixels -- the ones that actually carry the
|
|
5
|
+
orientation and pattern-center signal -- and runs Levenberg-Marquardt on those.
|
|
6
|
+
Refines orientation and pattern center jointly.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import torch
|
|
13
|
+
import torch.nn.functional as F
|
|
14
|
+
|
|
15
|
+
from de_ebsd.core.constants import DIVISION_FLOOR
|
|
16
|
+
from de_ebsd.core.device import resolve_device
|
|
17
|
+
from de_ebsd.core.geometry import detector_rays, sample_to_detector_tensor
|
|
18
|
+
from de_ebsd.core.rotation import quaternion_to_matrix, rotation_vector_to_matrix
|
|
19
|
+
from de_ebsd.index.refine.levenberg import adapt_damping
|
|
20
|
+
|
|
21
|
+
__all__ = ["direction_cosines_from_pattern_center", "edge_importance_samples", "refine_hr"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def direction_cosines_from_pattern_center(
|
|
25
|
+
detector, pattern_center: torch.Tensor, rows: torch.Tensor, cols: torch.Tensor
|
|
26
|
+
) -> torch.Tensor:
|
|
27
|
+
"""Sample-frame direction cosines at pixels ``(row, col)``, differentiable in
|
|
28
|
+
``pattern_center``.
|
|
29
|
+
|
|
30
|
+
A ``grid_sample``-free rewrite of
|
|
31
|
+
:meth:`~de_ebsd.sim.detector.EBSDDetector.direction_cosines` over an arbitrary
|
|
32
|
+
pixel set, as pure torch arithmetic -- so it works under forward-mode AD, unlike
|
|
33
|
+
the ``grid_sample`` back-projection. The detector-frame ray is
|
|
34
|
+
|
|
35
|
+
``x_d = aspect * ((col + 0.5) / ncols - pcx)``,
|
|
36
|
+
``y_d = pcy - (row + 0.5) / nrows``,
|
|
37
|
+
``z_d = pcz``,
|
|
38
|
+
|
|
39
|
+
rotated into the sample frame and normalized. Reproduces
|
|
40
|
+
``EBSDDetector.direction_cosines`` to ~1e-16.
|
|
41
|
+
|
|
42
|
+
``pattern_center`` is ``(..., 3)``, overriding ``detector.pattern_center``, and broadcasts
|
|
43
|
+
against the
|
|
44
|
+
leading dims of the ``(..., K)`` fractional ``rows``/``cols``. Returns unit
|
|
45
|
+
direction cosines ``(..., K, 3)``.
|
|
46
|
+
"""
|
|
47
|
+
om = sample_to_detector_tensor(detector, rows.device, rows.dtype)
|
|
48
|
+
ray = detector_rays(detector, pattern_center, rows, cols) # (..., K, 3)
|
|
49
|
+
dc = ray @ om # sample-frame ray (rows: v @ om, matching direction_cosines)
|
|
50
|
+
return dc / dc.norm(dim=-1, keepdim=True).clamp_min(DIVISION_FLOOR)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def direction_cosine_derivative(
|
|
54
|
+
detector, pattern_center: torch.Tensor, rows: torch.Tensor, cols: torch.Tensor
|
|
55
|
+
) -> torch.Tensor:
|
|
56
|
+
p = pattern_center.detach()
|
|
57
|
+
|
|
58
|
+
def f(pc_arg: torch.Tensor) -> torch.Tensor:
|
|
59
|
+
return direction_cosines_from_pattern_center(detector, pc_arg, rows, cols)
|
|
60
|
+
|
|
61
|
+
cols_ddc = []
|
|
62
|
+
for c in range(3):
|
|
63
|
+
tangent = torch.zeros_like(p)
|
|
64
|
+
tangent[..., c] = 1.0
|
|
65
|
+
_, dd = torch.func.jvp(f, (p,), (tangent,))
|
|
66
|
+
cols_ddc.append(dd)
|
|
67
|
+
return torch.stack(cols_ddc, dim=-1) # (..., K, 3, 3)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _sobel_gradient_magnitude(patterns: torch.Tensor) -> torch.Tensor:
|
|
71
|
+
kx = patterns.new_tensor([[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]])
|
|
72
|
+
ky = kx.t().contiguous()
|
|
73
|
+
p = F.pad(patterns[:, None], (1, 1, 1, 1), mode="replicate")
|
|
74
|
+
gx = F.conv2d(p, kx[None, None])
|
|
75
|
+
gy = F.conv2d(p, ky[None, None])
|
|
76
|
+
return torch.sqrt(gx * gx + gy * gy)[:, 0]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def edge_importance_samples(
|
|
80
|
+
patterns: torch.Tensor,
|
|
81
|
+
n_tiles: int = 16,
|
|
82
|
+
samples_per_tile: int = 8,
|
|
83
|
+
all_pixels: bool = False,
|
|
84
|
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
85
|
+
"""Pick the band-edge sample pixels for each pattern (done once, up front).
|
|
86
|
+
|
|
87
|
+
Partitions the detector into ``n_tiles x n_tiles`` tiles and keeps the
|
|
88
|
+
``samples_per_tile`` highest-Sobel-gradient pixels in each. Spreading the pick
|
|
89
|
+
across tiles keeps every region represented, and the tile grouping drives the
|
|
90
|
+
per-tile normalization of the objective. ``all_pixels`` ignores the gradient
|
|
91
|
+
pick and uses every pixel (the slow reference path).
|
|
92
|
+
|
|
93
|
+
Returns ``(rows, cols, flat_idx, tile_ids, counts)``: fractional coordinates
|
|
94
|
+
``(Np, K)``, flat indices ``(Np, K)``, the tile index ``(K,)`` of each sample
|
|
95
|
+
slot (shared across patterns), and the slot count per tile. A tile with fewer
|
|
96
|
+
pixels than ``samples_per_tile`` is padded by repeating its best pixel, so ``K``
|
|
97
|
+
stays uniform.
|
|
98
|
+
"""
|
|
99
|
+
np_, height, width = patterns.shape
|
|
100
|
+
device, dtype = patterns.device, patterns.dtype
|
|
101
|
+
n_total = n_tiles * n_tiles
|
|
102
|
+
|
|
103
|
+
row_tile = (torch.arange(height, device=device) * n_tiles // height).clamp_max(n_tiles - 1)
|
|
104
|
+
col_tile = (torch.arange(width, device=device) * n_tiles // width).clamp_max(n_tiles - 1)
|
|
105
|
+
tile_map = (row_tile[:, None] * n_tiles + col_tile[None, :]).reshape(-1) # (H*W,)
|
|
106
|
+
|
|
107
|
+
if all_pixels:
|
|
108
|
+
flat = torch.arange(height * width, device=device)
|
|
109
|
+
tile_ids = tile_map
|
|
110
|
+
counts = torch.bincount(tile_ids, minlength=n_total)
|
|
111
|
+
flat = flat[None].expand(np_, -1).contiguous()
|
|
112
|
+
else:
|
|
113
|
+
grad = _sobel_gradient_magnitude(patterns).reshape(np_, height * width)
|
|
114
|
+
th, tw = height // n_tiles, width // n_tiles
|
|
115
|
+
# regular blocks: one topk over (Np, n_total, tile_pixels) instead of a
|
|
116
|
+
# per-tile Python loop, whose nonzero/topk dominate the whole refine
|
|
117
|
+
if height % n_tiles == 0 and width % n_tiles == 0 and th * tw >= samples_per_tile:
|
|
118
|
+
gt = (
|
|
119
|
+
grad.view(np_, n_tiles, th, n_tiles, tw)
|
|
120
|
+
.permute(0, 1, 3, 2, 4)
|
|
121
|
+
.reshape(np_, n_total, th * tw)
|
|
122
|
+
)
|
|
123
|
+
top = gt.topk(samples_per_tile, dim=2).indices # (Np, n_total, spt), within-tile
|
|
124
|
+
# within-tile slot (r * tw + c) -> global flat pixel ((a*th+r)*W + b*tw+c)
|
|
125
|
+
a = torch.arange(n_tiles, device=device)[:, None, None, None]
|
|
126
|
+
b = torch.arange(n_tiles, device=device)[None, :, None, None]
|
|
127
|
+
r = torch.arange(th, device=device)[None, None, :, None]
|
|
128
|
+
c = torch.arange(tw, device=device)[None, None, None, :]
|
|
129
|
+
tile_to_flat = ((a * th + r) * width + (b * tw + c)).reshape(n_total, th * tw)
|
|
130
|
+
flat = torch.gather(tile_to_flat[None].expand(np_, -1, -1), 2, top)
|
|
131
|
+
flat = flat.reshape(np_, n_total * samples_per_tile) # (Np, K)
|
|
132
|
+
tile_ids = torch.arange(n_total, device=device).repeat_interleave(samples_per_tile)
|
|
133
|
+
counts = torch.bincount(tile_ids, minlength=n_total)
|
|
134
|
+
else: # irregular tiles: per-tile fallback (correct, rarely hit)
|
|
135
|
+
flat_cols, tid_cols = [], []
|
|
136
|
+
for t in range(n_total):
|
|
137
|
+
pix = torch.nonzero(tile_map == t, as_tuple=False).reshape(-1)
|
|
138
|
+
if pix.numel() == 0:
|
|
139
|
+
continue
|
|
140
|
+
k = min(samples_per_tile, pix.numel())
|
|
141
|
+
top = grad[:, pix].topk(k, dim=1).indices # (Np, k) into pix
|
|
142
|
+
sel = pix[top] # (Np, k) flat pixel indices
|
|
143
|
+
if k < samples_per_tile: # pad short tiles by repeating their best pixel
|
|
144
|
+
sel = torch.cat([sel, sel[:, :1].expand(np_, samples_per_tile - k)], dim=1)
|
|
145
|
+
flat_cols.append(sel)
|
|
146
|
+
tid_cols.append(torch.full((samples_per_tile,), t, device=device, dtype=torch.long))
|
|
147
|
+
flat = torch.cat(flat_cols, dim=1) # (Np, K)
|
|
148
|
+
tile_ids = torch.cat(tid_cols) # (K,)
|
|
149
|
+
counts = torch.bincount(tile_ids, minlength=n_total)
|
|
150
|
+
|
|
151
|
+
rows = (flat // width).to(dtype)
|
|
152
|
+
cols = (flat % width).to(dtype)
|
|
153
|
+
return rows, cols, flat, tile_ids, counts
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _seg_broadcast_sum(x: torch.Tensor, tile_ids: torch.Tensor, n_total: int) -> torch.Tensor:
|
|
157
|
+
sums = x.new_zeros((x.shape[0], n_total, *x.shape[2:]))
|
|
158
|
+
sums.index_add_(1, tile_ids, x)
|
|
159
|
+
return sums.index_select(1, tile_ids)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def standardize_tiled(
|
|
163
|
+
x: torch.Tensor, tile_ids: torch.Tensor, counts_ps: torch.Tensor, n_total: int
|
|
164
|
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
165
|
+
mean = _seg_broadcast_sum(x, tile_ids, n_total) / counts_ps
|
|
166
|
+
c = x - mean
|
|
167
|
+
s = _seg_broadcast_sum(c * c, tile_ids, n_total).sqrt().clamp_min(DIVISION_FLOOR)
|
|
168
|
+
return c, s, c / s
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def normalized_residual_jacobian(
|
|
172
|
+
m: torch.Tensor,
|
|
173
|
+
e_hat: torch.Tensor,
|
|
174
|
+
g: torch.Tensor,
|
|
175
|
+
tile_ids: torch.Tensor,
|
|
176
|
+
counts_ps: torch.Tensor,
|
|
177
|
+
n_total: int,
|
|
178
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
179
|
+
c, s, m_hat = standardize_tiled(m, tile_ids, counts_ps, n_total)
|
|
180
|
+
g_mean = _seg_broadcast_sum(g, tile_ids, n_total) / counts_ps.view(1, -1, 1)
|
|
181
|
+
nu = _seg_broadcast_sum(c.unsqueeze(-1) * g, tile_ids, n_total) / s.unsqueeze(-1)
|
|
182
|
+
dm_hat = ((g - g_mean) - m_hat.unsqueeze(-1) * nu) / s.unsqueeze(-1)
|
|
183
|
+
return e_hat - m_hat, -dm_hat
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def levenberg_marquardt_refine(
|
|
187
|
+
master,
|
|
188
|
+
detector,
|
|
189
|
+
rows,
|
|
190
|
+
cols,
|
|
191
|
+
e_raw,
|
|
192
|
+
tile_ids,
|
|
193
|
+
counts,
|
|
194
|
+
r0_mat,
|
|
195
|
+
initial_pattern_center,
|
|
196
|
+
refine_pattern_center,
|
|
197
|
+
n_steps,
|
|
198
|
+
damping,
|
|
199
|
+
keep_best,
|
|
200
|
+
device,
|
|
201
|
+
dtype,
|
|
202
|
+
):
|
|
203
|
+
n_total = int(counts.shape[0])
|
|
204
|
+
n_active = int((counts > 0).sum())
|
|
205
|
+
counts_ps = counts.index_select(0, tile_ids).to(dtype) # (K,)
|
|
206
|
+
_, _, e_hat = standardize_tiled(e_raw, tile_ids, counts_ps, n_total)
|
|
207
|
+
|
|
208
|
+
n_p = r0_mat.shape[0]
|
|
209
|
+
r_mat = r0_mat.clone()
|
|
210
|
+
pattern_center = initial_pattern_center.clone()
|
|
211
|
+
damping = torch.full((n_p,), float(damping), device=device, dtype=dtype)
|
|
212
|
+
|
|
213
|
+
# orientation-only: the pattern center never moves, so build its direction
|
|
214
|
+
# cosines once instead of rebuilding them every step
|
|
215
|
+
dc_fixed = (
|
|
216
|
+
None
|
|
217
|
+
if refine_pattern_center
|
|
218
|
+
else direction_cosines_from_pattern_center(
|
|
219
|
+
detector, initial_pattern_center, rows, cols
|
|
220
|
+
).detach()
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def _ncc(r_cur, pc_cur):
|
|
224
|
+
dc = (
|
|
225
|
+
dc_fixed
|
|
226
|
+
if dc_fixed is not None
|
|
227
|
+
else direction_cosines_from_pattern_center(detector, pc_cur, rows, cols)
|
|
228
|
+
)
|
|
229
|
+
m = master.sample(dc @ r_cur.transpose(-1, -2)).to(device=device, dtype=dtype)
|
|
230
|
+
_, _, m_hat = standardize_tiled(m, tile_ids, counts_ps, n_total)
|
|
231
|
+
return (e_hat * m_hat).sum(-1) / n_active
|
|
232
|
+
|
|
233
|
+
seed_score = _ncc(r0_mat, initial_pattern_center).detach()
|
|
234
|
+
cur_score = seed_score.clone()
|
|
235
|
+
|
|
236
|
+
for _ in range(n_steps):
|
|
237
|
+
# (Np, K, 3); constant when the pattern center is fixed, so reuse the precompute
|
|
238
|
+
dc = (
|
|
239
|
+
dc_fixed
|
|
240
|
+
if dc_fixed is not None
|
|
241
|
+
else direction_cosines_from_pattern_center(
|
|
242
|
+
detector, pattern_center, rows, cols
|
|
243
|
+
).detach()
|
|
244
|
+
)
|
|
245
|
+
# one reverse-mode grid_sample pass -> per-point master gradient dM/dv
|
|
246
|
+
with torch.enable_grad():
|
|
247
|
+
v = (dc @ r_mat.transpose(-1, -2)).detach().requires_grad_(True)
|
|
248
|
+
m_raw = master.sample(v)
|
|
249
|
+
(g_m,) = torch.autograd.grad(m_raw.sum(), v)
|
|
250
|
+
m = m_raw.detach().to(device=device, dtype=dtype)
|
|
251
|
+
g_m = g_m.to(device=device, dtype=dtype)
|
|
252
|
+
|
|
253
|
+
q = torch.einsum("pka,pab->pkb", g_m, r_mat) # gM @ R -> (Np, K, 3)
|
|
254
|
+
dm_dw = torch.linalg.cross(dc, q, dim=-1) # dc x (gM @ R), (Np, K, 3)
|
|
255
|
+
if refine_pattern_center:
|
|
256
|
+
ddc_dpc = direction_cosine_derivative(
|
|
257
|
+
detector, pattern_center, rows, cols
|
|
258
|
+
) # (Np, K, 3, 3)
|
|
259
|
+
dm_dpc = torch.einsum("pkb,pkbc->pkc", q, ddc_dpc) # (Np, K, 3)
|
|
260
|
+
g_raw = torch.cat([dm_dw, dm_dpc], dim=-1) # (Np, K, 6)
|
|
261
|
+
else:
|
|
262
|
+
g_raw = dm_dw
|
|
263
|
+
|
|
264
|
+
res, j_r = normalized_residual_jacobian(m, e_hat, g_raw, tile_ids, counts_ps, n_total)
|
|
265
|
+
with torch.no_grad():
|
|
266
|
+
jtr = torch.einsum("pk,pki->pi", res, j_r) # (Np, P)
|
|
267
|
+
jtj = torch.einsum("pki,pkj->pij", j_r, j_r) # (Np, P, P)
|
|
268
|
+
diag = torch.diagonal(jtj, dim1=-2, dim2=-1).clamp_min(1e-9)
|
|
269
|
+
a = jtj + damping[:, None, None] * torch.diag_embed(diag)
|
|
270
|
+
delta = -torch.linalg.solve(a, jtr[..., None])[..., 0] # (Np, P)
|
|
271
|
+
r_new = r_mat @ rotation_vector_to_matrix(delta[:, :3])
|
|
272
|
+
pc_new = pattern_center + delta[:, 3:6] if refine_pattern_center else pattern_center
|
|
273
|
+
new_score = _ncc(r_new, pc_new)
|
|
274
|
+
# per-pattern accept/reject: robust to the bilinear grid_sample kinks,
|
|
275
|
+
# and monotone by construction
|
|
276
|
+
better = new_score > cur_score
|
|
277
|
+
r_mat = torch.where(better[:, None, None], r_new, r_mat)
|
|
278
|
+
pattern_center = torch.where(better[:, None], pc_new, pattern_center)
|
|
279
|
+
damping = adapt_damping(damping, better)
|
|
280
|
+
cur_score = torch.where(better, new_score, cur_score)
|
|
281
|
+
|
|
282
|
+
if keep_best:
|
|
283
|
+
keep = (cur_score >= seed_score).reshape(-1)
|
|
284
|
+
r_mat = torch.where(keep[:, None, None], r_mat, r0_mat)
|
|
285
|
+
pattern_center = torch.where(keep[:, None], pattern_center, initial_pattern_center)
|
|
286
|
+
cur_score = torch.maximum(cur_score, seed_score)
|
|
287
|
+
return r_mat, pattern_center, cur_score
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def refine_hr(
|
|
291
|
+
patterns,
|
|
292
|
+
master,
|
|
293
|
+
detector,
|
|
294
|
+
rotations,
|
|
295
|
+
pattern_center=None,
|
|
296
|
+
refine_pattern_center=False,
|
|
297
|
+
n_tiles=16,
|
|
298
|
+
samples_per_tile=8,
|
|
299
|
+
n_steps=15,
|
|
300
|
+
damping=1e-3,
|
|
301
|
+
keep_best=True,
|
|
302
|
+
device=None,
|
|
303
|
+
all_pixels=False,
|
|
304
|
+
batch_size=None,
|
|
305
|
+
sample_by="master",
|
|
306
|
+
):
|
|
307
|
+
"""High-precision joint orientation + pattern-center refinement.
|
|
308
|
+
|
|
309
|
+
The sub-0.1-degree-capable path. Rather than touching every detector pixel, it
|
|
310
|
+
importance-samples a few thousand band-edge points spread across a tile grid and
|
|
311
|
+
runs batched Levenberg-Marquardt on ``(R, pattern_center)`` against the continuous master,
|
|
312
|
+
over an objective normalized per tile (robust to intensity ramps). The sample
|
|
313
|
+
budget (``n_tiles``, ``samples_per_tile``) is the accuracy/speed knob.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
patterns
|
|
318
|
+
A detector image ``(H, W)`` or stack ``(Np, H, W)``. The raw sampled values
|
|
319
|
+
are used -- the experimental signal is never back-projected or interpolated.
|
|
320
|
+
master, detector
|
|
321
|
+
The continuous :class:`de_ebsd.sim.MasterPattern` and the
|
|
322
|
+
:class:`~de_ebsd.sim.detector.EBSDDetector` geometry.
|
|
323
|
+
rotations
|
|
324
|
+
Seed :class:`orix.quaternion.Rotation`, typically a coarse index result.
|
|
325
|
+
pattern_center
|
|
326
|
+
``None`` uses ``detector.pattern_center``. A ``(3,)`` value shares one center across the
|
|
327
|
+
stack; ``(Np, 3)`` gives a per-pattern center. Returned in the same shape.
|
|
328
|
+
refine_pattern_center
|
|
329
|
+
Refine the pattern center jointly (6 DOF) and return it, or (default) leave
|
|
330
|
+
it untouched and refine orientation only (3 DOF).
|
|
331
|
+
n_tiles, samples_per_tile
|
|
332
|
+
Tiles per detector side and samples kept per tile; the defaults give
|
|
333
|
+
``256 * 8 = 2048`` samples. Raising them improves accuracy proportionally.
|
|
334
|
+
n_steps
|
|
335
|
+
Levenberg-Marquardt steps (default 15); second-order, so a coarse seed
|
|
336
|
+
sharpens well inside this budget.
|
|
337
|
+
damping
|
|
338
|
+
Initial damping, adapted per pattern each step (x0.5 accepted, x3 rejected).
|
|
339
|
+
keep_best
|
|
340
|
+
Never return a ``(R, pattern_center)`` scoring below the seed, making refinement
|
|
341
|
+
monotone.
|
|
342
|
+
device
|
|
343
|
+
Defaults to ``master.data.device``; otherwise everything is moved there.
|
|
344
|
+
all_pixels
|
|
345
|
+
Use every detector pixel instead of the band-edge subsample (slow reference
|
|
346
|
+
path for validation).
|
|
347
|
+
batch_size
|
|
348
|
+
Patterns per mini-batch. ``None`` picks a memory-aware size; the result is
|
|
349
|
+
identical for any batch size.
|
|
350
|
+
sample_by
|
|
351
|
+
Which gradient picks the sample pixels. ``"master"`` (default) renders the
|
|
352
|
+
simulated pattern at the seed and samples its noise-free gradient, so
|
|
353
|
+
samples land on true band edges. ``"experimental"`` samples the raw pattern,
|
|
354
|
+
which on real data is noise-dominated and adds substantial refinement
|
|
355
|
+
scatter; it is retained for noise-free/synthetic use.
|
|
356
|
+
|
|
357
|
+
Returns
|
|
358
|
+
-------
|
|
359
|
+
rotation : orix.quaternion.Rotation
|
|
360
|
+
The refined orientation(s), in the convention ``MasterPattern.project``
|
|
361
|
+
consumes.
|
|
362
|
+
score : torch.Tensor
|
|
363
|
+
Mean per-tile NCC per pattern, in ``[-1, 1]``; ``>=`` the seed when
|
|
364
|
+
``keep_best``.
|
|
365
|
+
pattern_center : torch.Tensor
|
|
366
|
+
Only when ``refine_pattern_center``: the refined pattern center, shaped like the input.
|
|
367
|
+
"""
|
|
368
|
+
device = resolve_device(device, master.data.device)
|
|
369
|
+
dtype = master.data.dtype
|
|
370
|
+
if master.data.device != device:
|
|
371
|
+
master = master.to(device)
|
|
372
|
+
|
|
373
|
+
patt = torch.as_tensor(patterns, device=device, dtype=dtype)
|
|
374
|
+
single = patt.ndim == 2
|
|
375
|
+
if single:
|
|
376
|
+
patt = patt[None]
|
|
377
|
+
if patt.ndim != 3:
|
|
378
|
+
raise ValueError(f"patterns must be (H, W) or (Np, H, W), got shape {tuple(patt.shape)}")
|
|
379
|
+
n_p = patt.shape[0]
|
|
380
|
+
|
|
381
|
+
q0 = torch.as_tensor(np.asarray(rotations.data), device=device, dtype=dtype).reshape(-1, 4)
|
|
382
|
+
if q0.shape[0] != n_p:
|
|
383
|
+
raise ValueError(f"rotations length {q0.shape[0]} != number of patterns {n_p}")
|
|
384
|
+
r0_mat = quaternion_to_matrix(q0).detach() # (Np, 3, 3)
|
|
385
|
+
|
|
386
|
+
if pattern_center is None:
|
|
387
|
+
pc_init = torch.as_tensor(detector.pattern_center, device=device, dtype=dtype)
|
|
388
|
+
else:
|
|
389
|
+
pc_init = torch.as_tensor(pattern_center, device=device, dtype=dtype)
|
|
390
|
+
if pc_init.ndim == 1 and pc_init.shape[0] == 3:
|
|
391
|
+
pc_shared = True
|
|
392
|
+
initial_pattern_center = pc_init[None].expand(n_p, 3).contiguous()
|
|
393
|
+
elif pc_init.ndim == 2 and pc_init.shape == (n_p, 3):
|
|
394
|
+
pc_shared = False
|
|
395
|
+
initial_pattern_center = pc_init.contiguous()
|
|
396
|
+
else:
|
|
397
|
+
raise ValueError(
|
|
398
|
+
f"pattern_center must be (3,) shared or (Np, 3) per-pattern (Np={n_p}), got "
|
|
399
|
+
f"{tuple(pc_init.shape)}"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# size the mini-batch from the dominant per-step working set -- a (bs, K, 3, 3)
|
|
403
|
+
# d dc/d pattern_center block with pattern-center refinement, (bs, K, 3) without -- to ~256 MB
|
|
404
|
+
k = int(n_tiles * n_tiles * samples_per_tile) if not all_pixels else patt[0].numel()
|
|
405
|
+
if batch_size is not None:
|
|
406
|
+
bs = int(batch_size)
|
|
407
|
+
else:
|
|
408
|
+
per = k * (
|
|
409
|
+
36 if refine_pattern_center else 12
|
|
410
|
+
) # bytes/pattern of the dominant intermediate
|
|
411
|
+
if sample_by == "master":
|
|
412
|
+
# the seed render also holds a (bs, H*W, 3) direction-cosine block
|
|
413
|
+
per = max(per, int(detector.nrows) * int(detector.ncols) * 12)
|
|
414
|
+
bs = int(max(1, min(n_p, 256_000_000 // max(per, 1))))
|
|
415
|
+
|
|
416
|
+
# full pixel grid, for rendering the seed pattern that picks the sample pixels
|
|
417
|
+
if sample_by == "master":
|
|
418
|
+
hh, ww = int(detector.nrows), int(detector.ncols)
|
|
419
|
+
rr, cc = torch.meshgrid(
|
|
420
|
+
torch.arange(hh, device=device, dtype=dtype),
|
|
421
|
+
torch.arange(ww, device=device, dtype=dtype),
|
|
422
|
+
indexing="ij",
|
|
423
|
+
)
|
|
424
|
+
full_rows, full_cols = rr.reshape(-1), cc.reshape(-1)
|
|
425
|
+
elif sample_by != "experimental":
|
|
426
|
+
raise ValueError(f"sample_by must be 'master' or 'experimental', got {sample_by!r}")
|
|
427
|
+
|
|
428
|
+
r_parts, pc_parts, score_parts = [], [], []
|
|
429
|
+
for i in range(0, n_p, bs):
|
|
430
|
+
sl = slice(i, i + bs)
|
|
431
|
+
p_chunk = patt[sl]
|
|
432
|
+
if sample_by == "master":
|
|
433
|
+
# the master is noise-free, so its gradient marks true band edges; the
|
|
434
|
+
# experimental gradient adds ~0.5 deg of scatter on real patterns
|
|
435
|
+
with torch.no_grad():
|
|
436
|
+
dc_full = direction_cosines_from_pattern_center(
|
|
437
|
+
detector, initial_pattern_center[sl], full_rows, full_cols
|
|
438
|
+
)
|
|
439
|
+
sim = master.sample(dc_full @ r0_mat[sl].transpose(-1, -2))
|
|
440
|
+
sim = sim.to(device=device, dtype=dtype).reshape(-1, hh, ww)
|
|
441
|
+
rows, cols, flat, tile_ids, counts = edge_importance_samples(
|
|
442
|
+
sim, n_tiles, samples_per_tile, all_pixels
|
|
443
|
+
)
|
|
444
|
+
else:
|
|
445
|
+
rows, cols, flat, tile_ids, counts = edge_importance_samples(
|
|
446
|
+
p_chunk, n_tiles, samples_per_tile, all_pixels
|
|
447
|
+
)
|
|
448
|
+
e_raw = p_chunk.reshape(p_chunk.shape[0], -1).gather(1, flat.long())
|
|
449
|
+
rf, pf, sc = levenberg_marquardt_refine(
|
|
450
|
+
master,
|
|
451
|
+
detector,
|
|
452
|
+
rows,
|
|
453
|
+
cols,
|
|
454
|
+
e_raw,
|
|
455
|
+
tile_ids,
|
|
456
|
+
counts,
|
|
457
|
+
r0_mat[sl],
|
|
458
|
+
initial_pattern_center[sl],
|
|
459
|
+
refine_pattern_center,
|
|
460
|
+
n_steps,
|
|
461
|
+
damping,
|
|
462
|
+
keep_best,
|
|
463
|
+
device,
|
|
464
|
+
dtype,
|
|
465
|
+
)
|
|
466
|
+
r_parts.append(rf)
|
|
467
|
+
pc_parts.append(pf)
|
|
468
|
+
score_parts.append(sc)
|
|
469
|
+
r_final = torch.cat(r_parts)
|
|
470
|
+
pc_final = torch.cat(pc_parts)
|
|
471
|
+
score = torch.cat(score_parts)
|
|
472
|
+
|
|
473
|
+
from orix.quaternion import Rotation
|
|
474
|
+
|
|
475
|
+
rotation = Rotation.from_matrix(r_final.cpu().numpy())
|
|
476
|
+
if not refine_pattern_center:
|
|
477
|
+
return rotation, score
|
|
478
|
+
pc_out = pc_final.mean(0) if pc_shared else pc_final
|
|
479
|
+
return rotation, score, pc_out.detach()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""The Levenberg-Marquardt damping schedule shared by the refiners.
|
|
2
|
+
|
|
3
|
+
A step that improves the score is accepted and the damping eased toward
|
|
4
|
+
Gauss-Newton; a step that does not is rejected and the damping raised toward
|
|
5
|
+
steepest descent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
|
|
12
|
+
__all__ = ["DAMPING_CEILING", "DAMPING_FLOOR", "adapt_damping", "adapt_shared_damping"]
|
|
13
|
+
|
|
14
|
+
DAMPING_FLOOR = 1e-6
|
|
15
|
+
DAMPING_CEILING = 1e2
|
|
16
|
+
_EASE = 0.5
|
|
17
|
+
_RAISE = 3.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def adapt_damping(damping: torch.Tensor, accepted: torch.Tensor) -> torch.Tensor:
|
|
21
|
+
"""Per-pattern damping ``(N,)`` after a step that was *accepted* ``(N,)`` or not."""
|
|
22
|
+
return torch.where(
|
|
23
|
+
accepted,
|
|
24
|
+
(damping * _EASE).clamp_min(DAMPING_FLOOR),
|
|
25
|
+
(damping * _RAISE).clamp_max(DAMPING_CEILING),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def adapt_shared_damping(damping: float, accepted: bool) -> float:
|
|
30
|
+
"""The damping of one shared fit after a step that was *accepted* or not."""
|
|
31
|
+
if accepted:
|
|
32
|
+
return max(damping * _EASE, DAMPING_FLOOR)
|
|
33
|
+
return min(damping * _RAISE, DAMPING_CEILING)
|