mcsamplerNFlow: make it usable as a portfolio member, and fix its evidence - #358
Merged
Merged
Conversation
oshaughnessy-junior
deployed
to
private-review-dispatch-rift
September 16, 2026 14:29 — with
GitHub Actions
Active
oshaughnessy-junior
force-pushed
the
claude/nflow-portfolio-member
branch
from
October 1, 2026 10:05
308dcec to
cbb551d
Compare
oshaughnessy-junior
deployed
to
private-review-dispatch-rift
October 1, 2026 10:05 — with
GitHub Actions
Active
…dence --sampler-portfolio NFlow is offered by util_ConstructIntrinsicPosterior_GenericCoordinates and util_ConstructEOSPosterior, and the configuration could not run. Four defects, each of which had to be fixed before the next became visible. 1. RETURN ORDER. draw_simplified returned (rv, p_s, p_prior), while MCSamplerGeneric and every other implementation return (p_s, p_prior, rv), which is what mcsamplerPortfolio.draw() unpacks -- so the SAMPLES were assigned to joint_p_s. Not reliably loud. On base 40ceccb, [AV, NFlow] over a unit Gaussian in [-5,5]^d (with portfolio_allow_stratified_density=True, since defect 2 otherwise refuses the portfolio at setup): d=2 raised a broadcast ValueError, but d=1 COMPLETED 3.7 to 4.2 nats low over seeds 1-4. Same seeds here: within 0.05. 2. NO sampling_density. The portfolio builds q_mix from it, and since the member p_s contract landed (80bd766) it REFUSES [AV, NFlow] at setup() rather than falling back. 3. TRUNCATION. enforce_bounds discarded the out-of-box flow samples without refilling, so a trained flow returned fewer samples than asked and the portfolio aborted copying into its fixed-width slice; and the p_s reported for the survivors was the undivided q(x) rather than q(x)/A. draw_simplified now refills to the requested count and divides by the acceptance accumulated over the CURRENT flow generation. Not run-long (a pooled A left ln Z biased -0.179 nats over 6 seeds) and not per-batch (unbiased, but 0.13 nats of per-chunk noise at the ~46 draws a floored member gets, which left AV+NFlow resolved at -0.0201 +- 0.0042). 4. d=1 could not train at all: num_layers = int(d/2) is 0, so the transform had no trainable weights, and np.diag(np.cov(...)) raises on a (1,n) array. E[prior/p_s] over fresh draws is 1 exactly when the reported p_s is a normalized density on the box, and 1/A when it is not. One trained flow per dimension: base n_ret/1500 base E[prior/ps] branch branch E[prior/ps] d=2 242 6.29 1500/1500 1.0014 d=4 383 4.21 1500/1500 0.9925 d=6 534 2.88 1500/1500 1.0620 d=8 737 2.18 1500/1500 0.9699 NFlow's own evidence was biased HIGH by ln(1/A). test_NF_reuse.py, by arm: base 40ceccb this branch TRAIN+SAVE +0.465 -0.010 COLD +0.561 +0.071 WARM(reuse) +0.351 +0.014 WARM(polish) +0.321 +0.013 That test passes on both: its tolerance is 4*|cold bias|+0.1, derived from the arm it compares against, so a bias shared by all four arms inflates the bound with it. Portfolio ln Z against an exact answer, 8 seeds each, unit Gaussian in [-5,5]^d, every arm within its sem: arm d neff mean d sem AV+NFlow 2 300 -0.0072 0.0097 NFlow 2 300 -0.0001 0.0085 GMM+NFlow 2 300 +0.0008 0.0059 AV+AV 2 300 +0.0032 0.0081 AV+GMM 2 300 -0.0040 0.0066 AV+NFlow 1 300 +0.0069 0.0112 An adversarial review of an earlier revision found seven further defects, all acted on here: constant-density test stand-ins that could not see a row misalignment (three silent-wrong-answer mutations passed the gate); an uninvalidated acceptance cache; a raise on draw_simplified(0); an 8x overdraw and a bounds-enforcing probe under enforce_bounds=False; a per-pass memory cap that did not bound what it claimed; an inaccurate READ-ONLY docstring; and the per-batch normalizer above. Verified against a real nflows install. The 21-test gate needs neither nflows nor torch and is registered in .travis/test-integrate.sh with a collection count and a passed floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oshaughnessy-junior
force-pushed
the
claude/nflow-portfolio-member
branch
from
October 1, 2026 11:10
cbb551d to
fd18a72
Compare
oshaughnessy-junior
had a problem deploying
to
private-review-dispatch-rift
October 1, 2026 11:10 — with
GitHub Actions
Error
oshaughnessy-junior
marked this pull request as ready for review
October 1, 2026 11:10
oshaughnessy-junior
deployed
to
private-review-dispatch-rift
October 1, 2026 11:10 — with
GitHub Actions
Active
oshaughnessy-junior
deployed
to
private-review-dispatch-rift
October 1, 2026 18:38 — with
GitHub Actions
Active
This branch was successfully deployed
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.
mcsamplerNFlowcould not run as amcsamplerPortfoliomember.--sampler-portfolio NFlowis offered byutil_ConstructIntrinsicPosterior_GenericCoordinatesandutil_ConstructEOSPosterior, so the configuration is reachable. Four defects, each hidden behind the previous one.1. Return order.
draw_simplifiedreturned(rv, p_s, p_prior).MCSamplerGenericand every other implementation return(p_s, p_prior, rv), whichmcsamplerPortfolio.draw()unpacks, so an NFlow member had its samples assigned tojoint_p_s. Not always loud. On base 40ceccb, [AV, NFlow] over a unit Gaussian in [-5,5]^d, withportfolio_allow_stratified_density=Truesince defect 2 otherwise refuses the portfolio atsetup():ValueError: could not broadcast input array from shape (2,200) into shape (200,)At d=1 the (1,n) sample array broadcasts into the (n,) density slot, so the run returns a number several nats low with no error.
2. No
sampling_density. The portfolio buildsq_mix = sum_m frac_m q_mfrom it. Since the member p_s contract merged (80bd766),[AV, NFlow]is refused atsetup()rather than falling back.3. Truncation.
enforce_boundsdiscarded the out-of-box flow samples, with two consequences. A trained flow returned fewer samples than asked, and the portfolio aborted copying into its fixed-width slice. And thep_sreported for the survivors was the rawq(x)rather thanq(x)/A, so NFlow's own evidence was biased high byln(1/A).draw_simplifiednow refills to the requested count and divides by the batch acceptance. A is per-batch on purpose: the flow is retrained between chunks, and a cumulative A left ln Z biased -0.179 nats over 6 seeds.4. d=1 could not train.
num_layers = int(d/2)is 0, so the transform had no trainable weights, andnp.diag(np.cov(...))raises on a(1,n)array.NFlow's own evidence
test_NF_reuse.py(rostered OPTDEP), bias in nats, same seeds:That test passes on both. Its tolerance is
4*|cold bias| + 0.1, derived from the arm it compares against, so a bias shared by all four arms inflates the bound along with it. It checks reuse against cold, and cannot see an absolute offset.Portfolio evidence against an exact answer
8 seeds per arm, unit Gaussian in [-5,5]^d, prior uniform on the box:
Every arm is within its sem. GMM+NFlow holds at acceptance 0.46 to 0.66, where the 1/A factor reaches about 0.7 nats, which is what exercises the normalization.
An earlier revision normalized by the LATEST batch rather than the current flow generation. That is unbiased, but at the ~46 draws a floored portfolio member actually gets it injects about 0.13 nats of per-chunk noise, and it left AV+NFlow at -0.0201 +- 0.0042, resolved. Accumulating within one generation is the same estimator over a larger sample and removes it.
Normalization, swept over dimension
Every ln Z measurement above is at d=1 or d=2.
E[prior/p_s]over fresh draws is 1 exactly when the reportedp_sis a normalized density on the box, and1/Awhen it is not, so it separates the two without relying on the 1/A algebra. One trained flow per dimension, 4000 fresh draws:n_ret/n_reqE[prior/p_s]n_ret/n_reqE[prior/p_s]The base misses the requested count at every dimension and its reported density is off by 1/A. The branch fills every chunk, including at acceptance 0.10, and
sampling_densityagrees with the reportedp_sto 1e-6 (the flow is float32). Acceptance is not monotonic in d here; it is set by where training happened to leave each flow.Verification
nflowsandtorchare absent from the CIT IGWN conda python. Real-dependency runs used~/.conda/envs/myigwn-py310-testing(python 3.10.14, torch 2.1.2) onldas-grid, no cupy.test_NF_reuse.py --as-test.travis/test-ci-roster.pyxfail(run=False)The gate needs neither package: it drives the untrained uniform branch plus strict in-file flow stand-ins, and stubs both at import when absent. Registered in
.travis/test-integrate.shwith a collection count and a passed floor. The passed floor was wrong first time, scoring 0 on a green run because the count starts the line, so the block is executed in the checks above rather than read.Two mutations survived the first battery. Capping the refill at one pass changes nothing, because one pass already over-asks by 1/A, so the faithful mutation removes the headroom too. And
int(n_to_get)alone no longer decides anything, because the refill loop sizes each pass withint(np.ceil(...)); only removing both breaks the contract. The code comment says this.Adversarial review
An adversarial subagent review of cbb551d found eight items. Seven are acted on here.
rv,log_psandlog_p; it demonstrated three silent-wrong-answer mutations passing the gate_GradedFlow), and the alignment is asserted elementwise. All three are caughtsampling_densityis documented READ-ONLY and advances the torch RNGflow_acceptance's cache was never invalidated when the flow was replaced; measured 1.05x wrong on a retrain-then-query path_reset_flow_acceptance()at all three install sites, pinned by an AST test that the SITES call itdraw_simplified(0)raisedneed at least one array to concatenate-- new in this branch, in the configuration the branch is forenforce_bounds=Falsedrew 8x what it was asked for and took a bounds-enforcing probe on behalf of a caller that asked for no boundsThe eighth was that
E[1/Ahat] != 1/Acosts under 0.003 nats, measured against a closed-form A over 400 repeats. That is a confirmation, not a defect.Second battery, 19 mutations: all caught except changing the refill headroom from 1.15 to 1.00, which is a performance knob (the loop runs until the chunk is full, so it changes only the pass count). The code says so where it could be mistaken for a guard.
Not changed
nf_method='iterative'builds aTanhTransformFrozenlayer, supported on the box by construction, so its acceptance is 1 and defect 3 never applied to it. The defaultnf_methoduses an unboundedPointwiseAffineTransform, hence acceptance below 1. I did not change which architecture is the default.🤖 Generated with Claude Code