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
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__)
|
de_ebsd/core/__init__.py
ADDED
|
@@ -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)
|
de_ebsd/core/geometry.py
ADDED
|
@@ -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)
|
de_ebsd/core/lambert.py
ADDED
|
@@ -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]
|
de_ebsd/core/rotation.py
ADDED
|
@@ -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
|