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.
@@ -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
+ [![License](https://img.shields.io/pypi/l/torch-scattering.svg?color=green)](https://github.com/joelyeois/torch-scattering/raw/main/LICENSE)
28
+ [![PyPI](https://img.shields.io/pypi/v/torch-scattering.svg?color=green)](https://pypi.org/project/torch-scattering)
29
+ [![Python Version](https://img.shields.io/pypi/pyversions/torch-scattering.svg?color=green)](https://python.org)
30
+ [![CI](https://github.com/joelyeois/torch-scattering/actions/workflows/ci.yml/badge.svg)](https://github.com/joelyeois/torch-scattering/actions/workflows/ci.yml)
31
+ [![codecov](https://codecov.io/gh/joelyeois/torch-scattering/branch/main/graph/badge.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.