Skip to content

convergence test: document the weight-order trap (documentation only; no functional change) - #57

Merged
oshaughnessy-junior merged 3 commits into
rift_O4dfrom
rift_O4d_weights_sort_budget
Aug 10, 2026
Merged

convergence test: document the weight-order trap (documentation only; no functional change)#57
oshaughnessy-junior merged 3 commits into
rift_O4dfrom
rift_O4d_weights_sort_budget

Conversation

@oshaughnessy-junior

@oshaughnessy-junior oshaughnessy-junior commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Documentation only. No functional change ships here: the two samplers differ from rift_O4d by a corrected comment, and one deprecation note in helper_LDG_Events.py. An earlier revision of this branch carried a draw-order reorder in convergence_test_NormalSubIntegrals; it was withdrawn after review, for the reasons below.

The trap

convergence_test_NormalSubIntegrals splits _rvs into ncopies CONTIGUOUS segments and assumes they are independent. They are not: the save-P thresholding block (mcsampler.py:739-752) reindexes every _rvs column by cumulative-weight order, so surviving rows are sorted by weight ascending.

segments max/min segment mass
weight-ordered (what the test sees) 1.15e3
draw-ordered 1.83

Two candidate repairs, both of which fail

1. The in-tree workaround does nothing. mcsampler.py:1138 / mcsamplerGPU.py:1559 said "rvs['weights'] is sorted ... Recalculated weights are not. Use explicitly calculated weights until sorting effect identified." Measured, cached and recomputed weights are both 100% ascending -- the components were permuted by the same reindexing. That claim was false and is removed.

2. Reordering by sample_n also fails -- this is what the review caught, plus a second mode found while checking it:

after appending, before the threshold block:  len(integrand)=3000  len(sample_n)=1500   MISMATCH
after the block re-runs:                      len(integrand)=2999  len(sample_n)=2999   ok

In that window, numpy fancy-indexing silently returns the shorter array and drops every new sample. And even once the lengths agree, sample_n is arange(len(...)) assigned to an already-permuted array, so it encodes the order at the start of that call:

after two integrate() calls ascending segment max/min
weight-ordered 1.000 1.12e3
reordered by sample_n 0.752 373
single call, for reference 0.491 1.83

So the reorder was partly ineffective even with a length guard.

Why nothing is fixed here

A real repair needs stable ids assigned where samples are appended, in every sampler. This test has no live callers -- every production call site is commented out, only test/test_like_and_samp.py uses it -- so that plumbing is not justified. Since sorting here also drives output downselection, the conservative option was preferred.

What ships

  • Corrected comments in both samplers: the false claim removed, both failure modes and the required plumbing stated.
  • test_convergence_sample_order.py, 5 checks, holding the measurements. test_sample_n_does_not_encode_draw_order_after_reuse is written to FAIL if the samplers ever grow stable append-time ids -- the signal to revisit the note rather than let it rot.
  • Deprecation note on the bayestar-skymap branch of helper_LDG_Events.py: unused in production ~a decade, and the only place --n-chunk drops to 500, which makes it the least-tested regime for anything chunk-scaled (--portfolio-weight-clip uses tau = C*sqrt(n_chunk)*mean(w), ~4.5x more aggressive there than at the n_chunk=1e4 all clip testing used).

CI: the two failing container canaries are unrelated upstream pygsl-lite/setuptools failures.

oshaughnessy-junior and others added 2 commits August 8, 2026 12:44
…did anything

The third cached-column divergence flagged in the #55 audit, now identified.

mcsampler.py:1138 and mcsamplerGPU.py:1559 carried the note "rvs['weights'] is
*sorted* (side effect?), breaking test.  Recalculated weights are not.  Use
explicitly calculated weights until sorting effect identified."

The effect is real and its source is the save-P thresholding block
(mcsampler.py:739-752): it reindexes EVERY _rvs column by cumulative-weight
order, so surviving rows come out sorted by weight ascending.
convergence_test_NormalSubIntegrals then splits them into `ncopies` CONTIGUOUS
segments and assumes those are independent -- but sorted rows put the smallest
weights in the first segment and the largest in the last.  Measured on a
reconstructed record, max/min segment mass is 1.15e3 weight-ordered versus 1.83
in draw order: the sub-integrals differ by construction and the normality check
is meaningless.

The workaround does NOT help, and this is the part worth recording: the
components were permuted by the same reindexing, so recomputing from them
reproduces the identical order.  Measured, cached and recomputed weights are both
100% ascending.  The comment's claim that recalculated weights are unsorted is
simply false.

What survives the permutation is `sample_n`, written immediately before that
block precisely as an iteration number.  Both copies of the test now reorder by
it before segmenting.

SEVERITY, stated honestly: every production caller of this test is commented out
(util_ConstructIntrinsicPosterior_GenericCoordinates.py:693 and two siblings);
the only live caller is test/test_like_and_samp.py.  So this was a latent trap
rather than a live defect -- but a misleading comment plus an ineffective
workaround is exactly how one survives, and re-enabling the test would have
silently produced nonsense.

Adds test_convergence_sample_order.py (3 checks), including one that pins WHY the
old workaround was ineffective so it is not reinstated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d it; deprecate the skymap branch

Review of #57 was right, and measuring it turned up a second problem in the same
place, so the source hunks are withdrawn.  The diff against upstream in
mcsampler.py / mcsamplerGPU.py is now COMMENT-ONLY.

[P2, confirmed] A reused sampler appends rows to the weight columns while
sample_n still has the previous length -- measured 3000 weights against 1500 ids
-- and numpy fancy-indexing then silently returns the shorter array, dropping
every new sample.

[not raised, found while checking] Even after the threshold block re-runs and the
lengths agree, reordering by sample_n does NOT restore draw order: it is
(re)created as arange(len(...)) against an ALREADY-permuted array, so it encodes
the order at the start of that call.  Measured after two integrate() calls,
0.752 ascending (draw order is ~0.5) and a segment ratio of 373 rather than 1.83.
So the proposed fix would have been partly ineffective even with a length guard.

A real fix needs stable ids assigned where samples are APPENDED, in every
sampler.  This test has no live callers -- every production call site is
commented out, only test/test_like_and_samp.py uses it -- so that plumbing is not
justified.  What ships instead: the false claim is removed (the old note said
recomputing from components avoids the sorting; measured, cached and recomputed
are BOTH 100% ascending), and the caveat now states what is actually true,
including what anyone re-enabling the test must do first.

Tests keep the evidence: 5 checks now, including both reuse failure modes.  The
draw-order test is written so it FAILS if the samplers ever grow stable
append-time ids, which is the signal to revisit the caveat.

Separately: flags the bayestar skymap branch as deprecated.  It is unused in
production for ~a decade and is the only place n-chunk drops to 500, making it
the least-tested regime for anything scaling with chunk size -- notably
--portfolio-weight-clip, whose tau = C*sqrt(n_chunk)*mean(w) is ~4.5x more
aggressive there than at the n_chunk=1e4 every clip test used.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@oshaughnessy-junior

Copy link
Copy Markdown
Owner Author

You were right, and checking it turned up a second problem in the same place -- so I have withdrawn the source hunks. The diff against rift_O4d in mcsampler.py / mcsamplerGPU.py is now comment-only.

[P2] confirmed

Measured the two-integrate() scenario directly:

after appending, before the threshold block:  len(integrand)=3000  len(sample_n)=1500   MISMATCH
after the block re-runs:                      len(integrand)=2999  len(sample_n)=2999   ok

So in the window you identified, numpy fancy-indexing silently returns the shorter array and every new sample is dropped. Exactly as described.

A second problem, which is what settled it

Even once the block re-runs and the lengths agree, reordering by sample_n does not restore draw order. It is (re)created as arange(len(...)) against an already-permuted array, so it encodes the order at the start of that call:

after two integrate() calls ascending fraction segment max/min mass
weight-ordered 1.000 1.12e3
reordered by sample_n 0.752 373
(single call, for reference) 0.491 1.83

So the recommended fix would have been partly ineffective even with a length guard. Getting it right needs stable ids assigned where samples are APPENDED, in every sampler -- and this test has no live callers (all production call sites commented out; only test/test_like_and_samp.py), so that plumbing is not justified.

What ships instead

  • The false claim is removed. The old note said recomputing the weights from the components avoids the sorting; measured, cached and recomputed weights are both 100% ascending, because the components were permuted by the same reindexing.
  • The caveat now states what is actually true, including both reuse failure modes and what anyone re-enabling the test must do first.
  • Tests keep the evidence: 5 checks, including both reuse modes. test_sample_n_does_not_encode_draw_order_after_reuse is written to fail if the samplers ever grow stable append-time ids -- that is the signal to revisit the caveat rather than leave it stale.

On "silly to keep a dead array": agreed, and that is roughly why this ended where it did. sample_n is not currently a usable draw-order record, and making it one is real work whose only beneficiary is a test nobody runs. Documenting the trap costs nothing and does not touch a shipped code path.

Also in this branch: the skymap branch flagged deprecated

Unrelated to the ordering issue, surfaced while auditing chunk-size-dependent behaviour. The bayestar-skymap path is unused in production for ~a decade and is the only place --n-chunk drops to 500 -- which makes it the least-tested regime for anything scaling with the chunk size. In particular --portfolio-weight-clip uses tau = C*sqrt(n_chunk)*mean(w), so it is ~4.5x more aggressive there than at the n_chunk=1e4 used for all clip testing. Flagged in place, with a note not to treat clip validation as covering it.

CI: agreed the two failing container canaries are unrelated upstream pygsl-lite/setuptools failures.

…ests prove

The module header still said sample_n 'is what survives' the permutation -- written
before the reuse tests showed it is stale-and-short between calls and does not
encode draw order after one.  It now states both failed repairs, the measurements,
and that this module is documentation only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@oshaughnessy-junior oshaughnessy-junior changed the title convergence test: use draw order, not weight order (the recompute workaround never did anything) convergence test: document the weight-order trap (documentation only; no functional change) Aug 8, 2026
@oshaughnessy-junior
oshaughnessy-junior merged commit e73aaa0 into rift_O4d Aug 10, 2026
18 of 20 checks passed
@oshaughnessy-junior
oshaughnessy-junior deleted the rift_O4d_weights_sort_budget branch August 13, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant