Repository navigation
feat: opt-in quadrature pooling of unequal real/imag sigma on the sparse path #617
Description
Overview
The sparse interferometer path (sparse_terms_from_chunks, Interferometer.from_stream, Interferometer.apply_sparse_operator) refuses any noise map whose real and imaginary sigmas differ beyond rtol=1e-5. Noise estimated by differencing adjacent visibilities typically shows 1-2 % real/imag scatter, so every such chunk raises. This adds an opt-in pool_noise_map=True that pools the two sigmas in quadrature, sigma^2 = (sigma_re^2 + sigma_im^2)/2 (preserves total variance), reports the size of the difference, and documents the equal-sigma assumption. Requested by @HRSAstro (pyuvimage) in https://github.com/orgs/PyAutoLabs/discussions/13 (item 2).
Plan
- Add two helpers beside
check_noise_map_real_imag_equal: one returns the quadrature-pooled noise map, one reports the median and maximum fractional real/imag difference. - Add
pool_noise_map: bool = Falsetosparse_terms_from_chunks,Interferometer.from_streamandInterferometer.apply_sparse_operator; the default keeps raising exactly as today. - When pooling, every sparse term (precision operator, dirty image, dirty beam, data term, noise normalization) is built from the pooled noise map, and
apply_sparse_operatorreturns the dataset carrying the pooled noise map so dense and sparse statistics agree. apply_sparse_operator_from_chunks(pool_noise_map=True)is refused (likephase_centre), because it keeps the unpoolednoise_map.- One log line per call: nothing when already equal, INFO for a median difference up to 25 %, WARNING above 25 % naming the dense mapping path as exact.
- Docstrings and the error text state the equal-sigma assumption, how to pool, the dropped cos(a+b) term, and the evidence-comparison caveat.
- Tests: witness (pooled stream == pre-pooled chunks, bit-identical), default raise tests unchanged, pooled
apply_sparse_operatorparity vs the mapping path, log levels, chunks refusal, and one documentation pin of the approximation size at 2 % asymmetry.
Tier: judge — merge mode: human /prm
Detailed implementation plan
Work Classification
Library (PyAutoArray only)
Affected Repositories
- PyAutoArray (primary)
Branch Survey
| Repository | Current Branch | Dirty? |
|---|---|---|
| ./array/PyAutoArray | main (34c2e83) | clean |
Claims on PyAutoArray: imshow-origin-lower-overlays (PR #616 merged, pending release, no worktree) and community-pages (README only). This task touches only inversion_interferometer_util.py, dataset/interferometer/dataset.py and their tests — disjoint files.
Suggested branch: feature/sparse-noise-map-pooling
Worktree root: ~/Code/PyAutoLabs-wt/sparse-noise-map-pooling/
Implementation Steps
iiu = autoarray/inversion/inversion/interferometer/inversion_interferometer_util.py, ds = autoarray/dataset/interferometer/dataset.py.
iiu:noise_map_pooled_from(noise_map) -> np.ndarray(complexs + 1j*s,s = sqrt((re^2+im^2)/2); accepts the forms_complex_visibilities_fromaccepts) andnoise_map_real_imag_asymmetry_from(noise_map) -> (median, max)of|re-im|/max(|re|,|im|).iiu.sparse_terms_from_chunks(..., pool_noise_map=False): after_complex_visibilities_from, if pooling record per-chunk asymmetry and replacenoise_mapwith the pooled one before every term; otherwisecheck_noise_map_real_imag_equalas today. Accumulate stats across the generator; log once at the end via a shared helper (_log_noise_map_pooling): no message when all chunks are equal within rtol 1e-5; INFO "pooled sigma_re/sigma_im in quadrature: median difference X %, max Y %" for median <= 25 %; WARNING above 25 % namingInversionInterferometerMapping(noapply_sparse_operator) as the exact path.ds.Interferometer.from_stream(..., pool_noise_map=False): pass through.ds.Interferometer.apply_sparse_operator(..., pool_noise_map=False): poolself.noise_map, build every term from it, return the dataset with the poolednoise_map.ds.Interferometer.apply_sparse_operator_from_chunks:pool_noise_map=TrueraisesDatasetException(message: pool the dataset first vianoise_map_pooled_from, or usefrom_stream(..., pool_noise_map=True)).- Docstrings:
from_stream,apply_sparse_operatorPrecondition,apply_sparse_operator_from_chunks,sparse_terms_from_chunkschunk contract + phase-centre paragraph (pooled data_term exactly phase-invariant), error text ofcheck_noise_map_real_imag_equal; cos(a+b) caveat (curvature = sum wbar cos(a-b) + dw cos(a+b), W~ only holds the Toeplitz part; pooling differs from wbar at O(eps^2)); do not compare log-evidences of pooled vs unpooled fits. - No
SparseTermsprovenance field.
Tests
test_inversion_interferometer_util.py: pooled helper preserves re^2+im^2 with equal parts; asymmetry helper on a known 2 % map; witness:sparse_terms_from_chunks(chunks, pool_noise_map=True)== default call on pre-pooled chunks, field by field, bit-identical; caplog INFO at 2 %, WARNING with the percentage at 30 %, nothing at equality; existing raise tests unchanged.test_dataset.py:from_streampass-through;apply_sparse_operator(pool_noise_map=True)returns the pooled noise map and sparse log_evidence == mapping-path log_evidence on the pooled dataset;apply_sparse_operator_from_chunks(pool_noise_map=True)raises; one documentation pin of pooled-sparse vs unpooled-mapping log_evidence at 2 % (measured number recorded, order-of-magnitude bound).
Key Files
autoarray/inversion/inversion/interferometer/inversion_interferometer_util.pyautoarray/dataset/interferometer/dataset.pytest_autoarray/inversion/inversion/interferometer/test_inversion_interferometer_util.pytest_autoarray/dataset/interferometer/test_dataset.py
Risks
Pooling is approximate when sigmas truly differ (drops the cos(a+b) curvature term); under the opt-in the returned noise_map differs from the input. Must merge before sparse_terms_oversampled_fine_grids (same repo; the fine grids inherit the equal-sigma K).
Original Prompt
Click to expand starting prompt
Sparse interferometer path: opt-in quadrature pooling of unequal real/imag noise sigma
Type: feature
Target: PyAutoArray
Repos:
- PyAutoArray
Themes: - interferometer
- sparse-operator
- community
Autonomy: supervised
Priority: normal
Status: draft
Filed: 2026-10-07
Difficulty: small
Consequence: judge
Witness:sparse_terms_from_chunks(chunks, pool_noise_map=True)on chunks with a 2 % real/imag sigma asymmetry returns, field by field and bit-identically, the sameSparseTermsas the default call on the same chunks pre-pooled withnoise_map_pooled_from(today the unequal chunks raise incheck_noise_map_real_imag_equal); the default call still raises (existing teststest_dataset.py:229,:654, util:1255,:1265unchanged).
Review-minutes: 8
Unattended: ready
Source: GitHub Discussion https://github.com/orgs/PyAutoLabs/discussions/13 (external contributor @HRSAstro, pyuvimage), comment Streaming visibilities for memory efficiency .github#13 (comment) item 2; technical review 2026-10-07 (Item A, accept opt-in).
Request (verbatim)
2. Unequal real and imaginary sigma
At the moment,
sparse_terms_from_chunksraises an error when σ_re ≠ σ_im. For thermal noise, the real and imaginary parts of one integration should have the same variance, so in practice a difference usually comes from the noise estimator. With noise estimated by differencing adjacent visibilities we typically see a 1–2% difference. Pooling in quadrature, σ² = (σ_re² + σ_im²)/2, preserves the total variance and is the better estimate of both.It might be friendlier to offer pooling as an option, with a warning that reports the size of the difference (and a stronger one when it is large, e.g. above ~25%, where it may be real), rather than refusing the chunk. At minimum, the docstrings of
from_streamandapply_sparse_operatorcould say that the sparse path assumes equal sigmas and suggest pooling before passing the data in. This is what pyuvimage does now before handing chunks over.
What
iiu = autoarray/inversion/inversion/interferometer/inversion_interferometer_util.py, ds = autoarray/dataset/interferometer/dataset.py (origin/main lines).
iiu:32 check_noise_map_real_imag_equalusesnp.allclose(re, im, atol=0.0)(rtol 1e-5): 1-2 % estimator scatter always raises. Called atds:530(apply_sparse_operator) andiiu:2207(sparse_terms_from_chunks, per chunk;from_streamds:379andapply_sparse_operator_from_chunksds:738inherit it).- Only W~ (
iiu:2219, builderiiu:584w = 1/sigma^2) and dirty beam /sum_weights(iiu:2244) use sigma_re alone; dirty image,data_term,noise_normalizationalready use both. The dense mapping path is exact for unequal sigmas (iiu:89-124,inversion/interferometer/mapping.py:90-104). - Why sparse cannot be exact: curvature =
sum wbar cos(a-b) + dw cos(a+b); W~ only represents the Toeplitzcos(a-b)part. Quadrature poolingw = 2/(s_r^2+s_i^2)differs fromwbarat O(eps^2) (4e-4 at 2 %) and preserves total variance, so it is the right substitute.
Plan
- Helpers in
iiubeside the check:noise_map_pooled_from(noise_map)→ complexs + 1j*s,s = sqrt((re^2+im^2)/2)(accept the forms_complex_visibilities_from,iiu:2051, accepts);noise_map_real_imag_asymmetry_from(noise_map)→ (median, max) of|re-im|/max(re,im). pool_noise_map: bool = False(default keeps raising — no silent behaviour change):sparse_terms_from_chunks(iiu:2066): after_complex_visibilities_from(iiu:2196) record chunk asymmetry and replace the noise map before every term (W~, dirty image, beam, data_term, noise_normalization). Accumulate stats across the generator; log once at the end.from_stream(ds:328): pass through.apply_sparse_operator(ds:419): poolself.noise_map, build from it, and return the dataset with the pooled noise_map (ds:604-611returnsself.noise_maptoday) so dense residual/chi^2/noise_normalization agree with the cached sparse terms.apply_sparse_operator_from_chunks(ds:613): refusepool_noise_map=TruewithDatasetException, mirroring the phase_centre refusalds:701-709; message: pool first (Interferometer(..., noise_map=noise_map_pooled_from(nm))) or usefrom_stream(..., pool_noise_map=True).
- Messages via module
logger: none at exact equality (rtol 1e-5); INFO "pooled sigma_re/sigma_im in quadrature: median difference X %, max Y % (consistent with noise-estimator scatter)" for median <= 25 %; WARNING above 25 % (contributor'sREIM_ASYMMETRY_WARN): "difference may be real; pooling weights real and imaginary parts equally, which the noise map does not; the dense InversionInterferometerMapping path (no apply_sparse_operator) is exact". - Docstrings (ship even if the option slips):
from_stream(ds:342-378),apply_sparse_operatorPrecondition (ds:516-526),apply_sparse_operator_from_chunks, chunk contract (iiu:2104), error text (iiu:69-86): equal-sigma assumption,pool_noise_map=True/noise_map_pooled_from, cos(a+b) caveat; phase-centre paragraph (iiu:2120-2131): with poolingdata_termis exactly phase-invariant. Evidence caveat: do not compare log-evidences of pooled vs unpooled fits (noise_normalization differs O(eps^2) per visibility). - No
SparseTerms.noise_pooledprovenance field (pooled + exactly-equal chunks sum correctly).
Tests (test_inversion_interferometer_util.py, test_autoarray/dataset/interferometer/test_dataset.py): pooled helper preserves re^2+im^2 with equal parts; asymmetry helper on a known 2 % map; the witness; apply_sparse_operator(pool_noise_map=True) returns pooled noise_map and sparse log_evidence == mapping-path log_evidence on the pooled dataset (existing tolerance); caplog INFO at 2 %, WARNING with percentage at 30 %, nothing at equality; apply_sparse_operator_from_chunks(pool_noise_map=True) raises; one documentation pin of pooled-sparse vs unpooled-mapping log_evidence at 2 % (record the number, assert an order-of-magnitude bound).
Risks: pooling is approximate when sigmas truly differ (drops cos(a+b)); returned noise_map differs from input under the opt-in only. Must merge before sparse_terms_oversampled_fine_grids (same repo claim; fine grids inherit the equal-sigma K).
Activity
Jammy2211 commentedon Oct 7, 2026
Library PR opened: #619 (judge tier — human /prm). Heart YELLOW at ship (manifest drift: shared-standards blocks; release validation stale) acknowledged per the human's pre-authorisation. Tests: full test_autoarray 1991 passed, 4 xfailed. Workspace impact: none (additive opt-in kwarg, default unchanged); no smoke run.
Jammy2211 commentedon Oct 7, 2026
Shipped
Merged in #619 (merge commit 2c5cb69, 2026-10-07) —
sparse_terms_from_chunks(..., pool_noise_map=True)opt-in quadrature pooling, σ² = (σ_re² + σ_im²)/2, of unequal real/imaginary sigma on the sparse interferometer path. The default call still raises on unequal sigma. All three CI legs were green.Measured documentation pin: a 2 % real/imag asymmetry moves the log likelihood by 9.9e-3 nats (7.7e-5 relative) in-pytest, and by 0.01–0.096 nats across seeds.
Thanks to @HRSAstro for the suggestion in https://github.com/orgs/PyAutoLabs/discussions/13 (PyAutoLabs/.github#13 (comment)).
Pending release: this is merged to
mainbut is not yet in a published PyAutoArray release.