torch-scattering 0.6.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.
- torch_scattering/__init__.py +32 -0
- torch_scattering/_core.py +309 -0
- torch_scattering/firstborn.py +97 -0
- torch_scattering/multislice.py +95 -0
- torch_scattering/projection.py +55 -0
- torch_scattering/rytov.py +94 -0
- torch_scattering-0.6.0.dist-info/METADATA +212 -0
- torch_scattering-0.6.0.dist-info/RECORD +10 -0
- torch_scattering-0.6.0.dist-info/WHEEL +4 -0
- torch_scattering-0.6.0.dist-info/licenses/LICENSE +29 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Multislice electron scattering simulation in PyTorch."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from torch_scattering._core import (
|
|
6
|
+
chunk_slices,
|
|
7
|
+
fresnel_propagator,
|
|
8
|
+
interaction_parameter,
|
|
9
|
+
multislice_step,
|
|
10
|
+
transmission_function,
|
|
11
|
+
)
|
|
12
|
+
from torch_scattering.firstborn import firstborn
|
|
13
|
+
from torch_scattering.multislice import multislice
|
|
14
|
+
from torch_scattering.projection import projection
|
|
15
|
+
from torch_scattering.rytov import rytov
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
__version__ = version("torch-scattering")
|
|
19
|
+
except PackageNotFoundError:
|
|
20
|
+
__version__ = "uninstalled"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"chunk_slices",
|
|
24
|
+
"firstborn",
|
|
25
|
+
"fresnel_propagator",
|
|
26
|
+
"interaction_parameter",
|
|
27
|
+
"multislice",
|
|
28
|
+
"multislice_step",
|
|
29
|
+
"projection",
|
|
30
|
+
"rytov",
|
|
31
|
+
"transmission_function",
|
|
32
|
+
]
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Pure-math primitives for scattering algorithms."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from numbers import Real
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
from scipy import constants as C
|
|
8
|
+
from torch_ctf import calculate_relativistic_electron_wavelength
|
|
9
|
+
from torch_grid_utils import fftfreq_grid
|
|
10
|
+
|
|
11
|
+
_SUPPORTED_POTENTIAL_DTYPES = {
|
|
12
|
+
torch.float32,
|
|
13
|
+
torch.float64,
|
|
14
|
+
torch.complex64,
|
|
15
|
+
torch.complex128,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _complex_dtype(dtype: torch.dtype) -> torch.dtype:
|
|
20
|
+
"""Return the complex dtype corresponding to a supported potential dtype."""
|
|
21
|
+
if dtype in {torch.float32, torch.complex64}:
|
|
22
|
+
return torch.complex64
|
|
23
|
+
return torch.complex128
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _validate_scalar(
|
|
27
|
+
value: float | torch.Tensor,
|
|
28
|
+
name: str,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Validate a positive, finite scalar without replacing differentiable tensors."""
|
|
31
|
+
if isinstance(value, torch.Tensor):
|
|
32
|
+
if value.ndim != 0:
|
|
33
|
+
raise ValueError(f"{name} must be a scalar")
|
|
34
|
+
if value.is_complex():
|
|
35
|
+
raise ValueError(f"{name} must be real")
|
|
36
|
+
if not bool(torch.isfinite(value).item()) or not bool((value > 0).item()):
|
|
37
|
+
raise ValueError(f"{name} must be positive and finite")
|
|
38
|
+
elif (
|
|
39
|
+
not isinstance(value, Real)
|
|
40
|
+
or isinstance(value, bool)
|
|
41
|
+
or not math.isfinite(float(value))
|
|
42
|
+
or value <= 0
|
|
43
|
+
):
|
|
44
|
+
raise ValueError(f"{name} must be a positive, finite scalar")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _validate_propagation_inputs(
|
|
48
|
+
potential: torch.Tensor,
|
|
49
|
+
pixel_size: float | torch.Tensor,
|
|
50
|
+
voltage: float | torch.Tensor,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Validate inputs shared by all high-level propagation modes."""
|
|
53
|
+
if not isinstance(potential, torch.Tensor):
|
|
54
|
+
raise TypeError("potential must be a torch.Tensor")
|
|
55
|
+
if potential.ndim < 3:
|
|
56
|
+
raise ValueError("potential must have shape (..., Z, H, W)")
|
|
57
|
+
if any(size == 0 for size in potential.shape[-3:]):
|
|
58
|
+
raise ValueError("potential spatial dimensions Z, H, and W must be nonempty")
|
|
59
|
+
if potential.dtype not in _SUPPORTED_POTENTIAL_DTYPES:
|
|
60
|
+
raise TypeError(
|
|
61
|
+
"potential dtype must be float32, float64, complex64, or complex128"
|
|
62
|
+
)
|
|
63
|
+
_validate_scalar(pixel_size, "pixel_size")
|
|
64
|
+
_validate_scalar(voltage, "voltage")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def fresnel_propagator(
|
|
68
|
+
frequency_grid: torch.Tensor,
|
|
69
|
+
wavelength: float | torch.Tensor,
|
|
70
|
+
dz: float | torch.Tensor,
|
|
71
|
+
) -> torch.Tensor:
|
|
72
|
+
"""
|
|
73
|
+
Compute the Fresnel free-space propagator for one multislice step.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
frequency_grid : torch.Tensor
|
|
78
|
+
Real-valued grid of spatial frequency magnitudes (1/Angstrom), e.g.
|
|
79
|
+
from ``torch_grid_utils.fftfreq_grid(..., norm=True)``.
|
|
80
|
+
wavelength : float | torch.Tensor
|
|
81
|
+
Relativistic electron wavelength in Angstroms.
|
|
82
|
+
dz : float | torch.Tensor
|
|
83
|
+
Propagation distance (slice thickness) in Angstroms.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
torch.Tensor
|
|
88
|
+
Complex-valued Fresnel propagator with the same shape as
|
|
89
|
+
`frequency_grid`.
|
|
90
|
+
|
|
91
|
+
Notes
|
|
92
|
+
-----
|
|
93
|
+
``H(k) = exp(i * pi * wavelength * dz * k^2)``. This is a pure phase
|
|
94
|
+
transfer function, so ``|H(k)| == 1`` for all `k`.
|
|
95
|
+
|
|
96
|
+
References
|
|
97
|
+
----------
|
|
98
|
+
.. [1] E. J. Kirkland, Advanced Computing in Electron Microscopy,
|
|
99
|
+
Springer US, Boston, MA, 2010.
|
|
100
|
+
"""
|
|
101
|
+
return torch.exp(1j * C.pi * wavelength * dz * frequency_grid**2)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def transmission_function(
|
|
105
|
+
potential_slice: torch.Tensor,
|
|
106
|
+
sigma: float | torch.Tensor,
|
|
107
|
+
dz: float | torch.Tensor,
|
|
108
|
+
) -> torch.Tensor:
|
|
109
|
+
"""
|
|
110
|
+
Compute the transmission function for one electrostatic-potential slice.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
potential_slice : torch.Tensor
|
|
115
|
+
Electrostatic potential in volts for a single slice. Real-valued for
|
|
116
|
+
a non-absorbing specimen, or complex-valued with a positive imaginary
|
|
117
|
+
part to model absorption.
|
|
118
|
+
sigma : float | torch.Tensor
|
|
119
|
+
Electron-specimen interaction parameter, e.g. from
|
|
120
|
+
`interaction_parameter`, in rad/(V*Angstrom).
|
|
121
|
+
dz : float | torch.Tensor
|
|
122
|
+
Slice thickness in Angstroms.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
torch.Tensor
|
|
127
|
+
Complex-valued transmission function, same shape as
|
|
128
|
+
`potential_slice`.
|
|
129
|
+
|
|
130
|
+
Notes
|
|
131
|
+
-----
|
|
132
|
+
``T = exp(i * sigma * dz * V)``. The phase shift imparted by a slice is
|
|
133
|
+
the line integral of `sigma * V` along the beam direction through the
|
|
134
|
+
slice; treating `V` as constant over the (thin) slice thickness reduces
|
|
135
|
+
that integral to `sigma * V * dz`. For real `V` this makes `T` a pure
|
|
136
|
+
phase mask (``|T| == 1``); a positive imaginary part of `V` attenuates
|
|
137
|
+
the wave.
|
|
138
|
+
"""
|
|
139
|
+
return torch.exp(1j * sigma * dz * potential_slice)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def multislice_step(
|
|
143
|
+
wave: torch.Tensor,
|
|
144
|
+
potential_slice: torch.Tensor,
|
|
145
|
+
propagator: torch.Tensor,
|
|
146
|
+
sigma: float | torch.Tensor,
|
|
147
|
+
dz: float | torch.Tensor,
|
|
148
|
+
) -> torch.Tensor:
|
|
149
|
+
"""
|
|
150
|
+
Advance the electron wave through one electrostatic-potential slice.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
wave : torch.Tensor
|
|
155
|
+
Complex-valued incident wave for this slice, shape (..., H, W).
|
|
156
|
+
potential_slice : torch.Tensor
|
|
157
|
+
Electrostatic potential in volts for this slice, shape (..., H, W).
|
|
158
|
+
propagator : torch.Tensor
|
|
159
|
+
Fresnel propagator for this slice thickness, e.g. from
|
|
160
|
+
`fresnel_propagator`, shape (H, W).
|
|
161
|
+
sigma : float | torch.Tensor
|
|
162
|
+
Electron-specimen interaction parameter, in rad/(V*Angstrom).
|
|
163
|
+
dz : float | torch.Tensor
|
|
164
|
+
Slice thickness in Angstroms.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
torch.Tensor
|
|
169
|
+
Complex-valued wave after transmission through and propagation
|
|
170
|
+
past this slice, same shape as `wave`.
|
|
171
|
+
|
|
172
|
+
Notes
|
|
173
|
+
-----
|
|
174
|
+
Each step alternates transmission through the slice (real space) with
|
|
175
|
+
Fresnel propagation to the next slice (Fourier space):
|
|
176
|
+
``wave_out = IFFT(FFT(wave * T) * H)``
|
|
177
|
+
"""
|
|
178
|
+
transmitted = wave * transmission_function(potential_slice, sigma, dz)
|
|
179
|
+
propagated_wave: torch.Tensor = torch.fft.ifft2(
|
|
180
|
+
torch.fft.fft2(transmitted) * propagator
|
|
181
|
+
)
|
|
182
|
+
return propagated_wave
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def chunk_slices(total_slices: int, n_chunks: int) -> list[int]:
|
|
186
|
+
"""
|
|
187
|
+
Compute contiguous, back-loaded chunk sizes for coarsened multislice.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
total_slices : int
|
|
192
|
+
Total number of slices along the beam direction.
|
|
193
|
+
n_chunks : int
|
|
194
|
+
Desired number of chunks. Must satisfy ``0 < n_chunks <= total_slices``.
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
list[int]
|
|
199
|
+
Chunk sizes summing to `total_slices`, length `n_chunks`, suitable
|
|
200
|
+
for `torch.split(..., split_size_or_sections=sizes, dim=...)`. Every
|
|
201
|
+
chunk has ``total_slices // n_chunks`` slices except the last,
|
|
202
|
+
which absorbs the remainder ``total_slices % n_chunks``. This is a
|
|
203
|
+
back-loaded remainder, unlike `torch.chunk`/`torch.tensor_split`,
|
|
204
|
+
which front-load it.
|
|
205
|
+
|
|
206
|
+
Raises
|
|
207
|
+
------
|
|
208
|
+
ValueError
|
|
209
|
+
If `n_chunks` is not in ``(0, total_slices]``.
|
|
210
|
+
|
|
211
|
+
Examples
|
|
212
|
+
--------
|
|
213
|
+
>>> chunk_slices(10, 3)
|
|
214
|
+
[3, 3, 4]
|
|
215
|
+
"""
|
|
216
|
+
if not (0 < n_chunks <= total_slices):
|
|
217
|
+
raise ValueError(
|
|
218
|
+
"n_chunks must satisfy 0 < n_chunks <= total_slices "
|
|
219
|
+
f"({total_slices}), got {n_chunks}"
|
|
220
|
+
)
|
|
221
|
+
base = total_slices // n_chunks
|
|
222
|
+
sizes = [base] * n_chunks
|
|
223
|
+
sizes[-1] += total_slices - base * n_chunks
|
|
224
|
+
return sizes
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def interaction_parameter(voltage: float | torch.Tensor) -> float | torch.Tensor:
|
|
228
|
+
"""
|
|
229
|
+
Calculate the electron-specimen interaction parameter.
|
|
230
|
+
|
|
231
|
+
Computes the interaction constant sigma for electron scattering,
|
|
232
|
+
following Kirkland Eq. (5.6). Wavelength is computed internally from
|
|
233
|
+
`voltage` via `torch_ctf.calculate_relativistic_electron_wavelength`.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
voltage : float | torch.Tensor
|
|
238
|
+
Electron beam acceleration voltage in kilovolts.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
float | torch.Tensor
|
|
243
|
+
Interaction parameter sigma in units of rad/(V*Angstrom). A tensor
|
|
244
|
+
if `voltage` is a tensor, otherwise a plain float.
|
|
245
|
+
|
|
246
|
+
References
|
|
247
|
+
----------
|
|
248
|
+
.. [1] E. J. Kirkland, Advanced Computing in Electron Microscopy,
|
|
249
|
+
Eq. (5.6), Springer US, Boston, MA, 2010.
|
|
250
|
+
"""
|
|
251
|
+
wavelength_m = calculate_relativistic_electron_wavelength(voltage * 1.0e3)
|
|
252
|
+
wavelength = wavelength_m * 1.0e10 # meters -> Angstroms
|
|
253
|
+
# Electron rest mass energy, m0*c^2, in electronvolts.
|
|
254
|
+
rest_ev = C.electron_mass * C.speed_of_light**2 / C.elementary_charge
|
|
255
|
+
ev = voltage * 1.0e3 # [eV]
|
|
256
|
+
sigma: float | torch.Tensor = (
|
|
257
|
+
2.0 * C.pi / (wavelength * ev) * ((ev + rest_ev) / (ev + 2.0 * rest_ev))
|
|
258
|
+
)
|
|
259
|
+
return sigma
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _prepare_propagation_parameters(
|
|
263
|
+
potential: torch.Tensor,
|
|
264
|
+
pixel_size: float | torch.Tensor,
|
|
265
|
+
voltage: float | torch.Tensor,
|
|
266
|
+
) -> tuple[torch.Tensor, float | torch.Tensor, torch.Tensor]:
|
|
267
|
+
"""
|
|
268
|
+
Compute the wavelength, interaction parameter, and frequency grid.
|
|
269
|
+
|
|
270
|
+
Shared setup for the high-level wrapper functions (`firstborn`,
|
|
271
|
+
`rytov`, `multislice`).
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
potential : torch.Tensor
|
|
276
|
+
Real or complex 3D electrostatic potential in volts, shape
|
|
277
|
+
(..., Z, H, W).
|
|
278
|
+
pixel_size : float | torch.Tensor
|
|
279
|
+
Isotropic voxel spacing in Angstroms, used as both the in-plane pixel
|
|
280
|
+
spacing and slice thickness.
|
|
281
|
+
voltage : float | torch.Tensor
|
|
282
|
+
Electron beam acceleration voltage in kilovolts.
|
|
283
|
+
|
|
284
|
+
Returns
|
|
285
|
+
-------
|
|
286
|
+
tuple[torch.Tensor, float | torch.Tensor, torch.Tensor]
|
|
287
|
+
`wavelength` in Angstroms, `sigma` (interaction parameter) in
|
|
288
|
+
rad/(V*Angstrom), and `frequency_grid`, the real-valued grid of
|
|
289
|
+
spatial frequency magnitudes (1/Angstrom), shape (H, W).
|
|
290
|
+
"""
|
|
291
|
+
_validate_propagation_inputs(potential, pixel_size, voltage)
|
|
292
|
+
wavelength_m = calculate_relativistic_electron_wavelength(voltage * 1.0e3)
|
|
293
|
+
wavelength = wavelength_m * 1.0e10 # meters -> Angstroms
|
|
294
|
+
sigma = interaction_parameter(voltage=voltage)
|
|
295
|
+
|
|
296
|
+
height, width = potential.shape[-2:]
|
|
297
|
+
spacing = (
|
|
298
|
+
float(pixel_size.detach().cpu())
|
|
299
|
+
if isinstance(pixel_size, torch.Tensor)
|
|
300
|
+
else float(pixel_size)
|
|
301
|
+
)
|
|
302
|
+
frequency_grid = fftfreq_grid(
|
|
303
|
+
image_shape=(height, width),
|
|
304
|
+
rfft=False,
|
|
305
|
+
spacing=spacing,
|
|
306
|
+
norm=True,
|
|
307
|
+
device=potential.device,
|
|
308
|
+
).to(dtype=potential.real.dtype)
|
|
309
|
+
return wavelength, sigma, frequency_grid
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""High-level first Born approximation wrapper.
|
|
2
|
+
|
|
3
|
+
Wires up frequency grids, wavelength, and a per-slice Fresnel propagator
|
|
4
|
+
stack on top of the pure-math primitives in `_core`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from torch_scattering._core import (
|
|
10
|
+
_prepare_propagation_parameters,
|
|
11
|
+
chunk_slices,
|
|
12
|
+
fresnel_propagator,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def firstborn(
|
|
17
|
+
potential: torch.Tensor,
|
|
18
|
+
pixel_size: float | torch.Tensor,
|
|
19
|
+
voltage: float | torch.Tensor,
|
|
20
|
+
n_slices: int | None = None,
|
|
21
|
+
) -> torch.Tensor:
|
|
22
|
+
"""
|
|
23
|
+
Compute the exit wave using the first Born approximation.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
potential : torch.Tensor
|
|
28
|
+
Real-valued electrostatic potential in volts for a non-absorbing
|
|
29
|
+
specimen, or complex-valued potential when modelling absorption.
|
|
30
|
+
Shape (..., Z, H, W), where Z is the beam direction.
|
|
31
|
+
pixel_size : float | torch.Tensor
|
|
32
|
+
Positive finite isotropic voxel spacing in Angstroms. This is both
|
|
33
|
+
the in-plane pixel spacing and slice thickness.
|
|
34
|
+
voltage : float | torch.Tensor
|
|
35
|
+
Electron beam acceleration voltage in kilovolts (e.g. 300 for 300 kV).
|
|
36
|
+
n_slices : int | None
|
|
37
|
+
Number of terms to sum in the Born series. If `None` (default),
|
|
38
|
+
every slice of `potential` is treated individually (equivalent to
|
|
39
|
+
`n_slices=Z`). If smaller than `Z`, slices are grouped into
|
|
40
|
+
`n_slices` contiguous chunks via `chunk_slices` (all but the last
|
|
41
|
+
chunk have the same size; the last absorbs the remainder), and each
|
|
42
|
+
chunk's summed potential is propagated as one thicker slab. Must
|
|
43
|
+
satisfy ``0 < n_slices <= Z``.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
torch.Tensor
|
|
48
|
+
Complex-valued 2D exit wave, shape (..., H, W).
|
|
49
|
+
|
|
50
|
+
Notes
|
|
51
|
+
-----
|
|
52
|
+
Each chunk's scattered contribution is propagated directly to the exit
|
|
53
|
+
plane and summed, rather than being propagated incrementally
|
|
54
|
+
slice-by-slice as in `multislice`:
|
|
55
|
+
``psi = 1 + sum_c IFFT(FFT(i * sigma * dz * V_c) * H_c)``, where `V_c`
|
|
56
|
+
is the summed potential of chunk `c` and `H_c` is the Fresnel
|
|
57
|
+
propagator for the distance from chunk `c` to the exit plane. This is
|
|
58
|
+
the linearised (first-order) term of the exponentiated Born series
|
|
59
|
+
used by `rytov`, valid only for weak potentials (``sigma * dz * V <<
|
|
60
|
+
1``). Faster than `multislice` (one FFT pass instead of one per chunk)
|
|
61
|
+
but less accurate for thick or strongly scattering specimens, and less
|
|
62
|
+
accurate than `rytov` for the same potential since it does not
|
|
63
|
+
exponentiate the scattered sum.
|
|
64
|
+
|
|
65
|
+
References
|
|
66
|
+
----------
|
|
67
|
+
.. [1] E. J. Kirkland, Advanced Computing in Electron Microscopy,
|
|
68
|
+
Springer US, Boston, MA, 2010.
|
|
69
|
+
"""
|
|
70
|
+
wavelength, sigma, frequency_grid = _prepare_propagation_parameters(
|
|
71
|
+
potential, pixel_size, voltage
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
total_slices = potential.shape[-3]
|
|
75
|
+
if n_slices is None:
|
|
76
|
+
n_slices = total_slices
|
|
77
|
+
sizes = chunk_slices(total_slices, n_slices)
|
|
78
|
+
chunk_potential = torch.stack(
|
|
79
|
+
[chunk.sum(dim=-3) for chunk in torch.split(potential, sizes, dim=-3)],
|
|
80
|
+
dim=-3,
|
|
81
|
+
) # (..., n_slices, H, W)
|
|
82
|
+
|
|
83
|
+
# Chunk c is (sum of sizes[c:]) slice thicknesses from the exit plane:
|
|
84
|
+
# the wave still has to cross the remainder of chunk c itself plus
|
|
85
|
+
# every chunk after it.
|
|
86
|
+
size_tensor = torch.as_tensor(sizes, device=potential.device)
|
|
87
|
+
remaining_slices = size_tensor.flip(0).cumsum(0).flip(0)
|
|
88
|
+
distances = remaining_slices * pixel_size
|
|
89
|
+
propagators = fresnel_propagator(
|
|
90
|
+
frequency_grid, wavelength=wavelength, dz=distances[:, None, None]
|
|
91
|
+
) # (n_slices, H, W)
|
|
92
|
+
|
|
93
|
+
scattered = torch.fft.ifft2(
|
|
94
|
+
torch.fft.fft2(1j * sigma * pixel_size * chunk_potential) * propagators
|
|
95
|
+
)
|
|
96
|
+
exit_wave: torch.Tensor = 1 + scattered.sum(dim=-3)
|
|
97
|
+
return exit_wave
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""High-level multislice wrapper.
|
|
2
|
+
|
|
3
|
+
Wires up frequency grids, wavelength, and the per-slice loop on top of the
|
|
4
|
+
pure-math primitives in `_core`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from torch_scattering._core import (
|
|
10
|
+
_complex_dtype,
|
|
11
|
+
_prepare_propagation_parameters,
|
|
12
|
+
chunk_slices,
|
|
13
|
+
fresnel_propagator,
|
|
14
|
+
multislice_step,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def multislice(
|
|
19
|
+
potential: torch.Tensor,
|
|
20
|
+
pixel_size: float | torch.Tensor,
|
|
21
|
+
voltage: float | torch.Tensor,
|
|
22
|
+
n_slices: int | None = None,
|
|
23
|
+
) -> torch.Tensor:
|
|
24
|
+
"""
|
|
25
|
+
Compute the 2D exit wave from a 3D electrostatic potential.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
potential : torch.Tensor
|
|
30
|
+
Real-valued electrostatic potential in volts for a non-absorbing
|
|
31
|
+
specimen, or complex-valued potential when modelling absorption.
|
|
32
|
+
Shape (..., Z, H, W), where Z is the beam direction.
|
|
33
|
+
pixel_size : float | torch.Tensor
|
|
34
|
+
Positive finite isotropic voxel spacing in Angstroms. This is both
|
|
35
|
+
the in-plane pixel spacing and slice thickness.
|
|
36
|
+
voltage : float | torch.Tensor
|
|
37
|
+
Electron beam acceleration voltage in kilovolts (e.g. 300 for 300 kV).
|
|
38
|
+
n_slices : int | None
|
|
39
|
+
Number of multislice steps to take. If `None` (default), every
|
|
40
|
+
slice of `potential` is propagated individually (equivalent to
|
|
41
|
+
`n_slices=Z`). If smaller than `Z`, slices are grouped into
|
|
42
|
+
`n_slices` contiguous chunks via `chunk_slices` (all but the last
|
|
43
|
+
chunk have the same size; the last absorbs the remainder), each
|
|
44
|
+
chunk's potential is summed, and propagated as one thicker slab.
|
|
45
|
+
Must satisfy ``0 < n_slices <= Z``.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
torch.Tensor
|
|
50
|
+
Complex-valued 2D exit wave, shape (..., H, W).
|
|
51
|
+
|
|
52
|
+
Notes
|
|
53
|
+
-----
|
|
54
|
+
The incident wave entering the specimen is a uniform (unit-amplitude,
|
|
55
|
+
zero-phase) plane wave. Each chunk's summed potential is transmitted
|
|
56
|
+
with `dz=pixel_size` (the per-voxel Riemann-sum weight) and then
|
|
57
|
+
propagated with `dz=chunk_size * pixel_size` (the chunk's physical
|
|
58
|
+
thickness). `n_slices=None` propagates one true slice at a time and
|
|
59
|
+
is the most accurate setting; grouping trades accuracy for speed.
|
|
60
|
+
|
|
61
|
+
`torch_grid_utils.fftfreq_grid` only accepts a plain float for its
|
|
62
|
+
`spacing` argument, so if `pixel_size` is a tensor, the frequency grid
|
|
63
|
+
itself is built from a detached float copy; `pixel_size` still flows
|
|
64
|
+
as a tensor into the propagator and transmission function, so gradients
|
|
65
|
+
with respect to it are not lost, just not routed through the grid
|
|
66
|
+
spacing.
|
|
67
|
+
"""
|
|
68
|
+
wavelength, sigma, frequency_grid = _prepare_propagation_parameters(
|
|
69
|
+
potential, pixel_size, voltage
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
height, width = potential.shape[-2:]
|
|
73
|
+
wave = torch.ones(
|
|
74
|
+
(*potential.shape[:-3], height, width),
|
|
75
|
+
dtype=_complex_dtype(potential.dtype),
|
|
76
|
+
device=potential.device,
|
|
77
|
+
)
|
|
78
|
+
total_slices = potential.shape[-3]
|
|
79
|
+
if n_slices is None:
|
|
80
|
+
n_slices = total_slices
|
|
81
|
+
sizes = chunk_slices(total_slices, n_slices)
|
|
82
|
+
# At most 2 distinct chunk sizes occur; build one propagator per size.
|
|
83
|
+
propagators = {
|
|
84
|
+
size: fresnel_propagator(
|
|
85
|
+
frequency_grid, wavelength=wavelength, dz=pixel_size * size
|
|
86
|
+
)
|
|
87
|
+
for size in set(sizes)
|
|
88
|
+
}
|
|
89
|
+
for chunk in torch.split(potential, sizes, dim=-3):
|
|
90
|
+
chunk_size = chunk.shape[-3]
|
|
91
|
+
potential_chunk = chunk.sum(dim=-3)
|
|
92
|
+
wave = multislice_step(
|
|
93
|
+
wave, potential_chunk, propagators[chunk_size], sigma, pixel_size
|
|
94
|
+
)
|
|
95
|
+
return wave
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""High-level projection-approximation wrapper.
|
|
2
|
+
|
|
3
|
+
Wires up the interaction parameter and slice summation on top of the
|
|
4
|
+
pure-math primitives in `_core`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from torch_scattering._core import (
|
|
10
|
+
_validate_propagation_inputs,
|
|
11
|
+
interaction_parameter,
|
|
12
|
+
transmission_function,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def projection(
|
|
17
|
+
potential: torch.Tensor,
|
|
18
|
+
pixel_size: float | torch.Tensor,
|
|
19
|
+
voltage: float | torch.Tensor,
|
|
20
|
+
) -> torch.Tensor:
|
|
21
|
+
"""
|
|
22
|
+
Compute the exit wave using the projection approximation.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
potential : torch.Tensor
|
|
27
|
+
Real-valued electrostatic potential in volts for a non-absorbing
|
|
28
|
+
specimen, or complex-valued potential when modelling absorption.
|
|
29
|
+
Shape (..., Z, H, W), where Z is the beam direction.
|
|
30
|
+
pixel_size : float | torch.Tensor
|
|
31
|
+
Positive finite isotropic voxel spacing in Angstroms. This is both
|
|
32
|
+
the in-plane pixel spacing and slice thickness.
|
|
33
|
+
voltage : float | torch.Tensor
|
|
34
|
+
Electron beam acceleration voltage in kilovolts (e.g. 300 for 300 kV).
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
torch.Tensor
|
|
39
|
+
Complex-valued 2D exit wave, shape (..., H, W).
|
|
40
|
+
|
|
41
|
+
Notes
|
|
42
|
+
-----
|
|
43
|
+
This wave-propagation approximation numerically sums a sampled 3D
|
|
44
|
+
potential along the beam direction, then treats the specimen as
|
|
45
|
+
infinitely thin and ignores Fresnel propagation between slices:
|
|
46
|
+
``psi = exp(i * sigma * dz * sum_z V(z))``. This is valid only when the
|
|
47
|
+
specimen is thin enough that propagation effects within it are
|
|
48
|
+
negligible. It is distinct from an analytic projected-potential
|
|
49
|
+
calculation and from projection alignment used when fitting structures
|
|
50
|
+
into maps.
|
|
51
|
+
"""
|
|
52
|
+
_validate_propagation_inputs(potential, pixel_size, voltage)
|
|
53
|
+
sigma = interaction_parameter(voltage=voltage)
|
|
54
|
+
projected_potential = potential.sum(dim=-3)
|
|
55
|
+
return transmission_function(projected_potential, sigma=sigma, dz=pixel_size)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""High-level Rytov-approximation wrapper.
|
|
2
|
+
|
|
3
|
+
Wires up frequency grids, wavelength, and a per-slice Fresnel propagator
|
|
4
|
+
stack on top of the pure-math primitives in `_core`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from torch_scattering._core import (
|
|
10
|
+
_prepare_propagation_parameters,
|
|
11
|
+
chunk_slices,
|
|
12
|
+
fresnel_propagator,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def rytov(
|
|
17
|
+
potential: torch.Tensor,
|
|
18
|
+
pixel_size: float | torch.Tensor,
|
|
19
|
+
voltage: float | torch.Tensor,
|
|
20
|
+
n_slices: int | None = None,
|
|
21
|
+
) -> torch.Tensor:
|
|
22
|
+
"""
|
|
23
|
+
Compute the exit wave using the Rytov approximation.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
potential : torch.Tensor
|
|
28
|
+
Real-valued electrostatic potential in volts for a non-absorbing
|
|
29
|
+
specimen, or complex-valued potential when modelling absorption.
|
|
30
|
+
Shape (..., Z, H, W), where Z is the beam direction.
|
|
31
|
+
pixel_size : float | torch.Tensor
|
|
32
|
+
Positive finite isotropic voxel spacing in Angstroms. This is both
|
|
33
|
+
the in-plane pixel spacing and slice thickness.
|
|
34
|
+
voltage : float | torch.Tensor
|
|
35
|
+
Electron beam acceleration voltage in kilovolts (e.g. 300 for 300 kV).
|
|
36
|
+
n_slices : int | None
|
|
37
|
+
Number of terms to sum in the exponentiated Born series. If `None`
|
|
38
|
+
(default), every slice of `potential` is treated individually
|
|
39
|
+
(equivalent to `n_slices=Z`). If smaller than `Z`, slices are
|
|
40
|
+
grouped into `n_slices` contiguous chunks via `chunk_slices` (all
|
|
41
|
+
but the last chunk have the same size; the last absorbs the
|
|
42
|
+
remainder), and each chunk's summed potential is propagated as one
|
|
43
|
+
thicker slab. Must satisfy ``0 < n_slices <= Z``.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
torch.Tensor
|
|
48
|
+
Complex-valued 2D exit wave, shape (..., H, W).
|
|
49
|
+
|
|
50
|
+
Notes
|
|
51
|
+
-----
|
|
52
|
+
Each chunk's scattered contribution is propagated directly to the exit
|
|
53
|
+
plane and summed inside a single exponential, rather than being
|
|
54
|
+
propagated incrementally slice-by-slice as in `multislice`:
|
|
55
|
+
``psi = exp(sum_c IFFT(FFT(i * sigma * dz * V_c) * H_c))``, where `V_c`
|
|
56
|
+
is the summed potential of chunk `c` and `H_c` is the Fresnel
|
|
57
|
+
propagator for the distance from chunk `c` to the exit plane. This is
|
|
58
|
+
the exponentiated Born series, reducing to the first Born approximation
|
|
59
|
+
for small `V`. Faster than `multislice` (one FFT pass instead of one
|
|
60
|
+
per chunk) but less accurate for thick specimens. `n_slices=None` is
|
|
61
|
+
the most accurate setting; grouping trades accuracy for speed.
|
|
62
|
+
|
|
63
|
+
References
|
|
64
|
+
----------
|
|
65
|
+
.. [1] E. J. Kirkland, Advanced Computing in Electron Microscopy,
|
|
66
|
+
Springer US, Boston, MA, 2010.
|
|
67
|
+
"""
|
|
68
|
+
wavelength, sigma, frequency_grid = _prepare_propagation_parameters(
|
|
69
|
+
potential, pixel_size, voltage
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
total_slices = potential.shape[-3]
|
|
73
|
+
if n_slices is None:
|
|
74
|
+
n_slices = total_slices
|
|
75
|
+
sizes = chunk_slices(total_slices, n_slices)
|
|
76
|
+
chunk_potential = torch.stack(
|
|
77
|
+
[chunk.sum(dim=-3) for chunk in torch.split(potential, sizes, dim=-3)],
|
|
78
|
+
dim=-3,
|
|
79
|
+
) # (..., n_slices, H, W)
|
|
80
|
+
|
|
81
|
+
# Chunk c is (sum of sizes[c:]) slice thicknesses from the exit plane:
|
|
82
|
+
# the wave still has to cross the remainder of chunk c itself plus
|
|
83
|
+
# every chunk after it.
|
|
84
|
+
size_tensor = torch.as_tensor(sizes, device=potential.device)
|
|
85
|
+
remaining_slices = size_tensor.flip(0).cumsum(0).flip(0)
|
|
86
|
+
distances = remaining_slices * pixel_size
|
|
87
|
+
propagators = fresnel_propagator(
|
|
88
|
+
frequency_grid, wavelength=wavelength, dz=distances[:, None, None]
|
|
89
|
+
) # (n_slices, H, W)
|
|
90
|
+
|
|
91
|
+
scattered = torch.fft.ifft2(
|
|
92
|
+
torch.fft.fft2(1j * sigma * pixel_size * chunk_potential) * propagators
|
|
93
|
+
)
|
|
94
|
+
return torch.exp(scattered.sum(dim=-3))
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: torch-scattering
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Multislice electron scattering simulation in PyTorch
|
|
5
|
+
Project-URL: homepage, https://github.com/teamtomo/teamtomo
|
|
6
|
+
Project-URL: repository, https://github.com/teamtomo/teamtomo
|
|
7
|
+
Author-email: Joel Yeo <joelyeo.is@gmail.com>
|
|
8
|
+
License: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: scipy
|
|
20
|
+
Requires-Dist: torch
|
|
21
|
+
Requires-Dist: torch-ctf
|
|
22
|
+
Requires-Dist: torch-grid-utils
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# torch-scattering
|
|
26
|
+
|
|
27
|
+
[](https://github.com/joelyeois/torch-scattering/raw/main/LICENSE)
|
|
28
|
+
[](https://pypi.org/project/torch-scattering)
|
|
29
|
+
[](https://python.org)
|
|
30
|
+
[](https://github.com/joelyeois/torch-scattering/actions/workflows/ci.yml)
|
|
31
|
+
[](https://codecov.io/gh/joelyeois/torch-scattering)
|
|
32
|
+
|
|
33
|
+
Multislice electron scattering simulation in PyTorch, for cryo-EM/cryo-ET forward modelling.
|
|
34
|
+
|
|
35
|
+
## Overview
|
|
36
|
+
|
|
37
|
+
`torch_scattering` computes the 2D exit wave produced by propagating an electron
|
|
38
|
+
beam through a 3D electrostatic potential in volts. The potential has shape
|
|
39
|
+
`(..., Z, H, W)`, where Z is the beam direction. `pixel_size` is the isotropic
|
|
40
|
+
voxel spacing in Angstroms, so it specifies both the Y/X pixel spacing and the
|
|
41
|
+
Z slice thickness. Every function returns a complex exit wave of shape
|
|
42
|
+
`(..., H, W)`.
|
|
43
|
+
|
|
44
|
+
Real `float32` and `float64` potentials model non-absorbing specimens and can be
|
|
45
|
+
passed directly; callers do not need to cast them to complex. Complex potentials
|
|
46
|
+
remain supported for modelling absorption.
|
|
47
|
+
|
|
48
|
+
Four propagation modes are provided, trading physical accuracy for speed:
|
|
49
|
+
|
|
50
|
+
* `multislice()` - full multislice propagation (Kirkland, *Advanced Computing in
|
|
51
|
+
Electron Microscopy*), alternating transmission through each slice with Fresnel
|
|
52
|
+
propagation to the next. The most accurate mode.
|
|
53
|
+
* `rytov()` - Rytov approximation, accumulating phase in the exponent rather than
|
|
54
|
+
the wave itself.
|
|
55
|
+
* `firstborn()` - first Born approximation, summing single-scattering
|
|
56
|
+
contributions from each slice.
|
|
57
|
+
* `projection()` - projection approximation, treating the specimen as infinitely
|
|
58
|
+
thin and skipping inter-slice propagation entirely. The fastest and least
|
|
59
|
+
accurate mode.
|
|
60
|
+
|
|
61
|
+
All four share the same required inputs and can be swapped in for one another.
|
|
62
|
+
`multislice`, `rytov`, and `firstborn` also accept an `n_slices` argument to
|
|
63
|
+
coarsen the potential into fewer, thicker slabs before propagating.
|
|
64
|
+
|
|
65
|
+
Lower-level, pure-math primitives (`fresnel_propagator`, `transmission_function`,
|
|
66
|
+
`multislice_step`, `chunk_slices`, `interaction_parameter`) are also exposed for
|
|
67
|
+
building custom propagation schemes.
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
```shell
|
|
72
|
+
pip install torch-scattering
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import torch
|
|
79
|
+
from torch_scattering import multislice
|
|
80
|
+
|
|
81
|
+
# A real electrostatic potential in volts, shape (Z, H, W).
|
|
82
|
+
potential = torch.zeros((50, 64, 64), dtype=torch.float32)
|
|
83
|
+
|
|
84
|
+
# propagate a plane wave through it
|
|
85
|
+
exit_wave = multislice(
|
|
86
|
+
potential=potential,
|
|
87
|
+
pixel_size=1.0, # Angstroms
|
|
88
|
+
voltage=300, # kV
|
|
89
|
+
)
|
|
90
|
+
# exit_wave.shape is (64, 64)
|
|
91
|
+
# exit_wave.dtype is torch.complex64
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`rytov`, `firstborn`, and `projection` share the same call signature:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from torch_scattering import firstborn, projection, rytov
|
|
98
|
+
|
|
99
|
+
exit_wave = rytov(potential, pixel_size=1.0, voltage=300)
|
|
100
|
+
exit_wave = firstborn(potential, pixel_size=1.0, voltage=300)
|
|
101
|
+
exit_wave = projection(potential, pixel_size=1.0, voltage=300) # n_slices not applicable
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Coarsening slices
|
|
105
|
+
|
|
106
|
+
`n_slices` groups the potential into fewer, thicker slabs before propagating.
|
|
107
|
+
By default (`n_slices=None`), every slice of the potential is propagated
|
|
108
|
+
individually - the most accurate but slowest setting.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# propagate as 10 chunks instead of all 50 slices individually
|
|
112
|
+
exit_wave = multislice(potential, pixel_size=1.0, voltage=300, n_slices=10)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Batching
|
|
116
|
+
|
|
117
|
+
All functions accept arbitrary leading batch dimensions on `potential`:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
potential = torch.zeros((8, 50, 64, 64), dtype=torch.complex64) # batch of 8
|
|
121
|
+
exit_wave = multislice(potential, pixel_size=1.0, voltage=300)
|
|
122
|
+
# exit_wave.shape is (8, 64, 64)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Structure-to-wave pipeline
|
|
126
|
+
|
|
127
|
+
Structure handling and potential generation are deliberately separate packages.
|
|
128
|
+
They are not runtime dependencies of `torch-scattering`; their real tensor
|
|
129
|
+
output is passed through the public tensor API:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
import pandas as pd
|
|
133
|
+
from torch_calculate_electrostatic_potential import (
|
|
134
|
+
GridConfig,
|
|
135
|
+
potential_from_structure_3d,
|
|
136
|
+
)
|
|
137
|
+
from torch_scattering import multislice
|
|
138
|
+
from torch_structure_manipulation import (
|
|
139
|
+
AtomicStructure,
|
|
140
|
+
annotate_bonding_environments,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# mmdf-compatible coordinates are in Angstroms.
|
|
144
|
+
atoms = pd.DataFrame(
|
|
145
|
+
[
|
|
146
|
+
("A", 1, "ALA", "C", "C", 0.0, 0.0, 0.0),
|
|
147
|
+
("A", 1, "ALA", "O", "O", 1.2, 0.0, 0.0),
|
|
148
|
+
("A", 1, "ALA", "CA", "C", -1.2, 0.0, 0.0),
|
|
149
|
+
("A", 2, "GLY", "N", "N", 2.4, 0.0, 0.0),
|
|
150
|
+
],
|
|
151
|
+
columns=[
|
|
152
|
+
"chain", "residue_id", "residue", "atom", "element", "x", "y", "z"
|
|
153
|
+
],
|
|
154
|
+
)
|
|
155
|
+
atoms["b_isotropic"] = 10.0 # Angstrom squared
|
|
156
|
+
atoms["occupancy"] = 1.0
|
|
157
|
+
|
|
158
|
+
# Annotate a complete local residue context, then build the desired structure.
|
|
159
|
+
annotated = annotate_bonding_environments(atoms, include_hydrogens=False)
|
|
160
|
+
structure = AtomicStructure.from_dataframe(annotated.iloc[[0]])
|
|
161
|
+
|
|
162
|
+
grid = GridConfig.from_grid_shape_and_voxel_size(
|
|
163
|
+
grid_shape=(9, 9, 9), # Z, Y, X
|
|
164
|
+
voxel_size=(1.0, 1.0, 1.0), # Angstroms; isotropic for scattering
|
|
165
|
+
center_zyx=(0.0, 0.0, 0.0),
|
|
166
|
+
sublattice_radius=4.0,
|
|
167
|
+
)
|
|
168
|
+
elemental_volts = potential_from_structure_3d(structure, grid)
|
|
169
|
+
bonded_volts = potential_from_structure_3d(
|
|
170
|
+
structure,
|
|
171
|
+
grid,
|
|
172
|
+
scattering_factors="peng_bonded",
|
|
173
|
+
bonded_fallback="error",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Both volumes are real tensors in volts and are accepted directly.
|
|
177
|
+
elemental_wave = multislice(elemental_volts, pixel_size=1.0, voltage=300.0)
|
|
178
|
+
bonded_wave = multislice(bonded_volts, pixel_size=1.0, voltage=300.0)
|
|
179
|
+
# Both waves are complex tensors; voltage is in kV.
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`projection()` is a wave-propagation approximation that numerically sums this
|
|
183
|
+
sampled 3D volume along Z. It is distinct from the electrostatic package's
|
|
184
|
+
analytic 2D projected-potential calculation and from projection alignment in
|
|
185
|
+
`torch-fit-in-map`.
|
|
186
|
+
|
|
187
|
+
## Low-level primitives
|
|
188
|
+
|
|
189
|
+
For building custom propagation schemes directly on top of the multislice
|
|
190
|
+
recurrence:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
import torch
|
|
194
|
+
from torch_grid_utils import fftfreq_grid
|
|
195
|
+
from torch_scattering import (
|
|
196
|
+
fresnel_propagator,
|
|
197
|
+
interaction_parameter,
|
|
198
|
+
multislice_step,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
frequency_grid = fftfreq_grid(image_shape=(64, 64), rfft=False, spacing=1.0, norm=True)
|
|
202
|
+
propagator = fresnel_propagator(frequency_grid, wavelength=0.01969, dz=1.0)
|
|
203
|
+
sigma = interaction_parameter(voltage=300)
|
|
204
|
+
|
|
205
|
+
wave = torch.ones((64, 64), dtype=torch.complex64)
|
|
206
|
+
potential_slice = torch.zeros((64, 64), dtype=torch.complex64)
|
|
207
|
+
wave = multislice_step(wave, potential_slice, propagator, sigma, dz=1.0)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
torch_scattering/__init__.py,sha256=SCLWYPfsGT1d8e4aggxkk01ZW97ewWaG5eEpV0NzUSE,792
|
|
2
|
+
torch_scattering/_core.py,sha256=i_Y6wUcB2yIGmQoUY-VhOUUVaCsVz4ValZFJgvh4mCw,10345
|
|
3
|
+
torch_scattering/firstborn.py,sha256=dnizhedOFoek5ZrtPnk7N4q3zlfGCbi1WyRAo0Mld7A,3772
|
|
4
|
+
torch_scattering/multislice.py,sha256=NjfqqhogZoLxd34kFVMZ21sLmdrNvyuaSfr28DtiPpQ,3532
|
|
5
|
+
torch_scattering/projection.py,sha256=Z29xlUCA8wfYNBvubmTw70hkn83HrrrzyLCyNQdxSqw,1936
|
|
6
|
+
torch_scattering/rytov.py,sha256=l2m7oL8ZuIWuGElVtZbKyLp07pvhp5EcFmhAtE9pcQY,3660
|
|
7
|
+
torch_scattering-0.6.0.dist-info/METADATA,sha256=0cqRCdkmIldmKjSvkoKkOMQx22Xas0hiv0Z6BjR3698,7786
|
|
8
|
+
torch_scattering-0.6.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
9
|
+
torch_scattering-0.6.0.dist-info/licenses/LICENSE,sha256=Nt6shgig6_4SzvDw6xDx2NTmbmbwnqylMO5P54YuT3Y,1520
|
|
10
|
+
torch_scattering-0.6.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2026, TeamTomo
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|