neuralsbi 0.6.56
-
log_lik()andlog_ratio()now reject a non-logicalsum_iidinstead of silently changing what they return. Both functions branch onsum_iidthroughsurrogate_score()’sif (!isTRUE(sum_iid)), andisTRUE()only recognizes the literal valueTRUE:sum_iid = "yes"orsum_iid = 1took the “don’t sum” branch with no error, returning ann_theta x n_obsmatrix instead of the documented per-thetavector.surrogate_score()now validatessum_iidwith a newcheck_flag()helper (R/check.R), matching howmax_batchis already checked in the same function, so a typo is caught at the call rather than read off a wrong-shaped result (#321) (#326).
neuralsbi 0.6.55
-
The slice sampler’s
widthargument now rejects a length that doesn’t match the number of parameters, instead of being silently recycled or truncated.slice_sample_run()fedwidthstraight intorep_len(as.numeric(width), dim)undersuppressWarnings(), which accepts any length: an over-long vector was truncated and a short one recycled, both with no warning.posterior.nsbi_nle()/posterior.nsbi_nre()documentwidthas one value per parameter dimension (or a single value to broadcast), so a mis-sized vector silently sized the initial slice interval for the wrong coordinate, the only symptom being a slow- or badly-mixing chain with nothing pointing back to the cause. This is the same class of bug #288/#289/#290/#292 already fixed fortheta,x_obs,obs, andwithin_support()’s bounds, all routed throughcheck_matrix()/check_bound()for exactly this reason;widthwas the one per-parameter vector in the MCMC path left checking only its values, never its length.check_slice_width_length()now checkslength(width) %in% c(1, dim)beforerep_len()runs, and errors naming the expected length otherwise (#320) (#325).
neuralsbi 0.6.54
-
sample()on an NLE/NRE posterior withseed = ...no longer permanently reseeds the caller’s global RNG stream.mcmc_draws()calledset.seed(ctl$seed)directly with no save/restore, unlike every other RNG touch point in the package (with_fixed_seed(),set_torch_seed(),rng_streams(),with_rng_stream()). A seededsample()call therefore left.Random.seedat whateverset.seed(ctl$seed)produced, so any later random draw in the same session became a deterministic function ofctl$seedalone regardless of the caller’s own RNG state – the same bug class #272/#274 fixed forsurrogate_potential()’s prior probe and #282/#283 fixed forlog_prob()’s acceptance-constant draw.map_estimate()andposterior_predictive()callsample()internally and inherited the same leak. The sampler call now runs underwith_fixed_seed(ctl$seed, ...), which parks R’s RNG at the seed only for that call and restores the caller’s prior state afterward, soseedstill makes the run reproducible without leaving a trace on anything the caller does next (#319) (#324).
neuralsbi 0.6.53
-
summary()on posterior draws no longer crashes whenprobshas a single element.summary.nsbi_samples()built its quantile columns witht(apply(m, 2, stats::quantile, probs = probs)):apply()returns alength(probs) x ncol(m)matrix in general, whicht()fixes up toncol(m) x length(probs), but simplifies its result to a plain length-ncol(m)vector whenlength(probs) == 1, sot()produced a1 x ncol(m)matrix and the followingcolnames<-(length 1) failed with “length of ‘dimnames’ [2] not equal to array extent”.summary(draws, probs = 0.5)andsummary(post, probs = 0.5)therefore crashed on any fit with more than one parameter – an obvious way to ask for just the median. The dropped dimension is now restored before transposing, the same guardexpected_coverage()already used for the analogousvapply()drop, andprobsis validated withcheck_probs()so an out-of-range value errors clearly instead of failing insidequantile()(#318) (#323).
neuralsbi 0.6.52
-
npe_sequential(seed = ...)no longer spends part of every round after the first re-simulating round 1’s parameter draws. The round loop passed the caller’sseedstraight through to the per-roundnpe()call, andnpe()callsset.seed(seed)at the top of its own call. For an estimator that consumes no R-level randomness while fitting, which coverslinear_gaussianand any caller-supplieddensity_estimatorfunction, R’s RNG was therefore left in the same state at the end of every round: round r + 1 opened from the state round r opened from,sample_prior()handed it the same candidate matrix, and only the acceptance threshold differed. Three rounds of 200 simulations produced 600 training rows of which 516 were unique; four rounds produced 800 rows and 634 unique. Nothing reported this, so a run that asked for reproducibility quietly threw away a large part of its simulation budget and over-weighted whatever region cleared the threshold in two consecutive rounds, the opposite of what the truncation is for. Each round now derives its own seed from the streamnpe_sequential()seeds at the top of the call, which keeps the whole run reproducible from the top-levelseedand keeps torch’s generator seeded for the neural estimators as #215 requires (#317) (#322).
neuralsbi 0.6.51
-
stan_code()’s generated MAF likelihood now builds the alpha-clamp matrix-vector product once per transform, instead of once per element.stan_fn_maf()emitted the log-scale head asfor (i in 1:P) al_k[i] = fmin(fmax((Walpha_k * prev + balpha_k)[i], -8.0), 8.0);, which recomputes the fullWalpha_k * prev + balpha_kproduct – O(P x hidden) work – on every one of itsPloop iterations, even thoughmu_ktwo lines above builds the analogous product once as a plainvector[P] mu_k = W * prev + b;. That waste compounds acrossn_transformsstacked MADE transforms (5 by default) and runs on every leapfrog evaluation of every NUTS iteration for any NLE-MAF fit exported to Stan. The product is now built once asvector[P] al_raw_k, and the loop only appliesfmin(fmax(al_raw_k[i], -8.0), 8.0); the clamped values are unchanged, this is a performance fix (#314) (#316).
neuralsbi 0.6.50
-
stan_code()’s generated MDN likelihood now builds each mixture component once per_sum_lpdfcall, instead of once per observation.stan_fn_mdn()’s<name>_from_head()took the MLP’s flat output and rebuilt every component’s mean and full Cholesky factor – softplus on the diagonal, arep_matrix()plus per-entry assignment for each of theKcomponents – on every call, even inside the i.i.d. loop wherehead(and so every component) is identical across allNrows:stan_sum_lines()already hoisted the MLP forward pass out of that loop, but the Cholesky assembly downstream of it still ranNtimes, on every leapfrog step of every NUTS iteration.<name>_head()is now followed by<name>_mu()/<name>_L(), which build theKmeans and Cholesky factors as Stan arrays, and<name>_from_components()evaluates the mixture log density from those prebuilt arrays;<name>_sum_lpdf()calls_mu()/_L()once in its precompute block (the same mechanismstan_fn_lingauss()already used) and reuses the result across allNrows, and<name>_lpdf()calls the same three functions for its one observation. The generated log density is unchanged (#313) (#315).
neuralsbi 0.6.49
-
tarp()now rejectsn_tarpbelow 2 instead of silently returning a degenerate coverage curve. Distances are computed after z-scoring the true parameter draws by their own spread (fit_standardizer(theta_true)), butsd()of a single value isNA, andfit_standardizer()’s guard against zero-spread columns quietly resets thatNAto a scale of 1 with no warning, since it was never told which argumenttheta_truecame from. Withn_tarp = 1that leavestheta_zat exactly zero for the one trial. Under the defaultreferences = "uniform", the reference box isrange(theta_z)per parameter, which collapses to that same zero point, forcingd_truth = 0and every trial to read as uncovered until the last level – an ECP curve that jumps from 0 to 1 at the first step, indistinguishable from a badly miscalibrated posterior regardless of fit quality.references = "prior"sidesteps the forced-zero distance but still standardizes by the same degenerate, unscaledstd, so its distances are not comparable across parameters either.n_tarpnow goes throughcheck_count(n_tarp, "n_tarp", min = 2), the same guard used elsewhere in the package, so both reference modes get a real spread to standardize by (#311) (#312).
neuralsbi 0.6.48
-
expected_coverage()no longer excludes SBC ranks that land exactly on the credible-interval boundary. Ranks are integers in{0, ..., L}(L = n_posterior_samples);u <- rank / Lapproximates the posterior CDF at truth, and a trial was counted as covered only whenlo < u < hi, strict on both ends. Wheneverlo * Lorhi * Lis itself an integer – the defaultL = 1000andalpha = 0.9givelo = 0.05,hi = 0.95, solo * L = 50andhi * L = 950– a rank landing exactly on that boundary is genuinely inside the central interval but was thrown out, biasing empirical coverage down by aboutO(1/L)for a perfectly calibrated posterior (899 covered trials read as 0.898 instead of 0.9 out of 1000, say). The bias was systematic, not sampling noise, so it didn’t shrink with more SBC trials. The comparison is now closed on both ends (lo <= u <= hi).plot_coverage()callsexpected_coverage()directly and picks up the fix with no change of its own (#308) (#310).
neuralsbi 0.6.47
-
nre()now rejectsbatch_sizebelow what its atomic loss needs, instead of training on it silently.minibatches()only ever merges the trailing short batch into the one before it, so abatch_sizebelow the 2-row floornre_atomic_log_prob()needs is not a one-off: every interior minibatch of every epoch stays that small, and the atomic loss returns a constant zero gradient below 2 rows. Withbatch_size = 1, that is every minibatch but the one merged trailing batch, so training ran almost entirely on the signal from that one batch, with no error or warning to say so.check_train_controls()now checksbatch_sizeagainstmin_val_rows, the same floor it already enforces on both sides of the train/validation split (#188, #239), andnre()reports it before simulating rather than after (#307) (#309).
neuralsbi 0.6.46
-
npe_sequential()now validatesembedding_netbefore round 1 spends its simulation budget.embedding_netreaches round 1 through..., alongsiden_bins,device, and the rest ofnpe()’s estimator/training-control arguments, and #251/#262 already moved those checks up front so a bad value fails beforeprepare_simulations()spends round 1’s draws.embedding_netwas left out of that pass:npe_sequential(prior, simulator, n_rounds = 3, n_simulations = 500, embedding_net = list(bogus = TRUE))ran the full round-1 simulation budget and only then failed inside the round-1npe()call with`embedding_net` must be built with embedding_mlp().The same checknpe()already runs now also runs up front innpe_sequential(), so the error is identical but arrives before any simulation happens (#301) (#305).
neuralsbi 0.6.45
-
npe()/nre()now warn whenembedding_netis supplied alongside a function-valueddensity_estimator/classifier. The existing check only fired foridentical(density_estimator, "linear_gaussian"), which isFALSEfor a function value, so a caller-supplied fitter passed tonpe(..., density_estimator = my_fitter, embedding_net = embedding_mlp(4))silently dropped the embedding:fit_density_estimator()/fit_ratio_estimator()forward onlytheta/xto a custom function, neverembedding_net. Both entry points now warn in that case too, symmetric with the"linear_gaussian"/"logistic"cases (#300) (#304).
neuralsbi 0.6.44
-
embedding_mlp()no longer silently truncates a non-integeroutput_dimorhiddenwidth. Its manual checks testedoutput_dimagainst< 1but never againsttrunc(), so a fractional value passed the guard and was silently floored byas.integer()two lines below;hiddenwas coerced withas.integer()before its own check ran, so a fractional entry was truncated first and the check never saw the original number.embedding_mlp(output_dim = 2.7, hidden = c(10.9, 5.2))used to returnoutput_dim = 2L, hidden = c(10L, 5L)with no warning. Both arguments now go throughcheck_count()/check_counts(), the same helpers every other size argument in the package uses, so a fractional value errors instead of being rounded away. An emptyhiddenstill means a single linear map tooutput_dim, unchanged (#302) (#303).
neuralsbi 0.6.43
-
stan_data()now takes amodelargument and requiresx_obswhen it isTRUE.stan_code()’s default (model = TRUE) always emits adatablock that declaresNandxas required, butstan_data()madex_obsoptional and, when it was omitted, returned a list with neither field. Pairing that program with that data list compiled fine in R and then failed deep insidecmdstanr/rstanwith an opaque “variable does not exist” error instead of a message from this package.stan_data()now mirrorsstan_code()’smodelargument (defaultTRUE) and errors up front, namingx_obs, whenmodel = TRUEandx_obsisNULL; passmodel = FALSEto build a data list for a functions-only export, which has noN/xto fill (#298) (#299).
neuralsbi 0.6.42
npe()/nle()/nre()no longer guess the layout of a flattened multi-parameterthetaon the pre-computedtheta =/x =path.prepare_simulations()passed a barethetavector straight toas_theta_matrix(), which reshapes anything that isn’t a single row withmatrix(theta, ncol = d, byrow = TRUE)– silently assuming row-major order. Whenlength(theta)happened to be an exact multiple ofprior$dim, that assumption was never checked against anything:theta_flat <- as.vector(theta_matrix), the ordinary column-major way to flatten ann x dmatrix in R, came back scrambled, and training proceeded on the wrong parameter values with no warning or error. A single simulation (length(theta) == prior$dim) stays unambiguous and keeps working as before; for a multi-parameter prior, any other bare vector now errors and asks for a matrix or data frame instead of guessing (#291) (#295).within_support()no longer silently reshapes a wrong-lengththetainto several parameter sets. It handedthetatoas_theta_matrix(), which reshapes whatever length it is given rather than checking it matchesprior$dim– sowithin_support(prior_uniform(low = c(-2, -2), high = c(2, 2)), c(0.1, 0.2, 0.3))recycled the length-3 vector into a 2-row matrix with a cryptic recycling warning, andwithin_support(prior, c(0.1, 0.2, 5.0, 0.3))(length 4, a clean multiple ofdim = 2) silently became 2 rows with no warning at all. This is the same bug class #289 fixed at five other call sites;within_support()was scoped out of that PR for separate consideration. It now goes throughcheck_matrix()like the rest: a bare vector is read as a single row and must have exactlyprior$dimentries, and a mismatched length errors instead of recycling (#292) (#293).posterior()on annpefit no longer silently reshapes a wrong-lengthx_obs.posterior.nsbi_npe()handedx_obstoas_theta_matrix(), which reshapes whatever length it is given rather than checking it matchesfit$dim_x– the exact bug class #288/#289 fixed at five other call sites, in a spot that fix missed.posterior(fit, x_obs = c(10, 20, 30, 40))against adim_x = 2fit silently became a 2-rowx_obswith no warning at all, since length 4 is a clean multiple of 2.mcmc_posterior(), the sibling path for NLE/NRE, already usedcheck_matrix()for this;posterior.nsbi_npe()now does too, matching the errorposterior()on an NLE/NRE fit already gives for the same shape mistake (#290) (#294).
neuralsbi 0.6.41
A wrong-length
theta/x_obsno longer gets silently reshaped into several parameter sets or observations.log_prob.nsbi_posterior()’stheta,resolve_obs()’sx/obs(backingsample(),log_prob()andmap_estimate()),mcmc_posterior()’sx_obs(shared byposterior.nsbi_nle()/posterior.nsbi_nre()),likelihood_fn()’sx_obs, andstan_data()’sx_obsall handed their checked value toas_theta_matrix(), which reshapes whatever length it is given rather than checking it matches the fit’s dimension – solog_prob(post, theta = c(0.1, 0.2, 0.3, 0.4))against a two-parameter fit silently became a 2-rowthetaand returned a plausible-looking length-2 answer instead of an error. This was worse for NLE/NRE posteriors:resolve_x_iid()treats every row as an independent observation with no row-count check to catch the reshape at all, sosample(nle_post, obs = c(0.1, 0.2, 0.9, 0.9))on adim_x = 2fit silently conditioned on 2 fabricated observations. All five call sites now go throughcheck_matrix(), whichcheck_x_obs()already used fornpe_sequential(): a bare vector is read as a single row and must have exactly the fit’s dimension, and a mismatched length errors instead of recycling (#288) (#289).npe_sequential()no longer reports round 1’s acceptance as a hardcoded 1.00 when the simulator fails on some prior draws. The main loop initializedacceptance <- 1for round 1 and only recomputed it from whatdrop_failed_sims()actually kept whenr > 1L, even thoughdrop_failed_sims()runs – and can shrinktheta_new/x_newby dropping non-finite rows – for round 1 too. A simulator that routinely fails on some parameter draws (an ODE solver that diverges for certain values, say) then printed a self-contradictory verbose log, e.g. “Round 1/3: 850 new simulations (850 total), proposal acceptance 1.00” after 1000 were requested, and left the wrong value in the returnedfit$rounds[[1]]$acceptanceand inprint.nsbi_snpe()’s acceptance-per-round line.acceptance <- nrow(theta_new) / max(tried, 1L)now runs unconditionally afterdrop_failed_sims(), withtriedset to round 1’s full simulation budget since round 1 has no rejection-sampling stage to have already tried fewer draws (#285) (#287).
neuralsbi 0.6.39
-
sample()’s shortfall warning no longer blames prior leakage for an unbounded prior. When a density estimator’s draws come up short of the requested count,sample.nsbi_posterior()warned “Only X/Y samples inside prior support … The estimator is leaking mass outside the prior” regardless of whether the prior was bounded. For a bounded prior (prior_uniform(), orprior_custom()withlower/upper) that wording is correct: the shortfall comes from rejection-sampling draws outside the prior’s actual support. For an unbounded prior (prior_normal()and the like) there is no support boundary to leak past – the shortfall there is the finite-row filter dropping NaN/Inf draws thatde_sample()itself produced, and the warning was pointing at the wrong fix. The warning now branches onbounded: the bounded wording is unchanged, and the unbounded wording says the density estimator produced non-finite draws (#284) (#286).
neuralsbi 0.6.38
-
log_prob()on a bounded-prior posterior no longer mutates the caller’s RNG stream.normalize = TRUE’s acceptance-constant estimate drawsn_normalizationsamples from the density estimator viade_sample(), and that draw had no save/restore around it – the same bug class #274 fixed insurrogate_potential()’s prior probe, just inlog_prob.nsbi_posterior()instead.log_prob()reads as a pure evaluation function, unlikesample(), which is documented to consume randomness, so calling it on a bounded prior silently perturbed R’s RNG and, for a neural (MAF/NSF/MDN) estimator, torch’s global RNG too (de_sample_flow()draws withtorch_randn()). The draw now runs underwith_fixed_seed(), and, only when the density estimator actually has a torch network (linear_gaussiandoes not), torch’s RNG is saved and restored withset_torch_seed()/torch_set_rng_state(), the same pattern #276 established for a seeded fit orc2st()call (#282) (#283).
neuralsbi 0.6.37
-
de_log_prob()on alinear_gaussianestimator no longer crashes on a zero-rowtheta. Its broadcast check only stretched the single-observation conditional meanmuup totheta’s row count whennrow(mu) == 1L && nrow(theta) > 1L; with a zero-rowthetathat condition is false, somustayed a 1-row matrix anddmvnorm_chol(theta, mu, chol)computedtheta - mubetween a 0-row and a 1-row matrix, failing with “non-conformable arrays”. This is the same corner #271 fixed insidedmvnorm_chol()itself (a non-emptymeanarriving as a plain vector); heremuis already a matrix, so that fix did not cover it.de_log_prob.nsbi_de_lingauss()now returnsnumeric(0)for a zero-rowthetadirectly, beforelingauss_mean()/dmvnorm_chol()ever run – the torch estimators (MDN/MAF/NSF) never hit this, since PyTorch’s broadcasting already expands a size-1 dimension against size-0 (#279) (#281).
neuralsbi 0.6.36
-
c2st(..., classifier = "logistic")no longer errors on a single-column comparison.c2st_logistic_prob()built the training frame asdata.frame(y = y_train, x_train)and the prediction frame asdata.frame(x_test).data.frame()names a lone unnamed column after the deparsed argument it was given, so an unnamed, one-columnx_train/x_testpair landed under different names in the two frames –x_trainin one,x_testin the other.glm()fit a coefficient under whichever name reached the training frame, andpredict(fit, newdata = ...)then failed to find it under the prediction frame’s name. Both frames now get the same, argument-independent column names beforeglm()/predict()run, so the fitted formula andnewdataline up regardless of column count or the input matrices’ own names (#278) (#280).
neuralsbi 0.6.35
-
Seeded training and
c2st()calls no longer permanently reseed torch’s global RNG for the rest of the R session.train_restarts()andc2st()’s MLP path both calledtorch::torch_manual_seed(seed)directly whenever a caller passedseed, and neither saved nor restored the generator’s prior state afterward – unlike every other RNG touch point in the package, and unlike what #272/#274 already fixed for R’s own RNG. Since torch has one global generator with no way to ask for an independent stream, a single seedednpe()/nle()/nre()fit or a seededc2st()call left every later unseeded torch call in the same session – another fit,de_sample(), an unseededc2st()– drawing from wherever the seeded call left the generator, instead of from a fresh stream. Both call sites now go throughset_torch_seed(), which savestorch::torch_get_rng_state()before reseeding so the caller can restore it withon.exit(), the torch analogue ofwith_fixed_seed()(#275) (#276).
neuralsbi 0.6.34
-
log_prob()on an NLE/NRE posterior no longer mutates the caller’s RNG stream, and no longer rebuildssurrogate_potential()on every call.mcmc_log_prob()calledsurrogate_potential()fresh on everylog_prob()call, unlikeslice_sample_surrogate(), which builds it once per chain and reuses it for every MCMC step;map_estimate()’s optimizer callslog_prob()once per evaluation against the samex_obs, so a Nelder-Mead/BFGS run of a few hundred iterations rebuilt the whole evaluator that many times, re-tensorizingx_obsand never letting an MDN’s JIT trace warm up. Insidesurrogate_potential(), the prior-log_prob probe (prior$log_prob(sample_prior(prior, 2L))) also drew from the global RNG stream and never restored it, unlike every other RNG touch point in the package – soset.seed(42); log_prob(post, theta);left.Random.seeddifferent from whatset.seed(42)alone would give, breaking reproducibility for any code that calledlog_prob()in between two seeded random draws. The potential closure is now cached on the posterior (cached_surrogate_potential(), rebuilt only whenx_obs/max_batchchanges), and the probe runs underwith_fixed_seed(), the same save/restore patternrng_streams()andwith_rng_stream()already use (#272) (#274).
neuralsbi 0.6.33
-
A zero-row
x/x_obsno longer crashescross_iid()/mdn_iid_blocks()with a bare base-R error, for every neural estimator and NRE.check_matrix()accepts a0 x dmatrix untouched, and it reacheslog_lik(),log_ratio(), andposterior(fit, x_obs = ...)for MAF, NSF, MDN, and NRE alike – a user who filters an observation set down to nothing, or subsetsx_obsby mistake, hit this every time.cross_iid()computedobs_chunk >= 1and calledseq.int(1L, n_obs, by = obs_chunk); withn_obs = 0that becomesseq.int(1, 0, by = 1), which R rejects with “wrong sign in ‘by’ argument”, naming neither the package nor the actual problem.mdn_iid_blocks()had the identical pattern independently.linear_gaussian’s ownde_iid_evaluator.nsbi_de_lingauss()already got this right and returns a log-likelihood of 0, the correct empty-product value;cross_iid()andmdn_iid_blocks()now short-circuit on a zero-rowthetaorxbefore eitherseq.int()call runs, since every caller’s pre-allocated output is already zero, or already the right shape, and needs no further work.dmvnorm_chol()(R/density_estimator.R) had a related, non-crashing bug in the same corner: recycling a non-empty conditional mean into a zero-row matrix triggered R’s “non-empty data for zero-extent matrix” warning on every call, even though the resulting empty matrix was correct; it now builds that shape directly (#271) (#273).
neuralsbi 0.6.32
-
c2st()now stratifies its cross-validation folds by class, instead of shuffling one fold assignment over both sample sets together. Its docstring claims to reproducesbibm/metrics/c2st.py, which reachessklearn’s defaultStratifiedKFoldthroughcross_val_score()– but the fold assignment was plain unstratified shuffling (base::sample(rep_len(seq_len(n_folds), n))), so at small sample sizes or with an unlucky shuffle, a fold’s test set could land with zero rows of one class.roc_auc()already returnsNA_real_for such a fold (nothing to rank), andmean(aucs)carries nona.rm, soc2st()$auccame backNAwith no warning; a 200-seed repro atn_each = 6,n_folds = 5,classifier = "logistic"hit this on 173 seeds. Folds are now assigned within the x-rows and within the y-rows separately (c2st_stratified_folds()), then concatenated, which is whatStratifiedKFoldguarantees for a two-class target and keeps every fold’s test set at least one draw of each class for anyn_foldsthe existing validation allows (#269) (#270).
neuralsbi 0.6.31
-
is_improper_uniform_prior()now sees an improperprior_uniform()nested insideprior_independent(), instead of only checking the joint prior’s own top-leveltype. #263 taughtsurrogate_potential()to detect an infinite-boundprior_uniform()and point atprior_truncated()before probing the prior, but the check only fired whenprior$typewas literally"uniform".prior_independent(mu = prior_uniform(-Inf, Inf), nu = prior_normal(0, 1))hastype = "independent"regardless of what is inside it, and an improper component forcesprior_independent()off its marginals fast path, so the resulting joint prior carried no bounds of its own to inspect either –is_improper_uniform_prior()returnedFALSE,surrogate_potential()fell through to the old probe, and #263’s exact misdiagnosis (pointing atprior_custom(..., log_prob_fn = )instead ofprior_truncated()) reappeared one level of composition away.prior_independent()now keeps its component priors themselves inparams$components(previously just their type names, discarded once the fast path was skipped), andis_improper_uniform_prior()recurses into them, so the same check covers aprior_uniform()at any depth ofprior_independent()nesting without special-casing composite shapes (#267) (#268).
neuralsbi 0.6.30
-
posterior()/sample()on annsbi_nle/nsbi_nrefit now diagnoses an improperprior_uniform()correctly, instead of pointing at the wrong fix.prior_uniform(low = -Inf, high = Inf)(or any infinite bound) is documented and supported specifically so it can be bounded later withprior_truncated();sample_prior()already refuses to draw from it directly, with a message namingprior_truncated().surrogate_potential()’s prior-log-density probe calledsample_prior()to check the prior works, caught that correct error in atryCatch(), and turned it intoNA_real_– soall(is.na(probe))fired and told the user to rebuild the prior withprior_custom(..., log_prob_fn = ), which is not the problem:prior_uniform()’slog_prob_fnworks fine, the prior is just unbounded.R/stan.Ralready refused this same case for thestan_code()/stan_data()path (#252);surrogate_potential()now runs the same check (factored intois_improper_uniform_prior(), shared by both call sites) before the probe, and reports the actual fix,prior_truncated()(#263) (#266).
neuralsbi 0.6.29
-
npe_sequential()now validatesdensity_estimatorbefore round 1 simulates.npe(),nle()andnre()all resolvedensity_estimator/classifierwithmatch.arg()and callcheck_torch_for_estimator()beforeprepare_simulations()runs the simulator (#250) – butnpe_sequential()only ever forwardeddensity_estimatorthrough...to thenpe()call at the end of round 1, so a typo’d name or a"maf"/"mdn"/"nsf"choice with torch unavailable was only caught after round 1’s whole simulation budget was already spent.npe_sequential()now resolvesdensity_estimatorand callscheck_torch_for_estimator()in the same pre-flight block that already checksn_bins,device, and the rest of round 1’snpe()arguments (#251), and passes the resolved value to each round’snpe()call rather than re-runningmatch.arg()every round (#262).
neuralsbi 0.6.28
-
npe()/nle()/nre()now validate a caller-supplieddensity_estimator/classifierfunction before running the simulator, instead of only discovering it is broken at the very end offit_density_estimator()/fit_ratio_estimator(). Every other argument to these three functions is checked beforeprepare_simulations()spends the simulation budget (#250), andcheck_function()already exists for exactly this, used forsimulatorand forprior_custom()’ssample_fn/log_prob_fn. A caller-supplied estimator or classifier was the one documented extension point that skipped it:npe(prior, simulator, n_simulations = 100000, density_estimator = my_fitter)with a wrong-arity or typo’dmy_fitterran the full 100,000 simulations before failing deep inside training.npe()andnle()now callcheck_function(density_estimator, "density_estimator", ...), andnre()calls the analogous check onclassifier, alongside the other pre-simulation checks; the built-in string options ("maf"/"mdn"/"nsf"/"resnet"/etc.) are untouched (#259) (#261).
neuralsbi 0.6.27
-
slice_sample_run()(R/mcmc.R) now coerces a non-finite log-density to-Inffor every candidate it proposes, not just the chains’ initial state. The initial state was checked for finiteness up front, but the stepping-out loop and the shrinkage loop each fedlog_prob_fn(cand)’s output straight into<=/>comparisons androwSums()/max.col(), with no guard.log_prob_fnhere issurrogate_potential()’s closure, which adds a trained MAF/NSF/MDN/NRE network’s output straight into the log-density – an out-of-distributionthetainside the prior’s support but outside the estimator’s training distribution can make thatNaN, the same failure modeposterior.Randnpe_sequential()already guard against (#221/#234/#244). Left unguarded here, a singleNaNcorrupted an entire batch of candidates at once viaNA-taintedrowSums()/max.col(), surfacing as a bare “NAs are not allowed in subscripted assignments” or asNaNsilently carried into the retained chain state – hittingsample()on annsbi_nle/nsbi_nreposterior, and anysbc()/tarp()run against one, since every trial starts a fresh chain. Both loops now coercelp[!is.finite(lp)] <- -Infright after eachlog_prob_fn()call, the same treatment the package already gives non-finite draws elsewhere (#258) (#260).
neuralsbi 0.6.26
-
posterior()on annsbi_nle/nsbi_nrefit now honorsmax_batch, instead of silently ignoring it.log_lik()andlog_ratio()both chunk the(theta, x)pairs they evaluate according tomax_batch, andposterior()’s MCMC path reaches the same evaluator,surrogate_potential()– butmcmc_posterior()stored a caller’smax_batchunchecked intocontrol$dots, and neitherslice_sample_surrogate()normcmc_log_prob()(which backsample()andlog_prob()on the resulting posterior) ever read it, so every MCMC evaluation ran at the defaultmax_batch = 1e5regardless of what was passed. This bit hardest fornre(), whose ratio classifier has no i.i.d. fast path: every observation costs a forward pass, so there was no way through the public API to shrink a batch and avoid an out-of-memory error on many chains against a large observation set.posterior.nsbi_nle()andposterior.nsbi_nre()now takemax_batchas a named argument (default1e5, matching prior behavior), validated the same waylog_lik()/log_ratio()validate it (#230), and threaded through to everysurrogate_potential()call the resulting posterior makes (#256) (#257).
neuralsbi 0.6.25
npe_sequential()now checks round 1’s estimator/training-control arguments before round 1 simulates.n_rounds,n_simulations,epsilon,n_truncation_samplesandmax_proposal_batcheswere already validated up front, closing this gap for #226, but everything else that round 1 forwards through...to its internalnpe()call –n_bins,tail_bound,hidden,n_components,n_transforms,batch_size,lr,patience,n_restarts,clip_grad_norm,max_epochs,validation_fraction,device– was still only checked once thatnpe()call actually ran, at the end of the round, afterprepare_simulations()had already spent round 1’s simulation budget.npe_sequential(prior, simulator, x_obs = 0.5, n_rounds = 1, n_simulations = 500, density_estimator = "linear_gaussian", n_bins = 1)ran the simulator 500 times before raisingn_bins’s “at least 2” error.npe_sequential()now runsnpe()’s owncheck_architecture()/check_train_controls()/check_device_arg()on round 1’s...args before the firstprepare_simulations()call, filling innpe()’s own defaults for anything the caller left unset so the two checks cannot drift apart (#251) (#254).stan_data()/stan_code()now refuse aprior_uniform()with an infinitelow/highbound instead of writing it out asnsbi_low = -Inf, nsbi_high = Inf.prior_uniform(low = -Inf, high = Inf)is a legal, if improper, prior – it exists soprior_truncated()can bound it – andsample_prior()already refuses to draw from it directly. Annle()fit built from pre-computedtheta/xnever callssample_prior(), so the improper prior previously reachedstan_data()andstan_prior_blocks()(R/stan.R) untouched:stan_data()returnednsbi_low = -Inf, nsbi_high = Inf, andstan_code()emitted an unconstrainedvector[...] theta;with no error from this package, leaving the failure (if any) to surface later inside Stan. Both functions now call a newcheck_finite_uniform_bounds(), matching the existingprior_custom()rejection in style and message (#252) (#255).
neuralsbi 0.6.24
-
npe()/nle()/nre()now fail before running the simulator when the chosen estimator ordeviceneeds torch and torch is unavailable, instead of after. All three validate their cheap arguments – device, prior, architecture – beforeprepare_simulations()runs the simulator, since the simulation budget is the expensive part of a call.require_torch()was only reached deep insidetrain_restarts()(R/train.R), which runs afterprepare_simulations()has already called the simulatorn_simulationstimes, sonpe(prior, simulator, n_simulations = 500)(the defaultdensity_estimator = "maf") ran the simulator 500 times on a machine without torch before erroring – the same fornle()’s default estimator andnre()’s defaultclassifier = "resnet".resolve_device()’s CUDA/MPS availability check had the identical ordering problem:device = "cuda"on a non-CUDA machine only failed after simulating. A newcheck_torch_for_estimator()(R/check.R) runs both checks right after the other cheap-argument checks, for any stringdensity_estimator/classifierthat needs torch;density_estimator = "linear_gaussian",classifier = "logistic", and a caller-supplied estimator/classifier function are unaffected (#250) (#253).
neuralsbi 0.6.23
-
log_lik()/log_ratio()’smax_batchnow bounds memory on the flow/NRE path even whenx, nottheta, is the large dimension.cross_iid()(R/likelihood.R) chunkedthetainto blocks sized sotheta_chunk * n_obs <= max_batch, but oncen_obsalone exceededmax_batch,theta_chunkfloored to 1 and that single call still handedscore()the entiren_obs-row observation set – the observation side was never chunked at all, contradicting the “regardless of which side is large” contract restored for the MDN path by #243. This is the pathposterior()’s MCMC sampling uses for an NLE or NRE fit, and NLE/NRE’s headline use case is conditioning on a large i.i.d. observation set (thousands of trials) without retraining, so this is exactly the shape most likely to spike memory.cross_iid()now blocksthetafirst as before and chunks observations within each theta block, accumulating into the same flat(theta, x)vector before callingcollect()– no call toscore()sees more thanmax_batchpairs, and every caller’scollect(idx, lp)contract is unchanged (#248) (#249).
neuralsbi 0.6.22
-
npe_sequential()no longer hands aNaN-filled parameter row to the user’s simulator during round 2+’s truncated proposal rejection. Round 2+ candidates are drawn fromsample_prior(prior, n_needed)– the full prior, not the truncated proposal – which is exactly the out-of-distribution regime where a MAF/NSF density estimator can returnNaNfromlog_prob()(#221). The proposal filter compared that vector to the truncation threshold with a bare>= threshold, so aNaNlog-density madekeepNAat that row; R’s matrix indexing keeps, rather than drops, a row selected by anNAlogical index and fills it withNA, so the corrupted row survived intotheta_newand reachedrun_simulator()– wasting a simulation, or erroring outright for a simulator that checks its input. The loop now guards the comparison withis.finite(lp) & lp >= threshold, the same coercion already used at thelog_prob.nsbi_posterior()andmcmc_init_resample()/mcmc_init_proposal()call sites for this exactNA-vs-FALSEproblem.fit$rounds[[r]]$acceptanceis also now computed afterdrop_failed_sims()for round 2+, so it reflects proposals the simulator could actually use rather than the pre-simulation count a rejectedNaNrow used to inflate (#246) (#247).
neuralsbi 0.6.21
sample()on an unbounded posterior (e.g.prior_normal()) no longer lets a non-finite draw from the density estimator through.sample.nsbi_posterior()’s non-finite-row filter ran only insideif (bounded), since it was added by #234/#236 to work aroundwithin_support()returningNA(notFALSE) for a NaN row – a problem specific to the bounded rejection-sampling path. That leftbounded <- !is.null(prior$lower) || !is.null(prior$upper)FALSEfor an unbounded prior, the common case in the package’s own NPE examples, with no filter at all: a NaN/Inf row that an under-trained MAF/NSF/MDN occasionally produces wasrbind’d straight into the returned draws matrix,attr(draws, "acceptance_rate")still reported1.0, and the corruption propagated silently intosummary(),pairplot(), andsbc()/tarp()diagnostics – or surfaced downstream inmap_estimate()as a confusing “thetacontains non-finite value” error blaming the seed draw.sample.nsbi_posterior()now drops a non-finite row fromde_sample()’s output unconditionally, before theboundedbranch’swithin_support()check runs, soacceptance_ratereflects the drop for every prior (#244) (#245).log_lik()/log_ratio()’smax_batchnow bounds memory for an MDN-based fit even whentheta, notx, is the large dimension. For MAF/NSF/linear_gaussian/NRE,cross_iid()(R/likelihood.R) chunks the(theta, x)cross product bythetarows, somax_batchbounds memory regardless of which side is large. The MDN’s fast i.i.d. path,mdn_iid_blocks(), only chunkedx: it ranmdn_mixture()– the MLP forward pass and Cholesky assembly – over the whole ofthetain one call before any chunking happened, so scoring a densethetagrid (a profile-likelihood plot, say) against a handful of observations materialized a(n_theta, K, dim_theta, dim_theta)tensor however smallmax_batchwas set.mdn_iid_blocks()now blocksthetafirst, the same waycross_iid()does, and chunks observations within each block as before;mdn_trace_cache()’s TorchScript shortcut only fires when a single call would cover both dimensions, since a trace recorded at one shape can’t stand in for the chunked path (#240) (#243).nre()no longer silently trains on zero gradient when the training split, not just the validation split, drops to one row.check_train_controls()(R/train.R) enforcedmin_val_rows(2 fornre()’s atomic contrastive objective, per #188) againstn_valonly, with no matching floor onn_tr = n - n_val. A largevalidation_fractioncan clear the validation-side floor while leavingn_trbelow it –n_simulations = 4,validation_fraction = 0.75givesn_val = 3(passes) andn_tr = 1(was never checked).train_restarts()then trained on that single row, andnre_atomic_log_prob()’sk < 2Lguard – the same branch #188 fixed for the validation side – returned a constant zero loss every step: no gradient, no error, training ran topatienceepochs and reported abest_val_lossas if it had actually trained.check_train_controls()now also requiresn - n_val >= min_val_rows, the same floor already applied to the validation side (#239) (#242).
neuralsbi 0.6.18
-
map_estimate()on a 1-D posterior no longer errors on aprior_custom()with one infinite bound. Itsfit$dim_theta == 1Lbranch chosestats::optim(method = "Brent")wheneverprior$lowerandprior$upperwere both non-NULL, on the assumption that aNULLbound is the only way a prior can be one-sided.prior_custom(dim = 1, lower = -Inf, upper = 5)has both fields set – one is just non-finite – so it took the Brent branch anyway, andstats::optim(method = "Brent", lower = -Inf, ...)errored immediately with"'lower' and 'upper' must be finite values"before the search ran at all. The Brent branch now also requiresis.finite(prior$lower) && is.finite(prior$upper); a bound that is present but infinite falls through to the existingL-BFGS-Bbranch, which already handlesInfon the missing side (#238) (#241).
neuralsbi 0.6.17
-
NSF’s spline no longer produces negative bin widths/heights at large
n_bins, which could surface as aNaNtraining loss.rq_spline()(R/nsf.R) turns a softmax overn_bins(K) bins into bin widths viamin_bin + (1 - min_bin * K) * softmax, valid only whilemin_bin * K < 1;check_architecture()enforcesn_bins >= 2but never capped it, sonpe(..., density_estimator = "nsf", n_bins = 2000)passed validation with the fixed defaultmin_bin = 1e-3and1 - min_bin * Kwent negative. Once that scale factor is negative, any softmax weight large enough (routine once an autoregressive net starts concentrating mass on one bin) makes its bin width negative, which breaks the rational-quadratic spline’s monotonicity and can send its log-determinant term (log()of a non-positive slope ratio) toNaN– including mid-training, far fromn_binswhere the actual mistake was made. The identical shape applied to bin heights, built the same way two lines down.rq_spline()now clampsmin_bintomin(min_bin, 0.5 / K)before rescaling, somin_bin * K <= 0.5holds for anyn_binsa caller chooses rather than only forn_bins < 1000(#235) (#237).
neuralsbi 0.6.16
-
sample(),log_prob(), andmap_estimate()no longer let aNaNdraw from the density estimator slip pastwithin_support()’s rejection filter.within_support()(R/prior.R) compares a row againstlower/upperwithsweep()/rowSums()/==, so a row containingNA/NaNreturnsNArather thanFALSE.log_prob.nsbi_posterior()already accounts for this on user-suppliedthetaby running it throughcheck_finite()first (#221), but three call sites test a vector that comes from the density estimator’s own output instead, and none of them treatedNAas reject:sample.nsbi_posterior()’s rejection filter (draw[within_support(prior, draw), ]) kept anNA-indexed row rather than dropping it – R’s matrix indexing fills it withNAinstead – so aNaNdraw from an under-trained MAF/NSF/MDN became an all-NArow counted towardnand returned as a real posterior draw, with no warning;log_prob()’snormalize = TRUEacceptance estimate (mean(within_support(prior, draw))) turnedNAand then crashed the following comparison with the unrelated base-R message"missing value where TRUE/FALSE needed"; andmap_estimate()’s objective crashed the same way if the optimizer ever proposed a non-finite parameter. All three now coercewithin_support()’sNAtoFALSEbefore using it, so a non-finite draw is rejected as leakage the same way an out-of-bounds one is (#234) (#236).
neuralsbi 0.6.15
-
log_lik()/log_ratio()on an NLE or NRE fit now reject a non-finitemax_batchinstead of an opaquerep()error. Both route throughsurrogate_score()(R/likelihood.R), which dividesmax_batchdown intocross_iid()’s per-block size; aNA/NaNmax_batch(Infwas already fine) reachedrep(..., times = NA)there and failed with the bare base-R message"invalid 'times' argument", naming neithermax_batchnorlog_lik()/log_ratio().surrogate_score()now runsmax_batchthroughcheck_positive(max_batch, "max_batch", allow_inf = TRUE)up front, matching the validation already applied to every other numeric argument at that boundary (#230) (#232).
neuralsbi 0.6.14
-
sample_prior()now validatesn. It was the one count-taking public entry point that never routed its count throughcheck_count(), unliken_simulations,n_sbc,n_init, andsimulate_for_sbi()’s ownn.sample_prior(prior, 2.5)silently returned only 2 rows, via base R’s recycling and an opaque warning;sample_prior(prior, -1)orsample_prior(prior, NA)failed with base R’s generic “invalid arguments” error, naming neither the argument nor the function.sample_prior()now runsnthroughcheck_count(n, "n")at the top, before it reachesprior$sample()(#231).
neuralsbi 0.6.13
-
npe_sequential()now validatesepsilonup front, before round 1 simulates. Every other round-controlling argument (n_rounds,n_simulations,n_truncation_samples,max_proposal_batches) is checked before any simulation runs, butepsilonfell through that net: it is only read starting in round 2, where it sets the truncation threshold viastats::quantile(lp_ref, probs = epsilon, ...). An out-of-rangeepsilon(e.g.1.5,-0.1,NA) passed construction and round 1 silently, spent round 1’s simulation budget, and only then failed in round 2 withstats::quantile()’s raw'probs' outside [0,1]error.npe_sequential()now runsepsilonthroughcheck_prob()alongside the other up-front checks (#226) (#228). -
prior_uniform()/prior_normal()no longer letNA/Infbuild a silently corrupted prior. Both constructors coerced their arguments with bareas.numeric()and never validated them, unlike every named family inR/prior_families.R.prior_uniform(low = c(mu = 0), high = c(mu = Inf))built without error, andsample_prior()on it returnedInfon every draw, sincerunif(n) * (high - low) + lowis always infinite once the range is;prior_uniform(low = NA, high = 5)crashed with a raw, unnamed base-R error (“missing value where TRUE/FALSE needed”) instead of a validated one, because thehigh <= lowcomparison came before any check thatlow/highwere finite; andprior_normal(mean = NA, sd = 1)built without error and silently returned an all-NAsample matrix.prior_uniform()now runslow/highthroughcheck_finite(allow_inf = TRUE), which rejectsNA/NaNbut still allowsInf– needed so an improper prior can be built forprior_truncated()/prior_independent()to bound – andprior_normal()runsmean/sdthroughcheck_finite()with no such allowance, since a normal prior has no legitimate use for an infinite mean or sd. Callingsample_prior()directly on an infinite-boundprior_uniform()now errors with a message pointing atprior_truncated(), rather than returningInf(#227).
neuralsbi 0.6.12
-
c2st()no longer silently corrupts a whole column on a non-finite entry.x/ywent throughcheck_numeric()alone, and with the defaultz_score = TRUEa singleNA/NaN/Infanywhere in a column reachesfit_standardizer()/apply_standardizer()(R/standardize.R), whosecolMeans()/sd()carry nona.rm– so that column standardizes toNAin every row, not just the offending one. The corrupted matrix then either crashes several frames away with no mention ofxory(classifier = "logistic"’sglm()call errors with “Argument mu must be a nonempty numeric vector” oncena.actiondrops every row;classifier = "mlp"’s training loop hits “missing value where TRUE/FALSE needed” once the propagatedNaNreaches the stopping check), or, worse, survives to report an accuracy/AUC computed on a corrupted column, the metric this package’s diagnostics are built around.c2st()now runscheck_finite()onxandyright aftercheck_numeric(), matchingsurrogate_score()(#202) andmcmc_log_prob()(#208/#209) (#222).
neuralsbi 0.6.11
-
log_prob()on an NPE posterior no longer returns a silentNaNfor a non-finitetheta.log_prob.nsbi_posterior()(R/posterior.R) validatedthetawithcheck_numeric()alone, nevercheck_finite(). With a bounded prior,within_support()on a row containingNA/NaNreturns logicalNA, and R leaves anNA-indexed position untouched on assignment, solp[!within_support(prior, theta)] <- -Infwas a no-op for that row and theNaNde_log_prob()produced reached the caller unnamed.log_prob.nsbi_posterior()now runsthetathroughcheck_finite(theta, "theta", allow_inf = TRUE)right aftercheck_numeric(), matchingmcmc_log_prob()’s precedent for the same “posteriorlog_prob” contract (#202) (#208) (#209):Infstays allowed, since it resolves to zero density or-Infthrough the prior or the estimator rather than signaling a bug, and onlyNA/NaNare rejected (#221) (#223).
neuralsbi 0.6.10
-
bulk_ess()no longer floors Geyer’stauestimate at1. The reference formula (Stan’s implementation, and Vehtari et al. 2021, whichsplit_rhat()already matches) floorstau_hatat1 / log10(n*k), not at1;min(n*k / tau, n*k * log10(n*k))andn*k / max(tau, 1/log10(n*k))are the same clamp written two ways, and the code used the first form for the upper bound but then separately flooredtauitself at1, discarding the lower bound the reference formula allows. A well-mixing or anti-correlated chain routinely has an uncappedtau_hatbelow1, and the wrong floor silently clamped its ESS atn*k, the raw draw count, instead of the larger value the reference computation gives – confirmed againstposterior::ess_bulk()on AR(1) chains with negative lag-1 autocorrelation, where the old code reported exactlyn*kwhile the correct value ran 2x-3x higher. A new test adds anti-correlated AR(1) fixtures (#219) (#220).
neuralsbi 0.6.9
-
bulk_ess()no longer over-corrects autocorrelation at every lag.autocov()returns the biased autocovariance (divided byn), and then / (n - 1)factor exists to convert only the lag-0 term into the unbiased quantity that matchesW, whichstats::varalready computes with then - 1divisor. The code instead appliedn / (n - 1)to the whole autocovariance vector before computingrho, which happened to leave lag 0 correct (acov[1] * n / (n - 1) == Walgebraically) but inflated every higher-lag autocorrelation entering Geyer’s paired sum, matching neither Stan’s own implementation nor rstan’smonitor.R::ess_rfun, which rescaleacov[0]alone and leave every lag>= 1unscaled. The effect shrinks asn -> Inf(then / (n - 1)factor approaches 1), which is why the package’s regression test againstposterior::ess_bulk()didn’t catch it: it ran atn = 400with a 1% tolerance, small enough to hide a discrepancy that reaches 1-2% at then = 20-100range typical ofnle()/nre()’s default slice-sampler settings. Only the diagnostic ESS number reported bymcmc_diagnostics()was affected, not the MCMC draws themselves orsplit_rhat(). A new test adds ann = 50fixture with a tolerance tight enough to have caught this (#217) (#218).
neuralsbi 0.6.8
-
npe_sequential()now honorsseedfor weight initialization, not just for the proposal/simulation randomness.npe_sequential()callsset.seed(seed)up front, which seeds R’s base RNG for the whole run, but its innernpe()call never forwardedseedon, sonpe()’s ownseedargument stayed at its defaultNULL.train_restarts()(R/train.R) only callstorch::torch_manual_seed(seed)whenseedis non-NULL, and that call is what drives reproducible network weight initialization every round – so twonpe_sequential(..., seed = 42)calls could still train different networks, depending on whatever torch RNG state happened to be ambient at call time. The fix passesseedthrough to the innernpe()call (#216).
neuralsbi 0.6.7
-
npe()/nle()/nre()now honorseedwhen called with pre-computedtheta/x. Theseedargument only ever reached R’s base RNG throughsimulate_for_sbi()’s ownset.seed(seed), which runs on the branch ofprepare_simulations()that calls the simulator; the documented, first-classtheta/xcode path skipped it entirely.train_restarts()(R/train.R) draws its train/validation split and every epoch’s minibatch order fromsample.int(), which reads R’s base RNG, nottorch’s – so two calls with an identicalseedand identical pre-computedtheta/xcould still train on different splits and minibatch orders, and so land on different weights, depending on whatever base RNG state happened to be ambient at call time. All three entry points now callset.seed(seed)themselves beforeprepare_simulations()runs, matching the pattern already used innpe_sequential(),c2st(), and the NLE MCMC posterior (#214).
neuralsbi 0.6.6
-
de_sample.nsbi_de_mdn()no longer errors on a 1-d target with more than one mixture component. It read the batch row of the means and Cholesky-factor tensors withparams$means[1, , ]/L[1, , , ], and R’storchpackage indexing follows base-Rdrop = TRUEsemantics: every size-1 dimension in the result is dropped, not just the batch dim being integer-indexed. Withdim_theta == 1(or, undernle()’s swapped roles,dim_x == 1) andn_components > 1– the estimator’s default is 10 – thepdimension collapsed too, someans/Larrcame back as bare vectors instead ofK x p/K x p x parrays andde_sample()errored with “incorrect number of dimensions”. The existingn_components == 1Lspecial case worked around the same drop forK, but nothing coveredp == 1. The fix reshapes explicitly from the full, un-indexed tensor viaarray(torch::as_array(...), dim = c(K, p))(andc(K, p, p)forLarr), which subsumes the oldK == 1Lbranch, so it is removed (#211) (#212).
neuralsbi 0.6.5
-
c2st()now runs the same test as thesbibmbenchmark, so its numbers are comparable with published ones. It fit a cross-validated logistic regression, which is linear: it scores two sample sets that share a mean and differ in spread at chance, and a difference in spread is exactly what a mis-trained posterior usually shows.R/c2st.Rreplaces it with the procedure insbibm/metrics/c2st.py– both sample sets z-scored by the mean and standard deviation ofx, then a two-hidden-layer ReLU network of10 * dunits per layer trained by Adam (lr = 1e-3, L21e-4, minibatches of 200, stopping once the epoch loss has gone 10 epochs without improving by1e-4), scored by accuracy over 5 shuffled folds. On two standard normals differing only in scale (sd 1 against sd 2) the old classifier returned 0.50 and the new one returns 0.74. The network trains on torch, like the rest of the package’s neural code, soclassifier = "mlp"needs torch installed and says so when it is missing;classifier = "logistic"is the torch-free way to get a number, and it is what the analytic-parity tests assert on in the CI job that has no libtorch. -
c2st()gainsclassifier,z_score,noise_scale,hidden,max_epochsanddevice.classifier = "logistic"is the old linear test, kept as a cheap screen.z_scoreandnoise_scalearesbibm’s arguments of the same name; the noise is for draws that are discrete or lie on a lower-dimensional set, where a classifier separates the two sides on an artefact of representation.devicesends the network to a GPU the same way thefit_*functions do. It also returns the ROC AUC alongside the accuracy, assbibmdoes. -
c2st()’s two sample sets are no longer symmetric, sincexalone sets the z-scoring. Pass the reference draws asx, followingsbibm. The call sites intests/andinst/benchmarks/were flipped to match. - New
inst/benchmarks/13_c2st_parity.Rand14_c2st_parity.pycheckc2st()againstsbibm/metrics/c2st.pyitself on five sample-set pairs whose answer is known by construction, so the parity claim rests on a run rather than on a reading of the Python source. The two agree to Monte-Carlo noise;inst/benchmarks/README.mdrecords the table.
neuralsbi 0.6.4
-
log_prob()on an NLE/NRE posterior no longer returns a silentNA/NaNfor a non-finitetheta.mcmc_log_prob()(R/nle_posterior.R), the shared body behindlog_prob.nsbi_mcmc_posterior(), validatedthetawithcheck_matrix()alone, which checks type and shape but never finiteness. AnNA/NaNentry reachedsurrogate_potential()’s closure, came back non-finite fromprior$log_prob()the same way an out-of-support draw does, and was left as a silentNA/NaNlog-prob instead of a named error.mcmc_log_prob()now runsthetathroughcheck_numeric()andcheck_finite()beforecheck_matrix(), matchingsurrogate_score()(#202) andstan_data()(#203).check_finite()gains anallow_infargument for this call site: anInfthetais not part of the bug, since it correctly resolves to-Infthrough the prior’s own density, so it stays allowed and onlyNA/NaNare rejected (#208) (#209).
neuralsbi 0.6.3
-
stan_data()now validatesx_obsbefore handing it to Stan.posterior.nsbi_npe()andmcmc_posterior()both checkx_obswithcheck_numeric()andcheck_finite()beforeas_theta_matrix()(#51), butstan_data()calledas_theta_matrix()directly. AnNAin an otherwise numericx_obs, or a character column thatstorage.mode<-silently turns intoNA, reachedcmdstan_model()$sample()/rstan::sampling()unnoticed and surfaced as an opaque Stan error instead of a namedneuralsbione.stan_data()now runs the same two checksposterior()andmcmc_posterior()already do (#203) (#206).
neuralsbi 0.6.2
log_lik()andlog_ratio()no longer return a silentNAfor a non-finitethetaorx.surrogate_score()(R/likelihood.R), the body both share, validated its arguments withcheck_matrix()alone, which checks type and shape but never finiteness. AnNA/NaN/Infentry standardized into anotherNAand came back as a silentNAlog-density or log-ratio instead of an error, the same failure mode fixed forthetapassed to a surrogate posterior’slog_prob()in #163.surrogate_score()now runsthetaandxthroughcheck_numeric()andcheck_finite()beforecheck_matrix(), matchingposterior.nsbi_npe()andmcmc_posterior()(#202) (#205).stan_data()now validatesx_obsbefore handing it to Stan.posterior.nsbi_npe()andmcmc_posterior()both checkx_obswithcheck_numeric()andcheck_finite()beforeas_theta_matrix()(#51), butstan_data()calledas_theta_matrix()directly. AnNAin an otherwise numericx_obs, or a character column thatstorage.mode<-silently turns intoNA, reachedcmdstan_model()$sample()/rstan::sampling()unnoticed and surfaced as an opaque Stan error instead of a namedneuralsbione.stan_data()now runs the same two checksposterior()andmcmc_posterior()already do (#203) (#206).
neuralsbi 0.6.1
-
The Stan-generated-code tests now run in CI.
tests/testthat/test-stan.Rchecks that the Stan codestan_code()emits forlinear_gaussian,mdn, andmafagrees numerically withlog_lik(), butskip_if_no_cmdstan()(tests/testthat/helper-stan.R) meant they ran nowhere: neitherR-CMD-check.yamlnortest-coverage.yamlinstalls a CmdStan toolchain. A new.github/workflows/stan-tests.yamlinstalls libtorch and CmdStan (both cached across runs) and runs the Stan tests onworkflow_dispatchand on a schedule every two weeks, onmain, rather than on every push – installing a compiler toolchain on the fast-feedback workflows would slow down every push and PR for a job that only exercises one file (#196) (#204).
neuralsbi 0.6.0
-
Priors are no longer three constructors.
prior_lognormal(),prior_exponential(),prior_gamma(),prior_beta(),prior_student_t(),prior_cauchy(),prior_half_normal()andprior_half_cauchy()(R/prior_families.R) build a prior from a named distribution family under Stan’s argument names, vectorized over parameters the wayprior_normal()already is. A named family setslower/upperfrom its own support, so the out-of-support rejection and thelog_probrenormalization inR/posterior.Rget the right region with nothing further to declare (#199) (#201). -
prior_independent()multiplies per-parameter priors into a joint one. This is what mostprior_custom()calls were written to do by hand, and doing it by hand is where the quiet mistakes live: alog_prob_fnthat returns one number instead of one per row, or alowerof the wrong length thatsweep()recycles into a support test rejecting the wrong draws. Components may be anynsbi_prior, including multi-parameter ones and aprior_custom(). Argument names name one-parameter components; a wider component keeps its ownparam_names(#199) (#201). -
prior_truncated()is Stan’sT[lower, upper], with the density renormalized by the mass it keeps. Leaving the normalizing constant off would be harmless for a posterior sampled by MCMC on its own, butnle()andnre()sum the prior density with a learned likelihood insurrogate_potential()(R/mcmc.R), so a prior short a constant is a prior of the wrong shape relative to that likelihood. Bounds intersect with the family’s own support and with any earlier truncation. Truncating aprior_custom()errors rather than returning an unnormalized density, since there is no CDF behind it to renormalize against (#199) (#201). -
stan_code()writes the new families out as sampling statements. Uniform and normal priors still travel through the data block, unchanged. Every other family becomes a literal sampling statement carryingT[,]on whichever side the support was cut, and the parameter block declares matching constraints: a shared bound givesvector<lower=...>[Q] theta, differing bounds give per-parameterreals assembled intransformed parameters. Aprior_custom()still errors, now naming?prior_familiesas the alternative (#199) (#201). -
prior_uniform()andprior_normal()now carry the same internal per-parameter form as the new families, so both can be truncated and composed. Their closures, printing and Stan output are unchanged.print.nsbi_prior()gains one line per marginal for the new types (p ~ beta(2, 15),s ~ normal(0, 1.5) T[0, ]) (#199) (#201). -
task_sir()’s prior (R/tasks.R) is now aprior_lognormal()rather than the same two log-normals written out throughprior_custom(), so annle()fit on that task exports with a model block instead of erroring (#199) (#201). - New
vignette("methods"), a guide to the methods this package implements: NPE, sequential NPE, NLE and NRE, the MDN/MAF/NSF density estimators, and the SBC, coverage, TARP and C2ST diagnostics. Each entry names the paper the method comes from and then the function that runs it, so a reader who knows a method by its citation can find the call. Citations resolve throughinst/REFERENCES.bib, so every entry renders with a link out to the DOI (#200) (#201).
neuralsbi 0.5.38
-
mcmc_init(strategy = "resample")no longer double-counts the prior in its SIR weight.mcmc_init_resample()(R/mcmc.R) draws its candidate pool from the prior – so the SIR proposal is the prior itself – and resampled without replacement usinglog_prob_fn(cand)as the weight, which fornle()/nre()posteriors issurrogate_potential()’s full unnormalized posterior,log p(theta) + log L(x|theta). A correct importance weight is target/proposal, and since target isp(theta) * L(x|theta)and the proposal isp(theta), the prior cancels and the weight should beL(x|theta)alone; weighting by the full posterior instead resampled fromp(theta)^2 * L(x|theta), pulling chain starts back toward the prior’s mode.prior_uniform()hid this, since a constant drops out of the Gumbel-top-k ranking either way – the bug only shows for a non-flat prior, such asprior_normal()or aprior_custom()with an asymmetric density.mcmc_init_resample()now subtractsprior$log_prob(found)from the accumulatedfound_lpbefore building the Gumbel keys, leaving the finiteness check that selectsfoundon the full posterior density unchanged (#195) (#197).
neuralsbi 0.5.37
-
npe_sequential()now errors whenn_simulationsdoesn’t matchn_rounds, instead of silently recycling it.n_simulationsis documented as “either a scalar or a vector of lengthn_rounds” (R/sequential.R), butcheck_counts()only validated each element, neverlength(n_simulations)againstn_rounds;budgets <- rep_len(n_simulations, n_rounds)then recycled any other length without a diagnostic.npe_sequential(prior, sim, n_rounds = 3, n_simulations = c(500, 1000))– a plausible mistake, such as forgetting the third round’s budget – silently ran with budgetsc(500, 1000, 500)instead of raising an error. Since every round retrains from scratch and the point of the multi-round scheme is spending a fixed budget deliberately across rounds, a silently wrong per-round budget wastes or misallocates simulator calls with nothing to catch it.npe_sequential()now checkslength(n_simulations) %in% c(1L, n_rounds)right aftercheck_counts()validates its elements, and errors naming both arguments beforerep_len()runs (#191) (#194).
neuralsbi 0.5.36
-
The slice sampler now validates
max_stepsandn_pool, closing the gap thewidthfix in 0.5.26 explicitly left open.slice_sample_run()(R/mcmc.R) fed a user-suppliedmax_stepsstraight intosteps_left <- max_steps; while (steps_left > 0L), andmcmc_init()’sn_poolfed straight intobatch <- max(n_pool, n_chains)insidemcmc_init_resample()/mcmc_init_proposal(), neither checked before use. Both are reachable fromposterior(nle_fit_or_nre_fit, x_obs, max_steps = ..., n_pool = ...):posterior.nsbi_nle()/posterior.nsbi_nre()forward them through...toslice_sample_surrogate()(R/nle_posterior.R), which passesdots$max_steps %||% 100Ltoslice_sample()anddots$n_pool %||% 1000Ltomcmc_init().max_steps = NAlooped forever instead of erroring, sinceNA > 0LisNAand awhile()condition only errors once evaluated; a zero or negativen_poolreachedmax()andsample_prior()with nothing said about which argument was wrong.slice_sample_run()andmcmc_init()now check both withcheck_count()(R/check.R), right wherecheck_slice_width()already checkswidth, before either value can drive a loop or size a batch (#190) (#192).
neuralsbi 0.5.35
-
nre()no longer silently keeps an untrained network when a smalln_simulationsleaves a 1-row validation split.check_train_controls()(R/train.R) only required the validation split to be non-empty, which is enough fornpe()/nle()– their estimators score a real, if noisy, log-density on a single validation row.nre()’s atomic contrastive objective (nre_atomic_log_prob(),R/nre.R) cannot: with fewer than 2 rows there is no contrast to score, and it returned a literal constant-zero loss every epoch. Insidetrain_restarts()that zero beat the initialInfat epoch 1, never changed afterward, so the “improved” branch never fired again, and training silently stopped afterpatiencemore epochs holding the epoch-1 (essentially untrained) network – while reportingbest_val_loss = 0, which reads as a perfect fit rather than the failure it is.nre(prior, simulator, n_simulations = 15)hit this directly: the defaultvalidation_fraction = 0.1givesn_val = max(1, floor(0.1 * 15)) = 1.check_train_controls()now takes amin_val_rowsargument (default1L, unchanged for every other caller) threaded throughtrain_conditional_de()andfit_torch_de();fit_nre_net()(R/nre.R) passesmin_val_rows = 2L, andnre()checks it early, before the simulator runs, whenever the true row count is already known (theta/xpassed directly, or a validn_simulations). The closed-form logistic classifier and a caller-supplied classifier function are unaffected – neither uses this validation split at all (#188) (#189).
neuralsbi 0.5.34
-
map_estimate()no longer optimizes a 1-parameter posterior with Nelder-Mead.stats::optim()’s own documentation and runtime warn that one-dimensional Nelder-Mead is unreliable (“use "Brent" or optimize() directly”), and single-parameter models are not a corner case for this package – one-parameter priors appear in thenle()/nre()doc examples themselves. Everymap_estimate()call on such a posterior hit that warning, and a test already worked around it withsuppressWarnings()instead of fixing the underlying method choice (tests/testthat/test-posterior-normalization.R).map_estimate()(R/posterior.R) now branches onfit$dim_theta == 1L: a bounded prior with both a lower and an upper limit getsoptim(method = "Brent")over that exact interval; a one-sided bound getsoptim(method = "L-BFGS-B")withInfon the missing side, since Brent needs both ends finite and plain BFGS’s finite-difference gradient can probe past the missing side into masked territory; a fully unbounded prior getsoptim(method = "BFGS"), which needs no interval and doesn’t trigger the warning. Multi-dimensional posteriors are unaffected and stay on Nelder-Mead (#186) (#187).
neuralsbi 0.5.33
-
mcmc_init(strategy = "resample")no longer starts several chains from identical points. It drew one pool ofmax(n_pool, n_chains)prior draws and kept whichever landed inside the posterior’s support; when that left fewer thann_chainsof them, it padded the shortfall withrep_len(ok, n_chains), recycling the same indices so several slice-sampler chains began from the same point (R/mcmc.R). Split-Rhat and bulk ESS both assume the chains they compare started from distinct locations, so a recycled start weakens mode coverage and understates disagreement between chains. A newmcmc_init_resample()draws more pools and accumulates their finite draws, the waymcmc_init_proposal()already does for"proposal", untiln_chainsdistinct finite draws are collected (up to 20 attempts) and only then runs the weighted resample without replacement. If the budget runs out first, the error distinguishes a genuinely degenerate surrogate (no draw at all landed in support) from a merely low acceptance rate (some did, just not enough) (#182) (#185).
neuralsbi 0.5.32
- Documentation only, no behavior change:
standardizer_log_jac()’s roxygen docstring (R/standardize.R) named the wrong transform direction. It said “inverse standardization (standardized -> original)”, but the value it returns,-sum(log(std$scale)), is the Jacobian of the forward transformz = (x - center) / scale(original -> standardized); the code was always correct, every call site already adds it to a standardized-space log-density to recover original units. The docstring andman/standardizer_log_jac.Rdnow describe the forward direction (#183) (#184).
neuralsbi 0.5.31
-
mcmc_init(strategy = "proposal")no longer fails on an ordinary, non-degenerate posterior with a modest acceptance rate. It drew one batch ofn_chainsprior draws and required all of them to be jointly finite, retried whole-batch up to 20 times (R/mcmc.R). If the posterior excludes even a third of the prior’s support – an entirely ordinary case, not a degenerate one – a batch is finite with probability0.65^20, about 3e-4, so the retries almost never succeeded, and the error claimed the surrogate likelihood “may be degenerate” regardless of why the batch failed. A newmcmc_init_proposal()pools finite draws across attempts instead, the way"resample"already pools its candidates, so the chance of collectingn_chainsfinite starting points grows with the total number of prior draws tried rather than with the luck of one batch. If the budget still comes up short, the error now distinguishes a truly degenerate surrogate (no draw at all landed in support) from a merely low acceptance rate (some did, just not enough), and only the former says “degenerate” (#179) (#181).
neuralsbi 0.5.30
-
npe_sequential()no longer crashes with an opaquerbind()error when a later round accepts zero proposals anddim_x > 1. The truncated-proposal loop (R/sequential.R) callsrun_simulator()for roundr’s acceptedtheta_new, but didn’t pass its optionaldargument (expected output width). When a round exhaustedmax_proposal_batcheswith zero accepted candidates,run_simulator()’s zero-row fast path (R/parallel.R) fell back tod %||% 1Land returned a0 x 1matrix regardless of the simulator’s real output width, sox_all <- rbind(x_all, x_new)failed with “number of columns of matrices must match” wheneverdim_x != 1– a message naming neithernpe_sequential()nor the real cause. Withdim_x == 1it didn’t crash, but silently contributed zero simulations to that round with only a warning to go on.npe_sequential()now passesd = ncol(x_all)once round 1 has run, and treats zero accepted proposals as a hard stop rather than “continuing with fewer simulations”: a round that adds nothing would otherwise refit on unchanged data and report that as roundr’s result, with no way for a caller to see that nothing happened. The new error names the round, the acceptance, and suggests a largerepsilon, moremax_proposal_batches, or fewer rounds (#178) (#180).
neuralsbi 0.5.29
-
expected_coverage()now returns the right shape for a single-parameter fit. For a fit with one parameter,sbc_result$rankshas one column, socolMeans()insideexpected_coverage()’s (R/diagnostics.R) per-levelsapply()returned a length-1 result at every nominal level;sapply()then simplified those to a plainlength(levels)vector instead of a matrix, and the followingt()turned that into a single-row matrix – one column per nominal level instead of one column for the parameter.colnames()then labeled those columnsparam1/param2/param3as if there were three parameters, each holding the same three numbers repeated across every nominal level, so the table read as flat (and wrongly shaped) regardless of how well calibrated the fit actually was.expected_coverage()now usesvapply()and restores the dimension it drops for a one-columnranksmatrix before transposing, matching the shape it already produced correctly for two or more parameters. Found while investigating #169: a one-parameter fit is not what that issue’s SIR model uses (three parameters), so this bug is not the cause of the undercoverage reported there, but it is a real, previously uncaught defect in any single-parameter calibration check (#169) (#177). - Test coverage: added a regression test pinning
sbc()’s calibration under a uniform prior that actively truncates the fitted density – alinear_gaussianfit with itsB/Sigmaset by hand to the exact conditional (theta | x ~ N(x, sigma^2 I)restricted to the prior box, the closed form for a flat prior and additive Gaussian noise), so the only code path left to trust is the rejection-sampling and acceptance-renormalization inR/posterior.Rand the rank binning insbc()itself. No existing test checked SBC calibration with an exact fit under an actively truncating bounded prior: the existing calibration tests all use an unboundedprior_normal(), and the existing bounded-prior tests only useprior_uniform()to check the “short draw errors” behavior with a fit deliberately broken to leak. This test comes back well calibrated, which rules out both of #169’s bounded-prior-handling and rank-scoring candidates as the source of that issue’s undercoverage (#169) (#177).
neuralsbi 0.5.28
-
posterior(fit, sampler = "stan")now falls back to rstan (or says clearly what to run) instead of crashing when cmdstanr has no CmdStan behind it.stan_sample_nle()(R/stan.R) picked its backend with a barerequireNamespace("cmdstanr", quietly = TRUE), which only checks that the R package is installed.install.packages("cmdstanr")does not install CmdStan itself – that is the separate, network-dependentcmdstanr::install_cmdstan()step, which fails outright on machines that block the GitHub release download it needs (many CI runners and managed environments). In that state the old code always took the cmdstanr branch and failed deep insidecmdstanr::cmdstan_model()with “CmdStan path has not been set yet.”, even when a working rstan install was sitting right there. A newcmdstan_ready()helper checkscmdstanr::cmdstan_version(error_on_NA = FALSE)as well as the package, mirroring the package-vs-runtime distinctionrequire_torch()already draws for the torch backend.stan_sample_nle()now uses it to pick the backend: it falls back to rstan (with a message explaining why) when cmdstanr is installed but CmdStan is not, and only errors when there is truly nothing to fall back to – with a message that tells cmdstanr-without-CmdStan apart from neither package being installed, since the two have different fixes (#172) (#176).
neuralsbi 0.5.27
-
posterior_predictive()now errors clearly when the posterior has no draws inside the prior support, instead of crashing on a dimnames assignment. For a bounded prior,sample.nsbi_posterior()can legitimately return zero rows when rejection sampling never lands inside the support for an observation far outside anything the fit was trained on – it warns, but does not stop.posterior_predictive()(R/diagnostics.R) passed that zero-rowthetastraight torun_simulator(), which produced a 0x0 matrix, and the function then rancolnames(pred) <- post$fit$x_namesagainst it: withdim_x > 1that failed with the genericlength of 'dimnames' [2] not equal to array extent, naming neither the function nor the actual problem, and withdim_x == 1it silently returned a useless empty matrix instead of erroring at all.posterior_predictive()now checksnrow(theta)right after sampling and stops with a message naming the function, the acceptance rate, and the likely cause (the observation being outside the range the fit was trained on) before the simulator or the dimnames assignment ever run.summary.nsbi_samples()(R/summaries.R) gets the same treatment for the related silent failure the issue flagged: summarizing a zero-row draw returned a row ofNaN/NAper parameter with no warning, and now says so (#171) (#174).
neuralsbi 0.5.26
-
The slice sampler now validates
widthinstead of letting a bad value crash the stepping-out loop several frames later.nle()/nre()posteriors forward awidthargument through...fromposterior()down toslice_sample()/slice_sample_run()(R/mcmc.R), which coerces it withwidth <- rep_len(as.numeric(width), dim)and never checked the result before using it to size the initial slice interval and drive the stepping-out loop’s array indexing.posterior(nle_fit, x_obs, width = NA)followed bysample()failed with “NAs are not allowed in subscripted assignments” – a generic R subscripting complaint naming neitherwidthnor the sampler.slice_sample_run()now checks the recycledwidthwith a newcheck_slice_width()(R/mcmc.R) right afterrep_len(), and stops with a message namingwidth, the values that were actually wrong, andslice_sample(), before any of them reachstats::runif()or the stepping-out loop. Negative/zerowidthand negativemax_steps/zeron_pooldegrade silently rather than crash and are not covered by this fix (#164) (#167).
neuralsbi 0.5.25
-
log_prob()on an NLE or NRE posterior now rejects a non-numericthetaby name instead of silently returningNA.log_prob.nsbi_mcmc_posterior()(R/nle_posterior.R) routesthetathroughmcmc_log_prob()intosurrogate_potential()’s closure (R/likelihood.R), which only reshapes it withas_theta_matrix()– nocheck_matrix()/check_numeric()anywhere on that path.as_theta_matrix()coerces a non-numeric column toNAwith a bare “NAs introduced by coercion” warning rather than erroring, solog_prob(post, theta = data.frame(mu = c("x", "y")))came back asNAwith nothing said abouttheta– worse than crashing, since there is no signal to a caller doingwhich.max(log_prob(...)). Every sibling entry point already validates:log_prob.nsbi_posterior()for NPE posteriors, andlog_lik()/log_ratio()for the same NLE/NRE fits viasurrogate_score().mcmc_log_prob()now checksthetawith the samecheck_matrix()callsurrogate_score()already uses, once, before building the potential – not insidesurrogate_potential()’s returned closure, which every MCMC step also calls with athetait built internally and has no need to re-validate (#163) (#166).
neuralsbi 0.5.24
-
sample()now validatesn(aliasedsize), matching every other draw-count argument in the package.sample.nsbi_posterior()(R/posterior.R) already checkedmax_sampling_batcheswithcheck_count(), butn– the argument that actually drives the rejection-sampling loop – reachedwhile (nrow(collected) < n ...)unchecked, sosample(post, n = NA)failed inside the loop condition,sample(post, n = "10")failed onn - nrow(collected), andsample(post, n = -5)failed insidematrix(), none of the messages namingnorsample().sample.nsbi_mcmc_posterior()(R/nle_posterior.R, backingnle()/nre()posteriors) had the same gap and it was worse there:nreachedmcmc_draws()’sceiling(n_draws / n_chains)unchecked, sosample(post, n = 2.5)did not error at all, it silently returned 2 draws. Both methods now checknwithcheck_count()up front, before any sampling work happens, giving the same named, actionable errorn_init,n_sbcand the rest of the package’s draw-count arguments already give.posterior_predictive()(R/diagnostics.R), which forwards its ownnstraight intosample(), inherits the fix without any change of its own (#162) (#165).
neuralsbi 0.5.23
-
map_estimate()now errors clearly instead of crashing when its seeding draw comes up short. It seedsstats::optim()withdraws <- sample(post, n = n_init, obs = x)and then picks the best of them bylog_prob(), butsample.nsbi_posterior()can legitimately return fewer thann_initrows – including zero – for a bounded prior when rejection sampling never lands inside the support aftermax_sampling_batchesrounds, and it only warns, it does not stop. A zero-row draw reachedwhich.max()and thenstats::optim()unchecked, and the failure surfaced deep insidedmvnorm_chol()(R/density_estimator.R) asError in x - mean : non-conformable arrays, naming neithermap_estimate()nor the leaking prior actually responsible.map_estimate()(R/posterior.R) now checksnrow(draws)right after thesample()call, matching the checkdiagnostic_draws()(R/diagnostics.R) already makes for the same failure mode, and stops with a message naming the function and the shortfall and suggesting more simulations or a look at the prior. A partial short draw errors too, not just an empty one – there is no way to known_initpoints were searched from when fewer were (#159) (#161).
neuralsbi 0.5.22
-
split_rhat()now includes the tail-Rhat component, so it matches its Vehtari et al. Rhat docstring in full. #157 fixed the missing rank-normalization but stopped at bulk-Rhat: it rank-normalized the raw split-chain draws and ran the classical Gelman-Rubin statistic on those, without also folding them around the median for a tail component. Vehtari et al. (2021)’s Rhat – andposterior::rhat()’s default – ismax(bulk-Rhat, tail-Rhat); bulk-Rhat alone tracks location but not scale, so it reads chains that agree in location but disagree in spread as converged. Four chains centered at 0 with two atsd = 1and two atsd = 4reproduce this: bulk-Rhat alone readsRhat < 1.01while the complete statistic readsRhat > 1.2.split_rhat()(R/mcmc.R) now computes both components through a sharedgelman_rubin_rhat()helper and takes their max, andtests/testthat/test-mcmc.R’sposterior::rhat()cross-check tightens fromtolerance = 1e-4back to1e-6now that the two compute the same statistic rather than a close approximation of it (#154) (#160).
neuralsbi 0.5.21
-
split_rhat()now actually rank-normalizes, matching what its docstring already claimed.mcmc_diagnostics()documented “the standard rank-free versions from Vehtari et al. (2021)” for both statistics it returns, andbulk_ess()did rank-normalize, butsplit_rhat()computed the classical 1992 Gelman-Rubin statistic straight off the raw split-chain values. Rank-normalization is what makes Rhat robust to heavy tails and multimodality – exactly the failure mode the vectorized slice sampler (R/mcmc.R) behindnle()/nre()posteriors is prone to – so a raw-value Rhat can read as converged when the rank-normalized one would flag it: a four-chain, Cauchy-noised fixture with real between-chain disagreement readsRhat < 1.01under the old code andRhat > 1.2under the fix.split_rhat()andbulk_ess()now share onerank_normalize()helper, so there is one implementation of Vehtari et al.’s rankit transform instead of the claim living in a docstring the code did not follow.tests/testthat/test-mcmc.Rgains the Cauchy regression fixture and tightens itsposterior::rhat()/posterior::ess_bulk()comparison tolerance now that the two are computing the same thing rather than merely landing close on near-Gaussian data (#154) (#157).
neuralsbi 0.5.20
-
log_prob(..., normalize = TRUE)now warns when its acceptance estimate hits the zero floor, instead of staying silent. For a bounded prior,log_prob.nsbi_posterior()(R/posterior.R) estimates the normalizing acceptance probability by samplingn_normalizationdraws and checkingwithin_support(); when none of them land inside the prior, it floors the estimate at1 / n_normalizationto avoidlog(0)and returns a normal-looking finite value with no sign that the normalizing constant is a floor rather than a measurement.sample.nsbi_posterior()already warns in the equivalent case, when rejection sampling comes up short: “The estimator is leaking mass outside the prior; consider more simulations.”log_prob()now raises the same warning whenever the floor is doing the work, so the two functions agree about surfacing this failure mode (#153) (#156).
neuralsbi 0.5.19
-
map_estimate()no longer returns a point outside a bounded prior’s support. It searches with unconstrainedstats::optim(..., method = "Nelder-Mead")and always callslog_prob(post, ..., normalize = FALSE), so the-Infmask thatlog_prob(normalize = TRUE)applies for a bounded prior (fromprior_uniform()or aprior_custom()withlower/upper) never reached the optimizer’s objective, and the search could converge outside the box. The objective inmap_estimate()(R/posterior.R) now checkswithin_support()directly and returnsInffor an out-of-support candidate, which is the same guarantee without paying to re-estimate the acceptance constant on every evaluation. Unbounded priors take the same code path as before and are unaffected (#152) (#155).
neuralsbi 0.5.18
-
Vignette figures are now all ggplot2, with a consistent theme and palette.
#136fixed the parts of #107 that don’t need libtorch to verify (pkgdown’s equation rendering andpairplot()’s truth-marker/axis-range styling); this closes the remaining part, converting every base-plot()call acrossvignettes/*.Rmd.orig(intro-to-sbi,neural-likelihood,neuralsbi,sir-epidemic,sir-time-varying-beta) toggplot2, matching thetheme_minimal()and steelblue/firebrick/grey palette the package’s ownplot_*()functions already use.neural-likelihood.Rmd.orig’s SBC panel loop also drops apar(mfrow = c(2, 2))wrapper around four calls toplot_sbc(), which is grid graphics and was never affected bypar(); the baked figures already rendered as four separate images, so removing the dead wrapper changes nothing about the output. These vignettes are precomputed under libtorch (seevignettes/precompute.R) and this environment has no libtorch, so the committed figures undervignettes/figures/are not yet regenerated from the new code – that rebake is tracked in #29 (#107) (#148).
neuralsbi 0.5.17
- Internal cleanup, no user-visible behavior change:
sample(),log_prob()andprint()each had a byte-identical method fornsbi_nle_posteriorand one fornsbi_nre_posterior, differing only in the class name they printed and, forlog_prob(), the “NLE”/“NRE” label in its unnormalized-posterior warning.mcmc_posterior()(R/nle_posterior.R) now stamps a sharednsbi_mcmc_posteriorclass into the middle of the class vector – ahead ofnsbi_posterior, behind the specificnsbi_nle_posterior/nsbi_nre_posteriorthatprint()output andexpect_s3_class()checks still key off – so the three pairs collapse intosample.nsbi_mcmc_posterior(),log_prob.nsbi_mcmc_posterior()andprint.nsbi_mcmc_posterior().log_prob.nsbi_mcmc_posterior()reads the “NLE”/“NRE” label offinherits(post$fit, "nsbi_nle")instead of taking it from the caller, andprint.nsbi_mcmc_posterior()gates its closingstan_code()line the same way.posterior.nsbi_nle()andposterior.nsbi_nre()are unchanged (#144) (#147).
neuralsbi 0.5.16
-
nre()adds Neural Ratio Estimation, the third factorization of the joint. Wherenpe()learns the posterior andnle()learns the likelihood,nre()(R/nre.R) learns neither density but their ratior(theta, x) = p(x | theta) / p(x), by training a classifier to tell(theta, x)pairs drawn from the joint apart from pairs whose parameter came from a different simulation. Defaults mirror Pythonsbi’sNRE(an alias for itsNRE_B): the"resnet"classifier (a residual MLP,hidden = 50,n_blocks = 2), the atomic loss of Durkan et al. (2020) atnum_atoms = 10, and the training controlsnpe()/nle()already share withsbi(batch_size = 200,lr = 5e-4,validation_fraction = 0.1,patience = 20,clip_grad_norm = 5)."mlp"and"linear"aresbi’s other two classifiers;"logistic"is a torch-free baseline.log_ratio(fit, theta, x)evaluates the learned ratio, summing over rows ofxas independent observations of the same parameter the waylog_lik()does for an NLE fit, andposterior(fit, x_obs)returns annsbi_nre_posteriorthat samplesr(theta, x) p(theta)with the same vectorized slice sampler an NLE posterior uses.sbc(),tarp(),expected_coverage(),posterior_predictive(),map_estimate(),summary()andsave_nre()/load_nre()all take the new fit. There is nostan_code()for a ratio estimator: the Stan exporter transpiles a fitted density, and a residual classifier has no such export. - The
"logistic"classifier is tonre()what"linear_gaussian"is tonpe()/nle(): a closed-form, torch-free baseline that is exact for a linear-Gaussian simulator and so serves as the CI oracle for the whole NRE path. It fits a ridge-penalized logistic regression on the quadratic basis[1, z, vech(z z')]forz = (theta, x). Two atoms make the atomic loss collapse to a logistic regression on the difference of two feature rows, which is what IRLS can solve in closed form; working in differences also cancels every term depending onxalone, including the evidencelog p(x), which is the one part of the log ratio a quadratic basis cannot represent. Contrasts are cyclic shifts rather than random draws, so the fit is deterministic given the simulations. - A ratio needs no change-of-variables correction, unlike a density: the standardization Jacobian cancels between
p(x | theta)andp(x), solog_ratio()in the estimator’s z-scored space is already the ratio in the units the simulator returned.surrogate_ops()(R/likelihood.R) is the one table where each surrogate fit type says which functions score it and whether the Jacobian travels with them, sosurrogate_potential()(which replaces the internalnle_potential()),log_lik()andlog_ratio()all read the answer off it instead of each restating it. -
linear_gaussian’s ridge no longer depends on the scale of the data.fit_linear_gaussian()(R/density_estimator.R) added a flat1e-06to the diagonals ofX'Xand of the residual covariance. That is only a negligible regularizer when the data are O(1), which is whatstandardize = TRUEarranges and why the default path was never affected. Understandardize = FALSE, a target column whose variance is2.5e-07had1e-06added to it and came back five times too wide, throwing the reported log-density off by 20 nats on a three-column example spanning1e-03to1e+03. The ridge is now measured per column against that column’s own scale:diag(X'X)for the design, and the target’s variance for the covariance, since a noiseless model leaves no residual to measure against and that is exactly when the ridge has to keepchol()alive. A column with no scale of its own borrows the largest one that has any. On standardized data both diagonals are already about 1, so the fits every other test pins are unchanged to six significant figures (#139). -
A final training minibatch of exactly one row no longer kills an
nre()fit. One simulation has no contrasting parameter to be scored against, so the atomic loss returned a constant that torch had not built andbackward()failed with “element 0 of tensors does not require grad and does not have a grad_fn”. Whether it happens at all depends on nothing butn_simulationsmodulobatch_size: 2001 simulations leave 1801 training rows, which at the defaultbatch_size = 200is nine full batches and one row.minibatches()(R/train.R) now folds a trailing single row into the batch before it, for every estimator rather than just the ratio ones – a gradient step from one sample is a bad step for any of them – and the atomic loss returns a tensor torch built, so a validation split of one row cannot reintroduce the failure (#139). -
The
"logistic"classifier’s ridge no longer depends on the scale of the data either.irls_logistic()(R/nre.R) added a flat1e-06to the diagonal ofX'WX, and its features are quadratic in(theta, x), so understandardize = FALSEthey carry the fourth power of the data’s units. On a simulator whose output has sd5e-04that ridge swamped the normal equations and the fitted ratio collapsed to noise: RMSE5.88against an analytic log ratio whose own spread is6.29. The ridge is now measured per column against that column’s own scale, through the sameridge_scale()thatfit_linear_gaussian()uses, and thestandardize = FALSEfit matches thestandardize = TRUEone to six decimal places (#139). - The
iid_matrix()/iid_evaluator()blocking loop is now tested without torch.linear_gaussianhas its ownde_log_lik_iid()andde_iid_evaluator()methods that score the whole observation set per parameter and ignoremax_batch, so the existing “blocking does not change the answer” test compared two calls that did the same thing;cross_iid(), the path every flow and every ratio estimator takes, was reached only by the torch tests that CI skips. A scorer that encodes which(theta, x)pair it was handed now pins the block layout across eight batch sizes, so a transposition cannot hide inside a row sum (#139). - The
"resnet"classifier is now pinned tonflows’ResidualNetnumerically rather than by comment.sbi’s"resnet"is that network, andnre_module()reimplements it; filling both with the same deterministic weights and evaluating them on the same rows agrees to7e-08, which is float32.test-nre.Rcarries the three reference numbers, so where the activations sit inside a block, which of its two linear layers is zero-initialized, and whether the final layer sees an activation are all checked against the original (#139). - Internal cleanup, no user-visible behavior change: the pieces
nle()andnre()posteriors share are now written once.log_lik()andlog_ratio()had the same body apart from the Jacobian and the pair of functions that score the estimator, so both now callsurrogate_score()(R/likelihood.R), which reads them offsurrogate_ops().npe(),nle()andnre()built the same thirteen-field list in the same order, and now callnew_nsbi_fit()(R/npe.R) with whichever fields are their own.cross_iid()(R/likelihood.R) takes the per-pair scorer as an argument, sode_log_lik_iid()’s andde_iid_evaluator()’s default methods and their ratio counterparts split intoiid_matrix()/iid_evaluator()over one blocking loop;R/nle_posterior.Rgrowsmcmc_posterior(),mcmc_draws(),mcmc_log_prob()andcat_mcmc_posterior()for the argument checking, construction, sampling, log-density and printing that both posterior classes do identically, andslice_sample_nle()becomesslice_sample_surrogate();summary.nsbi_npe()’s body moves tofit_summary(), which the NRE method calls withclassifierwhere the others passdensity_estimator.
neuralsbi 0.5.15
-
Rejection-sampling loops for bounded priors now request only the still-missing draws each round, not a full batch.
sample.nsbi_posterior()’s support-rejection loop (R/posterior.R) andnpe_sequential()’s truncated-proposal loop (R/sequential.R) both loopedwhile (collected < n) { draw n more; keep the accepted ones }, askingde_sample()/sample_prior()for a fulln(or full round budget) every round regardless of how many draws were already collected. For an estimator with real leakage at, say, 60% acceptance, requestingsize = 1000cost roughly1000 + 1000 + ...per round instead of1000 + 400 + ..., wasting sampling and support-checking work on later rounds. Both loops now requestn - nrow(collected)(orbudgets[r] - nrow(theta_new)) each round instead of the full count. Neither loop’s stopping condition or returned shape changes – both already stopped oncenaccepted draws were collected – so this is a pure efficiency fix, not a behavior change (#138) (#143).
neuralsbi 0.5.14
-
npe()/nle()rejectn_bins < 2for the NSF density estimator instead of crashing on the first forward pass.nsf_made_module()’sforwardsplits a per-dimension parameter tensor intoKbin widths,Kbin heights andK - 1interior derivatives by slicingparams[, , (2 * K + 1):(3 * K - 1)]; forK = 1that range is3:2, which R’s:reads as the descending indexc(3, 2)rather than an empty selection, and the parameter tensor only has 2 columns forK = 1, so the read went out of bounds.check_architecture()already validatedn_binswithcheck_count(), but its defaultmin = 1Lletn_bins = 1through; it now passesmin = 2L, the honest lower bound for a spline that needs an interior derivative to fit. Closes #137 (#140).
neuralsbi 0.5.13
-
Reference/help-topic pages on the pkgdown site now render their equations.
\eqn/\deqnmarkup inman/*.Rdreachesreference/*.htmlthrough a different pkgdown code path than vignettes andREADMEdo: reference pages are built straight from Rd XML (as_html.tag_eqn/as_html.tag_deqn) and never go through pandoc, so the LaTeX landed on the page as literal\(...\)/$$...$$text with nothing to render it, while vignettes (built from actual Markdown, through pandoc) rendered correctly under pkgdown’smathmldefault._pkgdown.ymlnow setstemplate: math-rendering: katex, which works client-side by scanning the whole rendered page for math delimiters regardless of which pipeline produced the surrounding HTML – fixing reference pages without changing how vignettes already render (#107) (#136). -
pairplot()’s truth markers are grey and dashed instead of a solid saturated red, and its panels now share a consistent axis range per parameter. The reference-value lines and cross marker usedcolour = "firebrick"; they’re now"grey30", thinner, and dashed, so they read as an overlay rather than competing with the density fill. Separately, whenlimitsisn’t supplied,lower_fn/diag_fneach built an independentggplot()object per panel, andggdensity::geom_hdr()/ggplot2::geom_density()estimate their density grid per panel, so a parameter’s drawn range could disagree between its diagonal panel and its off-diagonal panels, or between panels in the same row/column.pairplot()now defaultslimitsto each parameter’s own data range (padded 5%, matching ggplot2’s own default scale expansion) and applies it through the samecoord_cartesian()path the explicit-limitsargument already used, so every panel for a given parameter agrees (#107) (#136).
neuralsbi 0.5.12
-
sbc()no longer emits “Chi-squared approximation may be incorrect” during ordinary use. Its per-parameter uniformity test calledstats::chisq.test(tab)on the binned ranks, and R warns whenever a cell’s expected count is low, which happens routinely at the smalln_sbca test suite uses to keep runtime down. Switching tochisq.test(tab, simulate.p.value = TRUE)computes a Monte Carlo p-value instead of relying on the asymptotic approximation, so the warning never fires; the p-value keeps the same “large = calibrated” interpretationexpected_coverage()and existing tests rely on (#109) (#135). - Test coverage: added targeted tests for previously untested error paths and edge cases across
R/utils.R,R/progress.R,R/simulator.R,R/parallel.R,R/mcmc.R,R/sequential.R,R/tasks.R,R/diagnostics.RandR/check.R– mockedrequireNamespace()/torch_available()branches for the torch/ggplot2/GGally/ggdensity/cli “package not installed” error messages (these only ever ran in an environment that actually lacked the package, which the coverage CI job never is), zero-row and zero-draw edge cases in the simulator pipeline,mcmc_init()’s two failure-to-start branches andsplit_rhat()/bulk_ess()’s degenerate-run guards, andnpe_sequential()’s proposal-batch-cap warning andx_obsvalidation branches..github/workflows/test-coverage.yamlnow setsNOT_CRAN: "true", since a few tests (thefuture-multisession path inR/parallel.R, the slower two-moons NPE fit)skip_on_cran()to stay off CRAN’s check machine, a concern that does not apply to this internal coverage job (#109) (#135).
neuralsbi 0.5.11
-
The built-in training progress bar’s ETA now tracks a short rolling window of recent epochs instead of the lifetime average.
builtin_bar()(R/progress.R) previously extrapolated fromelapsed / possince the bar started, which is the average over the whole run so far. For NPE/NLE training that average is dragged down by the first epoch’s one-time setup cost (device/net initialization, first CUDA/MPS kernel compilation, R JIT warmup) and, acrossn_restarts > 1, blends in restarts whose per-epoch cost can differ from the current one. A newbar_rate()estimates steps/sec from the last 8 calls instead, falling back to the lifetime average only while that window is too short to trust;bar_eta()turns the resulting rate into seconds remaining. The moving-target denominator thattrain_progress_total()projects (best_epoch + patience, growing every time validation loss improves) is unchanged and remains documented in?nsbi_progressas a running estimate, not a promise – this fix addresses the rate half of the ETA, not the total (#108) (#134).
neuralsbi 0.5.10
-
npe()/nle()gain adeviceargument for training on a GPU.device = "cpu"(the default, matching Pythonsbi; GPU is opt-in, never auto-selected),"cuda","mps", or"gpu"/"auto"to resolve CUDA -> MPS -> CPU. Wrapping a fit intorch::with_device()used to crash with a mixed-device error:torch_tensor()built from an R matrix ignores that context and always lands on CPU, and MAF, MDN and NSF all build scratch tensors (torch_zeros(), an autoregressiverev_idx, the spline’s tail padding, the MDN’s Cholesky buffer) the same unqualified way, so a net moved to a GPU still hit CPU tensors partway through a forward or inverse pass.train_conditional_de()(R/train.R) now builds training/validation tensors on the resolved device and callsnet$to(device = ); the estimator-specific forward/inverse code (R/flows.R,R/mdn.R,R/nsf.R) derives every scratch tensor from an existing tensor’s device instead of relying on the default; and thede_log_prob()/de_sample()S3 methods, plus the MDN’s separate i.i.d. fast path used bynle()’s MCMC sampling (R/likelihood.R) andstan_code()’s weight export (R/stan.R), move inputs to the estimator’s device and results back to the CPU for the caller."cuda"/"mps"error clearly when the requested backend is not available on this machine, rather than silently downgrading to CPU – a request for a specific device that is not honored should not look like a slow CPU run; only"gpu"/"auto"falls back to CPU without complaint, since it never named a specific device.deviceis a no-op, not an error, fordensity_estimator = "linear_gaussian", which has no GPU concept. A fitted estimator remembers the device it trained on (fit$device,fit$de$device, stored as a plain string so it survivessave_npe()) andload_npe()/load_nle()always rebuild the network on the CPU on reload, regardless of the device it trained on, sincetorch::torch_load()already defaults there. Closes #82 (#132).
neuralsbi 0.5.9
- Internal cleanup, no user-visible behavior change:
stan_fn_lingauss(),stan_fn_mdn()andstan_fn_maf()(R/stan.R) each wrote their own copy of the generated_sum_lpdfentry point – thethetastandardization, thexcenter/scale declarations,real total = 0, thefor (n in 1:rows(x))loop, and the jacobian-corrected return – differing only in the loop body and in what, if anything, gets precomputed once outside the loop (the linear-Gaussian mean/Cholesky factor and the MDN’s mixture head; MAF has nothing to hoist).R/stan.Rnow carries one internal helper,stan_sum_lines(fit, P, body, precompute = ""), that all three call instead of repeating the block; each generator keeps only its own loop body and precompute expression inline. Generated Stan output is unchanged for all three estimators.stan_sum_lines()sums the same per-observation log density thatde_log_lik_iid()(R/likelihood.R) sums on the R side, so a comment now points from one to the other (#64) (#126).
neuralsbi 0.5.8
- Internal cleanup, no user-visible behavior change:
plot_coverage()andplot_tarp()each computed their own 99% Monte-Carlo binomial band by hand and built the samegeom_ribbon() + geom_abline() + coord_equal() + theme_minimal()calibration figure around it, andplot_sbc()computed a third version of that band, unscaled, for its histogram’s reference lines.R/plotting.Rnow carries two internal helpers,binom_band(nominal, n, level = 0.99)andcalibration_plot(df, band, xlab, ylab), so the confidence level and the binomial parameterization live in one place instead of three:plot_coverage()andplot_tarp()call both and add only their own curve and title on top, andplot_sbc()callsbinom_band()and scales it back to a count. All three functions keep their exact signatures and output (#63) (#125).
neuralsbi 0.5.7
- Internal cleanup, no user-visible behavior change:
resolve_x()(R/posterior.R) andresolve_x_iid()(R/nle_posterior.R) restated the same six lines – fall back to the posterior’sx_obs, stop if neither is supplied,check_numeric(),check_finite(),as_theta_matrix()– differing only in whether row 1 is kept (with a warning) or every row is kept.R/posterior.Rnow carries one internal helper,resolve_obs(post, x, first_row, arg), that both call instead of repeating the body;resolve_x()andresolve_x_iid()keep their exact signatures and behavior, so no call site changed (#62) (#124).
neuralsbi 0.5.6
-
sbc()’s rank denominator now comes from the draws a trial actually returned, not from the requestedn_posterior_samples. The chi-square uniformity test andexpected_coverage()both binsbc()’s ranks against a fixed scale, and that scale was read straight from then_posterior_samplesargument rather than fromnrow(draws). The two numbers happen to always agree today: a short draw (a bounded prior and a leaky estimator defeating rejection sampling) is already turned into a hard error bydiagnostic_draws()(0.4.3) before any rank is computed, so no trial reaches the binning step with fewer draws than requested. But the binning code was trusting an argument instead of reading back what actually happened, which is the wrong thing to trust even when the two currently coincide. The shared trial loop introduced below now reports the draw count it actually saw, and bothsbc()andtarp()(whose own per-trialmean()was already dividing bynrow(draws), and so was unaffected) size their scale from that instead (#122). - Internal cleanup, no user-visible behavior change otherwise:
sbc()andtarp()each restated the same preamble – check the fit and prior, draw truths from the prior, simulate, drop failed simulations – and the same progress-bar/tryCatchtrial loop, differing only in the metric computed per trial.R/diagnostics.Rnow carries two internal helpers:sbc_draws()for the preamble, which is also where theprior-width check now lives so both diagnostics get it from one place, andfor_each_trial()for the loop, which bothsbc()andtarp()call with their ownf(draws, i)(a rank row forsbc(), a scalar coverage value fortarp()) instead of repeating either block (#61) (#122).
neuralsbi 0.5.5
Internal cleanup, no user-visible behavior change for
print.nsbi_npe()andprint.nsbi_nle(): both printed the same five blocks by hand – parameter names, outcome names, the simulation count and any drops, the best validation loss, and the dead-network warning – differing only in the save-function name (save_npe()vssave_nle()) and a “per observation” suffix on the data line.R/utils.Rnow carries two internal helpers,cat_fit_common()andcat_dropped(), that both print methods call instead of repeating the blocks;print.nsbi_sbc()andprint.nsbi_tarp()also switch tocat_dropped()for their identical “N further trials dropped” line.print.nsbi_snpe()now callscat_fit_common()too, which means a sequential fit’s summary gains the parameter/data dimensions, dropped-simulation count, best validation loss and dead-network warning it never showed before, alongside the round table and not-amortized warning it already had (#60) (#121).Internal cleanup, no user-visible behavior change: the MADE trunk (
made_module()inR/flows.R,nsf_made_module()inR/nsf.R) and the plain MLP trunk (the MDN inR/mdn.R, the embedding network inR/embedding.R) each rebuilt the samelinear, relustacking loop.R/flows.Rnow carries one internal helper,mlp_layers(dims, masks = NULL), that both variants call – masked whenmade_masks()$hiddenis passed, plain otherwise – so the four call sites share one loop instead of four (#59) (#120). The autoregressive forward/inverse transform-stack loops inflows.R/nsf.Rare left untouched; the issue flagged those as a separate, riskier dedup.
neuralsbi 0.5.3
- Internal cleanup, no user-visible behavior change:
fit_density_estimator()restated everyfit_*()training default –n_components,hidden,max_epochs,batch_size,lr,validation_fraction,patience,n_restarts,clip_grad_norm– as a%||%fallback, once per estimator branch. Both callers,npe()andnle(), already pass every argument explicitly, so none of those fallbacks had ever fired; the numbers just had to be kept in sync by hand acrossfit_density_estimator(), eachfit_*()signature, and the two callers. It now forwards only the arguments the chosenfit_*()accepts (intersect(names(dots), names(formals(fn))), thendo.call()), so eachfit_*()’s own signature is the only place its defaults live.linear_gaussianonly takesridgeandverbose, so ann_componentsorembedding_netpassed alongside it is still silently dropped, as before (#58) (#118).
neuralsbi 0.5.2
- Internal cleanup, no user-visible behavior change: the MDN, MAF and NSF density estimators had three byte-identical copies of the tensor plumbing behind
de_log_prob.*and two behindde_sample.*(de_sample.nsbi_de_mdnsamples its mixture directly and shares nothing here), plus near-identicalfit_mdn()/fit_maf()/fit_nsf()wrappers aroundtrain_conditional_de().R/density_estimator.Rnow carries three internal helpers next to the generics they implement –de_log_prob_torch(),de_sample_flow()andfit_torch_de()– and each estimator’s file calls them instead of repeating the body.fit_mdn(),fit_maf()andfit_nsf()keep their exact signatures; only their bodies collapsed, soman/fit_mdn.Rd,man/fit_maf.Rdandman/fit_nsf.Rdneeded no edit (#57) (#117).
neuralsbi 0.5.1
-
A slice-sampled NLE posterior now reports how many likelihood evaluations its last run cost.
slice_sample_run()has always countedn_evals, but nothing pastslice_sample()itself read it. The count is attached as ann_evalsattribute on thediagnosticsdata frame next torhatandess_bulk– an attribute rather than a column, since it is one number for the whole run, not one per parameter – andprint()on annsbi_nle_posteriornow includes it alongside Rhat and ESS. It is the cost the adapted slice width is trying to keep down (see?nsbi_mcmc), and there was previously no way to see it (#114). - Internal cleanup, no user-visible behavior change otherwise:
helper-torch.R’sskip_if_no_torch()now calls the package’s owntorch_available()instead of re-implementing the same check inline, andrequire_torch()uses it too.drop_failed_sims()no longer returns anoklogical that nothing read.dmvnorm_chol()dropped itslogargument – every call site passedlog = TRUE, so theexp()branch never ran.builtin_bar(),hint_parallel()andprior_scale()dropped default arguments no caller ever overrode. A doubled verbose guard in the training loop (R/train.R) is now a singlecat()inside theif (verbose && ...)block that was already checking it. The commented-out neural-likelihood-estimation navbar entry in_pkgdown.ymlis restored, now that the vignette it links to builds cleanly (#33) (#114).
neuralsbi 0.5.0
-
CRAN submissions are now automated.
.github/workflows/release-check.yamlwatchesDESCRIPTIONonmain(plus a weekly schedule) and tags and publishes a prerelease once the version has had a minor or major bump, or once three weeks have passed since the last release tag on a patch-only bump; a patch bump inside that window publishes nothing (issue #110)..github/workflows/cran-submission.yamlfires on that prerelease and runscoatless-actions/cran-submission, which checks the package and submits the tarball. This release is the first tag the new workflow cuts, establishing thevX.Y.Ztagging habitCLAUDE.mdhas called for since 0.4.1 (#115).
neuralsbi 0.4.16
-
summary()now works on annle()fit. Onlysummary.nsbi_npewas registered, so an NLE fit fell through tosummary.defaultand printed a Length/Class/Mode table of the raw fit list, thedeelement holding the torch module included. Every other user-facing verb was extended to NLE in 0.4.3.summary.nsbi_nlecalls the NPE method, which reads only fields both classes carry and dispatchesprint()on the object, so the NLE fit reports its data dimension per observation the wayprint()already does.
neuralsbi 0.4.15
-
plot_sbc()now takes a parameter name as well as an index, and checks whichever it is given.plot_sbc(res, param = 99)reachedranks[, 99]and reportedsubscript out of bounds, which names neither the argument nor how many parameters there are. The rank matrix carriescolnamesand every other plotting function labels by name, soparam = "sigma"works too; a name that is not among the columns is refused with the ones that are. -
pairplot()checkslimitsagainst the number of parameters. A list or a matrix with the wrong number of entries indexed past its end and gave the samesubscript out of bounds. The message now says how many limit pairs it got and how many parameterssampleshas. -
expected_coverage()requireslevelsstrictly between 0 and 1.levels = c(-1, 2)used to be scored anyway: the central interval comes out empty or covers the whole line, so the row reads as coverage 0 or 1 and looks like a verdict on the fit.plot_coverage()inherits the check by passinglevelsthrough. -
print()on an NLE posterior reports unusable MCMC diagnostics as unavailable.split_rhat()andbulk_ess()returnNAfor a run with too few iterations, one chain, or a coordinate that never moved, andmax(na.rm = TRUE)over all-NAis-Infwith a warning, so the line readmax Rhat -Inf, min bulk ESS Inf. A partially scored run reports the parameters that did get a number and counts the ones that did not. -
stan_data()checks that the fit still has a live network, whichstan_code()has done since it was written. A fit restored with plainreadRDS()used to get the “save it withsave_npe()” message from one and a dangling external pointer from the other, for the same fit and the same cause. -
load_npe()andwrite_stan_model()check the path they are given.load_npe()handed anything straight toreadRDS(), which reports “cannot open the connection” and names the file only in the warning beside it; it now says the file does not exist, and refuses a value that is not one path the waysave_npe()always has.write_stan_model()checksfilebefore it transpiles the network, so a wrong destination costs no code generation.
neuralsbi 0.4.14
-
simulate_for_sbi()now recognises a call withsimulatorandpriorthe wrong way round. It takes the simulator first andnpe(),nle()andnpe_sequential()take it second, so the two functions a user calls in the same breath disagree. Getting it backwards used to fail insidesample_prior(), whosestopifnot()reportsinherits(prior, "nsbi_prior") is not TRUEabout a local that holds the simulator. A user reading that concludes the prior object is broken.inherits(simulator, "nsbi_prior") && is.function(prior)can only be the swap, since a prior is never a function and a simulator is never annsbi_prior, so that case now says so and shows the right call.?simulate_for_sbisays the order is reversed. -
simulate_for_sbi()also checks thatsimulatoris a function and thatprioris annsbi_priorbefore it draws anything.npe_sequential()has had the simulator check since it was written;simulate_for_sbi()left both to surface further down.
neuralsbi 0.4.13
-
sbc()andtarp()now check that thepriorthey are given covers the parameters the fit was trained on. Both takeprior = fit$prioras a default, so the argument is there to be overridden, and passing something else is legitimate:npe_sequential()trains on truncated proposals, and SBC against a narrower prior is a fair local calibration check. Nothing checked the width, though.sbc()sizes its rank matrix fromfit$dim_thetaand draws the truths fromprior, so a mismatch reachedsweep(), which recycles one against the other and ranks the truth against a comparison nobody asked for; intarp()the same mismatch reached the z-scoring and the distances. Both now callcheck_prior(prior, dim = fit$dim_theta)before any simulation runs, so a wrong prior costs no simulator calls.?sbcand?tarpsay what overridingpriorchanges about the diagnostic.
neuralsbi 0.4.12
-
c2st()now checks that its two sample sets are the same width, and that it has draws enough for the folds it was asked for. Two sets of different widths met each other atrbind(), which reports “numbers of columns of arguments do not match” and names neitherxnory; the message now gives both widths. More folds than draws was worse, because it was silent:rep_len()left the last folds empty,mean()of an empty test fold isNaN, and the accuracy came backNaNwith no error.n_foldsmust now be fewer than the number of draws in the smaller set, and the message says how many draws that is.xandyalso go throughcheck_numeric(), so a character column is named rather than coerced toNA. - Fixed:
c2st()assigned its cross-validation folds with the package’s ownsample()generic, which happened to work only becausesample.default()forwards tobase::sample(). It callsbase::sample()now, as the subsampling ten lines above already did.
neuralsbi 0.4.11
-
A non-finite observation is now rejected where it is supplied.
posterior()tookx_obson trust, and anNAin it travelled through standardization into the density estimator and came back as all-NaNdraws. The first complaint wasstats::quantile()’s “missing values and NaN’s not allowed if ‘na.rm’ is FALSE”, raised from insidesummary()and saying nothing about the observation. On the NLE side the sameNAmade every MCMC starting point non-finite, and the run failed reporting an initialization problem. The observation is the one input a user types out rather than simulates, so it is the one most likely to carry anNAfrom a real data set, and it was the only one the package did not check:drop_failed_sims()has always discarded non-finite simulations with a count and a rate, andslice_sample_run()has always refused a non-finite starting point.posterior()on either fit type, and an observation passed straight tosample(),log_prob()ormap_estimate(), now go throughcheck_finite(), which names the argument, counts the bad entries, says whether they areNA,NaNorInf, and gives the position of the first one. It errors rather than warns: there is nothing sensible to condition on. - Internally,
resolve_x_iid()takes anargargument so the message names the argument the caller used,obsforsample()andxforlog_prob().
neuralsbi 0.4.10
-
An error raised inside the simulator now names the simulation and the parameters that produced it.
as_sim_draw()has always reported everything wrong with a simulator’s return value, down to which column was not numeric, but a failure that happens before there is a return value got none of that.npe(prior, function(mu, ls) rnorm(1, mu, exp(ls)))on an unnamed prior answeredargument "ls" is missing, with no default, which is R’s description of the symptom and says nothing about the cause: an unnamed prior sends the whole parameter vector to the first formal, so the second one never gets a value. Under afutureplan the same error crossed a worker boundary before anyone saw it. The message is nowSimulation 3 failed: <the original message>, followed by the parameter values that produced it and a pointer to?nsbi_simulator. At most six values are shown, so a 40-parameter model does not fill the console. The original condition is kept as the parent and its class is carried onto the re-raised error, so a simulator that signals a custom condition can still be caught by class. - Internally,
call_sim_once()inR/simulator.Rwraps the per-draw call, anddescribe_params()joinsdescribe_value()inR/check.R.
neuralsbi 0.4.9
-
The
linear_gaussianestimator now checks the width ofxlike every other estimator.fit_linear_gaussian()recordeddim_thetaand notdim_x, so its two methods had nothing to compare an incomingxagainst and passed it straight to the matrix product. Anxof the wrong width came back as R’s “non-conformable arguments”, which names neither the argument nor the width expected of it, while the MDN, MAF and NSF all answer “Expected 3 columns but got 4”. That gap mattered more here than it would anywhere else:linear_gaussianis the default in the examples, the torch-free path, and the oracle the test suite is built on. A bare vector of lengthdim_xis now also read as one observation rather than a column of values, again matching the neural estimators. Fits serialized beforedim_xexisted keep working. - Internally,
R/density_estimator.Rgains a test file.fit_linear_gaussian(),lingauss_mean()and the twonsbi_de_lingaussmethods were named nowhere in the suite despite being what everything else is checked against.
neuralsbi 0.4.8
-
prior_custom()now checks its arguments, and probessample_fnandlog_prob_fnonce at construction. It is the one prior a user writes by hand and it took everything on trust, so the mistakes surfaced far from their cause. Alog_prob_fnreturning a single number instead of one density per row ofthetapassed the probe innle_potential()and came back from the sampler as “Some MCMC starting points have zero posterior density. This is an initialization failure, not a sampling one”, which is accurate about where it noticed and wrong about the cause. Alowerorupperof the wrong length was recycled bysweep()insidewithin_support()with nothing but R’s “STATS is longer than the extent of ‘dim(x)[MARGIN]’” warning, and the wrong support test then decided which posterior draws were rejected as leakage and whatlog_prob()renormalized by. Asample_fnwhose width disagreed withdimwas caught, but anonymously, as “Expected 2 columns but got 1”.dimmust now be a positive whole number,sample_fnandlog_prob_fnfunctions of one argument, andlower/uppernumeric of lengthdimwithupperabovelower.sample_fn(2)is called once at construction and has to return a 2 xdimnumeric matrix, andlog_prob_fnis evaluated on those two rows and has to return two numbers. Every message names the argument it rejected. -
lowerandupperaccept a single number, recycled to every parameter, so a positive-support prior islower = 0rather thanlower = rep(0, dim). The recycling is deliberate and documented; it is only the silent recycling of a wrong-length bound that is gone. -
prior_custom()gainsparam_names.new_prior()has always carried parameter names, andprior_uniform()/prior_normal()take them from the names oflow/mean, butprior_custom()had no way to pass them. That was not cosmetic:sim_dispatch()decides fromprior$param_nameswhether a simulator is called with one scalar per formal or with the whole parameter vector, so a custom prior could never use the named-simulator signature.?prior_customalso now says why a custom prior cannot be written out bystan_code(). - Fixed:
print()on a prior bounded on one side only no longer errors. It printedupperwheneverlowerwas set, andprior_custom(..., lower = 0)with noupperis a normal thing to build. - Internally,
check_function()andcheck_bound()join the validators inR/check.R.
neuralsbi 0.4.7
-
npe()andnle()now warn when a column ofthetaorxhas no spread. Standardization has always guarded against dividing by zero by leaving such a column at scale 1, and it did so without saying anything. A constant summary statistic does not respond to the parameters, so it tells the estimator nothing and the mistake is invisible from the outside: training converges and the posterior looks plausible while the coordinate does nothing. The warning names the columns, by name where the matrix has column names and by index otherwise, and says which side of the fit they are on. A single row gets its own wording, sincesd()of one value isNArather than zero. Thestandardize = FALSEpath stays silent: its degenerate standardizer is built from a one-row zero matrix on purpose. - Internally,
fit_standardizer()takes awhatargument naming the side it is standardizing, the waydrop_failed_sims()already did. Callers that leave itNULLwarn about nothing.
neuralsbi 0.4.6
-
Fixed: a non-numeric column in a pre-computed
thetaorxis now an error that names the column. The pre-computed path coerced its input withstorage.mode(x) <- "double", so a character or factor column turned into a column ofNAwith R’s generic “NAs introduced by coercion” warning. Every row was then dropped as non-finite and the run stopped with “All 50 simulations returned non-finite output (NA, NaN or Inf). Check the simulator on a single prior draw”, which is wrong twice over: no simulator ran, and the data was fine apart from one text column.npe(prior, theta =, x =),nle()andlog_prob()now say`x` has non-numeric columns: b, the wayas_sim_draw()has always reported the same mistake in simulator output. - Relatedly,
drop_failed_sims()no longer tells you to check the simulator when you did not call one. On the pre-computed path it points atthetaorxinstead. - Internally,
check_numeric()carries the type half ofcheck_matrix(), so entry points that disagree about shape (a bare vector is one parameter set tolog_lik()and a column of values tonpe(theta =, x =)) still share one message about types.
neuralsbi 0.4.5
-
The counts and training controls on the public surface are now checked, before any simulation runs. None of these numbers were validated, so a bad one was reported by whichever base function reached it first, usually after the simulation budget had already been spent:
n_simulations = -5failed insidestats::runif()as “invalid arguments”,validation_fraction = 1insideseq()as “wrong sign in ‘by’ argument”,batch_size = 0as “invalid ‘(to - from)/by’”, andn_restarts = 0skipped the restart loop and came back as “Training failed: no restart produced a finite validation loss”, which blames training for an argument.n_simulations = 0reachedcbind()and warned about recycling;n_simulations = 1trained on one draw and said nothing. The counts (n_simulations,n,n_sbc,n_tarp,n_posterior_samples,n_folds,n_rounds,n_init,n_normalization,n_truncation_samples,max_sampling_batches,max_proposal_batches), the training controls (max_epochs,batch_size,lr,validation_fraction,patience,n_restarts,clip_grad_norm) and the architecture arguments (n_components,n_transforms,hidden,n_bins,tail_bound) are now checked at the top of the entry point that takes them, innpe(),nle(),simulate_for_sbi(),npe_sequential(),sample(),log_prob(),map_estimate(),sbc(),tarp()andc2st(). Each message names the argument and the value it rejected.npe()andnle()resolvedensity_estimatorthere too, so a misspelled estimator is an error before the simulator runs rather than after. -
validation_fractionis also checked against the number of simulations, insidetrain_conditional_de(), which is the first point where that number is known. A fraction that leaves no training rows now says how many rows the split would need instead of failing inseq(). - Internally,
check_counts()validates a vector of counts (hidden), andcheck_positive()gainsallow_infforclip_grad_norm, whereInfis the documented way to disable clipping.
neuralsbi 0.4.4
- Internally, argument checking moves to a shared set of validators in
R/check.R:check_matrix(),check_count(),check_prob(),check_positive(),check_prior()andcheck_finite(). Each names the argument it rejects and says what was actually wrong, following the voice of the simulator-output checks. They are internal, and the entry points adopt them one at a time. -
Behaviour change:
log_lik()now rejects a wrong-lengththetaorxinstead of reshaping it. A vector shorter or longer than the fit’s width was recycled into a matrix of the right number of columns, solog_lik(fit, c(0.1, 0.2, 0.3), x)on a two-parameter fit returned two log-densities computed from recycled values, and a length-4 vector became two parameter sets with no message at all. At a public boundary a bare vector now means a single row, or it is an error. A matrix whose row count matches the expected width is told to transpose.as_theta_matrix()keeps its permissive reshaping for internal callers that rely on it. - Fixed: an NPE posterior given a multi-row observation now warns.
resolve_x()kept row 1 and said nothing about the rest, which is right for NPE and invisible to the user. The trap is the asymmetry withnle(), where the rows ofx_obsare independent observations that the log-likelihood sums over: the same matrix means “200 observations” tonle()and “the first observation” tonpe(), so moving a working call from one to the other silently dropped all but one data point. The warning names the row count and points atnle(). It stays a warning because taking row 1 of a simulation matrix is a reasonable thing to ask for, andsbc()/tarp()pass single rows anyway.
neuralsbi 0.4.3
-
New: Neural Likelihood Estimation, via
nle(). Wherenpe()learns the posterior directly,nle()learns a surrogate likelihood . The reason to want that is repeated observations. An NPE fit is trained for one fixed data dimension, so conditioning on independent trials means retraining for every or compressing to summary statistics; NLE learns the density of a single trial, so the log-likelihood of of them is a sum and is free at inference time. The cost is that the posterior is no longer a forward pass. For one fixed observation with high-dimensional data,npe()is still the better choice, and?nlesays so. -
log_lik(fit, theta, x)evaluates the surrogate likelihood, summing over the rows ofxas independent observations, andlikelihood_fn(fit, x_obs)returns it as a plain vectorizedfunction(theta). That closure is the point of contact with the rest of R: it goes straight intooptim(), an MCMC package, an importance sampler, or a profile likelihood, with nothing downstream needing to know aboutneuralsbi. -
posterior()on annle()fit returns an MCMC-backed posterior and gainssampler,n_chains,warmup,thinandinit_strategy. The default sampler is a vectorized univariate slice sampler, matching Pythonsbi’s default: nothing to tune, no dependency, and bounded priors need no special handling. Its vectorization runs across chains, so one step costs one batched forward pass rather thann_chainsseparate ones. The slice width adapts to the target during warmup, which matters because a width inherited from the prior is badly wrong once many observations have concentrated the posterior, and every unit of mismatch is paid for in wasted density evaluations. Draws carry split-Rhat and bulk ESS, and are cached on the posterior sosummary()and repeatsample()calls do not re-run a chain.n_chainsdefaults to 20 under"slice"and 4 under"stan", because a slice chain rides along in a batch someone else is already paying for and a Stan chain is a process with its own warmup. -
thindefaults to 2. With the adapted width,thin = 2already gives a bulk ESS of about 96% of the retained draws on a Gaussian target, so thinning harder buys very little for what it costs, and the reported ESS is there to tell you when to raise it. Pythonsbithins by 1 (it thinned by 10 up to v0.21), so this sits between the two. - The i.i.d. sum takes a shortcut where the estimator allows one. An MDN maps
thetato a Gaussian mixture overxand never seesx, so fornobservations the network runs once and allndensities come off the same mixture; the linear-Gaussian baseline behaves the same way. A flow’s transforms depend onxtoo, so it has to runntimes. With a few thousand observations that is the difference between seconds and minutes per MCMC step, and it is worth weighing when choosing an estimator for repeated data. - Sampling an NLE posterior is about five times faster than it was when the feature first worked, and samples exactly the same thing. Four changes get it there. The slice sampler now carries several of a chain’s future moves in one call: where an interval edge goes next is a fixed step, and which point a chain tries after a rejection depends only on that point’s side of the current value and a fresh uniform, so neither needs a density and both can be computed in advance. No call is ever made wider than the one that opened the coordinate, so this spends batch width that was already being paid for rather than adding any. Everything the observation alone decides – standardizing it, its Jacobian, turning it into a tensor – is now settled when the posterior is built instead of at every step. The summed log-likelihood is reduced where the densities are produced, so the
n_theta x n_obsmatrix, the largest object in the loop, is never built. And an MDN’s chunking is sized by the pair count rather than by pairs times components, which had been splitting a 5000-observation call ten ways for a 400 KB intermediate. On the four-parameter g-and-k model invignette("neural-likelihood"), 2000 draws from 20 chains went from 117s to 25s at 500 observations and from 342s to 68s at 5000, with split-Rhat and bulk ESS unchanged. - An MDN likelihood is replayed as TorchScript once a run is clearly a loop. Every torch operation crosses from R into libtorch, and at MCMC batch sizes that crossing costs more than the arithmetic behind it: roughly 0.2 ms each and thirty per evaluation, against a few hundred microseconds of real work.
torch::jit_trace()records the same code and replays it in one crossing. It is a shortcut, not a path. Nothing is recorded until an evaluator has been called a few times, so a singlelog_lik()never pays for a compiler; a trace fixes the shapes it saw, so there is one per parameter-row count and each is checked against the eager result before anything uses it; and tracing or checking failing just leaves the eager path in place.options(neuralsbi.jit = FALSE)turns it off. -
log_prob()on an NLE posterior returns the unnormalized log posterior. The evidence is not available, sonormalizeis ignored with a warning rather than returning a number that looks normalized and is not. -
New:
stan_code(),stan_data()andwrite_stan_model()export a fitted likelihood as Stan source. The generatedfunctionsblock recomputes in Stan’s own language with the trained weights passed as data, so Stan differentiates it and NUTS gets exact gradients, with nothing linked againsttorchat run time. The result is an ordinary Stan function oftheta, which is what makes it worth having: the surrogate stops being the whole model and becomes one term in a model you write, next to a hierarchical prior, covariates, or a second data source whose likelihood you do know."mdn","maf"and"linear_gaussian"are supported;"nsf"is refused with a message naming the alternatives.posterior(fit, x_obs, sampler = "stan")runs the generated model throughcmdstanrorrstanand returns draws like any other path. -
sbc(),tarp()andposterior_predictive()accept annle()fit, andsbc()/tarp()forward...toposterior()so the MCMC controls reach it. Every SBC trial is a separate MCMC run, so start small. -
save_npe()/load_npe()handlenle()fits too, withsave_nle()/load_nle()as aliases. - Internally,
npe()andnle()now shareprepare_simulations()instead of each carrying its own copy of the simulate/coerce/drop/standardize preamble. - Fixed:
sample_posterior()now goes through thesample()generic instead of callingsample.nsbi_posterior()directly. Annle()posterior inheritsnsbi_posterior, so the old call ran the NPE forward-pass sampler against an estimator whose target and condition are swapped. That errored withnon-conformable argumentswhendim_thetaanddim_xdiffer, and returned draws from the wrong distribution without complaint when they happen to match. - Fixed:
posterior()on annle()fit now validatesn_chains,warmupandthininstead of coercing them withas.integer().thin = 0ran the warmup and kept nothing, so the slice sampler returned its zero-initialized array and every draw was 0, with no error and an Rhat computed on those zeros. Values below the bound, fractional values andNAare now errors. - Fixed: the non-finite check now covers
thetaas well asx. A row was dropped only when its simulator output was non-finite, so anNAamong pre-computed parameters passed straight through to the estimator:npe(prior, theta = theta, x = x)with one missing parameter failed insidechol()with “the leading minor of order 1 is not positive”, and on the torch path it became “Training failed: no restart produced a finite validation loss”, which blames training for a bad input. Such rows are now dropped like any other failure, and the warning says which side was non-finite (“parameters”, “output”, or “parameters or output”). - Fixed:
npe_sequential()now checksx_obsagainst the simulator’s output width, at the end of round 1, which is the first moment that width is known. Anx_obsof the wrong length was reshaped into several rows and only the first was targeted, so the run truncated its proposals around a value the caller never gave whileprint()reported all of them as targeted. A TSNPE fit is not amortized, so targeting the wrong observation is the whole failure. A missing,NULL, non-numeric,NAor multi-rowx_obsis an error as well, andn_roundsmust be a single integer of at least 1:n_rounds = 0skipped the loop and returned a bare list classednsbi_snpe. - Fixed:
sbc()andtarp()now error when a trial’s posterior returns fewer draws thann_posterior_samplesinstead of scoring it anyway.sample()comes back short when a bounded prior and an estimator that leaks mass outside it defeat rejection sampling, and it only warns.sbc()then binned those ranks againstn_posterior_sampleswhile they were drawn from a smaller set, which compressed every rank toward zero: the chi-square uniformity test rejected andexpected_coverage()fell below the diagonal, so lost draws were reported as a miscalibrated posterior. Rescaling the short trial on its own is no better, since it is then scored on a different resolution from the rest, so both functions stop and name the trial and the shortfall. - Fixed:
npe()andnle()now matchdensity_estimatoragainst the allowed names before the simulator runs. The check lived infit_density_estimator(), which is reached only after the simulations are in hand, sodensity_estimator = "mfa"spent the whole budget and then failed on the typo. For the expensive simulators this package is for, that is minutes to hours. The same hoist fixes the embedding warning, which compared the unmatched value:match.arg()accepts abbreviations, sodensity_estimator = "linear"selectedlinear_gaussianand droppedembedding_netwithout saying so.fit_density_estimator()keeps its ownmatch.arg()because it is also reachable directly. - Fixed: a simulator whose formals match some parameter names but not all now warns. The choice between the two simulator signatures was all or nothing and the fallback was silent, so
prior_uniform(c(a = -3, b = -1), c(a = 3, b = 1))withfunction(mu, ls = 0)sent the whole two-parameter vector tomu, leftlsat its default, and trained on the result without complaint. One typo in a prior name was enough. The warning names the parameters that found no formal and points at?nsbi_simulator. It is a warning and not an error because a vector-signature simulator whose first argument happens to carry a parameter’s name is legal.
neuralsbi 0.4.2
-
pairplot()’s lower triangle now shows highest-density regions (via the newggdensitySuggests,geom_hdr(), at the 50/80/95/99% probability levels) instead of a raw scatter of draws. A scatter of 10,000+ points overplots into an undifferentiated blob at the resolution most posteriors are viewed at; nested HDR contours show where the mass actually concentrates. Thetruthcross-hair markers are unchanged.colnow sets the region fill colour;alphaapplies only to the diagonal marginal densities, since the lower triangle shades itself by probability level.
neuralsbi 0.4.1
-
Breaking: the simulator is now called once per parameter set and returns one simulated observation. Most models a researcher already has work that way – an ODE solve, an agent-based model, a call out to
pompordeSolve– so meeting the old contract meant wrapping the model in an apply loop and thinking about column-major indexing before anything ran. Two signatures are accepted, decided fromformals(simulator): when every parameter name appears among the formals the parameters arrive by name, one scalar each (function(mu, sigma) ...); otherwise the whole named parameter vector goes to the first argument (function(theta) ...). A simulator returns a numeric vector of lengthd, a scalar, or a one-row matrix or data frame, and names on that output become the outcome names. See?nsbi_simulatorfor the contract and the migration examples. - This also fixes a correctness bug introduced in 0.4.0. Chunking meant a simulator written for a single parameter set could return a vector whose length happened to match the chunk’s row count, and that vector was accepted as one column of outcomes for several draws.
simulate_for_sbi(function(theta) c(theta[1] * 2, theta[2] * 2), prior, 100)returned a 100 x 1 matrix of nonsense with no error and no warning; it now returns the 100 x 2 matrix it should. -
npe(),simulate_for_sbi(),npe_sequential(),sbc(),tarp()andposterior_predictive()gainsim_args, a named list forwarded to every simulator call. Observed data, a time grid, a fixed population size, a design matrix or a solver tolerance travel from the call site instead of being captured in a closure – which also keeps them out of what gets serialized to afutureworker. A list rather than...because every one ofnpe()’s formals sits before..., so R’s partial matching would silently capturex,theta,norseed. - Simulations whose output contains
NA,NaNor an infinite value are dropped, together with their parameters, with one warning per run reporting the count and the rate. A single non-finite value would otherwise poison the training loss and surface much later as aNaNvalidation loss. The count is recorded on the fit and shown byprint(). Nothing left is an error. Insbc()andtarp()a failed draw removes the whole trial; inposterior_predictive()it reduces the number of predictive draws. Pre-computedtheta/xpassed tonpe()are checked the same way. Note that dropping conditions on the simulator having succeeded: when failure depends on the parameters, the fit targets the posterior given success, so a high drop rate is a modelling signal. - Random-number streams are now per simulation rather than per chunk, so a given seed produces the same simulations whatever the
futureplan and whatever the worker count. The 0.4.0 guarantee held only at a fixed chunk size. -
chunk_sizeis gone, fromnpe(),simulate_for_sbi(),npe_sequential(),sbc(),tarp()andposterior_predictive(), along withoptions(neuralsbi.chunks). Chunking existed in 0.4.0 to make results reproducible across backends: the split had to depend onnalone, which meant users had to know about it and could change their draws by changing it. Per-simulation RNG streams give that guarantee outright, so what is left is ordinary parallel scheduling. Batches are now sized from the worker count, cannot affect a result, and are not a setting. Running sequentially there are no batches at all, just a loop. - NSF’s
n_binsandtail_boundare explicitnpe()arguments;npe()no longer takes.... - New
save_npe()/load_npe(). A fit whose estimator is"maf","mdn"or"nsf"holds a torch module, which is an external pointer:saveRDS()writes the pointer, the file reloads without complaint, and the first call that touches the network fails withexternal pointer is not valid.save_npe()writes the weights withtorch::torch_save()and everything else as ordinary R objects, into one.rds;load_npe()rebuilds the network from the recorded architecture and restores them. An overnight fit can now be reloaded the next morning, which is what amortization was for.posterior()andprint()also detect a fit that came back fromreadRDS()and say so, instead of failing later with a torch error.
neuralsbi 0.4.0
The simulator can now run in parallel. Declare a
futureplan –library(future); plan(multisession)– and every function that calls a simulator (npe(),simulate_for_sbi(),npe_sequential(),sbc(),tarp(),posterior_predictive()) spreads the work across workers. There is no new argument to pass and no parallel variant to call: with no plan declared everything runs sequentially as before, andneuralsbimentions the two lines above once per session (options(neuralsbi.parallel_hint = FALSE)to silence it). Each chunk of parameters draws from its own L’Ecuyer-CMRG stream, so a givenset.seed()produces the same simulations sequentially and on any number of workers. See?nsbi_parallel.Long-running work now reports progress with an ETA – simulation and neural training alike, one progress step per simulation and per training epoch. With
progressrinstalled,neuralsbiemits standard progressr updates, soprogressr::handlers()andwith_progress()control reporting; without it, a built-in bar needing no extra packages does the job. The training bar targets the epoch at which early stopping would fire and revises that target as the validation loss improves. See?nsbi_progress.npe(),simulate_for_sbi(),npe_sequential(),sbc(),tarp(), andposterior_predictive()gain achunk_sizeargument controlling how many parameter rows go to the simulator per call. The default splits a run into about 64 chunks; because the split depends only on the number of simulations, results do not change with the number of workers. A simulator whose output must be produced in one call can setchunk_sizeto the full simulation budget.futureandprogressrareSuggests, not dependencies;parallel(base R) moves intoImports. # neuralsbi 0.3.7The test suite now skips its plotting tests when
ggplot2/GGallyare not installed, instead of failing. Both areSuggests, soR CMD checkunder_R_CHECK_FORCE_SUGGESTS_=false– the configuration CRAN uses on a machine without them – previously hit 5 errors fromrequire_ggplot2(). The newskip_if_no_ggplot2()/skip_if_no_ggally()helpers mirror theskip_if_no_torch()contract already used for the neural tests, so the suite runs everywhere.vignette("sir-time-varying-beta")reworks the epidemic model so that its posterior predictive tracks the observed case peaks instead of overshooting them. The introduction day and seed size are now inferred per state rather than fixed at two infections on 2020-01-21 (which left the epidemic’s phase to demographic noise: replicate simulations at one fixed parameter differed by a factor of several thousand at the peak bin), the ascertainment prior is pinned by spring-2020 seroprevalence (case counts alone identify only the product of ascertainment and incidence, and the unconstrained fit slides toward vast, barely-ascertained epidemics), and the regime-duration parameterization drops a coordinate that the simulator’s rescaling had left unidentified. The article reports the effective reproduction number Re(t) = beta(t) S(t) / (N gamma) computed from the simulator’s own susceptible trajectories, adds a prior-predictive check before training, and summarizes the posterior predictive by its median rather than its mean.
neuralsbi 0.3.6
- Parameters and outcomes can now be named. Name
prior_uniform()’slow/highorprior_normal()’smean(e.g.c(beta = 0, gamma = 0)), or attachcolnames()to a simulator’s output, and those names carry throughnpe(),sample(),map_estimate(),sbc(),expected_coverage(), andposterior_predictive()without any extra arguments.plot_sbc(),plot_coverage(),pairplot(), andplot_posterior_predictive()use the names for titles, legends, and facet labels; a name that happens to be valid R syntax ("beta[1]","rho","sigma^2") renders as its plotmath symbol (Greek letters, sub/superscripts) instead of literal text.
neuralsbi 0.3.5
- The SIR case study becomes a head-to-head comparison with the
pomppackage,vignette("sir-epidemic"). Both methods fit the same stochastic SIR epidemic:pompvia particle-filter MCMC (pmcmc),neuralsbivia neural posterior estimation. The vignette contrasts what each needs from the model —pompa measurement density,neuralsbionly a simulator — overlays the two posteriors, scores their agreement with a C2ST, and confirms the neural fit with SBC. The comparison is precomputed, sopompis needed only to regenerate the article, not to build or check the package.
neuralsbi 0.3.4
- Plotting is now built on
ggplot2andGGally::ggpairs()instead of base graphics.pairplot(),plot_sbc(),plot_coverage(),plot_tarp(), andplot_posterior_predictive()keep their signatures (pairplot()gains analphaargument) but now build and print aggplot/ggmatrixobject, returned invisibly for further customization.ggplot2andGGallymove toSuggests, following the same graceful-degradation pattern astorch: call any plotting function without them installed and you get an informative error, not a crash. - New vignette,
vignette("intro-to-sbi"): a short beginner tutorial covering the three ingredients (prior, simulator, observation), amortized training, and a first calibration check, using a g-and-k distribution simulator whose likelihood has no closed form.
neuralsbi 0.3.2
CRAN release: 2026-08-03
- README is now generated from
README.Rmd, so the usage example runs at render time and its output cannot drift from the code. The example is a plain linear regression: the posterior recovers the ground-truth coefficients, which the reader can check against ordinary least squares.
neuralsbi 0.3.1
- CRAN resubmission fixes. Routed the
summary()andas.data.frame()methods into the singlesummarieshelp topic (they had drifted into separate.Rdfiles with duplicated\aliasentries, which also produced duplicate HTML anchors). Wrappedtheta_{<d}in\eqn{}in themade_masksdocs so the Rd no longer drops braces. Dropped the bare “NPE” acronym from theDESCRIPTIONto avoid the spurious misspelling note.
neuralsbi 0.3.0
Defaults now match Python
sbi, so a workflow reads the same in both packages and results can be cross-checked. Changes tonpe()defaults: the density estimator is now"maf"(was"mdn"); MDN mixture components default to 10 (was 5); NSF spline bins default to 10 (was 8); the training batch size is 200 (was 100).max_epochsis raised to 2000 as a guard cap that early stopping (patience = 20) normally reaches first, mirroringsbi’s effectively-unbounded epoch budget.lr,validation_fraction,patience,clip_grad_norm,n_transforms, andhiddenalready matched. Pass any of these explicitly to recover the previous behavior.First CRAN submission. Dropped the development
.9000version suffix, removed the redundantAuthor/Maintainerfields (now derived fromAuthors@R), and tidied the package title.Embedding networks (roadmap v0.4).
embedding_mlp()builds a learned summary network that maps raw observations to a low-dimensional feature vector; pass it tonpe(..., embedding_net = )and the MDN, MAF, and NSF estimators condition on the features instead of the raw data, training the embedding jointly. The estimators still take rawxat thede_*boundary (dim_xis unchanged), so sampling andlog_probroute through the embedding automatically. Ignored, with a warning, bylinear_gaussian.
neuralsbi 0.2.4.9000 (development)
- Vignettes now show real output. They are precomputed: each vignette’s evaluated source lives in
vignettes/<name>.Rmd.orig, andvignettes/precompute.Rbakes it into a staticvignettes/<name>.Rmd(results, printed values, and figures inlined). CI and pkgdown re-render that static Markdown with no torch at build time, so the expensive neural training runs once, locally, instead of on every build. Re-runRscript vignettes/precompute.Rafter editing any.Rmd.orig. - Two-moons calibration study (
inst/benchmarks/two_moons_calibration.R): SBC, expected coverage, and TARP for a two-moons NSF fit, with figures written todocs/figures/(roadmap milestone M2).
neuralsbi 0.2.3.9000
- Package website built with pkgdown, deployed from CI to https://pedroliman.github.io/neuralsbi/.
- Four vignettes that build on each other: getting started, choosing a density estimator, checking the posterior, and the SIR case study (which now also demonstrates
npe_sequential()). Removed a truncated duplicate of the SIR vignette. - README rewritten to the standard terse form; authorship recorded in
DESCRIPTION(Pedro Nascimento de Lima, with ORCID).
neuralsbi 0.2.2.9000
- New
npe_sequential(): multi-round NPE targeting a single observation via truncated-prior proposals (TSNPE, Deistler et al. 2022). Each round truncates the prior to the highest-probability region of the current posterior and retrains on all accumulated simulations; the standard NPE loss stays valid, so no importance correction is needed. Returns annsbi_snpefit that works withposterior(),sample(), and the diagnostics, but is only valid at the targetedx_obs. Verified against the analytic linear-Gaussian posterior.
neuralsbi 0.2.1.9000
- New
tarp()diagnostic andplot_tarp()(Lemos et al. 2023): a joint expected-coverage test using random reference points, complementing the per-parametersbc()ranks. Detects posteriors with calibrated marginals but wrong correlation structure. - New
plot_posterior_predictive(): marginal predictive histograms with the observation marked; returns the observation’s predictive quantiles. - Leakage correction is now under test: with a bounded prior, the renormalized
log_prob()integrates to one over the support and returns-Infoutside it (test-posterior-normalization.R). - Fixed CI.
R CMD checkfailed on three counts: thenpe()example required libtorch (it now uses the torch-freelinear_gaussianestimator and runs unconditionally), the hand-maintainednpe.Rd/fit_mdn.Rdusage sections had drifted behind the code (missingn_restarts,clip_grad_norm,n_transforms, and the"maf"/"nsf"options), andCLAUDE.mdwas not in.Rbuildignore. Thetest-torchjob also failed because torch 0.17 refuses aTORCH_HOMEthat does not exist; the workflow now creates it first.
neuralsbi 0.2.0.9000
- Shared training engine for all neural estimators (
train_conditional_de()): best-of-n restarts, learning-rate decay on plateau, gradient clipping, per-epoch loss history. - Masked Autoregressive Flow (
density_estimator = "maf") and Neural Spline Flow ("nsf", autoregressive rational-quadratic splines) join the MDN and the closed-formlinear_gaussianbaseline. - Benchmark tasks (
task_gaussian_linear(),task_two_moons(),task_slcp(),task_sir()) shared between tests and theinst/benchmarks/head-to-head benchmark harness. -
summary()methods,as.data.frame()tidy accessor,plot_coverage(). - SIR applied case-study vignette.
- CI:
R CMD checkplus atest-torchjob with cached libtorch.
neuralsbi 0.1.0
- First pilot release: priors, single-round amortized
npe(),linear_gaussianand MDN estimators, posterior sampling with leakage correction, SBC, expected coverage, C2ST, posterior-predictive checks,pairplot(),plot_sbc().
