pyturb
Documentation: jacotay7.github.io/pyturb
Fast, GPU-optional atmospheric turbulence for adaptive optics.
pyturb generates the optical path differences (OPD) that an adaptive-optics
system sees through the atmosphere: full layered turbulence for a
representative sky, with per-layer wind, frozen-flow time evolution,
off-axis directions, and standard site profiles. It runs on NumPy by default
and switches to CUDA (via CuPy) with a single argument.
Install
pip install pyturb # CPU (NumPy + SciPy)
pip install pyturb[cuda13] # + CuPy for CUDA 13.x (with CUDA headers)
pip install pyturb[cuda12] # + CuPy for CUDA 12.x (with CUDA headers)
pip install pyturb[cuda11] # + CuPy 13.x for CUDA 11.x
pip install pyturb[accel] # + Numba, faster CPU frozen flow
Pick the extra matching your driver's CUDA version (nvidia-smi shows it). If
you install CuPy yourself and the first GPU call fails with "Failed to find
CUDA headers", install the headers too (pip install "cupy-cuda12x[ctk]", or
cupy-cuda13x[ctk]) or point CUDA_PATH at a CUDA toolkit.
Quickstart
import pyturb
atm = pyturb.Atmosphere.from_profile(
"paranal-median", seeing=0.8, zenith_angle=30, diameter=8.0, n=512, seed=1
)
print(atm.r0, atm.theta0, atm.tau0) # Fried param, isoplanatic angle, tau0
for t, opd in atm.frames(dt=1e-3, steps=200):
... # (512, 512) OPD [m], frozen flow
# The default spectral engine is periodic: keep runs under atm.time_to_wrap
# (0.25 s here) or pass engine="extrude" for unbounded, non-periodic flow.
ensemble = atm.sample(256) # (256, 512, 512) Monte-Carlo OPDs
atm = pyturb.Atmosphere.from_profile("paranal-median", seeing=0.8, device="gpu")
for t, opd in atm.frames(dt=1e-3, steps=200):
... # cupy arrays; ~4,600 fps at 512^2 on an RTX 4060
See Quickstart for
off-axis/tomography, boiling, LGS, non-periodic frozen flow, and the
lower-level PhaseScreen/InfinitePhaseScreen building blocks; and
Concepts for r0, L0, Cn²,
θ0 and τ0 if you're new to AO.
Arrays are indexed (y, x) — x along axis 1 (columns), y along axis 0 (rows) —
the convention of aobasis, makewfs, solvephase and HCIPy; wind directions,
off-axis (thx, thy) and Zernike modes all follow it. Upgrading from 1.x,
whose x was axis 0? See
Migrating to 2.0.
Benchmarks
Full 9-layer Paranal atmosphere, frames/s (Monte-Carlo: screens/s).
pyturb 1.1 on an RTX 4060 with an Arm (Neoverse-N1) host (artifact):
| screen | GPU spectral | GPU opd(t=times) |
GPU extrude | GPU Monte-Carlo |
|---|---|---|---|---|
| 256² | 6,451 | 27,510 | 1,174 | 33,292 |
| 512² | 4,591 | 6,653 | 482 | 6,993 |
| 1024² | 1,304 | 1,166 | 120 | 1,715 |
Spectral frames replay a captured CUDA graph, so they no longer depend on the
host CPU's speed; opd(t=times) evaluates an offline time series in one
batched transform.
pyturb 1.0.0 on an RTX 5090 and a 32-core x86 CPU (pyturb[accel];
before the 1.1 CUDA-graph and batching work,
artifact):
| screen | GPU spectral | GPU extrude | GPU Monte-Carlo screens/s | CPU spectral | CPU extrude |
|---|---|---|---|---|---|
| 256² | 3,219 | 4,489 | 104,981 | 1,014 | 2,571 |
| 512² | 3,133 | 1,733 | 29,789 | 283 | 324 |
| 1024² | 1,492 | 602 | 5,970 | 70 | 62 |
The Monte-Carlo column is Atmosphere.sample(), the full 9-layer atmosphere.
Layers that share an outer scale are drawn as one aggregate screen (their PSDs
add exactly), so a uniform-L0 profile costs one FFT, not nine. All
Monte-Carlo figures are batched, device-resident throughput (a batch of up to
64 kept on the GPU); a single call, or one that copies its result to the host,
is lower. Measure your own machine with
python -c "import pyturb; pyturb.benchmark()", or python benchmarks/bench_suite.py for the full per-use-case sweep. More hardware and
the head-to-head against aotools, soapy and HCIPy:
benchmarks/RESULTS.md and
Comparison.
Features
- Layered
Atmosphere— named site profiles (paranal-median,mauna-kea,keck,las-campanas, HV 5/7, ...) or a customCn²(h)model, summed to pupil OPD with per-layer wind and airmass/zenith scaling. - Two frozen-flow engines —
engine="spectral"(default): exact sub-pixel shift-theorem translation, all layers in one FFT, periodic.engine="extrude": Assémat–Wilson row extrusion, unbounded and non-periodic, read out by a fused CUDA kernel (GPU) or Numba kernel (CPU, with[accel]). FFT screens default to the pupil's size (fastest); passoversample=2–4when statistics across the whole pupil matter (tilt, low-order modes), since a pupil-sized periodic screen underestimates them. - Off-axis / tomography —
atm.opd(t, directions=[...])batches several guide-star directions through one call. - Boiling — temporal decorrelation on top of frozen flow (
tau_boil). - LGS cone effect — finite-range sodium beacon focal anisoplanatism
(
lgs_altitude). - Chromatic OPD — achromatic by default;
dispersion="edlen"/"ciddor"for the dry-air/water-vapour term. - Diagnostics (
pyturb.analysis) — Zernike decomposition, Noll (1976) mode variances, temporal PSD + power-law fit, angular decorrelation. - I/O —
pyturb.save/pyturb.loadfor FITS (optional astropy) and.npz, with provenance metadata. - GPU-optional — every class takes
device="gpu"(CuPy); the GPU path is statistically identical to CPU (validated against the same theory to numerical precision). Aseedreproduces the same atmosphere on any CUDA GPU (pyturb's GPU stream is a counter-based Philox generator, independent of the device). CPU (NumPy PCG64) and GPU use different generators, so a givenseeddraws a different realisation on each backend — the guarantee is matched statistics, not a bit-for-bit copy. - Validated against theory — structure function, Zernike spectrum, temporal PSD and angular decorrelation checked against analytic predictions in CI; see Validation.
See the API reference for every public function and class, and Interop for recipes with HCIPy, poppy, and DM-fitting loops.
License
MIT
Metadata
Release files for pyturb 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyturb-2.0.0.tar.gz | 6.8 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyturb-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 6.9 MB
Release files / pyturb-2.0.0.tar.gz
| Download URL | pyturb-2.0.0.tar.gz |
|---|---|
| Size | 6.8 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f4d3338796ba7f9bea41fcab22d54ee2ff68b3a420324fda7bdca8a2d12f99ef
|
|
BLAKE2b-256 checksum How to use checksums |
a43f487455e6e0919beca5281b2ae839834ea9c828c88ede391f027de91616d9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / pyturb-2.0.0-py3-none-any.whl
| Download URL | pyturb-2.0.0-py3-none-any.whl |
|---|---|
| Size | 120.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
531a811f3f4cd0dde35de5c2b33bcb471305c0dcd074ea412bf73001b8d0e140
|
|
BLAKE2b-256 checksum How to use checksums |
42a6de5ae8aead7763f0167a2cc231448545ab8c5a323e2c68ee2d428d5e0a8d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency log