ISRO Bharatiya Antariksh Hackathon 2026 — Problem Statement 9.
Given a time-series of Shack–Hartmann Wavefront Sensor (.bmp) frames, this
project (for every frame) reconstructs the wavefront phase map W(x,y) and
its Zernike coefficients, derives the turbulence Fried parameter r₀ and
coherence time τ₀, and computes a deformable-mirror actuator map in
stroke-length units with inter-actuator coupling deconvolved — fast enough
(< 10 ms/frame, target ≈ µs) to correct turbulence in real time.
Read
ARCHITECTURE.mdfirst — it is the primary deliverable and the implementation contract (problem analysis, algorithms, file formats, directory layout, validation plan, roadmap). Stage→research mapping is indocs/METHODS.md.
Two tiers (research note 06): a self-contained C11 real-time core (src/ →
bin/wfs_rt) that links only -lm (no BLAS/FFTW) — hand-rolled BMP I/O, a
portable AVX2/OpenMP-optional GEMV, TWCoG centroiding, and 2–3 precomputed
matrix–vector multiplies per frame; and a Python offline toolkit (aokit/)
that builds the calibration matrices, generates synthetic datasets with known
r₀/τ₀, runs the multi-method turbulence estimation, and validates everything.
The two communicate through a small self-describing binary matrix format
(AOMX) that both sides implement byte-for-byte.
.bmp ─▶ centroid (TWCoG) ─▶ slopes ─▶ { φ = R·s (zonal), a = M⁺·s (modal) } ─▶ a_dm = H⁺·(−W/2)
precomputed offline: R (Fried, waffle-nulled), M⁺/Z (Zernike), G = H⁺·(−½) (coupling deconvolved)
make # -> bin/wfs_rt (-O3 -march=native -fopenmp, links only -lm)
make test # built-in self-test: AOMX + BMP roundtrips, GEMV sanitypip install -r requirements.txt # numpy, scipy, matplotlib (if not present)
python3 -c "import aokit" # import sanity
python3 -m json.tool config/example_config.json # validate config JSON
MPLBACKEND=Agg python3 -m pytest -q # 226 unit + integration tests (headless)This is the full inject-r₀/τ₀ → recover loop used to produce the
Results below (10×10 lenslets, 256×256 frames, D = 1.5 mm pupil).
The injected r0 is chosen so D/r0 is order unity (the pupil is only 1.5 mm,
so a meaningful D/r0 ≈ 2.5 needs r0 ≈ 0.6 mm, not 0.15 m).
# 0. Build the C core
make
# 1. Build calibration matrices (AOMX) + the flat config.txt sidecar for the C core
python3 scripts/build_calibration.py --config config/example_config.json \
--out results/calib
# 2. Generate a synthetic dataset with KNOWN injected r0/tau0 + ground_truth.json
python3 scripts/generate_dataset.py --config config/example_config.json \
--r0 0.0006 --tau0 0.005 --frames 60 \
--out results/synthetic/run01 --model geometric --seed 123
# 3. Run the fast C real-time core over all frames (glob is expanded internally)
bin/wfs_rt --config results/calib/config.txt --calib results/calib \
--frames "results/synthetic/run01/frame_*.bmp" --out results/out_c
# 4. Run the pure-Python reference pipeline (same frames) + diagnostic plots
python3 scripts/run_pipeline_py.py --config config/example_config.json \
--calib results/calib --frames "results/synthetic/run01/frame_*.bmp" \
--out results/out_py --plots
# 5. Multi-method turbulence report (>=7 r0, >=6 tau0 estimators) + method plots
python3 scripts/analyze_turbulence.py --slopes results/out_py/slopes.csv \
--coeffs results/out_py/zernike_coeffs.csv \
--config config/example_config.json \
--ground-truth results/synthetic/run01/ground_truth.json \
--out results/turbulence_summary.json --plotsThe C core (step 3) and the Python reference (step 4) write the same artifact
filenames (slopes.csv, zernike_coeffs.csv, actuators.csv,
phase_NNNN.aomx), so their outputs are directly comparable — that comparison
is the C↔Python parity proof. bin/wfs_rt --selftest runs the built-in C
roundtrips; MPLBACKEND=Agg python3 -m pytest -q runs all 226 unit + integration
tests (including the end-to-end tests/test_integration.py).
ARCHITECTURE.md # master architecture (primary deliverable)
docs/METHODS.md # stage -> research-report map (+ integration conventions)
config/ # example_config.json (schema in ARCHITECTURE §4.1)
src/ # self-contained C11 real-time core (links only -lm)
├─ main.c # CLI, config sidecar parser, frame loop, timing report
├─ pipeline.c # per-frame: centroid -> slopes -> R/M+ -> G (zero in-loop alloc)
├─ centroid.c # TWCoG centroiding
├─ slopes.c # centroid -> slope (s = (c-ref)*p_pix/f)
├─ reconstruct.c # zonal phi=R*s + modal a=M+*s (one GEMV each)
├─ dmcmd.c # a_dm = G*phi, gain + stroke clip
├─ matio.c # AOMX binary matrix I/O (byte-matches aokit/matio.py)
├─ bmp.c # hand-rolled BMP reader
└─ linalg.c # portable AVX2/OpenMP-optional GEMV
aokit/ # Python offline toolkit (numpy/scipy/matplotlib)
# geometry zernike centroiding reconstructor dm turbulence
# datagen validation viz config bmpio matio
scripts/ # build_calibration / generate_dataset / run_pipeline_py / analyze_turbulence
tests/ # pytest: 219 unit tests + test_integration.py (end-to-end) + matio parity
results/ # plots/ (key figures) + *summary.json + c_core_timing_report.txt
# (bulky frame/AOMX/CSV artifacts are .gitignored)
research/ # seven deep-research reports backing every design choice
Fully implemented and validated end-to-end. Both tiers are complete and the whole inject-r₀/τ₀ → recover loop runs through both the C real-time core and the Python reference, cross-checked for parity:
ARCHITECTURE.md(deep, synthesizing all 7 research reports).- C core builds warning-clean (
-Wall -Wextra -Wshadow -Wpointer-arith -Wcast-qual, links only-lm);--selftestpasses. - AOMX format byte-matched on both sides (
src/matio.c↔aokit/matio.py). - All four top-level scripts implemented;
aokitfully functional. - 226 tests pass (
pytest -q): 219 unit + 7 end-to-end integration (tests/test_integration.py, which also asserts C↔Python parity via subprocess).
Measured on the validated run above (10×10 lenslets → 80 illuminated
sub-apertures, 256×256 frames, 101 valid Fried phase nodes, 14 Zernike modes,
60 frames; injected r₀ = 0.6 mm, τ₀ = 5 ms). Reproduce with the
Quickstart
commands; figures are in results/plots/, the numbers in
results/c_core_timing_report.txt and
results/turbulence_summary.json.
| Stage (per frame) | min | mean | max |
|---|---|---|---|
| read + decode (BMP) | 119.6 µs | 132.3 µs | 263.0 µs |
| centroid (TWCoG, 80 sub-aps) | 184.4 µs | 200.3 µs | 231.6 µs |
reconstruct (φ=R·s, a=M⁺·s) |
2.4 µs | 3.3 µs | 15.6 µs |
DM command (a=G·φ, clip) |
1.3 µs | 1.5 µs | 4.2 µs |
| TOTAL | 310 µs | 337 µs | 499 µs |
→ 2964 FPS, ≈30× under the 10 ms budget at the mean and ≈20× at the worst frame. The pure matrix-vector reconstruct + DM step (the optimized fast path) is ≈5 µs/frame — the centroiding and disk decode dominate, both still far under budget.
Same 60 frames through both pipelines, max absolute difference:
| Quantity | max |C − Python| | scale | interpretation |
|---|---|---|---|
| slopes (rad) | 8.1 × 10⁻⁹ | 2.1 × 10⁻³ | float32 round-off |
| Zernike coeffs (rad) | 4.1 × 10⁻⁶ | 1.4 × 10⁰ | float32 round-off |
| phase map (rad) | 1.3 × 10⁻⁸ | 2.9 × 10⁻³ | float32 round-off |
| DM actuators (m) | 1.1 × 10⁻¹⁴ | 1.9 × 10⁻⁹ | exact |
The C core stores/computes the matrices in float32; the only differences are at the float32 round-off floor, so the hand-rolled C GEMV path reproduces the NumPy/SciPy reference exactly.
Independent estimators across three data domains, median-combined (full
per-method tables in results/turbulence_summary.json):
| Parameter | injected | per-method values | combined median | recovery error |
|---|---|---|---|---|
| r₀ | 0.60 mm | R1 0.75, R2 0.86, R3 0.49, R4 0.90, R6 0.16 mm | 0.75 mm | 25.6 % |
| τ₀ | 5.00 ms | T1 6.2, T2 2.7, T3 6.3, T4 9.3, T5 6.3, T6 12.9 ms | 6.28 ms | 25.6 % |
5 r₀ estimators (Zernike-variance, slope-variance, DIMM, phase-variance, von
Kármán) and 6 τ₀ estimators (autocorrelation, PSD knee, Greenwood, structure
function, frozen-flow, Tyler) all run and agree to within their spread. The
research notes (research/04 §6) flag the load-bearing constants (slope-variance
0.170, DIMM 0.358, Greenwood 0.314…) as convention-dependent; the residual
~25 % is consistent with that systematic and with the genuine realization
scatter of turbulence statistics over a small 10×10 aperture and a short
sequence — which the combiner's reported spread captures honestly. See
results/plots/r0_method_comparison.png
and tau0_method_comparison.png.
| Metric | value |
|---|---|
| Zernike reconstruction RMS | 0.153 rad (λ/41) |
| Strehl (Maréchal, from coeff residual) | 0.977 |
| DM coupling-deconvolution fit residual | ≈0 rad (G = −½·H⁺ inverts H on the valid nodes) |
| mean illuminated sub-apertures | 80 / 80 |
Key figures (saved to results/plots/):
spot_field.png (SH-WFS frame + sub-aperture
boxes + measured centroids), phase_map.png
(reconstructed wavefront on the Fried nodes),
zernike_spectrum.png,
actuator_map.png (DM stroke map),
residual.png (reconstructed W vs DM correction),
and the r₀/τ₀ method-comparison bar charts.
Ubuntu 24.04, x86_64, 4 cores, no GPU; gcc 13 / clang 18, OpenMP, AVX2/AVX-512. Python 3.11. The C core has no external library dependency (only libm); the Python toolkit installs numpy/scipy/matplotlib from wheels.
Prepared for ISRO BAH 2026 PS9. Each algorithmic choice is justified in the
research/ reports (with primary-source URLs) and integrated in
ARCHITECTURE.md.