Skip to content

Filter non-finite de_sample() draws in sample() for unbounded priors too - #245

Merged
pedroliman merged 3 commits into
mainfrom
fix-244-unbounded-nonfinite-draw
Aug 28, 2026
Merged

Filter non-finite de_sample() draws in sample() for unbounded priors too#245
pedroliman merged 3 commits into
mainfrom
fix-244-unbounded-nonfinite-draw

Conversation

@pedroliman

Copy link
Copy Markdown
Owner

Fixes #244

Bug

sample.nsbi_posterior() (R/posterior.R) only filters a non-finite draw from the density estimator inside if (bounded) { ... }, where bounded <- !is.null(prior$lower) || !is.null(prior$upper). That guard was added by #234/#236 specifically to work around within_support() returning NA (not FALSE) for a NaN row, and R's matrix indexing keeping rather than dropping a row selected by an NA logical index.

The guard never runs for an unbounded prior (prior_normal(), or any prior_custom()/family without lower/upper) -- the common case; most of the package's own NPE examples use prior_normal(). For an unbounded prior, a NaN/Inf row that an under-trained MAF/NSF/MDN can produce was rbind'd straight into the returned draws matrix with no warning, and attr(draws, "acceptance_rate") still reported 1.0. This silently corrupts summary(), pairplot(), and sbc()/tarp() diagnostics built on sample(), and can surface downstream in map_estimate() as a confusing "theta contains non-finite value" error that blames the seed draw rather than the internal density-estimator artifact that produced it.

Fix

Moved the non-finite-row filter out of if (bounded) so it runs unconditionally, right after de_sample(), using a plain is.finite() row check independent of within_support(). The existing within_support()/rejection-sampling logic stays scoped to bounded exactly as before -- this is additive, not a replacement. Because collected is filtered before acceptance_rate <- nrow(collected) / max(n_tried, 1) is computed, the dropped row is reflected in acceptance_rate for every prior, bounded or not, with no separate accounting needed.

log_prob.nsbi_posterior()'s normalize = TRUE path needs no matching change: its de_sample() call for the acceptance-constant estimate is itself gated on normalize && bounded, so for an unbounded prior that call -- and any chance of it returning a non-finite draw -- never happens in the first place. de_log_prob() is called unconditionally in that function, but only on user-supplied theta, which is already run through check_finite() earlier in the same function; it isn't fed draws from de_sample(), so it needs no guard here.

Tests

Added two regression tests to tests/testthat/test-posterior-nonfinite-de-draw.R (mirroring the existing #234/#236 tests, but with prior_normal() instead of prior_uniform()):

  • sample() drops a mocked NaN row from de_sample() and returns the requested n finite draws, running a second sampling batch to make it up.
  • When every draw in the one batch is corrupted, sample() warns about the shortfall and acceptance_rate correctly reports 0, not the pre-fix 1.0.

Checks

  • R CMD INSTALL --no-docs . && (cd tests && Rscript testthat.R): FAIL 0 | WARN 0 | SKIP 79 | PASS 1373 (torch unavailable in this sandbox, so neural/MDN/MAF/NSF tests skip, as documented in CLAUDE.md; all validation-path tests, including the new ones, ran and passed).
  • DESCRIPTION bumped to 0.6.20, NEWS.md entry added (PR number filled in once assigned).
  • No exported function's signature or docs changed, so man/*.Rd needs no update.

Generated by Claude Code

sample.nsbi_posterior()'s non-finite-row filter ran only inside
`if (bounded)`, since it was added by #234/#236 specifically to work
around within_support() returning NA (not FALSE) for a NaN row. That
left the common case -- an unbounded prior like prior_normal() -- with
no filter at all: a NaN/Inf row from de_sample() (which MAF/NSF/MDN
can occasionally produce) was rbind'd straight into the returned
draws matrix, and acceptance_rate still reported 1.0.

Drop non-finite rows unconditionally, before the bounded branch's
within_support() rejection runs, so acceptance_rate reflects the drop
for every prior. log_prob.nsbi_posterior()'s normalize = TRUE path
needs no matching change: its de_sample() call for the acceptance
estimate is itself gated on `bounded`, so it never runs (and never
could see a non-finite draw) in the unbounded case.

Fixes #244.
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.38%. Comparing base (9b7451b) to head (0d540ba).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #245   +/-   ##
=======================================
  Coverage   98.38%   98.38%           
=======================================
  Files          31       31           
  Lines        4034     4035    +1     
=======================================
+ Hits         3969     3970    +1     
  Misses         65       65           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@pedroliman
pedroliman merged commit a4ebbfe into main Aug 28, 2026
5 checks passed
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.

sample()/log_prob() don't filter NaN draws from the density estimator when the prior is unbounded

1 participant