Changelog¶
All notable changes to pyTomoAO are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Entries for releases before this file existed were reconstructed from the git history and the tagged releases, so they summarise the significant changes rather than every commit. Releases are listed newest first; note that
0.1.0-test1was tagged before0.0.1, so the version numbers are not monotonic with the dates.
Unreleased¶
[2.0.0] - 2026-07-28¶
See the migration guide
for what an existing user needs to change. The short version: reconstructed wavefronts are
no longer transposed, two
modules were renamed, parameters are read from the object that owns them, the reference
configurations ship inside the package, and matplotlib is now an optional extra.
Breaking¶
matplotlibis no longer installed automatically, and_test_against_matlabis removed (#121).matplotlib was imported at module scope in two files but used by only three things:
visualize_reconstruction,visualize_commandsand thedisplay=Truebranch ofset_influence_function. It is now imported on demand and lives in aplotextra, soimport pyTomoAOno longer drags in matplotlib and its dependency tree — which matters on a real-time control machine that builds reconstructors and never plots. Install it withpip install "pyTomoAO[plot]"; calling a visualisation method without it raises anImportErrornaming the extra.Removed with it:
_test_against_matlab, 101 lines that readself.invCss— a name never assigned anywhere in the package — inside per-comparisontry/except Exceptionblocks that swallowed the resultingAttributeErrorand logged it as a failed check; the module-levelscipy.io.loadmatimport it required; a 155-line__main__demo indm_fittingand a 54-line one inreconstructor, both superseded byexamples/and the tutorial;sys.path.append("..")executed at import time indm_fitting; and a branch in thegridMaskproperty that returnedNoneunconditionally when taken.The reconstructor no longer forwards arbitrary attributes to the parameter objects, and assigning an unknown name now raises
AttributeError(#117).__getattr__and__setattr__used to resolve any unknown attribute by searching five parameter objects in turn — about 200 lines, plus a hand-maintained 15-entryspecial_attrslist and a bespoke fan-out fornLGS. The cost was that a misspelled parameter fell through toobject.__setattr__and became a new attribute:rec.r0_zenit = 0.1 # accepted silently before 2.0 rec.build_reconstructor() # built with the old r0
Nothing forwarded was visible to
dir(), IDE completion or type checkers, and the search ran on every internal assignment, doing up to fivehasattrcalls each — any of which could trigger a property getter that computes an array.nLGS,r0,r0_zenithandL0remain available directly on the reconstructor as explicit properties; settingnLGSstill updates every parameter object that tracks it. Those were the only forwarded names used anywhere in the tests, docs, examples or README. Everything else is reached through the object that owns it —rec.atmParams.altitude,rec.lgsWfsParams.nValidSubap,rec.dmParams.validActuators.Two modules were renamed so that no module shadows a class of the same name (#115):
before
after
pyTomoAO.tomographicReconstructorpyTomoAO.reconstructorpyTomoAO.fittingpyTomoAO.dm_fitting__init__.pyre-exportstomographicReconstructorandfitting, so those names bound to the classes and the modules underneath became unreachable by attribute lookup:import pyTomoAO.fitting as m; m.fittingraisedAttributeError, andunittest.mockstring targets such as"pyTomoAO.fitting.fitting"silently resolved to the class — working on Python 3.11+, which resolves modules first, and failing on 3.9 and 3.10, which walk attributes. The tests carriedimportlib.import_moduleworkarounds and a written explanation for exactly this; both are now gone, along with the corresponding caveats in the contributing and testing guides.Class names are unchanged:
tomographicReconstructor,fittingand the*Parametersclasses keep the naming the code-style guide deliberately preserves. Update imports of the formfrom pyTomoAO.fitting import fittingtofrom pyTomoAO.dm_fitting import fitting;from pyTomoAO import fittingis unaffected.Reconstruction grid points are now indexed consistently in C order, and
reconstruct_wavefrontno longer returns a transposed wavefront (#104)._sparseGradientMatrixAmplitudeWeightedindexed the grid in Fortran order — a MATLAB port artefact, since MATLAB is column-major throughout — while masking it with a C-order boolean, and the covariance kernels matched that.reconstruct_wavefrontthen scattered the result in C order, so the map it returned was the transpose of the real wavefront. Onlyvisualize_reconstructioncompensated, by displayingreconstructed_wavefront.T; the plots looked right while the returned array did not.Two consequences, both fixed:
A pure x-gradient slope vector reconstructed to a ramp along y. Verified directly: before, variation along x was 1.3e-22 against 1.6e-06 along y; after, the reverse.
On a pupil that is not symmetric under transpose, the gradient operator was simply wrong: a flat wavefront produced slopes of magnitude 1.0 (12 of 84 non-zero). On a symmetric pupil the two conventions coincide and the error vanishes, which is why every configuration shipped with the package — all of them symmetric — hid it.
The compensating transpose in
visualize_reconstructionis removed with the fix, so plots are unchanged. Anything consumingreconstruct_wavefrontdirectly, or comparing against a stored reference wavefront, will see the corrected orientation.
Added¶
examples/benchmark/benchmark.py, which timesbuild_reconstructoron every bundled configuration and on every available backend, checks CPU/GPU agreement, and can record and compare against a committedbaseline.json(--save-baseline,--check-baseline). It replacestest_auto.py,test_auto_gpu.pyandcompare_cpu_gpu.py— about 1700 lines that carried forked copies of the covariance kernels instead of calling the package, and had drifted far enough to contain none of the corrections made since. The benchmarks therefore reported numbers for code that was no longer shipped, and could not have caught thenFitSrc > 1defect below. Also removedtomographicReconstructorBenchmarking.py, which imported an undeclaredtomoAOdependency and carried a hard-coded/Users/...path (#119).The reference configurations now ship inside the package, so
pip install pyTomoAOis enough to run the documented examples. Previously the published wheel contained ten.pyfiles and no data, and the configuration path in the README raisedFileNotFoundErrorfor anyone who had not cloned the repository (#106):from pyTomoAO import example_config, list_example_configs rec = tomographicReconstructor(example_config("kapa"))
list_example_configs()returns['kapa', 'kapa-single-channel', 'keck', 'revolt']. The YAML files moved fromexamples/benchmark/topyTomoAO/data/;example_configreturns a path inside the installed package, so copy one before editing.pip install "pyTomoAO[gpu]"extra, which pulls incupy-cuda12x(#109).CITATION.cff, so GitHub renders a “Cite this repository” button (#108).Documentation site built with Sphinx, MyST and the Furo theme, published to GitHub Pages at https://keckobservatory.github.io/pyTomoAO/. The site covers installation, a quickstart, a configuration reference with units and validation rules for every key, a user guide (concepts, reconstruction modes, DM fitting, GPU acceleration), a KAPA LTAO tutorial, a generated API reference and a development section.
Documentationworkflow: builds the docs on pull requests and pushes tomain/devwith warnings treated as errors, and deploys to GitHub Pages on pushes tomain.Code healthworkflow:ruff checkandruff format --checkon pull requests and pushes, with the rule set configured inpyproject.toml.python_requires=">=3.9", so pip refuses to install on interpreters the package does not support.Package metadata now carries a long description (the README), so the PyPI project page is no longer blank.
Dependabot configuration for GitHub Actions and the pinned docs toolchain, targeting
dev.This changelog.
MIT license (#78).
Developer guide and usage examples (#76).
Changed¶
The GPU backend could not build a reconstructor with more than one optimisation direction.
_cross_correlationconcatenated the directions into a 2-D array on the GPU while the CPU kernel stacked them into a 3-D one, and_build_reconstructor_modelweighted the result withfitSrcWeight[:, None, None]— correct only for a single direction. With the bundledkeckconfiguration (nFitSrc = 7, so 49 directions) the broadcast asked for a 49× larger array and the build failed trying to allocate 66 GB on the device. The GPU kernel now returns the same rank as the CPU one.keckbuilds in 0.61 s on GPU against 36.1 s on CPU, agreeing to 7.1e-5 (#119).force_cpu=Truenow actually selects the CPU kernels. It used to flip a module-levelCUDAflag, but the GPU functions were already bound to module names at import time, so the reconstructor logged “Forcing CPU usage” and then ran the GPU kernels in float64.Cxx,Cox,CnZandRecStatSAcame back ascupy.ndarray, and the option could not serve its main purpose of side-stepping a misbehaving GPU. The backend is now resolved per instance by the newpyTomoAO.backendmodule and exposed asrec.backend("cpu"or"gpu"); constructing one reconstructor withforce_cpu=Trueno longer changes the backend of any other. The module-levelpyTomoAO.reconstructor.CUDAremains as a read-only “is CuPy importable” flag (#112).The reconstructor now solves the regularised system instead of forming an explicit inverse and multiplying.
Γ·Cxx·Γᵀ + Cₙis symmetric positive definite, so a Cholesky solve is both cheaper and better conditioned;build_reconstructordrops from 2.9 s to 2.4 s on CPU for the KAPA configuration. Results move only at round-off (8.7e-15 relative in float64).A CuPy that is installed but fails to load is now reported as a warning, with the underlying exception, instead of an
INFOmessage claiming CUDA is unavailable. A driver or toolkit mismatch was indistinguishable from CuPy simply not being installed, so users silently took the CPU path and a ~35× slowdown. CuPy genuinely not being installed stays atINFOand now points at the[gpu]extra (#110).The README no longer advertises MOAO support. There is one reconstructor and no MOAO-specific code path; the feature list now describes what the library actually does, and the roadmap ticks the items that are already shipped rather than listing GPU support and DM fitting as outstanding (#107).
The covariance kernels are ~5× faster and produce bit-identical results. They used to evaluate the covariance over the full
sampling × samplinggrid and only then cut it down to the valid pupil points, discarding 71% of the Bessel evaluations on the function that is 89% of runtime. The pupil mask is now applied to the coordinates, once per guide-star pair rather than once per turbulence layer.build_reconstructoron the KAPA configuration drops from 14.4 s to 2.9 s on CPU and 0.11 s to 0.08 s on GPU, and the CPU test suite from 63 s to 16 s (#101, #103).A real-valued
float64 → float64Bessel kernel (_kv56_real) is now used on the hot path. The distances are real and the result was immediately passed throughnp.real, so the complex overload only added acomplex128copy of the input and twice the arithmetic; the copy alone was ~7% ofbuild_reconstructor. The complex_kv56remains for compatibility, and both now share one set of module-level expansion constants so they cannot drift apart (#102).The whole codebase is now formatted with
ruff format(100-column lines). This is a formatting-only change; no behaviour was altered.Normalised the NumPy-style docstring sections in
tomographyUtilsCPUandtomographyUtilsGPU, and added docstrings to the CPU reconstructor builders, so the API reference renders them correctly.[docs]extra now installs the Sphinx/Furo/MyST toolchain, pinned indocs/requirements.txt.Run Pytestnow runs on pushes tomain/devas well as pull requests, tests a matrix of Python 3.9–3.13, and installs the package itself instead ofrequirements.txtso that dependency metadata is exercised. The coverage gate runs once, on 3.12. The workflow’sactions/checkout@v2andactions/setup-python@v2pins, which use a retired Node runtime, were updated to v4/v5. The plainpyteststep is skipped on 3.12, where the coverage gate already runs the same suite and fails the job on any test failure — previously the slowest job in the matrix ran every test twice (#95).The coverage wrapper now runs pytest as
sys.executable -m pytestinstead of whicheverpytestis first onPATH, so the coverage run always matches the environment under test.Publish Python Package to PyPInow verifies distributions withtwine check --strictand installs the wheel into a clean virtualenv before publishing, and publishes through apypiGitHub environment that can carry a review rule.The reconstruction integration test no longer pins 7-significant-figure mean-OPD values with
rtol=0. Those failed on any machine with a CUDA device, because the float32 GPU path lands 1.3e-3 away from the float64 reference — so the suite was red on developer machines and green in CI only because the runners have no GPU. Reconstruction accuracy is now checked by a physical round trip (known phase → gradient operator → reconstruction), which holds on both backends and survives numerical improvements (#98).Added
tests/conftest.pywith path fixtures resolved from__file__, so the tests no longer depend on being run from the repository root, and moved the temporary config written bysimple_configintotmp_path(#99).Contact email for Jacob Taylor updated to jacobataylor7@gmail.com.
Project URLs point at https://github.com/KeckObservatory/pyTomoAO.
Fixed¶
The
K_{5/6}Bessel kernel lost seven digits of accuracy abovez = 2. Three compounding defects: the1/z^5coefficient of the asymptotic expansion read5005/177147where the recurrencea_k = a_{k-1}(4v²-(2k-1)²)/(8k)gives40040/177147(exactly 8× too small); the series/asymptotic crossover sat atz = 2, where the asymptotic expansion is nowhere near converged; andΓ(11/6)was stored to only 12 digits, which the series’exp(z)cancellation amplified into the dominant error term abovez ≈ 4. Worst-case relative error againstscipy.special.kvdrops from 2.1e-3 to 3.6e-8 in double precision (2.1e-4 in single, where cancellation is the limit). With the KAPA configuration 17% of point pairs fell in the affected range, so reconstructor values shift slightly — about 0.1–0.4% on mean reconstructed OPD (#97).wfsLensletsRotationwas applied in the wrong units._create_guide_star_gridconverted the angle from radians to degrees and then passed it to_rotateWFS, which treats its argument as radians, so a requested rotation of θ was applied as57.3·θ. Both backends were affected. This was invisible in every shipped example configuration, all of which set the rotation to zero (#92).assemble_reconstructor_and_fittingwas not idempotent. Thesimuandkeckbranches wrote their X/Y block swap back into_reconstructor, so calling the method a second time — natural when tuningscalingFactor,rotationorstretch_factor— swapped the blocks again and silently returned a different, wrongFR. The reordering is now derived into a local andreconstructor/Rkeeps the matrixbuild_reconstructorproduced (#93).Valid grid points that reconstructed to exactly zero were turned into NaN.
reconstruct_wavefrontandvisualize_commandsused zero as the “outside the pupil” sentinel; they now build their output from the boolean mask (#94).Zero-separation covariance entries were wrong by a factor of ~1.887 on GPU. The CUDA tiny-argument shortcut for
K_{5/6}used a coefficient of1.89719where the small-argument limit gives2^(5/6)·Γ(5/6)/2 = 1.005635, and both backends selected the zero-separation case with an exactrho != 0test. Because the two coordinate grids are built by different arithmetic, a mathematically-zero separation could evaluate to a few ULPs instead and take the Bessel branch — corrupting the largest entries ofCxx/Cox. The constant is corrected and the selection is now tolerance-based (#90).The GPU interaction-matrix reconstructor could not run at all.
_build_reconstructor_imcalledcp.sqeeze, and theIMargument was never copied to the device (#89).Importing pyTomoAO no longer reconfigures logging for the whole application.
tomographicReconstructorcalledlogging.basicConfig(level=logging.DEBUG)andfittingcalled it withCRITICAL, so importing the package switched on debug logging for the host application (or not, depending on import order) and muted matplotlib’s logger. The package now attaches aNullHandlerand leaves configuration to the caller. To see pyTomoAO’s messages, calllogging.basicConfig(level=logging.INFO)yourself.pytestis no longer a runtime dependency. It was listed ininstall_requires, so every installation of pyTomoAO pulled in pytest; it now lives in thedevextra.Tests failed on Python 3.9 and 3.10.
test_tomographicReconstructorandtest_fittingpatched dotted string targets such as"pyTomoAO.tomographicReconstructor.atmosphereParameters". Because__init__.pyre-exports those classes under their modules’ names, the dotted path resolves to the class;unittest.mockresolves modules first on 3.11+ but walks attributes on older versions, so the patches raisedAttributeErrorandModuleNotFoundErrorthere. The tests now patch the module and class objects directly.tomographyUtilsGPUimportedgammafrom bothcupyx.scipy.specialandscipy.special, so the first import was dead. Removed it; the module only ever callsgammaon Python floats, so behaviour is unchanged.Unused imports and variables, bare
exceptclauses, identity/equality comparison slips and other issues surfaced by the new lint gate.
Removed¶
Support for Python 3.8, which reached end of life in October 2024. The supported and tested range is now 3.9 through 3.13.
The previous Read the Docs oriented Sphinx configuration, including the
sphinx_rtd_themedependency. Documentation is now published to GitHub Pages.Top-level
requirements.txt, which duplicatedinstall_requiresand had already drifted from it. Install the package instead:pip install -e ".[dev]".
1.0.1 - 2025-05-13¶
Added¶
User documentation: introduction, installation, basic usage and tutorial pages, with figures for the reconstructed wavefront and DM commands (#71, #72).
github_pytest_workflow.py, a wrapper that runs the test suite with coverage and fails the build below a coverage threshold or when large source files have no tests (#70).
Changed¶
Substantial work on
tomographicReconstructorandfitting, including the influence function model and the reconstructor/fitting assembly.Docstrings corrected across the package (#73).
Test and example configurations trimmed.
Removed¶
pyTomoAO/Fitting_template.py, superseded byfitting.py.
1.0.0 - 2025-04-28¶
First release published to the production PyPI index.
Added¶
Single source of truth for the version:
setup.pyreads__version__frompyTomoAO/__init__.py(#57, #59).
Changed¶
The publish workflow now uploads to PyPI rather than Test PyPI (#60).
Documentation build fixes (#56).
0.0.1 - 2025-04-25¶
Changed¶
Packaging and the publish workflow reworked for Test PyPI, with the version derived from the release tag (#53, #54).
Removed¶
The legacy
.iniconfiguration format; YAML is the only supported configuration format (#55).
0.1.0-test1 - 2025-04-22¶
First tagged release, published to Test PyPI.
Added¶
tomographicReconstructor, the configuration-driven entry point, with the model-based MMSE reconstructor built from the atmosphere, asterism, WFS and DM parameter classes.Interaction-matrix-based reconstructor (#50).
fittingwith the double-Gaussian DM influence function model (#37).CPU and GPU (CuPy) implementations of the covariance and reconstructor kernels, selected automatically at import.
Single-channel operation and reconstructor tuning for Keck K1 (#46).
Benchmarking scripts comparing the CPU and GPU paths (#39, #41).
Test suite covering the parameter classes, fitting and the reconstructor (#36), run by a GitHub Actions workflow (#23, #24).
Initial Sphinx documentation scaffolding (#29).