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,19 @@
|
|
|
1
|
+
"""Monte Carlo electron transport and the exit-depth statistics it produces."""
|
|
2
|
+
|
|
3
|
+
from de_ebsd.sim.montecarlo.depth import DepthDistribution
|
|
4
|
+
from de_ebsd.sim.montecarlo.transport import (
|
|
5
|
+
MaterialProperties,
|
|
6
|
+
TrajectoryResult,
|
|
7
|
+
simulate_depth_distribution,
|
|
8
|
+
simulate_trajectory,
|
|
9
|
+
soft_depth_histogram,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"DepthDistribution",
|
|
14
|
+
"MaterialProperties",
|
|
15
|
+
"TrajectoryResult",
|
|
16
|
+
"simulate_depth_distribution",
|
|
17
|
+
"simulate_trajectory",
|
|
18
|
+
"soft_depth_histogram",
|
|
19
|
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Backscatter yield versus energy and exit depth.
|
|
2
|
+
|
|
3
|
+
Loads EMsoft ``EMMCOpenCL`` statistics or builds them natively, and derives the
|
|
4
|
+
two quantities the master pattern needs: the 99% integration depth per energy
|
|
5
|
+
bin (EMsoft ``izzmax``) and the depth weighting ``lambda_E(z)``, corrected for
|
|
6
|
+
the normal absorption already present in the Bloch eigenvalues.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
import h5py
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
__all__ = ["DepthDistribution"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class DepthDistribution:
|
|
21
|
+
"""Backscatter yield versus energy and exit depth.
|
|
22
|
+
|
|
23
|
+
Attributes
|
|
24
|
+
----------
|
|
25
|
+
energies_kev
|
|
26
|
+
Energy bin centers in keV, ascending, shape (nE,).
|
|
27
|
+
depth_step
|
|
28
|
+
Depth bin size in nm.
|
|
29
|
+
accum_z
|
|
30
|
+
Electron counts per (energy, depth, y, x), shape (nE, nz, ny, nx);
|
|
31
|
+
the lateral window is EMsoft's central +/- numsx/10 region.
|
|
32
|
+
n_total
|
|
33
|
+
Total number of incident electrons (totnum_el * multiplier).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
energies_kev: np.ndarray
|
|
37
|
+
depth_step: float
|
|
38
|
+
accum_z: np.ndarray
|
|
39
|
+
n_total: float
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_emsoft(cls, path: str) -> DepthDistribution:
|
|
43
|
+
"""Load from an EMsoft Monte Carlo (or master pattern) HDF5 file."""
|
|
44
|
+
with h5py.File(path, "r") as f:
|
|
45
|
+
mc = f["EMData/MCOpenCL"]
|
|
46
|
+
nml = f["NMLparameters/MCCLNameList"]
|
|
47
|
+
# EMsoft writes Fortran order (nE, nz, nx, ny); h5py presents the
|
|
48
|
+
# reversed C order (ny, nx, nz, nE).
|
|
49
|
+
accum_z = np.asarray(mc["accum_z"]).transpose(3, 2, 1, 0).astype(np.float64)
|
|
50
|
+
e_min = float(nml["Ehistmin"][0])
|
|
51
|
+
e_bin = float(nml["Ebinsize"][0])
|
|
52
|
+
n_e = accum_z.shape[0]
|
|
53
|
+
energies = e_min + e_bin * np.arange(n_e)
|
|
54
|
+
depth_step = float(nml["depthstep"][0])
|
|
55
|
+
multiplier = float(np.asarray(mc.get("multiplier", [1]))[0])
|
|
56
|
+
n_total = float(np.asarray(mc["totnum_el"])[0]) * multiplier
|
|
57
|
+
return cls(
|
|
58
|
+
energies_kev=energies,
|
|
59
|
+
depth_step=depth_step,
|
|
60
|
+
accum_z=accum_z,
|
|
61
|
+
n_total=n_total,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def single_energy(
|
|
66
|
+
cls,
|
|
67
|
+
energy_kev: float,
|
|
68
|
+
thickness: float = 100.0,
|
|
69
|
+
depth_step: float = 1.0,
|
|
70
|
+
decay_length: float = 30.0,
|
|
71
|
+
) -> DepthDistribution:
|
|
72
|
+
"""A simple exponential exit-depth model for one energy bin.
|
|
73
|
+
|
|
74
|
+
A coarse stand-in that reproduces band *geometry* but not the full
|
|
75
|
+
dynamical depth contrast. Superseded as the standalone default by the
|
|
76
|
+
native Monte Carlo (:meth:`from_simulation`), which the simulator now
|
|
77
|
+
builds automatically when no depth profile is supplied; this remains a
|
|
78
|
+
fast zero-config fallback for direct backend use.
|
|
79
|
+
"""
|
|
80
|
+
n_z = int(round(thickness / depth_step))
|
|
81
|
+
z = depth_step * np.arange(n_z)
|
|
82
|
+
profile = np.exp(-z / decay_length)
|
|
83
|
+
return cls(
|
|
84
|
+
energies_kev=np.array([energy_kev]),
|
|
85
|
+
depth_step=depth_step,
|
|
86
|
+
accum_z=profile[None, :, None, None],
|
|
87
|
+
n_total=float(profile.sum()),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_simulation(cls, material, beam_energy_kev: float, **kwargs) -> DepthDistribution:
|
|
92
|
+
"""Build depth statistics from the native Monte Carlo (EMsoft-free).
|
|
93
|
+
|
|
94
|
+
Delegates to :func:`de_ebsd.sim.montecarlo.transport.simulate_depth_distribution`
|
|
95
|
+
(see it for the keyword arguments); ``material`` is a
|
|
96
|
+
:class:`~de_ebsd.crystal.lattice.Crystal` or a ``MaterialProperties``.
|
|
97
|
+
"""
|
|
98
|
+
from de_ebsd.sim.montecarlo.transport import simulate_depth_distribution
|
|
99
|
+
|
|
100
|
+
return simulate_depth_distribution(material, beam_energy_kev, **kwargs)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def n_energies(self) -> int:
|
|
104
|
+
return len(self.energies_kev)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def n_depths(self) -> int:
|
|
108
|
+
return self.accum_z.shape[1]
|
|
109
|
+
|
|
110
|
+
def energy_index(self, energy_kev: float) -> int:
|
|
111
|
+
"""Index of the energy bin closest to ``energy_kev``."""
|
|
112
|
+
return int(np.argmin(np.abs(self.energies_kev - energy_kev)))
|
|
113
|
+
|
|
114
|
+
def integration_depths(self, fraction: float = 0.99) -> np.ndarray:
|
|
115
|
+
"""Number of depth bins to integrate per energy (EMsoft ``izzmax``).
|
|
116
|
+
|
|
117
|
+
For each lateral pixel the depth reaching ``fraction`` of that
|
|
118
|
+
pixel's yield is found; the maximum over pixels (cumulative over
|
|
119
|
+
energies, as in EMsoft) is returned per energy bin.
|
|
120
|
+
"""
|
|
121
|
+
n_e, n_z = self.n_energies, self.n_depths
|
|
122
|
+
izz = np.zeros(n_e, dtype=np.int64)
|
|
123
|
+
running_max = 0
|
|
124
|
+
for i_e in range(n_e):
|
|
125
|
+
counts = self.accum_z[i_e] # (nz, ny, nx)
|
|
126
|
+
total = counts.sum(axis=0)
|
|
127
|
+
cum = np.cumsum(counts, axis=0)
|
|
128
|
+
need = fraction * total
|
|
129
|
+
# first depth index where the cumulative count reaches the target
|
|
130
|
+
reached = cum >= need[None, :, :]
|
|
131
|
+
first = np.where(reached.any(axis=0), reached.argmax(axis=0), n_z - 1)
|
|
132
|
+
running_max = max(running_max, int(first[total > 0].max(initial=0)) + 1)
|
|
133
|
+
izz[i_e] = running_max
|
|
134
|
+
return izz
|
|
135
|
+
|
|
136
|
+
def depth_weights(self, energy_kev: float, normal_absorption_length: float) -> np.ndarray:
|
|
137
|
+
"""The weighting lambda_E(z) for one energy bin (EMsoft ``lambdaE``).
|
|
138
|
+
|
|
139
|
+
The lateral-summed exit fraction per depth bin, multiplied by
|
|
140
|
+
``exp(+2 pi z / x_gp)`` to cancel the normal absorption that the
|
|
141
|
+
Bloch eigenvalues already contain.
|
|
142
|
+
"""
|
|
143
|
+
# EMsoft's lateral window is -nsx/10:nsx/10 with nsx = (numsx-1)/2; for
|
|
144
|
+
# the stored accum_z that window spans its full lateral extent, so the
|
|
145
|
+
# full lateral sum is the correct match.
|
|
146
|
+
i_e = self.energy_index(energy_kev)
|
|
147
|
+
fraction = self.accum_z[i_e].sum(axis=(1, 2)) / self.n_total
|
|
148
|
+
z = self.depth_step * np.arange(self.n_depths)
|
|
149
|
+
return fraction * np.exp(2.0 * np.pi * z / normal_absorption_length)
|
|
150
|
+
|
|
151
|
+
def __repr__(self) -> str:
|
|
152
|
+
e = self.energies_kev
|
|
153
|
+
return (
|
|
154
|
+
f"DepthDistribution({self.n_energies} energy bins "
|
|
155
|
+
f"[{e.min():g}..{e.max():g} keV], {self.n_depths} depth bins x "
|
|
156
|
+
f"{self.depth_step:g} nm, {self.n_total:.3g} electrons)"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def show(self, energy_kev: float | None = None):
|
|
160
|
+
"""Inspect exit-depth bins and the backend's 99% integration window.
|
|
161
|
+
|
|
162
|
+
Returns an energy/slice-selectable anywidget, also exportable to HTML.
|
|
163
|
+
Selection is inspection only: it does not truncate or mutate the model.
|
|
164
|
+
"""
|
|
165
|
+
from de_ebsd.viz.widgets import DepthWidget
|
|
166
|
+
|
|
167
|
+
return DepthWidget.from_distribution(self, energy_kev=energy_kev)
|