Documentation: jacotay7.github.io/makewfs
Configuration-driven adaptive-optics wavefront-sensor images.
makewfs turns a pupil-plane phase/OPD map into the image an adaptive-optics
wavefront sensor would actually record. It supports Shack–Hartmann and
four-face pyramid sensors, with modulation, broadband and finite-source
quadrature, and sodium-layer LGS geometry — everything from the pupil to the ADU.
The package owns the wavefront-sensor optics and nothing else. It deliberately
reuses pyturb for atmospheric OPD and
getframes for detector response and
noise; neither model is reimplemented here. It runs on NumPy by default and
switches to CUDA (via CuPy) with a single configuration field.
python -m pip install makewfs # CPU
python -m pip install "makewfs[gpu]" # + CuPy for CUDA 12.ximport makewfs
wfs = makewfs.WavefrontSensor.from_toml("wfs.toml")
frame = wfs.expose(opd_m, seed=0) # getframes.Frame, data in ADU
rate = wfs.photon_rate(opd_m) # ideal photons/s/native detector pixel
frame = wfs.expose_integrated(samples) # several temporal OPD samples, one exposureConstruct the sensor once and call expose() with each new residual wavefront:
the only per-frame inputs are the wavefront and an optional noise seed, because
telescope, source, sensor, sampling and camera choices all live in versioned
configuration. Set device = "gpu" under [numerics] and pass a CuPy OPD array
for the fully device-resident path; frame.data is then a CuPy ADU array, and
getframes.to_numpy(frame.data) marks an intentional host boundary.
Runnable starter configurations live at
examples/configs/shack_hartmann_minimal.toml
and examples/configs/pyramid_minimal.toml.
See Quickstart for the full walkthrough, Configuration for every field with its units and range, and Concepts plus Units and coordinates if you are new to wavefront sensing.
Warm end-to-end frame throughput on an AMD Ryzen 9 9950X3D and an NVIDIA RTX
5090, including optics, getframes detector noise, truth and ADU, with
device-resident input/output and no host transfers. The raw artifact and its
invocation are versioned with the benchmarks:
| Workflow | Output | Work samples | CPU (frames/s) | GPU (frames/s) | Speedup |
|---|---|---|---|---|---|
| 20×20 SH, float32 | 160×160 | 1 | 127 | 2,042 | 16.13× |
| 60×60 SH, float64 | 360×360 | 1 | 18 | 899 | 50.11× |
| 9-sample quadrature SH | 64×64 | 9 | 186 | 780 | 4.20× |
| Pyramid, 8-point modulation, float32 | 80×80 | 8 | 659 | 1,646 | 2.50× |
| Pyramid, 32-point modulation, float64 | 108×108 | 32 | 24 | 924 | 37.94× |
Higher is better. A tiny unmodulated 54×54 pyramid case remains CPU-faster
(3,526 versus 1,605 frames/s) because GPU launch overhead dominates its small
optical workload. The 9-sample row
(shack_hartmann_quadrature_9sample.toml) is a deliberate quadrature load — three
wavelengths crossed with three beacon ranges — not a physical beacon: a sodium
LGS is monochromatic at the 589 nm D2 line, broadened by only ~0.003 nm, so its
spectral axis exists there purely to exercise the polychromatic path. The
showcase clip above instead runs a physically correct narrowband beacon sampled
across the sodium layer's depth, which is where spot elongation actually comes
from. Compatible sampled-DFT Shack–Hartmann geometries are compiled
on first use, so warm each fixed sensor before measuring or entering a real-time
loop. Reproduce the table with
python benchmarks/run.py --device both --frames 100 \
$(printf -- '--config %s ' benchmarks/configs/*.toml)See the full snapshot and the performance guide for the methodology.
- Shack–Hartmann sensors — lenslet geometry with physical sampling controls
(
spot_sampling_pixels_per_lambda_over_d,minimum_illuminated_fraction), flux-conserving pixel integration, and explicit crop accounting; see Shack-Hartmann. - Four-face pyramid sensors — fixed-mask pyramid with circular modulation (radius and sample count) and configurable pupil separation; see Pyramid.
- Broadband and finite sources — deterministic spectral and angular quadrature, measured source curves, and user-supplied angular kernels, so extended and polychromatic beacons are integrated as intensities, not fields.
- Laser guide stars — sodium-range sampling with launch-position geometry and the resulting spot elongation; see Guide stars. The mean-altitude LGS OPD approximation is documented and deliberately distinct from range-resolved turbulence.
- Analytic pupils — central obscuration, segmented and rotated apertures, static OPD maps and user masks, hashed for provenance.
- Configuration as the contract — immutable TOML with units in every key name, unknown-key and range rejection with path-specific messages, a versioned schema, and digests of every referenced file; see Configuration.
- Precision you choose — matched
float32/complex64andfloat64/complex128paths, tested on both, with no silent promotion in the hot path. - GPU-optional, end to end —
numerics.device = "gpu"runs optics and thegetframesdetector device-resident, with CPU parity tests; compatible Shack–Hartmann geometries use a specialized CUDA execution plan and fall back automatically to the readable reference implementation for any unsupported optical feature. - Real-time friendly — construct once and reuse; static grids, masks and
normalization are cached on the sensor, and
expose()/expose_integrated()accept a caller-ownedout=array so high-rate loops control storage lifetime. - Built to interoperate —
pyturbsupplies the atmosphere andgetframesthe detector, including wavelength-resolved QE; see Interoperability. - Validated against theory — piston invariance, non-negative intensity, flux accounting, photon-rate linearity, stable sign/axis conventions and sampling convergence are asserted quantitatively; see Validation.
- Stable surface —
load_config,WavefrontSensorandsimulateare frozen under SemVer as of 1.0.
See the API reference for every public function and class, the documentation gallery and runnable examples for sensor comparisons, modulation trades, LGS elongation and closed-loop injection, and Troubleshooting when a frame does not look the way you expect.
See CONTRIBUTING.md and AGENTS.md. Run the checks locally with:
ruff check . && ruff format --check . && python -m mypy && pytestMIT — see LICENSE.
