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