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
de_ebsd/__init__.py ADDED
@@ -0,0 +1,76 @@
1
+ """de-ebsd: differentiable, GPU-accelerated dynamical EBSD master patterns.
2
+
3
+ Layered as :mod:`~de_ebsd.core` (frames, constants) -> :mod:`~de_ebsd.crystal`
4
+ (unit cells, structure factors) -> :mod:`~de_ebsd.diffraction` (Bloch waves) ->
5
+ :mod:`~de_ebsd.sim` (master patterns), with :mod:`~de_ebsd.index` matching
6
+ patterns back to orientations and :mod:`~de_ebsd.viz` drawing everything::
7
+
8
+ import de_ebsd as de
9
+
10
+ crystal = de.Crystal.from_parameters(
11
+ a=0.5431, space_group=227, atoms=[("Si", (0.125, 0.125, 0.125))]
12
+ )
13
+ microscope = de.Microscope(energy=20e3, sample_tilt=70.0)
14
+ sim = de.MasterPatternSimulator(crystal, microscope, npx=200, dmin=0.1)
15
+ master = sim.compute(device="cuda")
16
+
17
+ detector = de.EBSDDetector(shape=(480, 480), pattern_center=(0.5, 0.5, 0.6))
18
+ patterns = master.project(detector, orientation=rotation) # orix Rotation
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import importlib
24
+
25
+ from de_ebsd.crystal import AtomSite, Crystal
26
+ from de_ebsd.diffraction import (
27
+ BetheParameters,
28
+ BlochPrismBackend,
29
+ BlochWaveBackend,
30
+ MasterPatternBackend,
31
+ )
32
+ from de_ebsd.sim import (
33
+ DepthDistribution,
34
+ EBSDDetector,
35
+ MasterPattern,
36
+ MasterPatternSimulator,
37
+ Microscope,
38
+ )
39
+
40
+ __version__ = "0.2.0"
41
+
42
+ __all__ = [
43
+ "AtomSite",
44
+ "BetheParameters",
45
+ "BlochPrismBackend",
46
+ "BlochWaveBackend",
47
+ "Crystal",
48
+ "CrystalExplorer",
49
+ "DepthDistribution",
50
+ "EBSDDetector",
51
+ "MasterPattern",
52
+ "MasterPatternBackend",
53
+ "MasterPatternSimulator",
54
+ "Microscope",
55
+ "MicroscopeWidget",
56
+ "PhaseOverview",
57
+ "__version__",
58
+ ]
59
+
60
+ # the display stack is lazy so `import de_ebsd` stays functional on headless nodes
61
+ _LAZY = {
62
+ "CrystalExplorer": "de_ebsd.viz.explorer",
63
+ "PhaseOverview": "de_ebsd.viz.views",
64
+ "MicroscopeWidget": "de_ebsd.viz.widgets",
65
+ }
66
+
67
+
68
+ def __getattr__(name: str):
69
+ module = _LAZY.get(name)
70
+ if module is None:
71
+ raise AttributeError(f"module 'de_ebsd' has no attribute {name!r}")
72
+ return getattr(importlib.import_module(module), name)
73
+
74
+
75
+ def __dir__() -> list[str]:
76
+ return sorted(__all__)
@@ -0,0 +1,23 @@
1
+ """Frame conventions, physical constants and coordinate maps shared package-wide."""
2
+
3
+ from de_ebsd.core.constants import (
4
+ electron_wavelength_nm,
5
+ interaction_constant,
6
+ relativistic_correction,
7
+ )
8
+ from de_ebsd.core.geometry import sample_to_detector_matrix, sample_to_detector_tensor
9
+ from de_ebsd.core.lambert import master_pattern_grid, sphere_to_square, square_to_sphere
10
+ from de_ebsd.core.rotation import quaternion_to_matrix, rotate_vectors
11
+
12
+ __all__ = [
13
+ "electron_wavelength_nm",
14
+ "interaction_constant",
15
+ "master_pattern_grid",
16
+ "quaternion_to_matrix",
17
+ "relativistic_correction",
18
+ "rotate_vectors",
19
+ "sample_to_detector_matrix",
20
+ "sample_to_detector_tensor",
21
+ "sphere_to_square",
22
+ "square_to_sphere",
23
+ ]
@@ -0,0 +1,81 @@
1
+ """Physical constants and derived diffraction prefactors.
2
+
3
+ Values are transcribed verbatim from EMsoft (EMsoftOO) ``mod_global.f90`` so that
4
+ the structure-factor and wavelength results are bit-for-bit comparable with
5
+ EMsoft's Fortran implementation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+
12
+ # --- fundamental constants (EMsoft mod_global.f90, exact CODATA-2018 set) ----
13
+ C_PI = 3.14159265358979323846
14
+ C_LIGHT = 299792458.0 # speed of light [m/s]
15
+ C_PLANCK = 6.62607015e-34 # Planck constant [J s]
16
+ C_BOLTZMANN = 1.380649e-23 # Boltzmann constant [J/K]
17
+ C_CHARGE = 1.602176634e-19 # elementary charge [C]
18
+ C_RESTMASS = 9.1093837090e-31 # electron rest mass [kg]
19
+ C_AVOGADRO = 6.02214076e23
20
+
21
+ # electron rest energy in keV (EMsoft hard-codes 511 / 1022 inside the WK code)
22
+ ELECTRON_REST_KEV = 511.0
23
+
24
+ # derived diffraction prefactors (EMsoft mod_diffraction.f90)
25
+ # preg converts a potential Fourier coefficient V_g [volts] into the scaled
26
+ # coefficient U_g [nm^-2]: U_g = preg * V_g.
27
+ # preg = 2 * m0 * e / h^2 * 1e-18
28
+ PREG = 0.664840340614319
29
+
30
+ # Weickenmeier-Kohl unit-scaling constants (mod_diffraction.f90 WK branch).
31
+ # swk scales g [nm^-1] into WK reciprocal units (Angstrom * 2*pi).
32
+ SWK = 0.1 * 2.0 * C_PI # = 2*pi/10 = 0.6283185307...
33
+ # dwwk converts a Debye-Waller B factor [nm^2] into <u^2> [Angstrom^2]:
34
+ # ul = sqrt(B * dwwk).
35
+ DWWK = 100.0 / (8.0 * C_PI**2) # = 1.2665147955...
36
+
37
+ # Structure-factor prefactor coefficient. The full pref used by EMsoft is
38
+ # pref = PREF_COEF / V_cell[Angstrom^3] / (4*pi)
39
+ # and the potential-to-U scaling is pre = pref * PREG.
40
+ PREF_COEF = 0.04787801
41
+
42
+ # Doyle-Turner summation prefactor for kinematical X-ray form factors (XR mode).
43
+ XR_PREF = 0.4178214
44
+
45
+
46
+ def electron_wavelength_nm(voltage_kv: float, v0mod: float = 0.0) -> float:
47
+ """Relativistic electron wavelength in nm (EMsoft ``CalcWaveLength``).
48
+
49
+ Parameters
50
+ ----------
51
+ voltage_kv
52
+ Accelerating voltage in kV.
53
+ v0mod
54
+ Mean inner potential (the modulus of V_g for g = 0), in volts, added as
55
+ a refraction correction. Pass 0 to omit the correction.
56
+ """
57
+ temp1 = 1.0e9 * C_PLANCK / math.sqrt(2.0 * C_RESTMASS * C_CHARGE)
58
+ temp2 = C_CHARGE * 0.5 * voltage_kv * 1000.0 / C_RESTMASS / (C_LIGHT**2)
59
+ psihat = voltage_kv * (1.0 + temp2) * 1000.0 + v0mod
60
+ return temp1 / math.sqrt(psihat)
61
+
62
+
63
+ def relativistic_correction(voltage_kv: float) -> float:
64
+ """Relativistic correction factor gamma = 1 + e V / (m0 c^2)."""
65
+ temp2 = C_CHARGE * 0.5 * voltage_kv * 1000.0 / C_RESTMASS / (C_LIGHT**2)
66
+ return 1.0 + 2.0 * temp2
67
+
68
+
69
+ def interaction_constant(voltage_kv: float, v0mod: float = 0.0) -> float:
70
+ """Interaction constant sigma in (V nm)^-1 (EMsoft ``Sigma``)."""
71
+ lam = electron_wavelength_nm(voltage_kv, v0mod)
72
+ gamma = relativistic_correction(voltage_kv)
73
+ sigma = 2.0 * C_PI * C_RESTMASS * gamma * C_CHARGE * lam
74
+ return 1.0e-18 * sigma / C_PLANCK**2
75
+
76
+
77
+ # --- numerical floors for guarding a division or a norm ---------------------
78
+ #: Smallest divisor allowed in double-precision arithmetic.
79
+ DIVISION_FLOOR = 1e-12
80
+ #: Smallest divisor allowed where the data are single precision (patterns, scores).
81
+ SINGLE_PRECISION_FLOOR = 1e-6
de_ebsd/core/device.py ADDED
@@ -0,0 +1,12 @@
1
+ """Device selection shared by every entry point that takes ``device=``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+
7
+ __all__ = ["resolve_device"]
8
+
9
+
10
+ def resolve_device(device, default="cpu") -> torch.device:
11
+ """*device* as a :class:`torch.device`, or *default* when it is ``None``."""
12
+ return torch.device(default if device is None else device)
@@ -0,0 +1,67 @@
1
+ """Sample <-> detector frame geometry.
2
+
3
+ Shared by the forward projection (:mod:`de_ebsd.sim.detector`) and the
4
+ back-projection (:mod:`de_ebsd.index.backproject`) so both use one frame
5
+ convention. Port of kikuchipy's ``_sample_to_detector_matrix``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import numpy as np
11
+ import torch
12
+
13
+ __all__ = ["detector_rays", "sample_to_detector_matrix", "sample_to_detector_tensor"]
14
+
15
+
16
+ def _rodrigues_step(basis: np.ndarray, axis_row: int, angle: float) -> np.ndarray:
17
+ u = basis[axis_row]
18
+ u = u / np.linalg.norm(u)
19
+ c, s = np.cos(angle), np.sin(angle)
20
+ new = np.empty_like(basis)
21
+ for j in range(3):
22
+ v = basis[j]
23
+ new[j] = v * c + np.cross(u, v) * s + u * np.dot(u, v) * (1.0 - c)
24
+ return new
25
+
26
+
27
+ def sample_to_detector_matrix(
28
+ sample_tilt: float, tilt: float, azimuthal: float, twist: float
29
+ ) -> np.ndarray:
30
+ """Rows are the detector basis (X_d, Y_d, Z_d) in sample coordinates.
31
+
32
+ Angles in degrees. Depends on the tilt geometry only, not the pattern center.
33
+ """
34
+ basis = np.array([[0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]])
35
+ sigma, theta, omega, gamma = np.deg2rad([sample_tilt, tilt, azimuthal, twist])
36
+ for axis_row, angle in zip([0, 0, 1, 2], [-sigma, theta, -omega, -gamma], strict=True):
37
+ basis = _rodrigues_step(basis, axis_row, angle)
38
+ return basis
39
+
40
+
41
+ def sample_to_detector_tensor(detector, device, dtype: torch.dtype) -> torch.Tensor:
42
+ """:func:`sample_to_detector_matrix` for a detector, as a torch tensor."""
43
+ om = sample_to_detector_matrix(
44
+ detector.sample_tilt, detector.tilt, detector.azimuthal, detector.twist
45
+ )
46
+ return torch.as_tensor(om, device=device, dtype=dtype)
47
+
48
+
49
+ def detector_rays(detector, pattern_center: torch.Tensor, rows: torch.Tensor, cols: torch.Tensor):
50
+ """Detector-frame rays ``(..., K, 3)`` through fractional pixels, for a pattern center.
51
+
52
+ The un-normalized ray is
53
+
54
+ ``x_d = aspect * ((col + 0.5) / ncols - pcx)``,
55
+ ``y_d = pcy - (row + 0.5) / nrows``,
56
+ ``z_d = pcz``,
57
+
58
+ in pure torch arithmetic, so it differentiates in ``pattern_center`` (unlike the
59
+ ``grid_sample`` back-projection). ``pattern_center`` is ``(..., 3)`` and broadcasts against
60
+ the ``(..., K)`` ``rows`` and ``cols``.
61
+ """
62
+ aspect = detector.ncols / detector.nrows
63
+ pcx, pcy, pcz = pattern_center[..., 0:1], pattern_center[..., 1:2], pattern_center[..., 2:3]
64
+ x_d = aspect * ((cols + 0.5) / detector.ncols - pcx)
65
+ y_d = pcy - (rows + 0.5) / detector.nrows
66
+ z_d = pcz + torch.zeros_like(x_d)
67
+ return torch.stack([x_d, y_d, z_d], dim=-1)
@@ -0,0 +1,148 @@
1
+ """Equal-area Lambert projection between the unit sphere and the square grid.
2
+
3
+ The master pattern is stored on the square; ``sphere_to_square`` maps a
4
+ direction to its (x, y) in [-L, L]^2 with L = sqrt(pi/2), and
5
+ ``square_to_sphere`` inverts it. Both are differentiable and batched, and follow
6
+ kikuchipy's ``_vector2lambert`` and EMsoft's Rosca-Lambert mapping.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+
13
+ import torch
14
+ import torch.nn.functional as F
15
+
16
+ SQRT_PI = math.sqrt(math.pi)
17
+ SQRT_PI_HALF = math.sqrt(math.pi / 2.0) # the half-edge L of the square
18
+ _SQRT_PI_OVER_2 = SQRT_PI / 2.0
19
+ _TWO_OVER_SQRT_PI = 2.0 / SQRT_PI
20
+
21
+ __all__ = [
22
+ "SQRT_PI_HALF",
23
+ "master_pattern_grid",
24
+ "sample_texture",
25
+ "sphere_to_square",
26
+ "square_to_sphere",
27
+ ]
28
+
29
+
30
+ def sphere_to_square(v: torch.Tensor, eps: float = 1e-12) -> torch.Tensor:
31
+ """Unit vectors (..., 3) to square Lambert coordinates (..., 2), book units.
32
+
33
+ Matches kikuchipy ``_vector2lambert`` (validated against EMsoft). Both
34
+ hemispheres map onto the same square through ``|z|``.
35
+ """
36
+ v = v / v.norm(dim=-1, keepdim=True).clamp_min(eps)
37
+ x, y, z = v.unbind(-1)
38
+ abs_z = z.abs()
39
+ # clamped above zero: sqrt's slope at 0 is infinite, and at a pole (where |z|
40
+ # rounds to 1, within ~0.02 deg in float32) it made the gradient NaN even
41
+ # though the pole branch below discards the value
42
+ sqrt_z = torch.sqrt((2.0 * (1.0 - abs_z)).clamp_min(torch.finfo(v.dtype).tiny))
43
+
44
+ cond = y.abs() <= x.abs()
45
+
46
+ x_safe = torch.where(x.abs() < eps, torch.full_like(x, eps), x)
47
+ y_safe = torch.where(y.abs() < eps, torch.full_like(y, eps), y)
48
+ sign_x = torch.where(x >= 0, torch.ones_like(x), -torch.ones_like(x))
49
+ sign_y = torch.where(y >= 0, torch.ones_like(y), -torch.ones_like(y))
50
+
51
+ # branch |y| <= |x|
52
+ x_a = sign_x * sqrt_z * _SQRT_PI_OVER_2
53
+ y_a = sign_x * sqrt_z * _TWO_OVER_SQRT_PI * torch.atan(y / x_safe)
54
+ # branch |y| > |x|
55
+ x_b = sign_y * sqrt_z * _TWO_OVER_SQRT_PI * torch.atan(x / y_safe)
56
+ y_b = sign_y * sqrt_z * _SQRT_PI_OVER_2
57
+
58
+ lx = torch.where(cond, x_a, x_b)
59
+ ly = torch.where(cond, y_a, y_b)
60
+
61
+ at_pole = abs_z >= 1.0
62
+ lx = torch.where(at_pole, torch.zeros_like(lx), lx)
63
+ ly = torch.where(at_pole, torch.zeros_like(ly), ly)
64
+ return torch.stack([lx, ly], dim=-1)
65
+
66
+
67
+ def square_to_sphere(xy: torch.Tensor, eps: float = 1e-12) -> torch.Tensor:
68
+ """Square Lambert coordinates (..., 2, book units) to unit vectors (..., 3).
69
+
70
+ Inverse of :func:`sphere_to_square` for the northern hemisphere
71
+ (z >= 0); negate z for the southern hemisphere. Port of EMsoft
72
+ ``Lambert2DSquareForwardDouble`` (Rosca 2010, eqs. 8-9).
73
+ """
74
+ x, y = xy.unbind(-1)
75
+ cond = x.abs() <= y.abs()
76
+
77
+ # branch |x| <= |y|
78
+ y_safe = torch.where(y.abs() < eps, torch.full_like(y, eps), y)
79
+ q_a = 2.0 * y / math.pi * torch.sqrt((math.pi - y * y).clamp_min(0.0))
80
+ qq_a = x * math.pi * 0.25 / y_safe
81
+ vx_a = q_a * torch.sin(qq_a)
82
+ vy_a = q_a * torch.cos(qq_a)
83
+ vz_a = 1.0 - 2.0 * y * y / math.pi
84
+
85
+ # branch |x| > |y|
86
+ x_safe = torch.where(x.abs() < eps, torch.full_like(x, eps), x)
87
+ q_b = 2.0 * x / math.pi * torch.sqrt((math.pi - x * x).clamp_min(0.0))
88
+ qq_b = y * math.pi * 0.25 / x_safe
89
+ vx_b = q_b * torch.cos(qq_b)
90
+ vy_b = q_b * torch.sin(qq_b)
91
+ vz_b = 1.0 - 2.0 * x * x / math.pi
92
+
93
+ vx = torch.where(cond, vx_a, vx_b)
94
+ vy = torch.where(cond, vy_a, vy_b)
95
+ vz = torch.where(cond, vz_a, vz_b)
96
+
97
+ at_origin = (x.abs() < eps) & (y.abs() < eps)
98
+ vx = torch.where(at_origin, torch.zeros_like(vx), vx)
99
+ vy = torch.where(at_origin, torch.zeros_like(vy), vy)
100
+ vz = torch.where(at_origin, torch.ones_like(vz), vz)
101
+
102
+ v = torch.stack([vx, vy, vz], dim=-1)
103
+ return v / v.norm(dim=-1, keepdim=True).clamp_min(eps)
104
+
105
+
106
+ def master_pattern_grid(
107
+ npx: int,
108
+ device: torch.device | str | None = None,
109
+ dtype: torch.dtype = torch.float32,
110
+ ) -> torch.Tensor:
111
+ """Northern-hemisphere unit vectors for every master pattern pixel.
112
+
113
+ Returns a tensor of shape ``(2 npx + 1, 2 npx + 1, 3)`` where element
114
+ ``[row, col]`` is the direction whose square Lambert projection is
115
+ ``X = (col - npx)/npx * L`` and ``Y = (row - npx)/npx * L``. Negate the
116
+ z component for the southern hemisphere.
117
+ """
118
+ coords = torch.linspace(-1.0, 1.0, 2 * npx + 1, device=device, dtype=dtype)
119
+ ys, xs = torch.meshgrid(coords, coords, indexing="ij")
120
+ xy = torch.stack([xs, ys], dim=-1) * SQRT_PI_HALF
121
+ return square_to_sphere(xy)
122
+
123
+
124
+ def sample_texture(
125
+ texture: torch.Tensor, directions: torch.Tensor, phase: torch.Tensor | None = None
126
+ ) -> torch.Tensor:
127
+ """Bilinear lookup of master patterns at unit *directions* ``(..., 3)``.
128
+
129
+ *texture* ``(1, 2 * phases, n, n)`` holds each phase's northern and southern
130
+ hemisphere as consecutive channels; *phase* (integer, broadcast against the
131
+ leading dimensions of *directions*) picks the phase, default the first. Edge
132
+ handling is ``grid_sample(align_corners=True, padding_mode="border")``,
133
+ identical to kikuchipy's interpolation. The hemisphere is selected by the
134
+ sign of ``z``. Returns the values ``(...)``.
135
+ """
136
+ lead = directions.shape[:-1]
137
+ square = sphere_to_square(directions.reshape(-1, 3)) / SQRT_PI_HALF
138
+ values = F.grid_sample(
139
+ texture,
140
+ square.view(1, 1, -1, 2),
141
+ mode="bilinear",
142
+ align_corners=True,
143
+ padding_mode="border",
144
+ ).view(texture.shape[1], *lead)
145
+ channel = (directions[..., 2] < 0).long()
146
+ if phase is not None:
147
+ channel = channel + 2 * phase
148
+ return values.gather(0, channel[None])[0]
@@ -0,0 +1,139 @@
1
+ """Quaternion rotations, matching orix / kikuchipy conventions.
2
+
3
+ Quaternions are (a, b, c, d) with the scalar part first (the order of
4
+ ``orix.quaternion.Rotation.data``), so an orix ``Rotation`` feeds in directly
5
+ via ``torch.as_tensor(rotation.data)``. The matrix construction reproduces
6
+ kikuchipy's ``rotate_vector`` exactly , so
7
+ projections agree with kikuchipy to numerical precision.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+
14
+ import numpy as np
15
+ import torch
16
+
17
+ from de_ebsd.core.constants import DIVISION_FLOOR
18
+
19
+ __all__ = [
20
+ "axis_angle_matrix",
21
+ "matrix_to_quaternion",
22
+ "quaternion_to_matrix",
23
+ "rotate_vectors",
24
+ "rotation_vector_to_matrix",
25
+ "skew_matrix",
26
+ ]
27
+
28
+
29
+ def quaternion_to_matrix(q: torch.Tensor) -> torch.Tensor:
30
+ """Unit quaternions (..., 4), order (a, b, c, d), to matrices (..., 3, 3).
31
+
32
+ The returned R rotates column vectors: ``v_rotated = R @ v``.
33
+ """
34
+ a, b, c, d = q.unbind(-1)
35
+ aa, bb, cc, dd = a * a, b * b, c * c, d * d
36
+ ab, ac, ad = a * b, a * c, a * d
37
+ bc, bd, cd = b * c, b * d, c * d
38
+ row0 = torch.stack([aa + bb - cc - dd, 2 * (bc - ad), 2 * (ac + bd)], dim=-1)
39
+ row1 = torch.stack([2 * (ad + bc), aa - bb + cc - dd, 2 * (cd - ab)], dim=-1)
40
+ row2 = torch.stack([2 * (bd - ac), 2 * (ab + cd), aa - bb - cc + dd], dim=-1)
41
+ return torch.stack([row0, row1, row2], dim=-2)
42
+
43
+
44
+ def rotate_vectors(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
45
+ """Rotate vectors ``v`` (..., P, 3) by quaternions ``q`` (..., 4)."""
46
+ r = quaternion_to_matrix(q)
47
+ return torch.einsum("...ij,...pj->...pi", r, v)
48
+
49
+
50
+ def skew_matrix(w: torch.Tensor) -> torch.Tensor:
51
+ """Matrices ``(..., 3, 3)`` taking ``v`` to ``w x v``, for rotation vectors ``w``."""
52
+ zero = torch.zeros_like(w[..., 0])
53
+ row0 = torch.stack([zero, -w[..., 2], w[..., 1]], dim=-1)
54
+ row1 = torch.stack([w[..., 2], zero, -w[..., 0]], dim=-1)
55
+ row2 = torch.stack([-w[..., 1], w[..., 0], zero], dim=-1)
56
+ return torch.stack([row0, row1, row2], dim=-2)
57
+
58
+
59
+ def rotation_vector_to_matrix(w: torch.Tensor) -> torch.Tensor:
60
+ """Rotation vectors ``(..., 3)`` in radians as matrices ``(..., 3, 3)`` (Rodrigues).
61
+
62
+ Written with the series limits of ``sin(t) / t`` and ``(1 - cos t) / t^2`` at small
63
+ angles, so it and its gradient stay finite at ``w = 0``.
64
+ """
65
+ k = skew_matrix(w)
66
+ th2 = (w * w).sum(-1) # (...,)
67
+ th = th2.clamp_min(DIVISION_FLOOR).sqrt()
68
+ small = th2 < 1e-12
69
+ a = torch.where(small, torch.ones_like(th), torch.sin(th) / th) # sin th / th
70
+ b = torch.where(
71
+ small, torch.full_like(th, 0.5), (1.0 - torch.cos(th)) / th2.clamp_min(DIVISION_FLOOR)
72
+ )
73
+ eye = torch.eye(3, device=w.device, dtype=w.dtype).expand(k.shape)
74
+ return eye + a[..., None, None] * k + b[..., None, None] * (k @ k)
75
+
76
+
77
+ def axis_angle_matrix(axis: np.ndarray, angle_deg: float) -> np.ndarray:
78
+ """Rotation matrix about *axis* by *angle_deg* degrees (right-handed)."""
79
+ axis = np.asarray(axis, dtype=np.float64)
80
+ axis = axis / np.linalg.norm(axis)
81
+ t = math.radians(float(angle_deg))
82
+ c, s = math.cos(t), math.sin(t)
83
+ x, y, z = axis
84
+ return np.array(
85
+ [
86
+ [c + x * x * (1 - c), x * y * (1 - c) - z * s, x * z * (1 - c) + y * s],
87
+ [y * x * (1 - c) + z * s, c + y * y * (1 - c), y * z * (1 - c) - x * s],
88
+ [z * x * (1 - c) - y * s, z * y * (1 - c) + x * s, c + z * z * (1 - c)],
89
+ ]
90
+ )
91
+
92
+
93
+ def matrix_to_quaternion(matrix: np.ndarray) -> np.ndarray:
94
+ """Convert a proper 3x3 rotation matrix to scalar-first quaternion."""
95
+ r = np.asarray(matrix, dtype=np.float64)
96
+ if r.shape != (3, 3) or not np.isfinite(r).all():
97
+ raise ValueError("rotation matrix must be finite and have shape (3, 3)")
98
+ if not np.allclose(r.T @ r, np.eye(3), atol=2e-8) or np.linalg.det(r) <= 0:
99
+ raise ValueError("rotation matrix must be a proper rotation")
100
+ trace = float(np.trace(r))
101
+ if trace > 0:
102
+ s = 2.0 * math.sqrt(trace + 1.0)
103
+ q = np.array(
104
+ [0.25 * s, (r[2, 1] - r[1, 2]) / s, (r[0, 2] - r[2, 0]) / s, (r[1, 0] - r[0, 1]) / s]
105
+ )
106
+ else:
107
+ i = int(np.argmax(np.diag(r)))
108
+ if i == 0:
109
+ s = 2.0 * math.sqrt(max(1.0 + r[0, 0] - r[1, 1] - r[2, 2], 0.0))
110
+ q = np.array(
111
+ [
112
+ (r[2, 1] - r[1, 2]) / s,
113
+ 0.25 * s,
114
+ (r[0, 1] + r[1, 0]) / s,
115
+ (r[0, 2] + r[2, 0]) / s,
116
+ ]
117
+ )
118
+ elif i == 1:
119
+ s = 2.0 * math.sqrt(max(1.0 + r[1, 1] - r[0, 0] - r[2, 2], 0.0))
120
+ q = np.array(
121
+ [
122
+ (r[0, 2] - r[2, 0]) / s,
123
+ (r[0, 1] + r[1, 0]) / s,
124
+ 0.25 * s,
125
+ (r[1, 2] + r[2, 1]) / s,
126
+ ]
127
+ )
128
+ else:
129
+ s = 2.0 * math.sqrt(max(1.0 + r[2, 2] - r[0, 0] - r[1, 1], 0.0))
130
+ q = np.array(
131
+ [
132
+ (r[1, 0] - r[0, 1]) / s,
133
+ (r[0, 2] + r[2, 0]) / s,
134
+ (r[1, 2] + r[2, 1]) / s,
135
+ 0.25 * s,
136
+ ]
137
+ )
138
+ q = q / np.linalg.norm(q)
139
+ return q if q[0] >= 0 else -q
@@ -0,0 +1,22 @@
1
+ """Crystallography: unit cells, atomic scattering factors and structure factors."""
2
+
3
+ from de_ebsd.crystal.lattice import AtomSite, Crystal
4
+ from de_ebsd.crystal.scattering import atomic_number, atomic_weight, wk_scattering_factor
5
+ from de_ebsd.crystal.scattering_torch import wk_scattering_factor_torch
6
+ from de_ebsd.crystal.structure_factors import (
7
+ StructureFactors,
8
+ compute_structure_factors,
9
+ differentiable_ucg_lut,
10
+ )
11
+
12
+ __all__ = [
13
+ "AtomSite",
14
+ "Crystal",
15
+ "StructureFactors",
16
+ "atomic_number",
17
+ "atomic_weight",
18
+ "compute_structure_factors",
19
+ "differentiable_ucg_lut",
20
+ "wk_scattering_factor",
21
+ "wk_scattering_factor_torch",
22
+ ]
File without changes
@@ -0,0 +1,29 @@
1
+ """Standard atomic weights, indexed by atomic number.
2
+
3
+ ``ATOMIC_WEIGHTS[i]`` is the standard atomic weight (g/mol) of the element with
4
+ atomic number ``Z = i + 1`` (Z in 1..98), matching the ``ELEMENT_SYMBOLS``
5
+ ordering in :mod:`de_ebsd.crystal.data.scattering_tables`. Values are IUPAC
6
+ conventional atomic weights; for elements with no stable isotope the mass number
7
+ of the most stable common isotope is used (Tc, Pm, and Z >= 84 except Th/Pa/U).
8
+
9
+ Used by the Monte Carlo material model (density, mean atomic weight) -- five
10
+ significant figures is far finer than the depth statistics require.
11
+ """
12
+
13
+ # fmt: off
14
+ ATOMIC_WEIGHTS = [
15
+ 1.008, 4.0026, 6.94, 9.0122, 10.81, 12.011, 14.007, 15.999, # H ..O
16
+ 18.998, 20.180, 22.990, 24.305, 26.982, 28.085, 30.974, 32.06, # F ..S
17
+ 35.45, 39.95, 39.098, 40.078, 44.956, 47.867, 50.942, 51.996, # Cl ..Cr
18
+ 54.938, 55.845, 58.933, 58.693, 63.546, 65.38, 69.723, 72.630, # Mn ..Ge
19
+ 74.922, 78.971, 79.904, 83.798, 85.468, 87.62, 88.906, 91.224, # As ..Zr
20
+ 92.906, 95.95, 98.0, 101.07, 102.91, 106.42, 107.87, 112.41, # Nb ..Cd
21
+ 114.82, 118.71, 121.76, 127.60, 126.90, 131.29, 132.91, 137.33, # In ..Ba
22
+ 138.91, 140.12, 140.91, 144.24, 145.0, 150.36, 151.96, 157.25, # La ..Gd
23
+ 158.93, 162.50, 164.93, 167.26, 168.93, 173.05, 174.97, 178.49, # Tb ..Hf
24
+ 180.95, 183.84, 186.21, 190.23, 192.22, 195.08, 196.97, 200.59, # Ta ..Hg
25
+ 204.38, 207.2, 208.98, 209.0, 210.0, 222.0, 223.0, 226.0, # Tl ..Ra
26
+ 227.0, 232.04, 231.04, 238.03, 237.0, 244.0, 243.0, 247.0, # Ac ..Cm
27
+ 247.0, 251.0, # Bk ..Cf
28
+ ]
29
+ # fmt: on