Modal basis sets for adaptive-optics deformable mirrors: KL, Zernike, Fourier, Hadamard, zonal and zonal-fast modes, for any actuator geometry.
Every generator takes your actuator positions and returns a modal-to-command matrix (M2C), an (n_actuators, n_modes) NumPy array whose column k is the DM command for mode k. It's ready for a reconstructor, a calibration sequence or a real-time controller such as pyRTC.
pip install aobasis # numpy + scipy only
pip install "aobasis[plot]" # + matplotlib, for .plot()
pip install "aobasis[fits]" # + astropy, for save_fits / load_fits
pip install "aobasis[tutorials]" # + matplotlib, astropy and Jupyter, for the tutorialsaobasis is pure Python (3.8–3.14) and is tested on Linux, x86-64 and aarch64.
GPU (optional). KLBasisGenerator(..., use_gpu=True) builds and diagonalizes the covariance with CuPy. Install the CuPy wheel for your CUDA version, e.g. pip install cupy-cuda12x, or conda install -c conda-forge cupy. Without CuPy it falls back to the CPU with a warning, and the modes are the same either way.
import numpy as np
import aobasis
# 1. Actuator positions: (N, 2) in metres, centred, in your DM's actuator order
positions = aobasis.make_circular_actuator_grid(telescope_diameter=10.0, grid_size=20)
# 2. A generator and its physical parameters
kl = aobasis.KLBasisGenerator(positions, fried_parameter=0.16, outer_scale=30.0)
# 3. The M2C: (276, 50), piston-free
m2c = kl.generate(n_modes=50, ignore_piston=True)
# 4. Use, inspect, keep
coefficients = np.zeros(50); coefficients[3] = 1.0 # some of mode 3
commands = m2c @ coefficients # put modal coefficients on the DM
c2m = aobasis.command_to_mode_matrix(m2c) # and back
kl.plot(count=6) # needs aobasis[plot]
kl.save("kl_m2c.npz") # modes + parameters + options + eigenvaluesBases
| Generator | Modes | Typical use |
|---|---|---|
KLBasisGenerator |
Karhunen-Loève modes of Von Kármán (or Kolmogorov, outer_scale=np.inf) turbulence at the actuators, with eigenvalues |
closed-loop control; turbulence statistics and priors |
DMKLBasisGenerator |
KL modes of the DM surface from its influence functions (double diagonalization) | control with a real DM |
ZernikeBasisGenerator |
Noll-normalized Zernikes in Noll, OSA/ANSI or Fringe order; annular Zernikes with obscuration= |
aberration commands, optical testing, NCPA |
FourierBasisGenerator |
sines and cosines, aliased frequencies skipped | frequency response, spatial filtering |
HadamardBasisGenerator |
±1 patterns, Sylvester or Paley matrices (construction="smallest"), selection="balanced" |
low-noise interaction-matrix calibration |
ZonalBasisGenerator |
single-actuator pokes | simplest calibration |
ZonalFastBasisGenerator |
groups of pokes at least min_distance apart (optimal lattice colourings) |
calibration in a few frames |
Shaping a basis. These options are the same on every generator:
ignore_piston=Truemakes every mode exactly zero-mean;remove="tiptilt"(or any(n_actuators, k)array, e.g. waffle) keeps other modes out;orthonormalize=TrueGram-Schmidts the modes in order;normalize="rms" | "l2" | "peak" | "pv"sets the scale.
Unknown options raise TypeError.
Geometry.
make_circular_actuator_gridbuilds a square grid bygrid_sizeorpitch, with actuators on the rim or in cell centres.make_hexagonal_actuator_gridandmake_concentric_actuator_gridcover other layouts.- All three take a central obstruction and spider arms.
positions_from_maskreads a boolean DM map, and any(N, 2)array works too.
Influence functions. fit_to_influence_functions gives least-squares commands whose DM surface matches modes sampled on the pupil. gaussian_influence_functions and make_pupil_points build the inputs.
Using and keeping a basis.
command_to_mode_matrixandfit_coefficientsgive modal coefficients.basis_report(orgenerator.report()) summarizes rank, conditioning, orthogonality and piston content.save/load(.npz) andsave_fits/load_fitsstore the modes together with the generator's parameters, thegenerate()options, KL eigenvalues and the aobasis version.
Reproducible. KL modes follow a fixed sign and degenerate-rotation convention: tip along +x, tilt along +y. The same geometry gives the same modes on every machine, CPU or GPU.
tutorials/ holds eight Jupyter walkthroughs; see tutorials/README.md for a guide.
| Notebooks | |
|---|---|
| Getting started | 01 · Quickstart, 02 · A tour of the bases |
| Everyday use | 03 · Shaping a basis, 04 · Actuator geometry |
| In depth | 05 · KL and turbulence, 06 · Influence functions and DM KL, 07 · Calibration patterns, 08 · Saving and sharing |
examples/ holds ready-to-run scripts (each has --help):
python examples/make_m2c.py kl --grid-size 20 --n-modes 100 --remove tiptilt -o kl.fits --plot kl.png
python examples/dm_kl.py --n-modes 80 -o dmkl.fits
python examples/calibration.pypython examples/benchmark.py --grid-sizes 16 32 64 --n-modes 100 --gpu --markdown. Each entry is the best of 3 runs of generate(100) (zonal fast: its full pattern set) on a square grid clipped by a 10 m pupil.
Host: an 80-core Arm Neoverse-N1 server, using 4 cores (OPENBLAS_NUM_THREADS=4), with an NVIDIA RTX 4060 and an RTX A400 (select one with CUDA_DEVICE_ORDER=PCI_BUS_ID CUDA_VISIBLE_DEVICES=...).
| Basis (100 modes) | 16×16 (172 acts) | 32×32 (740 acts) | 64×64 (3096 acts) |
|---|---|---|---|
| KL (CPU) | 0.023 s | 0.158 s | 1.99 s |
| KL (GPU, RTX 4060) | 0.019 s | 0.061 s | 1.04 s |
| KL (GPU, RTX A400) | 0.026 s | 0.161 s | 4.41 s |
| Zernike | 0.005 s | 0.011 s | 0.033 s |
| Fourier | 0.006 s | 0.024 s | 0.081 s |
| Hadamard | 0.001 s | 0.021 s | 0.142 s |
| Zonal | <0.001 s | <0.001 s | 0.002 s |
| Zonal fast (3-pitch spacing) | 0.005 s | 0.023 s | 0.120 s |
Full bases (--n-modes all) on 3096 actuators take 6.4 s for KL on the CPU (2.9 s on the RTX 4060), 4.3 s for Zernike, 7.1 s for Fourier and 3.0 s for Hadamard. That includes the rank check.
Some notes on these numbers:
- KL costs O(N³). For a few modes the CPU uses a partial eigensolver, which is why 100 modes are faster than the full basis.
- The covariance is evaluated once per distinct actuator separation.
- KL works in float64, so GPU speed follows the card's float64 throughput. The RTX 4060 is about 2× faster than 4 CPU cores at 3096 actuators. The RTX A400, with far less float64 throughput, is slower than the CPU.
- The GPU path always computes every eigenpair (CuPy has no partial eigensolver), so its advantage is largest for full bases.
git clone https://github.com/jacotay7/aobasis.git
cd aobasis
pip install -e ".[dev]"
pytestCI runs the test suite on Python 3.8–3.14, on aarch64 and without matplotlib, runs every example script, and executes every tutorial notebook.
CI cannot run the GPU tests: they need CuPy and a CUDA device, and skip without them. Run them locally before changing GPU code:
pip install cupy-cuda12x
pytest -k gpu -rs # -rs shows a skip reason if no device is foundContributions are welcome. Open an issue to report a bug or propose a feature, or send a pull request with a test for the change and an entry in CHANGELOG.md.
Jacob Taylor, jacobataylor7@gmail.com
MIT; see LICENSE.