Read the MS SIGMA column per baseline instead of collapsing it to a scalar - #138
Open
chrisfinlay wants to merge 1 commit into
Open
Read the MS SIGMA column per baseline instead of collapsing it to a scalar#138chrisfinlay wants to merge 1 commit into
chrisfinlay wants to merge 1 commit into
Conversation
…calar read_ms did SIGMA.mean(), so the per-baseline noise that uvfits2ms estimates was computed, written to the MS, and then thrown away. Every run with data.noise: null was fit under a uniform noise. On EDA2 the per-baseline SIGMA spans ~30x, so a scalar under-weights the quietest baselines by up to ~200x in the likelihood. It is worse for anything fitting gains: the per-antenna noise correlates with the per-antenna gain (measured sigma_a ~ amplitude_a^0.76, R=0.96), so a uniform-noise likelihood cannot tell a loud antenna from a noisy one and the fitted gain absorbs the noise structure -- a bias in the calibration, not just lost efficiency. New tabascal/noise.py owns this. SIGMA is per row and constant in time per baseline, so it is reduced with a median over time rather than a mean, which keeps a few corrupted rows from dragging a baseline. Baselines with a non-positive or non-finite estimate take the median of the rest rather than a zero that would divide the likelihood by nothing; they are flagged out anyway. data.noise still overrides, and now also accepts an .npz carrying sigma_bl (per baseline) or s_ant (per antenna, combined as sqrt(s_p^2 + s_q^2)/sqrt(2), which reproduces itself for uniform antennas). Consumers split by what they actually need. The likelihood and reduced_chi2 take the per-baseline array; the time-integration sampling heuristic and the truth-metric normalisation take a new noise_scalar, since both reduce over every baseline and have no baseline axis left to align with. reduced_chi2 broadcasts the noise onto the data BEFORE masking. `x[~flags]` flattens, so applying a per-baseline noise afterwards would silently recycle values across baselines -- verified by mutation: reverting that ordering fails 2 tests, and only with a ragged flag mask, which is why the test uses one. Under sharding, noise was cast with float(); a per-baseline array cannot survive that, so it is made global and replicated instead. No reference re-recording: the simulated benchmark MS has a uniform SIGMA (one unique value), so per_baseline_sigma reproduces the old scalar exactly and all 10 pipeline chi^2 references pass unchanged. Progresses #121
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Progresses #121. Unblocks #110.
read_msdidSIGMA.mean(), so the per-baseline noise thatuvfits2msestimates was computed, written to the MS, and then thrown away. Every run withdata.noise: nullwas fit under a uniform noise.Why it matters
On EDA2 the per-baseline
SIGMAspans a factor of ~30, so a scalar under-weights the quietest baselines by up to ~200x in the likelihood.It is worse for anything that fits gains. The per-antenna noise correlates with the per-antenna gain — measured
sigma_a ~ amplitude_a^0.76, R = 0.96. A uniform-noise likelihood cannot tell a loud antenna from a noisy one, so the fitted gain absorbs the noise structure. That is a bias in the calibration solution, not merely lost efficiency.What changed
New
tabascal/noise.pyowns per-baseline noise:SIGMAis per row and constant in time per baseline, so a median keeps a few corrupted rows from dragging a baseline's estimate.data.noisestill overrides, and now also accepts an.npzcarryingsigma_bl(per baseline) ors_ant(per antenna, combined assqrt(s_p^2 + s_q^2)/sqrt(2)— normalised so uniform antennas reproduce themselves).Consumers split by what they actually need. The likelihood and
reduced_chi2take the per-baseline array. The time-integration sampling heuristic and the truth-metric normalisation take a newnoise_scalar, because both reduce over every baseline and have no baseline axis left to align with.The subtle part
reduced_chi2broadcasts the noise onto the data before masking:x[~flags]flattens, so applying a per-baseline noise afterwards would silently recycle values across baselines. Verified by mutation: reverting the ordering fails 2 tests — and only shows up under a ragged flag mask, which is why the test builds one (one sample dropped from baseline 0, baseline 3 dropped entirely).One test of mine was initially unsound and I fixed it: with the same residual on every baseline,
sum((r / noise_i)^2)is invariant under permuting the noise, so a shuffled pairing scores identically and proves nothing. The residual has to vary per baseline for the check to bite.Under sharding,
noisewas cast withfloat(); a per-baseline array cannot survive that, so it is made global and replicated instead.No reference re-recording
The simulated benchmark MS has a uniform
SIGMA— one unique value,0.6496226— soper_baseline_sigmareproduces the old scalar exactly. All 10 pipelinechi2references pass unchanged, on the nose. The variation this fixes lives in real EDA2 data.Testing
852 tests pass, including 29 new ones in
tests/test_noise.py. Docs updated fordata.noise.Not in this PR
#121 also asks for a model-free channel-differencing noise estimator, for data whose
SIGMAis absent or untrustworthy. That is a separate input path rather than part of unbreaking the existing one, and it is easier to review on its own — the issue stays open for it.