All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Suppressed a spurious
divide by zero encountered in divideRuntimeWarninginrotationmap.fit_annuli's weighted velocity average (niter > 1). The reciprocal weights are now computed withnp.divide(..., where=dvelo != 0, out=zeros), which skips the division at zero-error entries instead of evaluating it across the whole array and discarding the result.
- Structure function analysis. New
eddy.structurefunctionmodule with a numba-jitted 2D second-order structure function kernel, helpers for lag coordinates, axis/azimuthal profile extraction, and pair-count weighted combination across realizations. The 1D azimuthal spiral modelS2phi(withm=1,2,3modes) is included. StructureFunction2Dresult class holding the 2DS_2surface, pair counts, 1D profiles, and the underlying polar grid. Methods include.combine(...),.subtract(...),.compare_to(...),.fit_spiral(modes=...),.plot_2d(),.plot_profiles(), and.plot_comparison(...). Surfaced at the top level (from eddy import StructureFunction2D).StructureFunction2DStackcontainer — list-like over per-radiusStructureFunction2Dresults, with stacked array properties (S2_stack,S2_x_stack,S2_y_stack,S2_i_stack), batched.fit_spiral(modes=...),.half_power_lags(axis=...), the radius vs azimuth/radial-lag heatmaps (.plot_azimuthal_heatmap(),.plot_radial_heatmap(),.plot_anisotropy_heatmap()), and.plot_gridded()for the underlying deprojected field. Surfaced at the top level.momentmap.compute_structure_function(...)andcompute_structure_function_stack(ref_rs, ref_band, ...)— apply the structure function to a moment map after a polar deprojection throughimagecube.polar_deprojection; the stack variant sweeps reference radii on a single shared deprojection. Inherited byrotationmap, sofit_mapresiduals flow through the same entry point.- Anisotropic-GRF forward models.
grf_s2_slices,grf_s2_2d_global,predict_s2_slices,predict_s2_2d, andpredict_spiral_s2_2d— the Paciorek-Schervish-kernel structure functions used to fit(sigma, alphar, ell0r, ell0phi, alphaphi, pitch)from data. StructureFunction2D.fit_GRF(...)— dispatches on construction mode and apitchflag: reference-annulus slice fit, global-mode surface fit, or global-mode surface fit with the pitch freed (the only configuration that resolves the pitch sign). LM ('lsq') and emcee ('mcmc') back-ends share a single internal driver (_grf_fit_core).StructureFunction2DStack.fit_GRF(...)— joint anisotropic-GRF fit across the per-annulus slices.StructureFunction2DStack.measure_heuristics(...)— six model-free per-ring scalars (T1a..T4) mapping onto the GRF parameters: amplitude, radial/azimuthal correlation lengths, anisotropy, and stationarity slopes.- Spectral diagnostics on
linecube. NewSpectralACFcontainer andlinecube.spectral_acf(...)— the channel-to-channel autocorrelation diagnostic used to spot oversampled spectral axes before computing noise statistics. linecube.noise_structure_function(...)— empirical noiseS_2averaged over signal-free channels, returning aStructureFunction2D-compatible surface.linecube.gaussian_beam_s2(...)— analytic Gaussian-beam noiseS_2prediction matched to an empirical surface via an explicitmatch=argument (counts / lag-grid /noise_maskinherited).- Spectral smoothing in
linecube.to_momentmap. Newsmoothandpolyorderkwargs apply a Savitzky-Golay (or top-hat) smoother before collapse and re-estimate the RMS from the smoothed cube, matching thebettermomentsCLI flow. product=kwarg inlinecube.to_momentmap— pick a specific bettermoments product suffix (e.g.'v0'forquadratic) instead of always taking the first. The matching'd'-prefixed uncertainty product is attached to the returned map as.error.rotationmap.fit_map(optimize_kwargs=...)— forwarded to the pre-MCMCL-BFGS-Boptimizer. Supports'method'and'options'(a dict ofscipy.optimize.minimizeoptions).py.typedmarker —eddyis now PEP-561 type-stubs-compatible, so IDE / type-checker tooling no longer skips the package.structurefunctionoptional dependency — install the numba extra viapip install astro-eddy[structurefunction].import eddyworks without numba; only the structure-function entry points raise.- Tests. New
tests/test_structurefunction.py(27 cases) covering analytic recovery, NaN handling, reference-annulus mode, the result-classcombine/fit_spiralhelpers, the radius-sweep stack (including single-element-stack equivalence with a direct call), the GRF forward model (numba/Python kernel equivalence and physical limits), thefit_GRFsurface fit (parameter recovery, pitch recovery, and the non-positive-radius guard), themeasure_heuristicsscalars, thespectral_acf/noise_structure_function/gaussian_beam_s2linecube diagnostics, and end-to-end smoke tests throughmomentmap.compute_structure_functionandcompute_structure_function_stack.
linecube.to_momentmapis now keyword-only aftermethod. Everything else (product,clip,smooth,polyorder,bettermoments_kwargs) must be passed by name. An old positional call (e.g.cube.to_momentmap('quadratic', 3.0)) now raisesTypeErrorimmediately instead of bindingproduct=3.0and failing during product validation.linecube.to_momentmapno longer requires aBUNITheader card or the privatebettermoments.io._get_bunitshelper. A failed lookup falls back to a name-based velocity/intensity classification via the new_BM_VELOCITY_PRODUCTSset, so simulated cubes (which often lackBUNIT) and future bettermoments releases that move_get_bunitsboth degrade gracefully.rotationmap.fit_map(optimize_kwargs=...)rejects unknown top-level keys. Previously, anything other than'method'and'options'was silently dropped — including the common mistake of passing solver options (maxiter,ftol, ...) at the top level instead of nesting them under'options'. The new behavior raisesTypeErrorwith a hint about the nested form.- The momentmap polar pipeline leaves
S2_i = None. Itsdxis arcsec and itsdyis degrees, so an azimuthally-averagedsqrt(l_x^2 + l_y^2)mixes incommensurate units and is not a physical average.StructureFunction2D.S2_iis now anOptional[ndarray];plot_profiles,compare_to,plot_comparison,combine,subtract, andS2_i_stackall branch on it. - Uniform
rgrid/tgridare required by the structure-function pipeline.momentmap._structure_function_polar_gridnow raisesValueErroron log-spaced or otherwise non-uniform grids instead of silently mislabeling the lag axis with the mean spacing.
S2phi(dphi, Nphi, A1, A3=...)no longer silently drops the m=3 term whenA2 is None; them=3branch was previously nested inside them=2branch.StructureFunction2DStack.measure_heuristicsno longer raisesZeroDivisionErroron stacks with all-zero reliability weights (e.g. mixed rings where everyell_rorell_phihalf-power lag is NaN). Weight positivity is now folded into the radial/azimuthal masks and a clearValueErrorfires when nothing survives. The bareexcept Exceptionaround the slope fit was replaced with an explicitm.sum() < 2guard plus a targetedLinAlgErrorcatch.- Default-grid
fit_GRF(pitch=True, r_axis=...)no longer crashes inside scipy's SVD._grf_surface_setupdrops non-positive radii (with awarnings.warnnaming the count) before evaluatingell_r(r) = ell0r * (r/r0)**alphar, which was singular atr = 0. _grf_*_setupno longer crashes withmin() iterable argument is empty/zero-size array reductionwhen no annulus has positive lags. Degenerate cases now feednp.nanplaceholders to_grf_data_bounds, whose existing filter drops them and falls back to the module-wide defaults.imagecube.polar_deprojection'stgriddocstring now says[radians](matching the implementation, which buildsnp.linspace(-pi, pi, ...)); the previous "[degrees]" annotation silently produced ~57× inflated azimuthal lags for users who followed the docstring.pyproject.tomlversion had drifted fromeddy.__version__; both now report3.1.0.
StructureFunction2D.fit_GRF/StructureFunction2DStack.fit_GRFnow warn (a) when a user-suppliedp0entry is clipped onto a bound (naming the parameter, the requested value, and the bound), and (b) per parameter whose lsq solution sits on a bound (the reportedperris unreliable). The MCMC walker centre is also nudged strictly inside the box before scattering, so walkers no longer start with ~50 % of members at-infprior when a parameter pins. Gaussian priors are intersected with the flat bounds — a prior that pulls walkers past a bound is truncated there.
- All public 3.0.x
fit_map,fit_annuli,get_vlos,plot_*, and instantiation call signatures are preserved. - The
linecube.to_momentmapsignature change is the only positional break in 3.1.0. Migration is a one-line edit per call site (add,aftermethodand use keyword names for everything else).
TracerBoolConversionErrorin the pressure-corrected velocity profiles._vkep_pressureand_vpow_pressureselected the pressure taper with a Pythonif params['w_pressure'] > 0.0:, which cannot be traced whenw_pressureis a free parameter under the JIT/vmaplog-posterior built in_build_vectorized_ln_probability(the default single-processfit_mappath). The branch now usesjnp.wherewith a guarded denominator, so a fittedw_pressuretraces and differentiates cleanly while preserving the previous behaviour (no taper forw_pressure <= 0).
A major refactor centred on a JAX-backed model, a new class hierarchy, and an opt-in gradient-based MCMC sampler. All user-facing call signatures from 2.x are preserved.
- JAX backend.
rotationmap's model and likelihood (_make_model,disk_coords,_ln_likelihood, FFT beam convolution) are now JIT-compiled, autodifferentiable, and GPU-aware viajax.default_backend(). mcmc='numpyro'opt-in NUTS sampler inrotationmap.fit_map()andAnnulus3D.get_vlos_GP(). emcee remains the default backend. Legacynwalkers/nburnin/nstepskwargs map onto numpyro'snum_chains/num_warmup/num_samplesinternally; numpyro-specific controls (max_tree_depth,seed,chain_method) are passed throughmcmc_kwargs. See tutorial 6.imagecube.to_fits()— writesself.data(or a provided array) back to FITS with a header rebuilt from the livexaxis/yaxis/velaxso axis keywords stay in sync after FOV clipping etc.linecube.to_momentmap()— collapses a 3D spectral cube into a 2D moment map viabettermoments. Returns arotationmapfor velocity-typed methods (first,quadratic, ...) or amomentmapotherwise.tests/— a pytest smoke suite (24 tests) coveringimagecube,rotationmap, andannulusbasics. Tutorial FITS files are reused as fixtures..github/workflows/ci.yml— GitHub Actions matrix on Python 3.11 and 3.13, runs the smoke suite on every push tomaster/refactoringand on PRs.- Tutorial 6 (
tutorial_6_numpyro.ipynb) — short walkthrough of the numpyro path on the standard HD163296 setup, with recommendedmax_tree_depth=6and the prior-tightening tip for improper uniforms (e.g.r_taper). docsextra —pip install -e ".[docs]"now installs the Sphinx stack listed indocs/requirements.txt.
- Class hierarchy. The single
datacubeclass has been split into three:imagecube(shared FITS I/O, WCS, beam parsing, plotting),momentmap(2D),linecube(3D).rotationmapnow inherits frommomentmap. The legacyfrom eddy.datacube import datacubeimport still works (re-exportsimagecube as datacube). - Annulus split. The previous
annulusclass is nowAnnulus3D, with a new siblingAnnulus2Dfor pixel-velocity fits (used byrotationmap.fit_annuli).from eddy import annulusremains as an alias forAnnulus3D. - Gaussian Process backend swapped from
celeritetotinygp.quasisep.Matern32. JAX-compatible; lets the GP annulus path compose with the numpyro NUTS sampler. - Pre-MCMC optimizer switched from finite-difference
TNCtoL-BFGS-Bwith analytic JAX gradients. - Performance. ~16× cumulative speedup in warm
fit_mapcalls via JIT-closure +vmap'd batch log-probability. emcee path was the main beneficiary; numpyro path's per-iteration cost is documented in tutorial 6 and is being tracked separately. - Minimum Python version bumped from 3.8 to 3.10.
celeriteruntime dependency (replaced bytinygp).- Dead
_SHO_*private helpers inrotationmap(_fit_SHO,_SHO_chi2,_SHO_MCMC,_SHO_ln_*) — orphaned by thefit_annuli → Annulus2D.get_vlosrewire. ~140 lines. - Global
warnings.filterwarnings("ignore")calls at the top of each submodule — they were hiding three real bugs (all fixed; see below).
- NaN gradient hazards that prevented numpyro from running the
9-parameter 3D fit:
_analytic_zr_eff**psiat the cavity boundary (now wrapped in the double-wherepattern) and_ln_likelihood's NaN-data leakage viavjp(now zeros the masked data before the subtraction). dist.Uniform(0, inf)is degenerate in numpyro and aborted NUTS init; flat priors with±infbounds now dispatch todist.ImproperUniformon the matching constraint.- emcee 3.x
sampler.chaindeprecation warnings —fit_map's walker plot and PA-wrap now usesampler.get_chain(). np.log(1.0 / (hi - lo))RuntimeWarning for improper uniform priors.- Invalid
\-escape sequence inremove_hot_pixelsdocstring.
- All public
fit_map,fit_annuli,get_vlos,plot_*, and instantiation call signatures from 2.x are preserved. - Legacy import aliases retained:
from eddy.datacube import datacube,from eddy import annulus. - A few
DeprecationWarnings previously hidden by the global filter may now surface (notably from emcee 3.x when client code touchessampler.chaindirectly).
- If your environment pins
celerite, you can drop it. - If you maintain a subclass of
datacube, it still works via the shim. Consider migrating to the appropriate subclass (imagecube/momentmap/linecube) for clarity.
For releases earlier than 3.0.0, see the git history and the corresponding entries on PyPI.