PyTorch backend
specd.torch computes dispersion, eigenfunctions, and model gradients using
PyTorch tensors and automatic differentiation. It supports Love, VTI
Rayleigh/Scholte, and fully anisotropic waves, with optional constant-Q
attenuation. It runs independently of the C++/Fortran extensions.
Installation
From the repository root, use Python 3.10 or newer and install the optional dependency:
python -m pip install -e '.[torch]'
python -m pip install . installs the base package without requiring PyTorch.
For a published distribution, use python -m pip install 'specd[torch]'.
The provided Conda environment is another option:
conda env create -f specd/torch/environment.yml
conda activate specswd-torch
python -m pip install --no-deps -e .
OMP_NUM_THREADS=1 python -m specd.torch.example
To use both backends, also build and install the native extensions as described
in the build guide. Native imports remain from specd import SpecWorkSpace; tensor imports use from specd.torch import SpecWorkSpace.
Forward solve and automatic differentiation
Inputs use km, km/s, g/cm³, and Hz. Depths start at zero; repeated depths mark discontinuities. The last row describes the constant half-space and repeats the preceding depth.
import torch
from specd.torch import SpecWorkSpace
vsh = torch.tensor([3.3, 3.3, 5.5], dtype=torch.float64, requires_grad=True)
ws = SpecWorkSpace(device="cpu").initialize(
"love", [0., 35., 35.], [2.8, 2.8, 3.2],
vsh=vsh, vsv=[3., 3., 5.],
)
phase = ws.compute_egn(0.1)
group = ws.group_velocity()
loss = (phase[0].real - 3.5).square() + (group[0].real - 3.3).square()
loss.backward()
print(vsh.grad)
Tensor inputs preserve their autograd graphs, including tensors derived from
trainable parameters. Lists and NumPy arrays become internal trainable tensors
available through ws.parameters. Real inputs use float64, complex results
use complex128. CPU is the default; CUDA can be selected through device.
MPS does not support the required double-precision eigensolves.
After updating parameters or finishing a backward pass, call compute_egn
again to construct the graph for the next optimization step. Keep gradients
enabled for group velocity and kernel evaluation. For phase-only inference:
with torch.no_grad():
phase_only = ws.compute_egn(0.1, only_phase=True)
only_phase=True omits eigenvectors; recompute with only_phase=False before
requesting group velocity or eigenfunctions.
Model parameters
initialize(wavetype, z, rho, **parameters) accepts:
|
Required elastic parameters |
Optional attenuation parameters |
|---|---|---|
|
|
|
|
|
|
|
|
|
Arrays have shape (nz,), except c21 with shape (21,nz) and Qani with
shape (2,nz). Stiffness uses the upper triangle of the symmetric Voigt matrix
in order c11,c12,...,c16,c22,...,c66, with Voigt axes xx,yy,zz,yz,xz,xy.
Qani contains Qκ then Qμ. Supply all quality-factor arrays for the wave type,
or omit all for an elastic model. Q values must be positive and finite.
reference_frequency=1.0 specifies the storage-modulus reference in Hz.
For acoustic Rayleigh rows, set vsv=0, vph=vpv, and Qa=Qc when
attenuating. Anisotropic acoustic rows require an isotropic bulk block with
zero shear and equal Qκ/Qμ. Solid/fluid transitions require repeated depths.
specd.torch.isotropic_c21(rho, vp, vs) constructs the stiffness tensor while
preserving gradients. The tensor backend implements anisotropic attenuation
model 1; it does not accept the native qfunc_id argument.
Outputs and kernels
compute_egn(freq, ang_in_deg=0., only_phase=False) returns complex phase
velocities of shape (nmode,), sorted by increasing real phase speed.
group_velocity() returns (nmode,) for Love/Rayleigh and (2,nmode) for
anisotropy, whose rows are Cartesian x and y components. The azimuth is in
degrees.
ws.compute_egn(0.1)
phase_grad = ws.kernels(0, observable="phase", component="real")
group_grad = ws.kernels(0, observable="group", component="real")
print(phase_grad["vsh"]) # original input rows, derivative with respect to vsh
displacement = ws.get_egnfunc(0) # (ncomponent, nSEMpoints)
depth = ws.mesh.depth
kernels returns a dictionary matching the parameter names and shapes.
component selects "real", "imag", or "qinv"; the last differentiates
the wave observable 2*Im(value)/Re(value). Q parameter entries differentiate
with respect to Q itself. Set create_graph=True to differentiate kernels
further. Anisotropic group kernels differentiate radial group velocity.
The compatibility methods get_phase_kl(mode) and get_group_kl(mode) stack
rows in get_kernel_names() order and convert material-Q derivatives to
derivatives with respect to inverse Q. Elastic models return a tensor;
attenuating models return (velocity_kernel, wave_qinv_kernel). Tensor kernels
combine acoustic and solid contributions on the original model rows; there
are no separate vp_ac or rho_ac rows.
Displacement components are radial/vertical for Rayleigh and
radial/transverse/vertical for anisotropy. Use
get_egnfunc(mode, return_displ=False, return_left=True) for a raw left
eigenvector, or omit return_left for the raw right eigenvector.
Frequency and azimuth sweeps
Tensor tables retain autograd graphs in variable-length rows:
from specd.torch import DispersionTable
table = ws.compute_dispersion(
[0.05, 0.1, 0.2], azimuths_deg=[0., 30.], include_group=True,
)
row = 1 * table.azimuth_deg.numel() + 0 # frequency index 1, azimuth index 0
mode_indices, phase_row = table.phase_row(row)
group_modes, group_row = table.group_row(row)
table.to_hdf5("torch-dispersion.h5")
restored = DispersionTable.from_hdf5("torch-dispersion.h5")
Unlike the native table, phase_row and group_row take a single flattened
row index, in frequency-major, azimuth-minor order. A group row has shape
(nmode,ncomponent). Export explicitly detaches tensors and writes the shared
specswd-dispersion v1 HDF5 schema, readable by the native
specd.DispersionTable. Loaded tensors have no gradient history. The tensor
backend exports dispersion tables rather than complete native SolverOutput
files.
Numerical scope
Defaults are ngll=7, ngrl=30, and halfspace_wavelengths=50, matching the
native discretization. They are constructor arguments in the tensor backend.
Increasing the half-space scale requires checking GRL order: a larger scale
improves slowly decaying tails but spreads nodes farther apart. Phase and,
especially, group kernels near cutoff require a convergence check in both
scale and order.
The general quadratic problem is linearized and reduced with structured
linear solves before torch.linalg.eig. The tensor backend does not perform
generalized Schur/QZ decomposition. Group velocity uses operator frequency
and azimuth derivatives from torch.func.jvp; model kernels use reverse-mode
autodiff. Eigenvector derivatives require separated eigenvalues.
Mesh geometry, interpolation positions, and element counts are fixed during differentiation; depth gradients are unsupported. Mode sorting and selection are discrete. A gradient follows a selected mode locally, rather than differentiating changes in mode count or ordering.
Examples and validation
From the repository root:
OMP_NUM_THREADS=1 python -m pytest specd/torch/tests -q
OMP_NUM_THREADS=1 python example/paper/reproduce_paper_pytorch.py
OMP_NUM_THREADS=1 python -m specd.torch.benchmark \
--layers 1 4 --frequencies .05 .2 --repeats 5 --warmup 1 \
--workloads phase_kernel --output /tmp/specswd-phase-kernel
Native comparisons and benchmarks require a current compiled extension;
the remaining tensor tests run without it. The primary benchmark workload
phase_kernel includes a fresh forward solve and fundamental-mode phase
kernels. Paper figures are written to example/paper/pytorch.
The PyTorch API reference lists public signatures. See C++ and PyTorch efficiency for the four comparisons and their timing figure.