feat(interferometer): fits, save_attributes and aggregator reload on array-free datasets (streaming phase 2) - #639
Conversation
…array-free datasets (streaming phase 2, #638) profile_visibilities / profile_subtracted_visibilities / inversion_with_data guard the transformer-less dataset (pixelization and linear-light fits run; ordinary light profiles raise until phase 4). save_attributes writes dataset.fits via the new interferometer_hdu_list_from: array-free datasets persist their SparseTerms as FITS extensions (W~, dirty image, dirty beam, scalars + provenance in the header), in-memory datasets the same four arrays as before, now EXTNAME-tagged. The aggregator loader reads by EXTNAME with a positional fallback and rebuilds array-free datasets via from_sparse_terms with the sparse operator re-attached, so a reloaded fit reproduces log_evidence exactly. Scalars ride a lossless float64 HDU (FITS header cards truncate exponent-form float64); agg_util.mask_header_from now reads PIXSCAX for the x axis (pre-existing bug found in review). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JZZksyZ8LTA4LLxoZjQMNF
Independent review (Codex gpt-6-astra) of this branch pair before PR-open
FINDINGS (3) Disposition: FINDINGS (3) — #1 FITS header cards truncate float64 scalars (reproduced: 1.2345678901234567e+20 → 1.23456789012345e+20): fixed in-branch with a lossless 🤖 Generated with Claude Code |
Summary
Streaming phase 2 (Mind epic
streaming-visibilities; https://github.com/orgs/PyAutoLabs/discussions/13). Closes #638. Requires PyAutoLabs/PyAutoArray#593 (phase 1, onmain, pending release). The PyAutoLens half is PyAutoLabs/PyAutoLens#758.Phase 1 gave
aa.Interferometer.from_stream/from_sparse_terms: a dataset with nodata,noise_map,uv_wavelengthsortransformer. AnAnalysisInterferometersearch still could not run on it —save_attributeswrote the visibility arrays and the transformer class, the aggregator readdataset.fitsHDUs positionally and never re-attached a sparse operator, andFitInterferometer.profile_visibilitiesbuiltVisibilities.zeros(N_vis)through the transformer. This PR:profile_visibilitiesreturnsNoneon a transformer-less dataset with no ordinary light profile and raises a typedDatasetExceptionwhen there is one (non-linear light profiles arrive in phase 4; linear-light and pixelization fits work);profile_subtracted_visibilitiesevaluates it first (so a light-profile fit raises rather than silently fitting unsubtracted terms) then returnsNonewithout data;inversion_with_datareturns the inversion itself when there is no data. In-memory fits are unchanged.interferometer_hdu_list_from(dataset)(used bysave_attributeshere and in PyAutoLens): in-memory datasets write the same four arrays as before (EXTNAMEsMASK,DATA,NOISE_MAP,UV_WAVELENGTHS); array-free datasets write the mask,NUFFT_PRECISION_OPERATOR(W̃),DIRTY_IMAGEandDIRTY_BEAM(float64) plus a losslessSPARSE_TERMS_SCALARSfloat64 HDU (sum_weights,data_term,noise_normalization,n_vis,epsor NaN — order inSPARSE_TERMS_SCALARS_ORDER); the primary header carries readable copies (SUMW,DATATERM,NOISENRM,NVIS,EPS/TRNSFRMRwhen recorded). FITS header cards hold at most 20 characters, which truncates exponent-form float64, so the scalars are read from the HDU and only fall back to the cards for older files.transformer_class.jsonis written only when a transformer exists._interferometer_fromlooks HDUs up by EXTNAME (case-insensitive) with a positional fallback for pre-existing outputs; when the terms HDU is present it rebuildsaa.SparseTermsand returnsaa.Interferometer.from_sparse_terms(terms, real_space_mask)— array-free, operator re-attached. PyAutoLens imports this loader, so it needs no mirror.Witness (1e5 NUFFT visibilities, 616-pixel mask on a 40×40 grid streamed in 5 chunks, 20×20 rectangular pixelization,
af.m.MockSearchwithsave_for_aggregator=True, visualization disabled — dataset/fit plots need arrays until phase 3):dataset.fitsThe file is set by the real-space grid (mask 40×40, W̃ 56×56), not the number of visibilities: 48 B/vis × 1e5 would be 4.8 MB.
fits.infolistsMASK,NUFFT_PRECISION_OPERATOR,DIRTY_IMAGE,DIRTY_BEAMandSPARSE_TERMS_SCALARS, all float64, and notransformer_class.json.Also fixed here (review finding, pre-existing):
agg_util.mask_header_fromreadPIXSCAYfor both axes, so any saved mask with non-square pixel scales reloaded square; it now reads(PIXSCAY, PIXSCAX)with the old behaviour only for files lackingPIXSCAX. A second pre-existing finding — database-path searches never registerdataset.fitsthroughsave_fits, soFit.value("dataset")isNonethere — is out of scope and filed in Mind.API Changes
Additive plus small behaviour changes on array-free datasets only. New public name
autogalaxy.interferometer.model.analysis.interferometer_hdu_list_from(dataset)andSPARSE_TERMS_HEADER_KEYS.FitInterferometer.profile_visibilities/profile_subtracted_visibilitiesmay now returnNone(array-free dataset, no ordinary light profile) or raiseaa.exc.DatasetException(array-free dataset with one).save_attributeswrites EXTNAME-tagged HDUs (same arrays as before for in-memory datasets) and skipstransformer_class.jsonwithout a transformer. The aggregator loader reads by EXTNAME with positional fallback and re-attaches the sparse operator for array-free outputs; legacydataset.fitsfiles load as before.See full details below.
Test Plan
pytest test_autogalaxy— 1287 passed (1284 before the review fixes)jax.jit); linear-light-only fit works; ordinary light profile on an array-free dataset raises;interferometer_hdu_list_fromEXTNAMEs/header for both dataset kinds (in-memory arrays identical to before);save_attributes→_interferometer_fromround trips for array-free and in-memory datasets throughaf.DirectoryPathsintmp_pathnoise_map must have the same shape as data; got (6, 6) and (7, 7)— the positional read took W̃ as data); in-memory round trip still passesHeart RED override (development only)
Heart verdict at ship: RED, exact reason
release validation FAILED (stage integrate)(verdict 2026-09-30T12:42Z, unrelated release-integrate leg). The live human authorized the development-only override for issue #638 in-session on 2026-09-30 ("Authorize override for #638"): commit, push and the pending-release PRs only. Branch gates before the ask:test_autogalaxy1287 passed,test_autolens770 passed + 1 xfailed, round-trip red-check, independent Codex (gpt-6-astra) review: FINDINGS (3) — #1 FITS header cards truncate float64 scalars (reproduced: 1.2345678901234567e+20 → 1.23456789012345e+20): fixed in-branch with a losslessSPARSE_TERMS_SCALARSfloat64 HDU (header cards kept as readable copies, loader falls back to them for older files), red-checked; #3agg_util.mask_header_fromreadPIXSCAYfor both axes (pre-existing): fixed in-branch with a non-square round-trip test, red-checked; #2 database-path searches never registerdataset.fitsviasave_fits(pre-existing, in-memory path too): filed asdraft/bug/autogalaxy/database_paths_dataset_fits_not_registered.md. This PR does not claim to repair Heart; merge needs its own explicit human command with every required check green.Full API Changes (for automation & release notes)
Added
autogalaxy.interferometer.model.analysis.interferometer_hdu_list_from(dataset) -> HDUList— thedataset.fitscontents for in-memory (four arrays) or array-free (SparseTermsextensions) datasetsautogalaxy.interferometer.model.analysis.SPARSE_TERMS_HEADER_KEYS— header key map (SUMW,DATATERM,NOISENRM,NVIS,EPS,TRNSFRMR);SPARSE_TERMS_SCALARS_ORDER— field order of the losslessSPARSE_TERMS_SCALARSHDUChanged Behaviour
FitInterferometer.profile_visibilities→Noneon an array-free dataset with no ordinary light profile; raisesaa.exc.DatasetExceptionwith one.profile_subtracted_visibilities→Nonewhen the fit has no data.inversion_with_data→ the inversion itself when the fit has no data.AnalysisInterferometer.save_attributes— writes viainterferometer_hdu_list_from(EXTNAME-tagged; array-free datasets writeSparseTermsextensions + header scalars);transformer_class.jsononly when a transformer existsautogalaxy.aggregator.interferometer.interferometer._interferometer_from— EXTNAME lookup with positional fallback; rebuilds array-free datasets viaaa.Interferometer.from_sparse_termswith the operator re-attachedautogalaxy.aggregator.agg_util.mask_header_from— readsPIXSCAXfor the x pixel scale (wasPIXSCAYfor both axes); falls back toPIXSCAYonly whenPIXSCAXis absentMigration
Generated by the PyAutoLabs agent workflow.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JZZksyZ8LTA4LLxoZjQMNF