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,222 @@
|
|
|
1
|
+
"""Fused CUDA kernels for live indexing: a master pattern sampled at rotated
|
|
2
|
+
detector directions in one pass, compiled at run time by CuPy.
|
|
3
|
+
|
|
4
|
+
Projecting through torch operations writes every intermediate -- the rotated
|
|
5
|
+
directions, their Lambert coordinates, the sampled values -- to GPU memory and
|
|
6
|
+
reads it back, about 390 M samples/s on a Titan X. Here each thread rotates its
|
|
7
|
+
direction, maps it to the square Lambert grid and interpolates the master in
|
|
8
|
+
registers, reading only the master (small enough to stay in L2) and writing one
|
|
9
|
+
value; :func:`score` does not even write that, reducing each candidate's samples
|
|
10
|
+
straight to its normalized cross-correlation with its pattern.
|
|
11
|
+
|
|
12
|
+
The arithmetic is :func:`de_ebsd.core.lambert.sphere_to_square` followed by
|
|
13
|
+
``grid_sample(mode="bilinear", align_corners=True, padding_mode="border")``, as
|
|
14
|
+
:meth:`MasterPattern.sample` does it; ``tests/index/test_live.py`` compares them.
|
|
15
|
+
CuPy is optional: without it (or off CUDA) the torch path is used.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import functools
|
|
21
|
+
import importlib.util
|
|
22
|
+
|
|
23
|
+
import torch
|
|
24
|
+
|
|
25
|
+
_SOURCE = r"""
|
|
26
|
+
#define SQRT_PI_HALF 1.2533141373155003f
|
|
27
|
+
#define SQRT_PI_OVER_2 0.886226925452758f
|
|
28
|
+
#define TWO_OVER_SQRT_PI 1.1283791670955126f
|
|
29
|
+
|
|
30
|
+
__device__ __forceinline__ float sample_master(
|
|
31
|
+
const float* __restrict__ texture, int base, int n, float vx, float vy, float vz)
|
|
32
|
+
{
|
|
33
|
+
float inverse = rsqrtf(fmaxf(vx * vx + vy * vy + vz * vz, 1e-24f));
|
|
34
|
+
vx *= inverse; vy *= inverse; vz *= inverse;
|
|
35
|
+
int channel = base + (vz < 0.f ? 1 : 0);
|
|
36
|
+
float abs_z = fabsf(vz);
|
|
37
|
+
float lx = 0.f, ly = 0.f;
|
|
38
|
+
if (abs_z < 1.f) {
|
|
39
|
+
float root = sqrtf(fmaxf(2.f * (1.f - abs_z), 1e-30f));
|
|
40
|
+
if (fabsf(vy) <= fabsf(vx)) {
|
|
41
|
+
float sign = vx >= 0.f ? 1.f : -1.f;
|
|
42
|
+
float safe = fabsf(vx) < 1e-12f ? 1e-12f : vx;
|
|
43
|
+
lx = sign * root * SQRT_PI_OVER_2;
|
|
44
|
+
ly = sign * root * TWO_OVER_SQRT_PI * atanf(vy / safe);
|
|
45
|
+
} else {
|
|
46
|
+
float sign = vy >= 0.f ? 1.f : -1.f;
|
|
47
|
+
float safe = fabsf(vy) < 1e-12f ? 1e-12f : vy;
|
|
48
|
+
lx = sign * root * TWO_OVER_SQRT_PI * atanf(vx / safe);
|
|
49
|
+
ly = sign * root * SQRT_PI_OVER_2;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
float last = (float)(n - 1);
|
|
53
|
+
float px = fminf(fmaxf((lx / SQRT_PI_HALF + 1.f) * 0.5f * last, 0.f), last);
|
|
54
|
+
float py = fminf(fmaxf((ly / SQRT_PI_HALF + 1.f) * 0.5f * last, 0.f), last);
|
|
55
|
+
int x0 = (int)floorf(px), y0 = (int)floorf(py);
|
|
56
|
+
int x1 = min(x0 + 1, n - 1), y1 = min(y0 + 1, n - 1);
|
|
57
|
+
float fx = px - x0, fy = py - y0;
|
|
58
|
+
const float* t = texture + (size_t)channel * n * n;
|
|
59
|
+
float top = t[y0 * n + x0] * (1.f - fx) + t[y0 * n + x1] * fx;
|
|
60
|
+
float bottom = t[y1 * n + x0] * (1.f - fx) + t[y1 * n + x1] * fx;
|
|
61
|
+
return top * (1.f - fy) + bottom * fy;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
__device__ __forceinline__ float sample_rotated(
|
|
65
|
+
const float* __restrict__ texture, int base, int n,
|
|
66
|
+
const float* __restrict__ r, const float* __restrict__ d)
|
|
67
|
+
{
|
|
68
|
+
float dx = d[0], dy = d[1], dz = d[2];
|
|
69
|
+
return sample_master(texture, base, n,
|
|
70
|
+
r[0] * dx + r[1] * dy + r[2] * dz,
|
|
71
|
+
r[3] * dx + r[4] * dy + r[5] * dz,
|
|
72
|
+
r[6] * dx + r[7] * dy + r[8] * dz);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// out[m, p]: rotation m's pattern at direction p, from channel pair bases[m].
|
|
76
|
+
extern "C" __global__ void project(
|
|
77
|
+
const float* __restrict__ texture, int n,
|
|
78
|
+
const float* __restrict__ rotations, const int* __restrict__ bases,
|
|
79
|
+
const float* __restrict__ directions, int pixels, long long count,
|
|
80
|
+
float* __restrict__ out)
|
|
81
|
+
{
|
|
82
|
+
long long index = (long long)blockIdx.x * blockDim.x + threadIdx.x;
|
|
83
|
+
if (index >= count) return;
|
|
84
|
+
long long m = index / pixels;
|
|
85
|
+
int p = (int)(index - m * pixels);
|
|
86
|
+
out[index] = sample_rotated(texture, bases[m], n, rotations + m * 9, directions + p * 3);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// scores[m]: the normalized cross-correlation of rotation m's pattern with
|
|
90
|
+
// rows[m / per_row], a row already zero-mean and unit-norm. One block per m.
|
|
91
|
+
extern "C" __global__ void score(
|
|
92
|
+
const float* __restrict__ texture, int n,
|
|
93
|
+
const float* __restrict__ rotations, const int* __restrict__ bases,
|
|
94
|
+
const float* __restrict__ directions, int pixels,
|
|
95
|
+
const float* __restrict__ rows, int per_row,
|
|
96
|
+
float* __restrict__ scores)
|
|
97
|
+
{
|
|
98
|
+
extern __shared__ float shared[];
|
|
99
|
+
long long m = blockIdx.x;
|
|
100
|
+
const float* r = rotations + m * 9;
|
|
101
|
+
const float* row = rows + (m / per_row) * (long long)pixels;
|
|
102
|
+
int base = bases[m];
|
|
103
|
+
float sum = 0.f, squares = 0.f, product = 0.f;
|
|
104
|
+
for (int p = threadIdx.x; p < pixels; p += blockDim.x) {
|
|
105
|
+
float s = sample_rotated(texture, base, n, r, directions + p * 3);
|
|
106
|
+
sum += s; squares += s * s; product += s * row[p];
|
|
107
|
+
}
|
|
108
|
+
float* a = shared; float* b = shared + blockDim.x; float* c = shared + 2 * blockDim.x;
|
|
109
|
+
a[threadIdx.x] = sum; b[threadIdx.x] = squares; c[threadIdx.x] = product;
|
|
110
|
+
__syncthreads();
|
|
111
|
+
for (int half = blockDim.x / 2; half > 0; half >>= 1) {
|
|
112
|
+
if (threadIdx.x < half) {
|
|
113
|
+
a[threadIdx.x] += a[threadIdx.x + half];
|
|
114
|
+
b[threadIdx.x] += b[threadIdx.x + half];
|
|
115
|
+
c[threadIdx.x] += c[threadIdx.x + half];
|
|
116
|
+
}
|
|
117
|
+
__syncthreads();
|
|
118
|
+
}
|
|
119
|
+
if (threadIdx.x == 0) {
|
|
120
|
+
float variance = b[0] - a[0] * a[0] / pixels;
|
|
121
|
+
scores[m] = c[0] * rsqrtf(fmaxf(variance, 1e-12f));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
#: Threads per block of :func:`score`; a power of two for its reduction.
|
|
127
|
+
SCORE_THREADS = 256
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def available(device) -> bool:
|
|
131
|
+
"""Whether the fused kernels can run on *device*."""
|
|
132
|
+
return torch.device(device).type == "cuda" and importlib.util.find_spec("cupy") is not None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@functools.cache
|
|
136
|
+
def _module(device_index: int):
|
|
137
|
+
import cupy
|
|
138
|
+
|
|
139
|
+
with cupy.cuda.Device(device_index):
|
|
140
|
+
return cupy.RawModule(code=_SOURCE, options=("--use_fast_math",))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _cupy(tensor: torch.Tensor):
|
|
144
|
+
import cupy
|
|
145
|
+
|
|
146
|
+
return cupy.from_dlpack(tensor.contiguous())
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _launch(name, grid, block, arguments, shared_mem=0):
|
|
150
|
+
import cupy
|
|
151
|
+
|
|
152
|
+
device = torch.cuda.current_device()
|
|
153
|
+
stream = cupy.cuda.ExternalStream(torch.cuda.current_stream().cuda_stream)
|
|
154
|
+
with cupy.cuda.Device(device), stream:
|
|
155
|
+
_module(device).get_function(name)(grid, block, arguments, shared_mem=shared_mem)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def project(
|
|
159
|
+
texture: torch.Tensor, rotations: torch.Tensor, bases: torch.Tensor, directions: torch.Tensor
|
|
160
|
+
) -> torch.Tensor:
|
|
161
|
+
"""Patterns ``(M, pixels)``: *texture* ``(1, channels, n, n)`` sampled at
|
|
162
|
+
*directions* ``(pixels, 3)`` turned by *rotations* ``(M, 3, 3)``, each from the
|
|
163
|
+
hemisphere pair starting at its channel ``bases[m]``."""
|
|
164
|
+
import cupy
|
|
165
|
+
|
|
166
|
+
m, pixels = len(rotations), len(directions)
|
|
167
|
+
out = torch.empty(m, pixels, device=rotations.device, dtype=torch.float32)
|
|
168
|
+
count = m * pixels
|
|
169
|
+
threads = 256
|
|
170
|
+
texture = texture.contiguous()
|
|
171
|
+
_launch(
|
|
172
|
+
"project",
|
|
173
|
+
((count + threads - 1) // threads,),
|
|
174
|
+
(threads,),
|
|
175
|
+
(
|
|
176
|
+
_cupy(texture),
|
|
177
|
+
cupy.int32(texture.shape[-1]),
|
|
178
|
+
_cupy(rotations.float()),
|
|
179
|
+
_cupy(bases.to(torch.int32)),
|
|
180
|
+
_cupy(directions.float()),
|
|
181
|
+
cupy.int32(pixels),
|
|
182
|
+
cupy.int64(count),
|
|
183
|
+
_cupy(out),
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
return out
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def score(
|
|
190
|
+
texture: torch.Tensor,
|
|
191
|
+
rotations: torch.Tensor,
|
|
192
|
+
bases: torch.Tensor,
|
|
193
|
+
directions: torch.Tensor,
|
|
194
|
+
rows: torch.Tensor,
|
|
195
|
+
per_row: int,
|
|
196
|
+
) -> torch.Tensor:
|
|
197
|
+
"""Scores ``(M,)``: the normalized cross-correlation of each rotation's
|
|
198
|
+
pattern (as :func:`project`) with ``rows[m // per_row]``, rows being
|
|
199
|
+
zero-mean, unit-norm ``(N, pixels)``."""
|
|
200
|
+
import cupy
|
|
201
|
+
|
|
202
|
+
m = len(rotations)
|
|
203
|
+
out = torch.empty(m, device=rotations.device, dtype=torch.float32)
|
|
204
|
+
texture = texture.contiguous()
|
|
205
|
+
_launch(
|
|
206
|
+
"score",
|
|
207
|
+
(m,),
|
|
208
|
+
(SCORE_THREADS,),
|
|
209
|
+
(
|
|
210
|
+
_cupy(texture),
|
|
211
|
+
cupy.int32(texture.shape[-1]),
|
|
212
|
+
_cupy(rotations.float()),
|
|
213
|
+
_cupy(bases.to(torch.int32)),
|
|
214
|
+
_cupy(directions.float()),
|
|
215
|
+
cupy.int32(len(directions)),
|
|
216
|
+
_cupy(rows.float()),
|
|
217
|
+
cupy.int32(per_row),
|
|
218
|
+
_cupy(out),
|
|
219
|
+
),
|
|
220
|
+
shared_mem=3 * SCORE_THREADS * 4,
|
|
221
|
+
)
|
|
222
|
+
return out
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Back-project a detector pattern onto the unit sphere.
|
|
2
|
+
|
|
3
|
+
The inverse of the forward projection: each sphere direction is mapped to a
|
|
4
|
+
fractional detector pixel and bilinearly sampled, giving E(n) on a supplied
|
|
5
|
+
direction set plus the field-of-view mask. Differentiable in the pattern
|
|
6
|
+
center, which is what makes PC refinement possible.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import torch
|
|
12
|
+
import torch.nn.functional as F
|
|
13
|
+
|
|
14
|
+
from de_ebsd.core.geometry import sample_to_detector_tensor
|
|
15
|
+
from de_ebsd.sim.detector import EBSDDetector
|
|
16
|
+
|
|
17
|
+
__all__ = ["directions_to_pixels", "pattern_to_sphere"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _pc_components(
|
|
21
|
+
detector: EBSDDetector,
|
|
22
|
+
pattern_center: torch.Tensor | None,
|
|
23
|
+
device: torch.device,
|
|
24
|
+
dtype: torch.dtype,
|
|
25
|
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
26
|
+
if pattern_center is None:
|
|
27
|
+
pattern_center = torch.as_tensor(detector.pattern_center, device=device, dtype=dtype)
|
|
28
|
+
else:
|
|
29
|
+
if pattern_center.shape[-1] != 3:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"pattern_center must have last dim 3, got shape {tuple(pattern_center.shape)}"
|
|
32
|
+
)
|
|
33
|
+
pattern_center = pattern_center.to(device=device, dtype=dtype)
|
|
34
|
+
return pattern_center[0], pattern_center[1], pattern_center[2]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def directions_to_pixels(
|
|
38
|
+
directions: torch.Tensor,
|
|
39
|
+
detector: EBSDDetector,
|
|
40
|
+
pattern_center: torch.Tensor | None = None,
|
|
41
|
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
42
|
+
"""Map sphere directions to fractional detector pixels (inverse of the DC map).
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
directions
|
|
47
|
+
Unit vectors ``(M, 3)`` in the sample frame (need not be exactly normalized;
|
|
48
|
+
the gnomonic ratios are scale-invariant).
|
|
49
|
+
detector
|
|
50
|
+
The ``de_ebsd.sim`` :class:`~de_ebsd.sim.detector.EBSDDetector` geometry.
|
|
51
|
+
pattern_center
|
|
52
|
+
Optional pattern-center override ``(3,)`` (``pcx, pcy, pcz``). When supplied
|
|
53
|
+
with ``requires_grad``, the returned pixel coordinates are differentiable in
|
|
54
|
+
the pattern center. Defaults to ``detector.pattern_center``.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
col, row
|
|
59
|
+
Fractional pixel coordinates ``(M,)``, where integer values land on pixel
|
|
60
|
+
centers (``col = 0`` is the leftmost column, ``row = 0`` the top row) --
|
|
61
|
+
ready for :func:`torch.nn.functional.grid_sample` with ``align_corners=True``.
|
|
62
|
+
in_fov
|
|
63
|
+
Boolean field-of-view mask ``(M,)``: ``True`` where the direction points at
|
|
64
|
+
the detector (``d_z > 0``) and lands within the gnomonic bounds.
|
|
65
|
+
"""
|
|
66
|
+
device = directions.device
|
|
67
|
+
om = sample_to_detector_tensor(detector, device, directions.dtype)
|
|
68
|
+
dtype = directions.dtype
|
|
69
|
+
pcx, pcy, pcz = _pc_components(detector, pattern_center, device, dtype)
|
|
70
|
+
|
|
71
|
+
# detector-frame components d = (n . X_d, n . Y_d, n . Z_d) = directions @ Om.T
|
|
72
|
+
comp = directions @ om.transpose(-2, -1)
|
|
73
|
+
d_x, d_y, d_z = comp[..., 0], comp[..., 1], comp[..., 2]
|
|
74
|
+
# gnomonic projection back onto the detector plane (pattern-center independent)
|
|
75
|
+
d_z_safe = torch.where(d_z.abs() < 1e-12, torch.full_like(d_z, 1e-12), d_z)
|
|
76
|
+
gn_x = d_x / d_z_safe
|
|
77
|
+
gn_y = d_y / d_z_safe
|
|
78
|
+
|
|
79
|
+
# gnomonic bounds and node scales from the pattern center (torch, differentiable),
|
|
80
|
+
# matching EBSDDetector.gnomonic_bounds / direction_cosines exactly
|
|
81
|
+
aspect = detector.ncols / detector.nrows
|
|
82
|
+
x_min = -aspect * pcx / pcz
|
|
83
|
+
x_max = aspect * (1.0 - pcx) / pcz
|
|
84
|
+
y_min = -(1.0 - pcy) / pcz
|
|
85
|
+
y_max = pcy / pcz
|
|
86
|
+
x_scale = (x_max - x_min) / detector.ncols
|
|
87
|
+
y_scale = (y_max - y_min) / detector.nrows
|
|
88
|
+
|
|
89
|
+
col = (gn_x - x_min) / x_scale - 0.5
|
|
90
|
+
row = (y_max - gn_y) / y_scale - 0.5
|
|
91
|
+
|
|
92
|
+
in_fov = (d_z > 0) & (gn_x >= x_min) & (gn_x <= x_max) & (gn_y >= y_min) & (gn_y <= y_max)
|
|
93
|
+
return col, row, in_fov
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def pattern_to_sphere(
|
|
97
|
+
patterns: torch.Tensor,
|
|
98
|
+
detector: EBSDDetector,
|
|
99
|
+
directions: torch.Tensor,
|
|
100
|
+
pattern_center: torch.Tensor | None = None,
|
|
101
|
+
padding_mode: str = "border",
|
|
102
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
103
|
+
"""Back-project detector ``patterns`` onto ``directions`` on the unit sphere.
|
|
104
|
+
|
|
105
|
+
Each sphere direction reads the detector pixel that scatters along it (no crystal
|
|
106
|
+
orientation is applied), bilinearly interpolating the
|
|
107
|
+
pattern. The result is ``E(n)`` sampled on ``directions``, ready to feed
|
|
108
|
+
:meth:`de_ebsd.index.spherical.harmonics.SphericalHarmonicTransform.forward` (reshape the
|
|
109
|
+
``(N_theta, N_phi)`` grid directions to ``(M, 3)`` and the samples back).
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
patterns
|
|
114
|
+
Detector images ``(..., H, W)`` (``H = detector.nrows``, ``W = detector.ncols``),
|
|
115
|
+
batched over arbitrary leading dimensions. Real floating dtype.
|
|
116
|
+
detector
|
|
117
|
+
The ``de_ebsd.sim`` :class:`~de_ebsd.sim.detector.EBSDDetector` geometry
|
|
118
|
+
(fixed pattern center). Its pixel -> direction map is inverted here.
|
|
119
|
+
directions
|
|
120
|
+
Target sphere directions ``(M, 3)`` in the sample/detector frame, e.g.
|
|
121
|
+
``SphericalHarmonicTransform.grid_directions().reshape(-1, 3)``.
|
|
122
|
+
pattern_center
|
|
123
|
+
Optional pattern-center override ``(3,)`` (``pcx, pcy, pcz``); pass a tensor
|
|
124
|
+
with ``requires_grad`` to differentiate the samples in the pattern center
|
|
125
|
+
(I6 refinement). Defaults to ``detector.pattern_center``.
|
|
126
|
+
padding_mode
|
|
127
|
+
``grid_sample`` padding for directions that fall (just) outside the pixel
|
|
128
|
+
grid; ``"border"`` (default) clamps to the edge. Off-FOV samples are flagged
|
|
129
|
+
by the returned mask regardless.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
samples
|
|
134
|
+
Sphere samples ``(..., M)`` (same leading dims and dtype as ``patterns``).
|
|
135
|
+
Entries outside the field of view are the ``padding_mode`` read-off and
|
|
136
|
+
should be excluded via ``mask``. Differentiable in ``patterns`` (and, via
|
|
137
|
+
``pattern_center``, in the pattern center).
|
|
138
|
+
mask
|
|
139
|
+
Field-of-view weight ``(M,)`` in ``patterns``' dtype: ``1`` where the
|
|
140
|
+
direction lands on the detector, ``0`` otherwise (the detector's gnomonic
|
|
141
|
+
bounds). Shared across the batch since the geometry is.
|
|
142
|
+
"""
|
|
143
|
+
if patterns.shape[-2:] != (detector.nrows, detector.ncols):
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"patterns trailing shape {tuple(patterns.shape[-2:])} != detector "
|
|
146
|
+
f"{(detector.nrows, detector.ncols)}"
|
|
147
|
+
)
|
|
148
|
+
if directions.shape[-1] != 3 or directions.ndim != 2:
|
|
149
|
+
raise ValueError(f"directions must have shape (M, 3), got {tuple(directions.shape)}")
|
|
150
|
+
|
|
151
|
+
device = patterns.device
|
|
152
|
+
dirs = directions.to(device=device, dtype=patterns.dtype)
|
|
153
|
+
col, row, in_fov = directions_to_pixels(dirs, detector, pattern_center)
|
|
154
|
+
|
|
155
|
+
# normalized sampling coordinates for grid_sample(align_corners=True):
|
|
156
|
+
# col in [0, W-1] -> gx in [-1, 1]; row in [0, H-1] -> gy in [-1, 1]
|
|
157
|
+
gx = 2.0 * col / (detector.ncols - 1) - 1.0
|
|
158
|
+
gy = 2.0 * row / (detector.nrows - 1) - 1.0
|
|
159
|
+
grid = torch.stack([gx, gy], dim=-1) # (M, 2)
|
|
160
|
+
|
|
161
|
+
m = dirs.shape[0]
|
|
162
|
+
leading = patterns.shape[:-2]
|
|
163
|
+
batch = patterns.reshape(-1, 1, detector.nrows, detector.ncols)
|
|
164
|
+
n = batch.shape[0]
|
|
165
|
+
grid = grid.view(1, 1, m, 2).expand(n, 1, m, 2)
|
|
166
|
+
sampled = F.grid_sample(
|
|
167
|
+
batch, grid, mode="bilinear", align_corners=True, padding_mode=padding_mode
|
|
168
|
+
) # (N, 1, 1, M)
|
|
169
|
+
samples = sampled.reshape(*leading, m)
|
|
170
|
+
mask = in_fov.to(patterns.dtype)
|
|
171
|
+
return samples, mask
|