This release adds three new distributions (Tweedie, ExponentiallyModifiedGaussian, WrappedCauchy), three new goodness-of-fit tests (cramerVonMises, kolmogorovSmirnov, andersonDarling), a major expansion of the ran.process.Process API (fit(), lnL(), marginal(), params(), plus a new stochastic-process precision gate), and ran.dist.guess() for automatic candidate-distribution selection by BIC weight. ran.dist.VonMises gains a location parameter (mu) — the old single-argument constructor is deprecated and will be removed in v1.33.0. The remainder is a large batch of precision, overflow/underflow, and convergence fixes across Bessel/Marcum-Q/incomplete-gamma special functions and several noncentral distributions (NoncentralT, DoublyNoncentralT, DoublyNoncentralBeta, Skellam, and others), plus hot-path performance caching across ~35 distributions.
Added
-
ran.dist.Distribution.prototype.copy(): returns a fully independent copy of a distribution instance, including its current PRNG state — a thin named wrapper around the existingthis.constructor.load(this.save())round-trip, added so cloning aDistributioninstance doesn't require knowing that trick. Used internally byparams()'s new Distribution-instance-valued-field cloning (see### Fixed), and useful standalone — e.g. running two MCMC chains seeded from the same fitted distribution without them sharing PRNG state. See ADR-0051. -
scripts/precision-refs-process.pyand the generatedtest/precision-process.js: a stochastic-process precision gate, givingsrc/process/the same arbitrary-precision verification standardsrc/dist/already has fromscripts/precision-refs-continuous.py/-discrete.py. Process densities were previously checked only against scipy doubles at a uniform1e-10over a handful of hand-picked points; the new gate covers all nine processes that expose a closed-form time-tmarginal —AR1,BrownianBridge,BrownianMotion,CompoundPoisson,CoxIngersollRoss,GeometricBrownianMotion,OrnsteinUhlenbeck,Poisson, andRandomWalk— over a systematic 3-parameter-sets × 3-times × 5-interior-points grid, with the probex-values obtained by inverting the high-precision marginal CDF atp ∈ {0.1, 0.3, 0.53, 0.72, 0.9}(integer lattice points for the discretePoissonandRandomWalk). Each reference gates three independent code paths —pdf(x, t),marginal(t).pdf(x), andmarginal(t).cdf(x), the last of which previously had no external reference at any tolerance;marginal()derives its law's parameters separately frompdf(), so checking the two only against each other (astest/process.jsdoes at1e-10) would let a shared parameterization slip cancel out. Every marginal law in the generator is re-derived from the process's own SDE or update rule rather than read off the JavaScript:CompoundPoisson's reference in particular is summed directly as a Poisson-weighted mixture of Gammas, never through the compound-Poisson →Tweedieparameter mapping thatmarginal()applies, so it gates that mapping as well asTweedie's own Dunn & Smyth series. The generator self-checks 25 of those re-derivations against the values already vetted intest/process.jsand verifies that all nine compound Poisson-gamma mixtures normalize to 1 and reproduce Wald's mean, aborting before emitting a single literal on any mismatch. Seven of the nine processes hold at1e-14with no exception;RandomWalkatp = 0.3(3e-14pdf /2e-14cdf, log-gamma ULP amplification att = 30) andCompoundPoisson(6e-14pdf,Tweedieseries — its cdf stays gated at1e-14, since the two floors genuinely diverge) carry documented, mechanism-named bounds pinned just above their measured worst case. No process behavior changed: this is a pure regression guard, not a bug fix (#1223). -
scripts/check-subpath-runtime.js(npm run check-subpath-runtime): dynamically imports one representative built ESM subpath module from each of the three minified categories (dist/beta.esm.jsanddist/poisson.esm.jsfor distributions,dist/process/brownian-motion.esm.jsfor processes,dist/mc/rwm.esm.jsfor MCMC samplers) and asserts instantiation succeeds,constructor.namesurvives minification, and a known computed value matches — apdf/cdfvalue against an mpmath/scipy-sourced reference for Beta/Poisson/BrownianMotion, and a seeded, pinnedsample()array (in addition to its shape) for RWM. This is a direct regression guard for thekeep_classnames: truefix in #1220, wired into CI'sbuildjob (.github/workflows/ci.yml), sincenpm testonly ever exercisessrc/and never imports fromdist/(#1227). -
ran.process.Process.fit(path, dt)(static, per subclass): estimates process parameters from an observed discrete-time path, added as a throw-by-default hook onProcess(mirroringmarginal(t)'s rollout, #1132) and implemented forBrownianMotion,GeometricBrownianMotion, andOrnsteinUhlenbeckvia their exact closed-form MLE — increments (or log-returns, or the AR(1) transition already coded intoOrnsteinUhlenbeck._next()) are i.i.d./exactly linear-Gaussian, so sample mean/variance (or OLS regression ofX_{n+1}onX_n) recovers the parameters to machine precision as the path grows.CoxIngersollRoss.fit()instead uses two-stage Conditional Least Squares (Overbeck & Rydén, 1997), since CIR's true one-step conditional transition is a scaled noncentral chi-squared with generally non-integer degrees of freedom — a different object from theGammamarginal already implemented as itspdf(x,t)/marginal(t)(valid only because the class hardcodesx0 = 0) — andran.dist.NoncentralChi2rounds itskto the nearest integer, so it cannot represent CIR's non-integer degrees of freedom for a true conditional MLE. CLS is consistent but not maximally efficient, and its accuracy degrades near the Feller boundary and at largedt. See ADR-0044 (#1133). Extended toAR1.fit(path)(OLS regression ofX_{n+1}onX_n, reusing the sharedols()helper — the true transition has no intercept, but fitting through the intercept-plus-slope form still recoversphiconsistently since the true intercept is exactly 0);RandomWalk.fit(path)(the exact MLE: the fraction of +1 steps among all observed increments, algebraically identical to recoveringpfrom the sample mean of increments since every step is exactly ±1); andBrownianBridge.fit(path, T, dt)(the exact MLE forsigma, since each step's conditional variance is fully determined by the known, fixedT/dt— unlike the other four processes,Tis a required argument here rather than something to estimate, since the bridge's defining feature is a fixed, given endpoint).AR1andRandomWalkhave nodtparameter in their own model, so theirfit()drops it entirely rather than taking an unused argument (#1212). Extended to the counting-process family:Poisson.fit(path, dt)recovers the exact MLElambda = totalCount / (n*dt)from the path's net increase, since increments are i.i.d.Poisson(lambda*dt).CompoundPoisson.fit(path, dt, jumpDistConstructor)estimateslambdathe same way, treating every non-zero increment as exactly one jump — individual arrival counts within a singledtinterval are not observable from the cumulative path alone, so this is an approximation valid whenlambda*dtis small enough that multi-jump intervals are rare — and fits the jump-size distribution's own parameters by handing the recovered non-zero increments to the caller-suppliedjumpDistConstructor's staticfit()(#1213). -
ran.process.Process.prototype.lnL(path): transition log-likelihood of an observed discrete-time path, added as a throw-by-default hook onProcess(mirroringmarginal(t)'s andfit(path, dt)'s partial rollout) and implemented forBrownianMotion,OrnsteinUhlenbeck, andGeometricBrownianMotionvia a new protected_transitionLnPdf(xPrev, xNext)hook that each overrides with its closed-form one-step Gaussian (BM, OU) or log-Gaussian-with-Jacobian (GBM) transition density — the same law already encoded in each class's_next()and reused byfit()'s sufficient statistics, so no new numerical machinery was needed. Computes transition, not marginal, likelihood: a realized path's points are a dependent, Markov-correlated sequence, not independent draws from the marginal distribution, and conflating the two is a known trap in this codebase (CoxIngersollRoss's marginal/conditional Gamma mismatch, #1133) — see ADR-0046.GeometricBrownianMotion.lnL()returns-Infinity(not a thrown error) for a path that visits a non-positive state, mirroringpdf(x,t)'s existingx <= 0 => 0convention (#1153). -
ran.dist.Tweedie(mu, phi, p): the Tweedie exponential dispersion model for the compound Poisson-Gamma power range1 < p < 2— a point mass at zero (P(Y=0) = exp(-lambda)) plus a continuous positive tail, used for insurance claims, rainfall accumulation, and zero-inflated continuous GLM responses. Neither the PDF nor the CDF has a closed form:_pdfevaluates the Dunn & Smyth (2005) infinite series for the compound Poisson-Gamma density in log-space (all terms are positive for1 < p < 2, so no cancellation), locating the peak term via a closed-form Stirling estimate before summing;_cdfsums a Poisson-weighted series ofgammaLowerIncompleteevaluations with a purely relative convergence check (no absolute floor, avoiding the false-early-convergence failure mode documented forDoublyNoncentralBeta, #1108). Both series are capped a number of terms past their peak that scales withsqrt(peak)rather than by a constant, since the peak's own width grows the same way — a constant slack silently truncates both sums mid-peak once the peak clearsMAX_SERIES_ITER(atTweedie(50, 0.02, 1.5),lambda = 707, it leftpdf0.5% low,cdfplateauing at 0.970 instead of reaching 1, andq(p)returningNaNabove that plateau)._generator()samples via the exact compound Poisson-Gamma simulation (N ~ Poisson(lambda), then theNevents' total drawn as a singleGamma(N * shape, rate), which is an identity rather than an approximation and keeps a sample atO(1)instead ofO(lambda));_q(p)returns0for anyp <= P(Y=0)(the base class's root-finder cannot find a sign change in that region, sincecdf(x) - p >= 0everywhere) and root-finds otherwise;mean()/variance()/skewness()/kurtosis()are closed-form via EDM cumulant theory;_fitInit()seedspat the literature-typical1.5(no closed-form estimator exists) with method-of-moments formu/phi(#1136). -
ran.dist.ExponentiallyModifiedGaussian(mu, sigma, lambda): the exponentially modified Gaussian (EMG) distribution, the convolution of aNormal(mu, sigma^2)and anExponential(lambda)random variable — used for right-skewed data with exponential tails (chromatography peak modeling, reaction-time analysis, neuroscience). PDF/CDF use the closed-form erfc-based formula (Wikipedia: Exponentially modified Gaussian distribution), rewritten via the scaled complementary error functionerfcxto avoid theexp(large)·erfc(large→0)cancellation the naive formula hits for largelambda·sigma— the same technique already used forInverseGaussian's CDF._generator()samples as the sum of independentNormalandExponentialdraws;mean()/variance()/skewness()/kurtosis()are closed-form;_fitInit()uses method-of-moments (#1131). -
ran.process.Process.prototype.marginal(t): returns the process's marginal distribution at timetas a fully-functionalran.dist.Distributioninstance, unlockingquantile(),hazard(),survival(),likelihood(),aic(),bic(), andtest()on process marginals without any new numerical machinery. Implemented by composing each process's already-existingmean()/variance()/pdf()formulas:BrownianMotion,OrnsteinUhlenbeck, andBrownianBridgereturnNormal;GeometricBrownianMotionreturnsLogNormal;CoxIngersollRossreturnsGamma, reusing the shape/scale already derived for its ownpdf()— valid since the process always starts atx0 = 0, which collapses the general noncentral-chi-squared transition density to a plain Gamma. Throws fortoutside the domain where the marginal is genuinely a continuous distribution (t <= 0for all five; additionallyt >= TforBrownianBridge, where the process is pinned to a point mass) (#1132). Extended toPoissonandAR1, which returnran.dist.Poisson/Normalinstances the same way and likewise throw fort <= 0(the target class's own parameter validation can't express the degenerate zero-mean/zero-variance case att = 0); and toRandomWalk, which returns an instance of a new privateShiftedBinomialdistribution (src/dist/_shifted-binomial.js, not part of the publicran.distAPI — see ADR-0045) representing the pushforward ofBinomial(t, p)underx = 2k - t. UnlikePoisson/AR1,RandomWalk.marginal(0)does not throw, since a point mass at0is directly representable asShiftedBinomial(0, p)(#1156).CompoundPoisson(and its deprecated aliasCompoundPoissonProcess) now overridesmarginal(t): for aran.dist.GammajumpDist,X_tis by definition the compound Poisson-gamma total thatran.dist.Tweediealready represents, somarginal(t)returns aTweedieinstance via a closed-form parameter mapping derived from matching each representation's Poisson rate and gamma shape/rate — no new special function or Distribution subclass was needed, sinceTweediealready shipped in #1136. Every otherjumpDistthrows a specific, documented error instead of inheriting the generic base-class message: an arbitrary caller-supplied distribution makesX_ta Poisson mixture over sums of an unknown distribution, with no general closed form reducible to a single existingran.distclass (#1157). -
"engines": { "node": ">=20" }added topackage.json, documenting the Node.js version constraint that CI's test matrix andnyc@18(engines.node: "20 || >=22", see #960) already impose in practice, so npm/Yarn warn contributors and downstream consumers installing on Node 18 or earlier instead of failing later with a confusingnycinternal error (#1137). -
.github/dependabot.yml: weekly automatednpmdevDependency updates, restricted to patch-level bumps (minor/major are ignored, since devDependency major bumps like ESLint 7→9 or a Rollup major often carry breaking config/plugin-API changes that warrant manual review), grouped intobabel,lint,test,build, anddocsbuckets to keep PR volume low while staying atomic and reviewable; each PR runs through the existing CI gates (lint, jsdoclint, test+coverage, typecheck, docs-build, build) before merge (#1142). -
Versioned API docs: the published site now mirrors the latest tagged release at
/, keeps every past release permanently at/vX.Y.Z/, and publishes tip-of-mainat/unreleased/with an "unreleased" banner — instead of redeploying the entire site from whatever was onmainon every push (which had let unreleased distributions such asTweedieleak into the live docs ahead of their release). A version dropdown and an "outdated release" banner are populated client-side from aversions.jsonmanifest. Seedecisions/0043-versioned-docs-deployment.md. -
ran.test.cramerVonMises(values, cdf, alpha): the Cramér-von Mises single-sample goodness-of-fit test, testing the null hypothesis thatvaluesis drawn from the distributioncdfrepresents. The statisticT = n·ω² = 1/(12n) + Σᵢ[(2i-1)/(2n) − F(xᵢ)]²is computed over sorted, CDF-transformed order statistics (the same EDF-comparison family as the privateandersonDarlinghelper insrc/dist/_tests.js, but with squared-deviation rather than log-weighted terms); the asymptotic p-value sums the Csörgő & Faraway (1996, JRSS-B 58(1), eq. 1.2) convergent series for then → ∞limiting distribution's CDF, built entirely frombesselKnu/logGammaalready insrc/special/— no new special function or algorithm was needed. Returns{stat, passed, pValue}, the shape adopted by ADR-0042 for single-sample GoF tests newly exported fromran.test(extending, rather than replacing, the plain{stat, passed}shape the module's existing multi-sample comparison tests use) (#1134). -
ran.test.kolmogorovSmirnov(x, y, alpha): the two-sample Kolmogorov-Smirnov test, testing the null hypothesis thatxandyare drawn from the same distribution. The statisticD = sup|F1(x) - F2(x)|is computed over the pooled empirical CDFs of the two samples by binary-searching each sample's sorted values at every pooled point; the asymptotic p-value is obtained from the existingran.dist.Kolmogorovdistribution'ssurvival(), evaluated atsqrt(n1*n2/(n1+n2))·D. Returns{stat, passed, pValue}per ADR-0042 (#1138). -
ran.test.andersonDarling(values, cdf, alpha): the Anderson-Darling single-sample goodness-of-fit test, testing the null hypothesis thatvaluesis drawn from the distributioncdfrepresents. The statisticA² = -n - (1/n)·Σᵢ(2i-1)[ln F(xᵢ) + ln(1-F(x_{n+1-i}))]is computed over sorted, CDF-transformed order statistics; the asymptotic p-value uses the Marsaglia & Marsaglia (2004, JSS 9(2)) rational-function approximation to the limiting distribution, with their finite-sample correction. This is a thin public wrapper around the privateandersonDarlinghelper already implemented and tested insrc/dist/_tests.js(which continues to backDistribution.prototype.test()unchanged, with its own hardcoded α=0.01) — no new math was needed. Returns{stat, pValue, passed}per ADR-0042, which explicitly named this function as the next to adopt that shape (#1144). -
ran.process.Process.prototype.params(): returns the process's parameters (this.p), mirroringran.dist.Distribution.prototype.params()so that.fit()results and other downstream consumers can inspect a process's parameters through a stable public accessor instead of reaching into the internalthis.pstorage convention (#1251). -
ran.dist.guess(data, options): fits a set of candidate distributions to a dataset and ranks them by BIC weight — Δᵢ = BICᵢ − BIC_min, wᵢ = exp(−0.5·Δᵢ) / Σⱼ exp(−0.5·Δⱼ) — the estimated probability that each candidate is the best-fitting model in the set, given the data. "Guess" is intentional: this is a heuristic exploratory tool, not a verdict. Candidates are pre-filtered before the expensivefit()call: hard filters exclude type (continuous/discrete) and support mismatches, and soft, statistically-principled filters exclude symmetric-only or positive-skew-only families against sample skewness, Exponential-like families against an out-of-range coefficient of variation, and Poisson-like/NegativeBinomial-like families against an incompatible dispersion index. Throws ifdata.lengthis below20 * max_k(BIC's asymptotic approximation needs roughly 20 observations per parameter, evaluated against the largest parameter count among surviving candidates), and skips (rather than propagates) any candidate whosefit()throws. Returns a sorted array of{name, params, bicWeight, pValue}, carrying awarningstring property when every surviving candidate fails goodness-of-fit at α=0.05. The default candidate pool covers all distributions, includingVonMises,Rice,NoncentralChi2,NoncentralChi, andSkellam— an initial exclusion for their per-point Bessel-function evaluation cost was lifted after benchmarking showed theirfit()cost is comparable to already-included distributions of the same parameter count (#813, #1051). The soft filters' false-exclusion rates are empirically measured by Monte Carlo simulation (scripts/guess-filter-validation.js): the skewness filter's ~5% analytical target was initially confirmed forNormal(4.2%-4.7% measured) but found badly miscalibrated forLaplace(34.7%-51.1% measured, 7-10× the target) under a single normal-only threshold (2·√(6/n)) shared across everySYMMETRICfamily; the threshold is now computed per family as2·√(c/n), wherecis each family's own asymptotic skewness-estimator variance (Normal → 6,Uniform → 72/35,Laplace → 63, derived fromVar(g1)·n ≈ μ6/μ2³ − 6·μ4/μ2² + 9), bringing measured false exclusion to 4.2%-4.7% forNormal, 4.4%-5.6% forUniform, and 1.4%-4.2% forLaplace(#1054, #1064); the coefficient-of-variation and dispersion-index filters measured ~0% false exclusion for their representative distributions, well within safe bounds. -
ran.shape.max/ran.shape.minare now exported fromsrc/shape/index.js. Both files existed with public-style JSDoc (@memberof ran.shape) but were only reachable via direct relative imports (e.g. fromsrc/dispersion/range.js), not through the publicran.shapenamespace — missing wiring, not a missing implementation (#1233). -
ran.dist.WrappedCauchy(mu, rho): the wrapped Cauchy circular distribution, the standard heavy-tailed alternative toVonMises, parameterized by mean directionmuand concentrationrhoin(0, 1). UnlikeVonMises, whose CDF requires an infinite Bessel-function series, wrapped Cauchy's PDF, CDF, and quantile are all elementary closed forms built fromsin/cos/tan/atan2— no new special functions were needed. Support is themu-centred window[mu-pi, mu+pi](matching scipy'svonmises(loc=mu)convention) rather than a fixed[-pi, pi], since a circular distribution has no canonical cut point independent of its own location parameter;_cdfusesatan2(rather than a plainatanratio) to avoid thetan((x-mu)/2)singularity at the support boundary.mean()/variance()/skewness()/kurtosis()are left to the base class's numerical quadrature fallback (matchingVonMises's precedent) since these are the arithmetic, not circular, moments and are always finite on the bounded support._fitInit()uses the trigonometric moment estimator (mean resultant length/angle), since no closed-form MLE exists in general (Kent & Tyler, 1988) (#1135).
Changed
-
ran.dist.VonMisesgains a location parametermu(the mean direction), matching the parameterization on Wikipedia:pdf(x) = exp(kappa*cos(x-mu)) / (2*pi*I0(kappa)), with support[mu-pi, mu+pi]instead of the previously fixed[-pi, pi]. The constructor signature changes tonew VonMises(mu, kappa), matching every other location-shape distribution in the library (e.g.Cauchy(x0, gamma));.k(the parameter count.aic()/.bic()penalize against) is now 2, was 1._fitInit()recovers both parameters from the sample's circular resultant vector:muas its angle, re-anchored to the 2*pi "sheet" nearest the sample's own extremes and clamped into[xmax-pi, xmin+pi]so the fixed-width support is guaranteed to contain every sample (mirroring howUniform/Triangularderive their own support-defining parameters directly from the data extremes — needed soran.dist.guess()'s pre-fit probe never excludesVonMisesover an estimation-noise-driven support miss);kappais unchanged, still from the resultant length. -
ran.dist._tests.chi2(values, pmf, c)andran.dist._tests.andersonDarling(values, cdf)(and thereforeDistribution.test()for both discrete and continuous distributions) now return apValuefield alongside the existingstatistics/passedfields.chi2PValue()andandersonDarlingPValue()— sibling helpers that briefly exposed this without changing the parent functions' return shape — are removed now that both parents carry the field directly;ran.dist.guess()'s per-candidate p-value now readschi2(...).pValue/andersonDarling(...).pValueinstead (#1052, #1053). -
Hot-path
_pdf/_cdf/_generator/_qmethods on 14 distributions now read parameter-only constants (log-gamma normalizers, log-binomial/log-beta terms, Bessel/Poisson-mixing terms) fromthis.cinstead of recomputing them on every call:Gamma(and its subclassesChi2,Erlang, which now share the parent's cached log-normalizer instead of each callinglogGammaagain),InverseChi2,Poisson,NegativeBinomial,NoncentralChi2,NoncentralBeta(also speeding upNoncentralF, which delegates to it),DoublyNoncentralBeta,BetaBinomial,NegativeHypergeometric,Hypergeometric,Muth, andVonMises(which also caches the ratio-of-uniforms sampling constant used by_generator()).BrownianMotion,OrnsteinUhlenbeck, andGeometricBrownianMotion's_transitionLnPdfhot path (called once per step fromProcess.prototype.lnL(path), potentially many times in an MLE-calibration/MCMC loop) likewise now reads its precomputed log-scale constant (this.c.logSigmaDt/this.c.logNoise) instead of callingMath.log()on every transition. No behavior or return-value change. -
mean()/variance()/skewness()/kurtosis()on 24 distributions now share cached parameter-only raw/central moment terms (gamma/beta/Hurwitz-zeta/Riemann-zeta evaluations, series sums) instead of each method recomputing them independently:Frechet,GeneralizedExtremeValue,InvertedWeibull,Weibull(andDoubleWeibull, which now reusesWeibull's cached terms instead of callinggamma()again),Burr,Kumaraswamy,GeneralizedLogistic,FisherZ,Zeta,BenktanderII,HeadsMinusTails,Hyperexponential,ShiftedLogLogistic,Soliton(caches the harmonic number instead of re-summing an O(N) loop per method),UniformProduct,JohnsonSU,LogLogistic,LogSeries,LogGamma,LogLaplace, andExponentiatedWeibull.GeneralizedNormalandHalfGeneralizedNormalnow readGeneralizedGamma's already-cached log-gamma terms instead of bypassing the cache with their ownlogGamma()calls. No behavior or return-value change. -
The per-distribution, per-process, and per-MCMC-sampler subpath builds (
dist/<name>.esm.js,dist/process/<name>.esm.js,dist/mc/<name>.esm.js) are now minified with@rollup/plugin-terser(module: true, preserving ESM-safe mangling for downstream tree-shaking), the same waydist/ranjs.min.jsalready was — these were previously emitted with full variable names, JSDoc, and whitespace intact.keep_classnames: trueis set (at a negligible size cost) sinceDistribution.load()/Distribution.fit()(src/dist/_distribution.js) andHMC/NUTS's resumed-state validation (src/mc/_mcmc.js) interpolatethis.name/this.constructor.nameinto thrown error messages — without it, minification would silently replace e.g.Beta.fit() requires a _fitInit()...with a mangled single-letter class name in every subpath-imported distribution's error output. A representative subpath build (dist/beta.esm.js) shrinks from 93106 to ~22973 bytes raw (-75%) and from 30113 to ~10144 bytes gzipped (-66%), matching the single-distribution-import path README.md recommends as the low-footprint usage pattern (#1220). -
ran.process.AR1.marginal(t)no longer performs its ownvariance(t) <= 0pre-check, matching the pattern every other process'smarginal()already used (BrownianMotion,BrownianBridge,OrnsteinUhlenbeck,CoxIngersollRoss,GeometricBrownianMotion,Poisson,PoissonProcess,CompoundPoisson,RandomWalkall construct their target law straight frommean(t)/variance(t)and let its constructor validate the scale). The guard's only real-world trigger was thevariance()cancellation bug fixed earlier in this same release, which returned exactly0for near-unit-rootphiwith small fractionalt— so it was converting a silent precision defect in its own dependency into a confusingAR1.marginal(): variance is not positive at tdomain error rather than protecting against a genuinely non-positive variance. A 29700-combination sweep ofvariance(t)(densephigrid straddling the1e-14reformulation boundary,sigmaandtspanning underflow through overflow) found no strictly negative result for anyt > 0; the explosive|phi| >= 1branch diverges to+Infinitybut never flips sign, since its numerator and denominator change sign together.v <= 0remains reachable only by floating-point underflow (tbelow ~1e-322, orsigmabelow ~1.6e-161sosigma²underflows), and those inputs are still rejected with anError— nowInvalid parameters. ... sigma > 0fromNormal's own validation, so only the message changes (#1244).pdf(x, t)'s parallelv <= 0 => NaNguard is deliberately left in place: it predates the guard under discussion and uses a different return channel.
Deprecated
ran.dist.VonMises's single-argument constructor formnew VonMises(kappa)(implicitlymu = 0, the library's previous fixed behavior) is deprecated in favor ofnew VonMises(mu, kappa). The old form still constructs and behaves identically but emits a one-timeconsole.warnon first use; it will be removed in v1.33.0.
Fixed
ran.dist.Skellam(mu1, mu2).cdf(k)lost 3-4 orders of magnitude of precision (5e-10 to 6e-9 relative error, vs. the ~1e-12 to 1e-14 floor elsewhere) forkclose tomu1in highly asymmetric configurations (e.g.Skellam(5000, 1).cdf(k)forkin[4988, 4997]). Contrary to the issue's initial suspicion,src/special/marcum-q.js's_transitionBandis not implicated — for this call shape (marcumQ(k+1, mu2, mu1)withmu2 < 30), the dispatcher always routes through_series, whose only non-recurrence value is a singlegammaUpperIncomplete(mu, mu1)call. The bug is entirely insrc/special/gamma-incomplete.js's_gui(the upper-incomplete-gamma continued fraction): (1) its loop was capped at the sharedMAX_ITER=100with no regime-aware extension, unlike its sibling_gli, silently truncating before the ~150-160 iterations the near-diagonals≈mu1≈xregime needs (the same failure class #1286 fixed in_fc); (2) its shared prefactor with_gli,f * Math.exp(-x + s*Math.log(x) - logGamma(s)), cancels three O(mu1)-magnitude terms down to an O(1) result, an unavoidable ~1e-11 to 1e-12 floor no compensated summation of those specific terms can beat. Both are fixed via a newsrc/special/_deviance.jsmodule (log1pmx, relocated verbatim frommarcum-q.js's private_log1pmx;stirlerr, the Stirling series remainder;bd0, the Loader (2000) binomial-deviance term) that lets_gli/_guicomputef * Math.sqrt(s/(2*Math.PI)) * Math.exp(-bd0(s,x) - stirlerr(s))with every intermediate term O(1) or O(log s) instead of O(s), plus a regime-aware iteration budget and a throw-on-non-convergence guard (_assertGuiConverged, mirroring_fc's_assertFcConverged, ADR-0049) for_gui.bd0routes onlyx/snear1through the cancellation-safelog1pmxpath; far from1it uses the directx - s - s*Math.log(x/s)(no cancellation there, and routing extreme ratios throughlog1pmx(x/s - 1)would itself lose accuracy, sincex/s - 1rounds to exactly-1oncexis ~16 orders of magnitude belows). Deriving_gui's iteration budget also surfaced a second, unrelated latent bug: forsnear zero (not just larges), the continued fraction needs up to ~99 iterations at thex=s+1boundary regardless of how smallsis — previously silently wrong (caught live byTweedie.test()'s Anderson-Darling sweep once the new throw guard was in place);_gui's floor is raised fromMAX_ITER=100to200, empirically confirmed ≥2x the worst-case measured need acrosssfrom1e-20to20000.Skellam(5000,1).cdf(k)forkin[4988,4997]now matches mpmath (mp.dps=50) to ~1e-14 to 2e-15 relative error (previously up to 6e-9); the[5000,1]/[1000,1]/[2000,1]precision-gate groups' tolerances are unchanged since their floor is now set bySkellam._pdf's own, separate log-space cancellation (#1321, ~9e-12 worst case), not by this fix.scripts/precision-refs-discrete.py's[5000,1]k-grid gains two points inside the previously-withheld band (k=4990, 4995) (#1348).ran.dist.Skellam(mu1, mu2).pdf(x)returnedNaNfor highly asymmetricmu1/mu2(e.g.Skellam(1000, 1).pdf(999)) withxnear the mean, distinct from and un-fixed by #1309's earlier symmetric-large-muoverflow fix._pdfmultiplied three independently-scaled factors --expNegScaled(exp(-(√mu1-√mu2)²), which underflows to exactly0once the asymmetry betweenmu1andmu2grows large, contrary to a doc comment inherited from #1309's fix, which only holds for the symmetric case),Math.pow(sqrtRatio, x)(overflows toInfinity), andbesselIExpScaled(|x|, twoSqrtProd)(also underflows to exactly0, since the true scaled Bessel value at this Bessel order/argument combination -- e.g. order 999 against argument ~63.25 -- is genuinely non-representable as a double, ~1e-1092) -- a three-way0 * Infinity * 0collision even though the true pmf is a normal, representable number (~0.01-0.2).ran.special.bessel.jsgainslogBesselIExpScaled(n, x), the log-domain analogue ofbesselIExpScaled: it delegates tobesselIExpScaledand takes its log whenever that stays representable, falling back to a convergence-checked Taylor-series evaluation in log-space (leading term via the already-exportedlogGamma) only whenbesselIExpScaledunderflows to exactly0-- purely additive, with zero change tobesselIExpScaled's own behavior or precision-gated callers.Skellam._pdfnow combines all three log-space terms into a single exponent and callsMath.expexactly once, matching the codebase's established convention for this shape of computation (Poisson,Borel,Delaporte, etc.), rather than multiplying three separately-materialized factors. Combining terms whose individual magnitude grows withmu1while their sum staysO(1)near the mean does cost some precision at very largemu1(measured worst case ~5.7e-13 relative error inpdfatmu1=1000, up to ~6e-12 atmu1=5000) -- an inherent, honestly-documented trade-off (_LOG_CANCELtolerance override inscripts/precision-refs-discrete.py), and a dramatic improvement over the priorNaN. Closes #1321.ran.special.besselISpherical(n, x)threw a confusing"_hi: continued fraction failed to converge for n=..., x=... after NaN iterations"forn > 1and negativexwith|x| >= 1(the branch that delegates to the Wronskian-based continued-fraction helper_hi)._hi's iteration budget computesMath.ceil(7 * Math.sqrt(x)), which isNaNfor negativex, so itsforloop's condition was alwaysfalseand the loop never ran even once — the throw fired on an un-iterated ratio, not on genuine non-convergence.besselISpherical(n, x)is entire with onlyx^(n+2k)terms in its Taylor series, so it has definite parityi_n(-x) = (-1)^n i_n(x); the default branch now maps negativexto(n % 2 === 0 ? 1 : -1) * besselISpherical(n, -x)before reaching_hi, returning the mathematically correct value instead of throwing. Not reachable through any production call path —NoncentralChi/NoncentralChi2, the only internal callers, always pass a non-negative argument — only reachable via a direct call to the exportedbesselISpherical(n, x)(#1324).ran.dist.NoncentralT's internal CDF helper (fnm, an AS243-series implementation) rounded to exactly1.0/0whenever the true survival probability was closer to the boundary than adoublecan represent — not a fixable precision bug infnmitself (nodouble"1 minus something" can distinguish a gap smaller than ~1.11e-16), but a caller-visible information loss whenever two such saturated values were differenced (or summed and then subtracted from1). This brokeran.dist.DoublyNoncentralT.pdf(x)in thex*mu < 0branch at extreme parameters:DoublyNoncentralT(5, 5, 120).pdf(-0.7)remained ~1.7x off from its true value even after #1235's cancellation fix (the fix's own documented residual limitation).NoncentralTgains a direct survival sibling,snm(nu, mu, x)(computed via tanh-sinh quadrature over the noncentral-t's mixture representation, never as1 - fnm(...)), whichDoublyNoncentralT._pdfPoissonMixturenow falls back to for any Poisson-mixture term whosefnmdifference cannot be trusted (gated onnumagnitude, wherefnm's ownregularizedBetaIncomplete-derived series genuinely loses precision, and on the raw difference's magnitude) —DoublyNoncentralT(5, 5, 120).pdf(-0.7)now matches the mpmath reference (8.08e-15) to ~1e-14 relative error. The same root cause independently broke.cdf(x)forx < 0at the same extreme parameters —_cdfsums Poisson-weightedfnmterms directly and subtracts from1, so high-weight terms saturating to exactly1.0silently overcounted (DoublyNoncentralT(5, 5, 120).cdf(-0.7)returned6.66e-16against an mpmath reference of2.62e-16, ~154% relative error) — found while validating the.pdf()fix above;_cdfnow accumulates thex < 0complement termwise (sum(weight_i * (1 - fnm_i)), falling back tosnmunder the same gating) and matches the mpmath reference to ~1e-10 relative error, with no regression to.fit()/quantile-root-finding performance (#1250). The identical saturation was confirmed directly onran.dist.NoncentralT.pdf(x)itself (not justDoublyNoncentralT's use of it):NoncentralT(30, 5).pdf(40)returned exactly0while the mpmath reference is~1.54e-18, since_pdf's ownnu * (fnm(nu+2, mu, x*nuScale) - fnm(nu, mu, x)) / xdifferences twofnmcalls that both saturate to exactly1._pdfnow routes through the samenu-magnitude/diff-magnitude-gatedsnmfallback (reusingDoublyNoncentralT's thresholds verbatim), matching the mpmath reference to ~3e-15 relative error with no change to any ordinary (non-saturating)NoncentralTevaluation (#1302). Separately, that samenu-magnitude/diff-magnitude gate (as originally shipped by #1250, before the fix described next) had two further blind spots inDoublyNoncentralT._fnmDiff/_cdfTerm, both closed under #1298: (1)_fnmDiffmissed a single "knife-edge"nu0perx, where one of the twofnmcalls being differenced had separated fromfnm'sphi = 0.5*(1+erf(-mu/sqrt2))plateau and the other hadn't — their raw difference was then dominated by the still-plateaued operand's own error, which is wrong but not small (~1e-7, evading a< 1e-9magnitude check) — solely responsible forDoublyNoncentralT(5, 5, 120).pdf(-0.2)'s remaining~2e-3relative error; (2)_cdfTermmissed an entire low-nu0plateaued range whose raw complement is pinned at exactly1 - phi(~2.87e-7formu=5, also not< 1e-9) — solely responsible forDoublyNoncentralT(5, 5, 120).cdf(-0.1)being~14.5xwrong, a case #1298 itself did not anticipate (its own acceptance criteria assumedcdfwas unaffected, having only measuredcdf(-0.2)). Both helpers now check two independent conditions, since a rawfnmvalue can be untrustworthy either way and neither implies the other: whether it is still stuck atphi(nonu-magnitude pre-filter needed — this only fires when thenu-dependent correction is genuinely unresolved), or — the original #1250 mechanism, still needed since a value that has resolved away fromphican independently saturate toward the opposite0/1boundary asnugrows — the pre-existingnu0 >= 30 && |raw value| < 1e-9magnitude check.pdf(-0.2)andcdf(-0.1)— the two points issue #1298 itself reported broken — now match their mpmath references to~1.9e-14and~4.9e-14relative error respectively (worst case across all three reported points: pdf8.75e-14, cdf3.80e-8);test/guess.js's fit-all-distributions tests rose from a post-#1250 baseline of ~23-24s to ~46-50s in isolation, since the addedphi-check fires more often during.fit()'s optimizer exploration than the magnitude check alone did — combined with #1302's own new, independentNoncentralT._fnmDiffcost (above), this pushed both tests past their previous60000ms mocha timeout under full-suite--parallelCPU contention (isolated runs stayed under 60s; the full suite did not), so both timeouts were raised to120000ms (test/guess.js, matching.mocharc.yml's own global default).NoncentralT._fnmDiff(added by #1302, above) reused the original magnitude-only gate and was NOT updated with thisphi-check, soNoncentralT.pdf(x)still silently returned0(or, in a nearby regime, a badly wrong nonzero value) whenever bothfnmcalls stayed stuck atphiwithout ever separating — confirmed atNoncentralT(5, 6).pdf(-0.5)(returned0, mpmath reference~3.34e-10) andNoncentralT(1, 8).pdf(-0.3)(also0, reference~4.78e-16), both atnufar below the30floor the magnitude gate needs to even evaluate;NoncentralT(10, 6).pdf(-1.0)returned a nonzero but ~480x wrong value, showing the blind spot isn't only an exactly-zero case. PortingDoublyNoncentralT's correctedphi-equality gate verbatim was not sufficient on its own:NoncentralT.snm(its designated fallback) is only accurate fornu >= 30, per its own documented limitation, andNoncentralT._pdf's call site — unlikeDoublyNoncentralT's, which never invokessnmbelow that floor — needs it down tonu = 1.NoncentralT._fnmDiffis removed;_pdfnow inlines the corrected gate (phi = 0.5*(1+erf(-mu/sqrt2)), computed unconditionally — the sign-flipfnm's own internalx<0?-mu:muuses is fully internal to that function'sx>=0 ? z : 1-zreturn-value flip and does not propagate to callers) and, when it fires, falls back to a newNoncentralT._pdfDirect(nu, mu, x): a direct tanh-sinh quadrature of the density's own defining formula (already documented in the class JSDoc) rather than a CDF difference, so there is no cancellation to lose precision to at anynu. All three reported cases now match their mpmath references to~1e-14-1e-15relative error, withtest/guess.js's.fit()-exploration timing unaffected (#1318). Separately,_pdf's other saturation gate,nearOppositeBoundary(nu >= 30 && |a - b| < 1e-9, unchanged by #1318), missed a large-nuregime the flat1e-9threshold was never tuned for:fnm's own absolute noise floor grows roughly linearly withnu, and the fast path'snu * (a - b) / xidentity amplifies that noise by the samenu/xfactor, soNoncentralT(10000, 0).pdf(0.5)returned0.3520526413036684against a true0.35205267468981716(~9.5e-8 relative error, nine orders of magnitude worse than_pdfDirect's own ~1e-13) while|a - b| = 1.76e-5sailed straight past the flat threshold.nearOppositeBoundary's threshold is now scaled bynu(nu * Number.EPSILON * 1e10, empirically validated acrossnufrom 30 to 100000), correctly routing large-nuevaluations to_pdfDirectwhile leaving the already-accuratenuin[30, 300]regime #1318 validated untouched. This madeNoncentralT.fit()pay_pdfDirect's ~80x per-call cost whenever Powell's optimizer explores largenu— harmless for genuinely noncentral-t-shaped data (small interior optimum, few such evaluations), but data with no goodtfit (e.g. bounded/circular samples) has no interior optimum innuand drove the unbounded search into the tens of thousands, multiplying that cost across hundreds of thousands of likelihood evaluations (test/guess.js's VonMises-in-default-pool test regressed from ~34s to over 150s).NoncentralTgains astatic _powellOptions()bounded search budget ({ tol: 1e-3, maxIter: 15 }), mirroring the identicalDoublyNoncentralBeta/DoublyNoncentralFfix for the same class of problem (#1063) — cuts the pathological case back to ~9s while reproducing genuinely-matched-data fits' converged(nu, mu)to within floating-point noise (#1325).DoublyNoncentralT._pdfPoissonMixture's own_fnmDiffhelper had the structurally identical flat-1e-9nearOppositeBoundarygate, never updated by #1325 (whose scope was restricted toNoncentralT._pdf) — each Poisson-mixture term multiplies itsfnm-difference bynu0(the term's own degrees of freedom), the same amplification shape asNoncentralT._pdf'snu*(a-b)/x, soDoublyNoncentralT.pdf(x)accumulated the identical nu-scaled precision loss at largenu. Porting #1325'snu * Number.EPSILON * 1e10threshold into_fnmDiff's gate (keeping its existingnu0 >= 30guard) closes a real, already-reachable gap:DoublyNoncentralT(5, 2, 120).pdf(-0.7)tightens from ~1.7e-9 to ~7.3e-15 relative error, a ~235,000x improvement at parameters this library's own precision-gate suite already exercises. At genuinely extremenu(>= 10000, unreachable via.fit()or any realistic dataset) the fix only partially helps —_fnmDiff's fallback (NoncentralT.snm(lo) - NoncentralT.snm(hi)) is itself a difference of two ~1e-11-accurate quadratures, unlikeNoncentralT._pdf's cancellation-free_pdfDirectfallback, so it re-encounters a smaller-scale version of the same cancellation problem one level down once the true difference itself shrinks to a comparable magnitude —DoublyNoncentralT(50000, 0.01, 0.1).pdf(-0.5)improves from ~3.15e-6 to ~6.0e-7 relative error but is not made fully precise; this residual is a documented, accepted limitation, not a regression._cdfTerm(used by.cdf()) does not share this amplification (its Poisson-mixture sum does not multiply bynu0) and is left unchanged. The wider-firing gate has a second effect, caught only by the full test suite (not by any test targeting the fix itself):.fit()'s Powell optimizer, on data with no interior optimum innu/theta(e.g. the same VonMises(0,2)-sampled data #1325 used), now pays the addedNoncentralT.snm-fallback cost across hundreds of thousands of likelihood evaluations —DoublyNoncentralT.fit()on that data went from ~6s to ~68s with an unbounded search, exactly the class of regression #1325's own solution doc warned a hot-path-to-expensive-fallback fix must be checked for separately.DoublyNoncentralTgains astatic _powellOptions()({ tol: 1e-2, maxIter: 15 }, matchingDoublyNoncentralBeta's values), bounding the pathological case back to ~18s alone / ~34s insideguess()'s full default-pool sweep, with no intolerable quality loss on well-matched data. Seesolutions/correctness/2026-08-04-0823-doubly-noncentral-t-nu-scaled-fnmdiff-gate-fix.md(#1332).src/algorithms/powell.js's fractional convergence test (2*|fStart-fret| <= tol*(|fStart|+|fret|)) tolerates an absolute log-likelihood gap that grows with sample sizen, sinceDistribution.fit()'s objective is-lnL(data): issue #1338 measured this across every_powellOptions()-bounded distribution and foundDoublyNoncentralT(5,1,2)'s bounded-vs-unbounded gap growing roughly with n, from ~0.12 at n=100 to ~3.08 at n=3000, andDoublyNoncentralF(3,8,1,1)'s ranging non-monotonically from ~0.74 at n=100 to ~2.48 at n=3000 (peaking at ~3.51 at n=1000) — both non-trivial and not shrinking with more data.powell()gains an optionalcapAbsfield (defaultInfinity, so every existing caller not passing it is unaffected) that bounds the threshold viaMath.min(tol*(|fStart|+|fret|), capAbs), andDistribution.fit()now merges in a calibratedcapAbs=2default — chosen via Wilks'/LRT theory (the lnL gap at a confidence-region edge is~chi2_p/2, anO(1)quantity independent ofn) and confirmed against every affected distribution's own worst-case pathological-data wall-clock/call-count ceiling — unless a subclass's own_powellOptions()already setscapAbsitself. ClosesDoublyNoncentralT's gap from ~1.41/~3.08 to ~0.0003/~0.018 at n=1000/3000, andDoublyNoncentralF's from ~3.51/~2.48 to ~0.002/~0.045 at the same sample sizes; a no-op forNoncentralT(its 2-parameter (nu, mu) gap is already ~1e-11 to 1e-13 at every n) and only a partial improvement forDoublyNoncentralBeta, consistent with part of its gap being a genuine shape/noncentrality ridge (#1063) rather than purely a convergence-tolerance artifact.DoublyNoncentralF.fit()'s own custom ridge-penalized objective callspowell()directly rather than throughDistribution.fit(), so it does not receive the injected default. Seesolutions/testing/2026-08-05-1736-powell-fractional-convergence-n-scaling.md(#1342).ran.dist.Distribution.prototype.params()andran.process.Process.prototype.params()returnedthis.pby live reference, letting a caller silently corrupt a distribution's or process's internal state (e.g.const p = dist.params(); p.mu = 999). Both now return a shallow copy ({ ...this.p }); nothing in the codebase relied on the previous mutable-reference behavior. See ADR-0047 (#1257). The same live-reference issue was found inran.dist.Distribution.prototype.support(), which fed the mutable boundary objects directly intopdf/cdf/quantile/sample's internal_belowSupport/_aboveSupport/_atClosedBoundarychecks; it now returnsthis.s.map(b => ({ ...b })), copying the nested{closed, value}boundary objects as well as the array, since a shallow array spread alone would still leave them shared. The shallow{ ...this.p }copy itself left one gap: array-valued parameter fields (Hyperexponential'sweights/rates,Categorical'sweights) were still shared by reference, sodist.params().weights[0] = 0still reachedthis.p.weightsthrough the copied top-level key. Bothparams()implementations now additionally copy every array-valued field (Array.isArray(p[key]) ? [...p[key]] : p[key]), a targeted per-field copy rather than a generic recursive/structured clone — the latter would also try to clone non-array object fields such asCompoundPoisson'sjumpDist(a liveDistributioninstance), which ADR-0047 scoped out of this accessor's copy guarantee on the reasoning that "a caller mutating a nested distribution's own state goes through that distribution's own params()/setters, not through the outer process's accessor." See ADR-0050 (#1299). That reasoning turned out to be wrong:CompoundPoisson._next()samples directly fromthis.p.jumpDiston every step with no per-step reseed (onlyCompoundPoisson.prototype.seed()reseeds it, once, at seed time), sojumpDist's PRNG stream is live process state, not an isolated implementation detail — confirmed empirically, seeding two identically-constructed processes the same way but calling.seed()on one'sparams().jumpDistin between produced differentpath()output from the other, with neither process's own.seed()called again.params()now also clones anyDistribution-instance-valued field (via the newcopy()method, above), superseding ADR-0047's carve-out for this field shape specifically; its shallow-copy decision for plain primitive/array fields is unaffected. See ADR-0051.ran.dist.ReciprocalInverseGaussian.cdf(x)returned a value quantized to multiples of2^-53(essentially garbage) for smallx, where the internal1 - InverseGaussian.cdf(1/x)subtraction catastrophically cancelled becauseInverseGaussian.cdf(1/x)rounds to within 1 ULP of1in that regime.InverseGaussiangains a numerically stable_survival(x)(mirroring its own_cdf's erfc/erfcx cancellation fix, applied symmetrically), whichReciprocalInverseGaussian.cdf(x)now calls instead of subtracting from1.test/dist-cases-continuous.js'sNormal[0,2]far-tail (x = ±14)refValswere stale — 1 ULP off forpdf, ~2.3e-6 relative error forcdf— predating the cancellation-safe far-tail fix already shipped fortest/precision-continuous.jsunder #808, which was never back-ported to this file.scripts/precision-refs-continuous.py'sself_check()(only made to actually run under #1110) caught the discrepancy; the correct values were independently re-derived and confirmed via three agreeingmp.dps=50formulations (erf,erfc, mpmath's built-inncdf) (#1193).ran.special.marcumQ/ran.special.marcumPreturnedNaNin the quadrature branch (largex, deep lower tail) whenever the scaled argumenty/muwas far below 1 —_zetaxy()'s saddle-point formula catastrophically cancelled oncesqrt(1 + 4*x*y/mu²)rounded to exactly1.0, collapsing a denominator to0. This brokeran.dist.Rice.cdf(x)/.q(p),ran.dist.NoncentralChi.cdf(x)/.q(p), andran.dist.NoncentralChi2.cdf(x)/.q(p)nearx = 0and, for.q(p), at any probabilityp— the base class's quantile root-finder always probescdf(Number.EPSILON)first, and the resultingNaNsilently defeated the root-finder's own bracket-validity guard (NaNcomparisons are alwaysfalsein JS)._zetaxynow uses the exact identityd1 - eps = d2to fold the two near-cancelling terms into one well-conditioned expression whenever4*x*y/mu² < 0.5, leaving the existing near-transition-line formula (y/muclose tox/mu + 1) unchanged (#1179).scripts/precision-refs-continuous.py --emit --allow-prune --only Name1,Name2(dev-only tooling) silently ignored--onlyand recomputed every distribution instead of scoping to the named ones, because--only's parsing checked a fixed argv position while--allow-prune's was already position-independent.--onlyis now detected byargv.index('--only')in both the--emitand self-check branches, so it works regardless of where it appears relative to--allow-prune.scripts/precision-refs-continuous.py'sexisting_groups()(dev-only tooling), the guardrender()relies on to preserve hand-maintained precision-gate groups it can never reproduce (e.g.TruncatedExponential), silently dropped any group whose raw text didn't match its expectedname: '...', params: ..., tol: ...shape instead of preserving or flagging it — a future hand-edited group with different field order or an inserted field would then be neither reproduced nor preserved, reintroducing the exact silent-loss failure mode this mechanism exists to prevent. It now raisesRuntimeErrornaming the unparseable span so a maintainer can fix it before--emitruns.scripts/precision-refs-continuous.py's bare/--checkself-check (dev-only tooling) hung for 100+ minutes once it reachedDoublyNoncentralBeta'sLARGE_LAMBDA_ANCHORSregression case ((2,2,1200,1200)), never completing and never reaching the remaining ~90 distributions —dncbeta_cdf()called mpmath's expensive regularized-incomplete-beta function (betainc) fresh for every one of the ~800k-1M(r, si)pairs its nested double-Poisson-mixture summation visits at this lambda scale (dncbeta_pdf(), which needed no such call, was never the bottleneck).dncbeta_cdf()now tracks the incomplete-beta value itself via an exact recurrence (a standard DLMF 8.17.20-style contiguous relation, independently re-derived and numerically verified against directbetainc()calls at both toy and production scale before use) instead of recomputing it from scratch at every step, cuttingDoublyNoncentralBeta(2,2,1200,1200).cdf(0.3)from ~1235s to ~66s and.cdf(0.5)from ~2659s to ~67s with no change to the walk's structure, floor, or convergence semantics (the #1108/#1086 anti-regression fix), and no change to any already-vetted reference value.self_check() --only DoublyNoncentralBetanow completes in ~4 minutes with 0 mismatches (#1194).npm run standard/npm run lintsilently skipped every file sitting directly insrc/ortest/(e.g.src/index.js,test/ad.js,test/core.js,test/algorithms.js) because thelint/standardscripts passed an unquotedsrc/**/*.js test/**/*.jsglob to the shell — under a POSIX/bin/sh/dash shell (how npm actually invokes scripts on Linux, absent bash's non-defaultglobstaroption),**behaves like a single*, so only files exactly two path segments deep were ever linted. Both scripts now quote the globs ('src/**/*.js' 'test/**/*.js') sostandard's own bundled glob engine expands**correctly instead of the shell. Fixing the scope surfaced several previously-hidden, genuinely-live violations, now fixed: an over-precision numeric literal intest/ad.js(no-loss-of-precision) shortened to the value that round-trips exactly as a double, two similarly over-precisionrefVals/momentsreference literals intest/dist-cases-continuous.jscorrected the same way, and twonew SomeClass(...)calls used only for their deprecation-warning side effect intest/process.js(no-new) now capture the instance into a variable and assertinstanceofon it.ran.process.CoxIngersollRoss.pdf(0, t)returned+Infinitywhen the Feller condition is violated (alpha < 1), disagreeing with theGamma(alpha, 1/scale)instancemarginal(t)returns for the same process, whose ownpdf(0)is0there —Gamma's support (likeBeta's andWeibull's) is open at0whenever the shape parameter is below1, so the boundary point is excluded rather than evaluated.pdf(0, t)now returns0foralpha < 1, matchingmarginal(t).pdf(0); the already-correctalpha === 1(1/scale) andalpha > 1(0) cases are unaffected.ran.dist.NoncentralBeta.pdf(1)returned0forbeta < 1instead of the correct+Infinity. The density carries a(1 - x)^(beta - 1)factor that diverges asx → 1whenbeta < 1(dominated by thek = 0Poisson term regardless ofalpha/lambda), but the Poisson-mixture series evaluated at exactlyx = 1producedInfinity - Infinity = NaN, which the basepdf()silently collapsed to0via itsNaN→closed-boundary guard._pdfnow short-circuitsx === 1, beta < 1toInfinity;beta >= 1is unaffected ((1 - x)^(beta - 1)is0forbeta > 1, or1forbeta === 1, giving the finite Poisson meanalpha + lambda/2, both handled correctly by the existing series). The mpmath reference generator (scripts/precision-refs-continuous.py, dev-only) had the mirror-image bug — a blanketx >= 1 → 0early return that never inspectedbeta— and now returns+inf/alpha + lambda/2/0forbeta < 1/beta == 1/beta > 1respectively (#1121).ran.core.Xoshiro128p.next()is uniform on[0, 1)and can legitimately return exactly0(~1-in-2³² per call). Six generators fed that raw draw straight intoMath.log(...), which sendsMath.log(0) = -Infinitythrough the rest of the formula and can leak a literalInfinity(or, forUniformProduct, a silent0that violates its open lower bound) as a returned sample: the shared_exponential()helper (and thereforeExponentialandHyperExponential),YuleSimon,UniformProduct,LogSeries,FlorySchulz, andPolyaAeppli. All six now take1 - r.next()instead ofr.next()into the log, which is uniform on(0, 1]and can never hit the singularity at0.LogSeries's default GoF test seed sweep changes from[0, 42, 12345]to[1, 42, 12345]since the fix necessarily changes the deterministic sample sequence for a given seed, and seed0's new sequence happened to land in the chi-squared test's ~1% rejection region by chance (empirically confirmed: sample mean matches theory, and a 200-seed sweep shows a ~2% failure rate consistent with the test's own false-positive rate, not a systematic bias).ran.dist.BetaRectangular's parameter count (.k) was inherited fromBeta's constructor (2) despiteBetaRectangularhaving 5 free parameters (alpha,beta,theta,a,b), causing.aic()/.bic()to under-penalize its complexity..know correctly reports 5. A follow-up audit of every reparametrizingDistributionsubclass found the same defect in 11 more distributions and fixed all of them:PERT(3, was 2 fromBeta),Bates(3, was 1 fromIrwinHall),BetaBinomial(3, was 2 fromCategorical),SkewNormal(3, was 2 fromNormal),BirnbaumSaunders(3, was 2 fromNormal),JohnsonSB(4, was 2 fromNormal), andJohnsonSU(4, was 2 fromNormal) all under-counted their true free-parameter count;Gilbrat(0, was 2 fromLogNormal/Normal),PowerLaw(1, was 2 fromKumaraswamy),QExponential(2, was 3 fromGeneralizedPareto), andR(1, was 2 fromBeta) went the other way — each fixes one or more of its parent's parameters to a constant, so the inherited.kover-counted and over-penalized complexity (#1049). A further audit of every remainingDistributionsubclass extending a concrete distribution class found the same under-counting defect in 2 moreCategoricalsubclasses:Hypergeometric(3, was 2 fromCategorical) andNegativeHypergeometric(3, was 2 fromCategorical); every other such subclass was confirmed to already report the correct.k(#1094).ran.dist.PowerLaw,R,Gilbrat,JohnsonSU,JohnsonSB,SkewNormal,BirnbaumSaunders, andPERT— reparametrizingDistributionsubclasses that callsuper(...)with transformed or dummy values — leaked the parent constructor's internal parameter keys (and, forPowerLaw/R/Gilbrat, values the caller never supplied) into the public.params()method instead of exposing only the constructor's own declared natural parameters;BirnbaumSaundersadditionally stored its location parameter under the wrong keymu2instead of its declaredmu, so.params().mualways returned the leaked0rather than the constructor's actual value..params()now returns exactly the natural parameters named in each constructor's JSDoc, matching the fix already applied toChi2/Erlang/MaxwellBoltzmann/Rayleigh/DoubleWeibull/HalfNormal/Slash/LogCauchy/StudentZunder ADR-0018 (#1057).ran.dist.QExponential— the one distribution deliberately left out of that fix, since it previously relied onGeneralizedPareto's canonical{mu, sigma, xi}for its moment methods and IEEE-754 divergence-boundary tests — now follows the same convention:.params()returns{q, lambda}, and the relocated GP-space values live inthis.c, with no change topdf/cdf/quantileresults. Bringingskewness()/kurtosis()in line withGeneralizedPareto's own three-tier formula/Infinity/NaNsplit surfaced a latent discrepancy between the two: forxi >= 1/2(variance itself infinite, e.g.q = 1.8),QExponentialreturnedInfinitywhereGeneralizedPareto, given the identicalxi, correctly returnsNaNfor the same indeterminate ∞/∞ ratio (decisions/0015-return-value-and-error-conventions.md);QExponential.skewness()/.kurtosis()now returnNaNin that range, matchingGeneralizedPareto(#1058). The same leak is fixed for the remaining 9 reparametrizing subclasses:ran.dist.F,BaldingNichols,Weibull,NoncentralF,DoublyNoncentralF,GeneralizedGamma,GeneralizedNormal,DoublyNoncentralChi2, andExponentiatedWeibull.WeibullandGeneralizedNormalhad the same wrong-key-collision pattern asBirnbaumSaunders:Weibull.params().lambdareturned the leaked dummy1passed to the internalExponential(1)transform while the constructor's real scale was hidden under a syntheticlambda2;GeneralizedNormal.params().alpha/.betawere similarly shadowed by leakedGamma-space values, hidden underalpha2/beta2(ExponentiatedWeibull, which reparametrizesWeibull, inherited the samelambda/lambda2split and is fixed alongside it).DoublyNoncentralChi2.params()no longer exposes the internal collapsedk/lambdait computes internally (DoublyNoncentralChi2(k1,k2,λ1,λ2) ≡ NoncentralChi2(k1+k2,λ1+λ2)) alongside its ownk1/k2/lambda1/lambda2.NoncentralF,DoublyNoncentralF, andDoublyNoncentralChi2— whose immediate parent'spdf/cdf/sampling are non-trivial series algorithms rather than a one-line special-function call — now cache a correctly-parameterized instance of that parent and delegate to it (ADR-0039), instead of duplicating its internals or modifying the parent class (NoncentralBeta,DoublyNoncentralBeta,NoncentralChi2are themselves independent public distributions, unaffected).ran.dist.HalfGeneralizedNormal, which extendsGeneralizedNormal, is updated alongside it since it read the same leaked keys directly (#1070).HalfGeneralizedNormalitself was inadvertently left out of both that effort's and #1057/ADR-0018's scoped file lists: its own constructor never reassignedthis.paftersuper(0, alpha, beta), so.params()returned the inherited{ mu: 0, alpha, beta }instead of its own two natural parameters..params()now returns exactly{ alpha, beta }; sinceGeneralizedNormal.prototype._generator/_pdf/_cdfreadthis.p.mudirectly,HalfGeneralizedNormal's own overrides of those three methods are now inlined against themu = 0-folded formulas (mirroring theWeibull/Exponentialpattern) instead of delegating tosuper, with no change to sampled values,pdf/cdfresults, or.seed()-determined output (#1087).- The generated API docs (
npm run docs) now render the individual fields of every options-object constructor (e.g.ran.mc.RWM'soptions.logDensity,options.config,options.initialState;ran.mc.HMC's additionaloptions.gradLogDensity) as indented rows in the Parameters table, instead of silently dropping them behind a single opaqueoptions: Objectrow.documentation.jsnests dotted@paramtags (e.g.@param {Object} options.config) under the parent param'spropertiesarray rather than returning them as flat top-level params;docs/src/param-parser.jsnever read that array, so every JSDoc'd nested field for every options-object constructor in the codebase (RWM,AdaptiveMetropolis,HMC,NUTS,MALA,Gibbs,Slice,ParallelTempering) was invisible in the rendered docs even though it was correctly documented in the source JSDoc.docs/index.js's call-signature renderer is updated alongside to use only the top-level (depth-0) params, so signatures still read e.g.RWM(options)rather than incorrectly listing the newly-surfaced nested fields as separate positional arguments. ran.dist.DoublyNoncentralBeta.fit()(andDoublyNoncentralF.fit(), which delegates its_pdf/_cdftoDoublyNoncentralBeta) could take 13-30+ seconds on ordinary data, driven by two compounding issues indoubly-noncentral-beta.js: (1)_pdfRBackward/_cdfRBackward's Poisson-mixing outer loop had no iteration cap, unlike itsMAX_ITER-bounded forward counterpart, so it could run arbitrarily long as Powell's optimizer explored large trial non-centrality parameters — now capped atMAX_ITERto match; (2) more significantly, on data that does not genuinely belong to this family, the log-likelihood surface carries a long, near-flat ridge between the shape and non-centrality parameters that a full-precision Powell search (the base class's defaulttol=1e-8,maxIter=200) chases almost indefinitely — worse, each step further along the ridge is itself more expensive to evaluate, since larger non-centrality parameters require more series terms.DoublyNoncentralBetanow overridesstatic fit()with a bounded Powell search budget (tol=1e-2,maxIter=15), empirically verified to still recover parameters within this class's existing fit tolerances on well-matched data (matching the default optimizer's result within ordinary finite-sample noise across multiple seeds) while bounding worst-case cost to roughly 1-2s (#1063).ran.dist.DoublyNoncentralBeta.pdf()/.cdf()(andDoublyNoncentralF, which delegates to it) returnedNaNinstead of a finite probability once both non-centrality parameters were large (e.g.lambda1 = lambda2 = 2000), driven by two compounding overflow/underflow bugs in the double-Poisson-mixture summation: (1) the Poisson-weight speed-up constantspr0/ps0were computed as the unnormalizedlambda^k/k!with the compensatinge^{-lambda}deferred to a later multiplication, overflowingNumber.MAX_VALUEoncelambda1/lambda2exceeded ~1418 — before the compensator was ever applied; (2) independently,Beta(alpha+r0, beta+s0)underflows to exact0in double precision once bothr0 = round(lambda1/2)ands0 = round(lambda2/2)are large (e.g.Beta(1002,1002) ≈ 1e-604, far belowNumber.MIN_VALUE), while power-of-x/power-of-y terms in the same term are comparably extreme in the opposite direction — even though the combined term is an ordinary, representable double,0 * Infinity(or equivalent) producedNaNonce these isolated linear-space factors combined. The Poisson-weight normalization now defers to a single outer-scale multiplication (bit-identical to the prior, more precise formulation) whenever it is safe to do so, only folding it in directly when the unnormalized magnitude would itself overflow; the Beta-function constant and power-of-x/power-of-y terms are now tracked as logarithms, updated additively through the existing forward/backward recurrence, and combined via a singleexp()per term rather than ever being materialized in isolation.pdf/cdfare now finite forlambda1 = lambda2up to at least 50000, matching the issue's acceptance criteria, with existing small-lambda precision-gate values unchanged (#1075). Series-truncation precision at very large lambda was left out of scope for that fix and separately tracked as #1063/#1086:pdf/cdfcould return a finite-looking but silently wrong value — off by up to ~10 orders of magnitude — oncelambda1 + lambda2 ≳ 400-600andxmoved away from 0.5 (e.g.DoublyNoncentralBeta(2,2,1200,1200).pdf(0.3)previously returned9.5e-31against an mpmath (dps=50) reference of3.03e-21). Two compounding truncation bugs are now fixed: (1) the outer Poisson-mixing loops (_pdfRForward/_pdfRBackward/_cdfRForward/_cdfRBackward) were capped atMAX_ITER(100) steps from thex-independent Poisson mean(r0, s0), but the true summand peak shifts away from(r0, s0)asxmoves from 0.5 (e.g. a shift of ~146 steps forlambda1=lambda2=1200, x=0.3) — now capped at the widerMAX_SERIES_ITER(500), matching the cap already used elsewhere for this class of series; (2) more fundamentally, the inner per-rsum overs(_pdfSumOverS/_cdfSumOverS) relied on the sharedrecursiveSumhelper's convergence check, which floors its relative-error tolerance atEPS * max(|sum|, 1)— an absolute floor that falsely declares convergence after only 1-2 terms whenever a sum's true converged value is itself far below 1 in magnitude (routine here, since these densities can be astronomically small). A new file-local_seriesSumhelper drops that floor (safe here specifically because every summed term is a non-negative probability-weighted value, never subject to cancellation), fixing the truncation at its root rather than merely widening the outer loop's window.DoublyNoncentralF.fit()'s bounded Powell search budget (#1063) is unaffected — empirically re-verified at ~114000_pdfcalls and ~8s on the original #1063 reproduction, matching the pre-fix baseline. A residual gap remained even after theMAX_SERIES_ITERwidening: oncelambda1 + lambda2grows large enough (empirically>= ~8000) combined withxfar enough from 0.5, the true peak shifts beyond even that wider window, andpdf()/cdf()silently returned exactly0— not merely imprecise, flatly and incorrectly zero for parameter combinations already within this class's own tested range (#1102)._pdf/_cdfnow detect this case directly (the standard walk's own last term stays non-negligible relative to its total after exhausting its window, rather than estimating in advance whether relocation will be needed — an estimated-shift heuristic was tried first and found to misroute cases the standard window already handles correctly) and fall back to a walk centered on a closed-form peak-index estimate instead of(r0, s0), bounded by a separate, smaller iteration cap (RELOCATE_MAX_ITER) chosen specifically to keep this fallback's inherently costlier per-term evaluation from reintroducing the#1063fit()-search-cost regression. This fallback trades some precision for that bound — large-lambda values a fewxaway from 0.5 are now correct to within an order of magnitude rather than exact-0, not to full machine precision; already-correct small/moderate-lambda behavior is unchanged.ran.dist.Distribution.load(state)restoredthis.p/this.cdirectly from a serialized state with no shape validation, so loading a malformed or version-skewed snapshot (e.g. one saved before a distribution migrated itsthis.p/this.csplit under ADR-0018) silently read missing keys asundefinedand propagated toNaNfrompdf()/cdf()/sample()instead of throwing.load()now constructs a throwaway probe instance from the restored params (padded to the constructor's declared arity, so distributions likeCategoricalwhosethis.pintentionally holds fewer keys than constructor arguments are still validated correctly) and compares itsthis.p/this.ckey sets against the restored state's, throwing a clearErroron any mismatch before the state is otherwise used unchanged. Because the probe runs the real constructor,load()can also throw on a snapshot whosethis.p/this.cshape is unchanged but whose saved values now violate a constructor constraint that has since been tightened (e.g. a parameter that used to allow>= 0now requires> 0) — an intentional, accepted trade-off, not a regression (#1074,decisions/0038-distribution-load-probe-validation.md's "Consequences → Harder" section).ran.dist.NoncentralChi(1, lambda).pdf(0)hardcoded a return of0for everyk, but the true limit atk=1is finite and nonzero (sqrt(2/pi)*exp(-lambda^2/2), since only the underlying non-central chi-squared pdf'sj=0Poisson term diverges asv^(-1/2)nearv=0fordf=1) — matching the fix already applied toran.dist.Chi(1).pdf(0).k >= 2is unaffected, since the true limit there is genuinely0(#1122).ran.dist.DoublyNoncentralF's constructor built its internalDoublyNoncentralBetadelegate (the onepdf()/cdf()/sample()actually compute against) from raw, un-roundedd1/d2, while.params()reported the rounded integers its own JSDoc promises — a silent internal/public mismatch that also brokesave()+load()round-trips for non-integer inputs, since_afterLoad()rebuilt the delegate from the rounded post-restore params instead of the original raw ones.d1/d2are now rounded once, before any internal use, matching the pattern already used byNoncentralF/DoublyNoncentralChi2, so.params(),pdf()/cdf()/sample(), and asave()+load()round trip are now always internally consistent. Rounding early on its own discretizes the log-likelihood surfacefit()'s Powell search explores, re-triggering the#1063bounded-search regression at roughly double the_pdfcall count;DoublyNoncentralFnow overridesstatic fit()to searchDoublyNoncentralBeta's continuous space directly (only rounding the final returned instance) with a smooth squared-hinge penalty — zero within a plausible region of the moment-matched initial guess, growing quadratically only beyond it — that keeps Powell off#1063's near-flat ridge without ever excluding a finite parameter value outright, so genuinely large-parameter fits are never silently underfit the way a hard cutoff would (#1084).ran.dist.VonMises.cdf(x)(and therefore.q(), whose root-finder samplescdf()at arbitrary internal points) could return values far outside[0, 1]for concentrated distributions (kappagtrsim 6-9) wheneverxwas at or near a multiple ofpi/4— e.g.VonMises(9).cdf(-Math.PI / 4)returned-0.0074instead of0.0119, andVonMises(9).q(VonMises(9).cdf(-1))returned-pi/4instead of-1. The underlying Fourier-series summation checked convergence on each raw term, which happens to collapse to machine-epsilon atx = k*pi/4(sin(4x) ≈ 0there) well before the series had actually converged for concentratedkappa; convergence is now checked on the term's non-oscillating envelope instead, which cannot be fooled by an incidental zero ofsin(i*x).ran.special.besselI(0, x)(and thereforeran.dist.Rice,VonMises,Skellam(atk=0), andNoncentralChi/NoncentralChi2(atk=2) wherever the effective Bessel argument fell in the same range) was off by up to ~1.2e-9 relative error forxin roughly(10, 14], well outside the library's usual ~1e-14 precision — a "cold start" gap immediately after_besselIBackward's Miller backward-recurrence takes over from the|x| <= 10Taylor series, recovering smoothly byx ~ 15-16. The recurrence's run-up-margin formula scales its extra headroom assqrt(40 * n), which degenerates to exactly0forn = 0(the orderbesselI(0, x)dispatches to) while everyn >= 1order already receives adequate margin from the same term;nis now clamped toMath.max(n, 1)inside that formula, son = 0inheritsn = 1's already-validated margin with zero behavioral change for anyn >= 1. Also corrects a pre-existing self-referential reference literal intest/special.js's|x|=10routing-boundary test (it asserted a value computed from the pre-fix buggy code path instead of mpmath), and adds theRice[3.16,1]/NoncentralChi[2,3.5]/NoncentralChi2[2,8]/Skellam[6,5]precision-gate parameter sets that issue #1143's boundary-grid work deliberately withheld because they surfaced this exact gap (#1185).ran.dist.DoublyNoncentralT.pdf(x)'s general (mu != 0) branch returned significantly wrong densities (up to ~13% relative error observed) oncemuwas non-zero and large relative tonu, combined with largetheta— e.g.DoublyNoncentralT(5, 5, 120).pdf(1.3)returned0.8149681936132279against an mpmath (mp.dps=50) reference of0.71818185584468099.... The series walk advanced Kummer's₁F₁(a,b,z)across the series index via a three-term contiguous recurrence ina(_f11Forward/_f11Backward), which is numerically unstable in both directions once the series' peak index pushesalarge relative tob— confirmed by direct measurement (forward: 330% error from the very first recurrence step; backward: growing to 9 orders of magnitude of error near the series' start). Both private methods are removed; every series term now calls the already-correctf11()special function directly, matching the mpmath reference to ~1e-11 to ~1e-15 relative precision. Seesolutions/correctness/2026-07-30-1600-doubly-noncentral-t-pdf-f11-recurrence-instability.md(#1207).ran.dist.DoublyNoncentralT.cdf(x)returned badly wrong, non-monotonic values oncethetawas large enough thatexp(-theta/2)underflowed belowNumber.EPSILON(e.g.DoublyNoncentralT(5, 5, 120).cdf(-1)returned1while.cdf(0)returned~1.5e-31) — the Poisson-mixture summation's leading term satisfiedrecursiveSum's default absolute-floor convergence check after a single iteration, the same failure mode previously fixed forDoublyNoncentralBeta(#1086/#1103). Fixed by passing{ useFloor: false }, the opt-outrecursiveSumgained for that earlier fix. Discovered, and the boundary-adjacentDoublyNoncentralT[5, 0, 120]precision-gate parameter set added, while extending #1143's boundary-grid methodology tof11's|z|=50dispatch threshold (issue #1189).ran.special.besselInu(nu, x)returnedInfinityfor very negative fractional order (e.g.nu = -1.5, -2.5, -3.3) atxnear the ~710 series-overflow boundary, even though the true value is a large but finite number (e.g.besselInu(-1.5, 709)returnedInfinityagainst an mpmath (dps=50) reference of~1.23e+306) — the internalrecursiveSumaccumulator representing the series sum before the(x/2)^nuprefactor is applied overflowed pastNumber.MAX_VALUE, since for very negativenuthat prefactor is tiny and the unnormalized sum must be proportionally larger to compensate.besselInunow uses a hand-written loop that rescales the running sum and current term in lockstep (mirroring_besselIBackward's existing overflow-guard pattern) whenever the sum approaches double overflow, tracking a log-scale offset combined into the final result only when a rescale actually occurred — preserving the original direct-multiplication precision for every case that never needs it, includingbesselKnu's connection-formula cancellation path (#1215).ran.test.hsic()andran.test.mannWhitney()silently mis-calibrated their Type-I error rate, discovered via new Monte Carlo calibration tests added while extending the hypothesis-test suite's rigor bar (#1229).hsic()fit a Gamma null approximation following Gretton et al.'shsicTestGamma.mreference, whosebparameter is computed in MATLAB's shape/scale convention (Gammamean= a*b), but passed it directly asran.dist.Gamma's rate parameter (mean= a/rate) without inverting it, and additionally queried the loweralpha-quantile instead of the upper(1-alpha)-quantile appropriate for HSIC's right-tailed test (large statistic implies dependence) — together these suppressed the empirical Type-I error to ~0% instead of the nominal 5% (200-trial simulation: 0/200 rejections under H0 before the fix). Now usesnew Gamma(a, 1 / b).q(1 - alpha); re-simulation gives 12/200 (6%, consistent with alpha=0.05).mannWhitney()compared its already-foldedU = min(U1, U2)statistic againstNormal(0,1).q(1 - 2*alpha), but a folded two-sided statistic's correct critical value is thealpha/2-tail (P(U1<=c or U2<=c) = 2*Phi((c-m)/s) = alphaimpliesz = q(1-alpha/2)) — the original formula inflated empirical Type-I error to ~17.5% (35/200 rejections under H0 before the fix). Now usesNormal(0,1).q(1 - alpha / 2); re-simulation gives 11/200 (5.5%). Both fixes are verified against the pre-existing seeded regression tests (hsic's dependent-data rejection,mannWhitney's same/different-distribution pass/reject cases), which are unaffected.ran.dist.DoublyNoncentralT.pdf(x)had large relative error (up to ~130x observed) wheneverx*mu < 0, even after #1207 replaced the unstable₁F₁recurrence in the same branch with directf11()calls. The branch summed a series that alternates sign whenx*mu < 0, accelerated viawynnEpsilon; series acceleration cannot recover precision already lost to cancellation between individual terms many orders of magnitude larger than the converged sum._pdf'sx*mu < 0branch now uses a new private_pdfPoissonMixture(x), the term-by-term derivative of the cancellation-free Poisson(theta/2)-mixture-of-noncentral-t formula_cdfalready uses — every term is a Poisson weight times a difference of twoNoncentralT.fnmCDF values, never an alternating-sign term. Thex*mu >= 0branch is unchanged. Seesolutions/correctness/2026-07-31-1300-doubly-noncentral-t-pdf-cancellation-x-mu-negative.md(#1235).test/precision-continuous.js'sNoncentralChi2([268, 64])quantile round-trip gate (qtol) was too tight at1e-13, consistently failing (measured ~1.015e-13-1.05e-13) under full-parallel-suitenpm testruns while passing in isolation — the same JIT-order-dependent floating-point summation-order sensitivity already documented for siblingmarcumQ-adjacent groups.qtolis now5e-13, matching the established tolerance already used forNoncentralChi2([5, 58]),NoncentralChi2([5, 62]),NoncentralChi2([270, 64]), andNoncentralChi([5, 7.5]); no reference value orpdf/cdftolerance changed.ran.process.AR1.variance(t)lost all significance for near-unit-rootphi(phi²just outside the existing1e-14special-case band) combined with small fractionalt(< 0.1):Math.pow(phi2, t)rounds to exactly1.0in double precision there, so1 - Math.pow(phi2, t)evaluated to exactly0instead of the true small positive variance — e.g.variance(1e-6)returned0instead of~1e-6forphi2 = 1 - 2e-14. A numerical sweep (phi2deltas1e-14–1e-1,tup to1e300) found this was the only real failure mode — the originally-suspected large-tscenario (negative/NaN variance) never occurred. Fixed by replacing1 - Math.pow(phi2, t)with the cancellation-safe-Math.expm1(t * Math.log(phi2)), matching the existingexpm1/log1pidiom used elsewhere in the codebase (e.g.ran.dist.Pareto,ran.dist.Weibull); the1e-14special case is unchanged (still required atphi2 === 1to avoid0/0) (#1243).ran.process.AR1.covariogram(s, t)carried a second, independent copy of the same1 - Math.pow(phi2, min(s, t))expression and was left unfixed by that pass; it failed identically, and was caught by the sweep run for #1244. SinceCov(X_t, X_t) = Var(X_t)by definition, the two methods openly disagreed: forphi2 = 1 - 2e-14,covariogram(1e-6, 1e-6)returned exactly0againstvariance(1e-6)'s correct~1e-6(100% error), andcovariogram(0.01, 0.01)was off by 11%. The same-Math.expm1(...)reformulation is now applied there, andcovariogram()gains themin(s, t) === 0fast pathvariance()already had att === 0— without it the reformulation would have turned0 * Math.log(phi2)intoNaNwheneverphi2underflows to0or overflows toInfinity, which the oldMath.pow(phi2, 0) === 1identity had made safe (covariogram(0, 3)forphi = 1e200returnedNaNeven before this change, sinceInfinity * -0is alreadyNaN). Trade-off, stated plainly:-expm1(n·log(x))amplifieslog's rounding error byn, so for a strongly explosive process at largemin(s, t)the new form is less accurate thanMath.powwas — e.g.phi = 1.5, s = t = 200moves from3.4e-17to9.6e-15relative error against an mpmathmp.dps=60reference. That is a deliberate exchange of ~2 digits in a regime whose value is already~1e70and diverging, for the elimination of a 100% error near the unit root;variance()has made the identical trade since #1243, and keeping both methods on one formulation is what makes theCov(t,t) = Var(t)identity hold exactly.ran.special.marcumQ/marcumP's_fc(nu, z)(the modified-Lentz continued fraction forI_nu(z)/I_{nu-1}(z), seeding themu < 135transition-band recurrence) silently returned an unconverged value oncezgrew past roughly 250-300, because its loop was capped at the sharedMAX_ITER = 100with no convergence check on exit — the required depth scales as~6.2*sqrt(z), not a constant, so e.g.NoncentralChi2(200, 2000).cdf(2080)(z ≈ 2038, needing 189 iterations) was off by5.1e-08relative instead of the library's usual~1e-14floor, and themu = 134/mu = 135transition-band boundary carried a six-orders-of-magnitude accuracy discontinuity (_largeMu, used formu >= 135, never calls_fcand was unaffected)._fcnow computes a regime-aware local iteration budget (Math.max(MAX_ITER, Math.ceil(7 * Math.sqrt(z)) + 20), stress-tested acrossnuin(0, 135)andzup to1e5with zero non-convergent cases) and throws if that budget is ever exceeded, rather than returning the unconverged value — matching the existing "throw on exceeded iteration budget" convention insrc/algorithms/rejection.js.NoncentralChi2(200, 2000).cdf(2080)now matches the mpmath (mp.dps=50) reference to1.5e-12relative, the same value an effectively-uncapped_fcproduces, confirming the residual is_recurrence's own pre-existing seed/amplification floor rather than further_fctruncation. Adds the large-x recurrence-regime precision-gate set (NoncentralChi2[76, 692]) that #1190/#1143 deliberately withheld until this fix landed. Seesolutions/special-functions/2026-08-02-1200-marcum-fc-slow-convergence.md(#1286).scripts/precision-refs-continuous.py'sexisting_groups()(dev-only tooling) separately gains a fix for a different pre-existing parsing failure surfaced while regenerating this gate: its brace-depth scan tracked every{/}character in the file including ones inside//comments (e.g. a comment referencing the JS snippet{ useFloor: false }), so a balanced brace pair inside a comment was misread as a whole REFS group, corrupting every span parsed after it. Comment-only lines are now blanked out before the structural scan runs.ran.special.besselISpherical's_hi(n, x)continued-fraction helper (whose iteration budget #1292 widened toMath.max(MAX_ITER, Math.ceil(7 * Math.sqrt(x)) + 20), described below) had no convergence check on loop exit, unlikemarcum-q.js's sibling_fc, which already throws via_assertFcConverged(#1286) instead of returning an unconverged value silently._hinow gains an equivalent_assertHiConvergedcheck, thrown when|del/h| > EPSafter the loop exits, matching the "throw on exceeded iteration budget" convention insrc/algorithms/rejection.js. No valid distribution parameterization in this codebase (NoncentralChi,NoncentralChi2, the only two distributions that call_hi, always with a non-negativesqrt(lambda*x)-derived argument) is known to reach non-convergence within the existing regime-aware budget; the check is a defensive availability guard against an extreme, currently-unreached caller-supplied argument (e.g.NoncentralChi2(...).pdf(1e12)) rather than a fix for an observed wrong value._hiand_fc's shared "budget grows withsqrt(argument), no fixed upper ceiling" design is now documented as an explicit accepted trade-off in both files, made safe by each throwing on non-convergence instead of truncating silently or running unbounded (#1311).besselISpherical/besselISphericalExpScaled(src/special/bessel.js) andDistribution.prototype.pdf()(src/dist/_distribution.js) now carry@throwsJSDoc documenting this exception where it is actually reachable by a caller —pdf()carries a single precise tag naming its narrow scope (NoncentralChi/NoncentralChi2, oddkonly) rather than repeating it acrosshazard()/lnPdf()/lnL()/aic()/bic(), which all callpdf()and inherit the same documented exception. See ADR-0049, which reconcilesthrow(overNaN) againstdecisions/0015-return-value-and-error-conventions.md: the continued fraction's true value is finite and well-defined for any valid argument, so a non-convergence is an algorithmic budget failure, not the mathematically-indeterminate caseNaNis reserved for (#1326).ran.dist.NoncentralChi2.pdf(x)/ran.dist.NoncentralChi.pdf(x)returnedNaNoncelambda * x(orlambda^2 * x^2forNoncentralChi) grew past roughly5e5— e.g.NoncentralChi2(100, 900).pdf(1000)(an ordinary parameterization evaluated near its own mean) — because_pdfcombined a log-space prefactor (exp(-0.5*(x+lambda)), which underflows to exactly0in this regime) with a linear-space Bessel factor (besselI/besselISphericalevaluated atsqrt(lambda*x), which overflows pastNumber.MAX_VALUEonce its argument exceeds ~710-720):0 * InfinityisNaNeven though the true density is an ordinary, representable double. The same class of bug as #1075'sDoublyNoncentralBetaoverflow.src/special/bessel.jsgains two exponentially-scaled accessors —besselIExpScaled(n, x) = exp(-|x|) * I_n(x)(reusing_besselIBackward's existing internal ratio, which is already this exact quantity before its final* exp(x)step) andbesselISphericalExpScaled(n, x) = exp(-x) * i_n(x)forx >= 0(a Wronskian rebuilt from_knRaw's un-normalized upward-recurrence values instead of_kn'sexp(-x)-scaled ones, so the exponent never has to be materialized and immediately inverted back out) — and both_pdfmethods now fold the Bessel argument's exponent into the existing log-space prefactor before exponentiating, relying on the identity-0.5*(x+lambda) + sqrt(lambda*x) = -0.5*(sqrt(x)-sqrt(lambda))^2 <= 0(AM-GM) to keep the combined exponent always finite. Reaching this newly-representable regime also exposed a second, previously-unreachable defect:_hi's continued fraction (used bybesselISpherical's Wronskian branch) shares the sameMAX_ITER = 100cap_fcwas fixed for above (#1286) and silently under-converged pastx ~ 250— it now uses the identical regime-aware budget,Math.max(MAX_ITER, Math.ceil(7 * Math.sqrt(x)) + 20).NoncentralChi2(100, 900).pdf(1000),NoncentralChi2(200, 2000).pdf(2080), andNoncentralChi(200, 44.7).pdf(45.6)now return finite values matching an independent Poisson-mixture cross-check and an mpmath (mp.dps=50) reference; existing small-lambda precision-gate values are unchanged (#1292). The same defect shape hitran.dist.Skellam.pdf(x):_pdfcombined a separateexpNeg = exp(-mu1-mu2)prefactor (underflowing to exactly0oncemu1+mu2 > ~745) withbesselI(|x|, twoSqrtProd)(overflowing toInfinityoncetwoSqrtProd = 2*sqrt(mu1*mu2) > ~709-720, a lower threshold thanmu1+mu2itself) — e.g.Skellam(360, 360).pdf(0)returnedInfinity(only the Bessel factor had overflowed) andSkellam(400, 400).pdf(0)returnedNaN(0 * Infinity, both factors past their threshold). The constructor's speed-up constants now precomputeexpNegScaled = exp(-mu1-mu2+twoSqrtProd), which stays in(0, 1]since-mu1-mu2+twoSqrtProd = -(sqrt(mu1)-sqrt(mu2))^2 <= 0always, and_pdfcombines it withbesselIExpScaled(|x|, twoSqrtProd)(added by #1292) instead of the unscaledbesselI.Skellam(360, 360).pdf(0),Skellam(400, 400).pdf(0), andSkellam(2000, 2000).pdf(0)now return finite values matching mpmath (mp.dps=50) to the project's1e-14precision-gate tolerance; existing small-mu precision-gate values are unchanged (#1309). The same defect shape also hitran.dist.VonMises(mu, kappa).pdf(x)/.cdf(x),NaNforkappapast roughly 710-720 — e.g.VonMises(0, 720).pdf(0),VonMises(0, 800).pdf(0.001),VonMises(0, 800).cdf(0.5)— becauseexp(kappa*cos(x-mu))andbesselI(0,kappa)both independently overflow toInfinitythere, and_cdf's Fourier series hit the identicalInfinity/Infinityin every term._pdfis rewritten asexp(kappa*(cos(x-mu)-1)) / (2*pi*besselIExpScaled(0,kappa)), whose numerator exponent is bounded<= 0bycos(x-mu) <= 1;_cdf's series envelope substitutesbesselIExpScaled(i,kappa)/(besselIExpScaled(0,kappa)*i)for the oldbesselI(i,kappa)/(besselI0Kappa*i), an algebraically exact substitution since the sharedexp(-kappa)factor cancels, preserving the existing oscillating-term-safe convergence check (solutions/correctness/2026-07-26-1339-vonmises-cdf-oscillating-term-premature-convergence.md) unchanged._cdf's return is now also clamped to[0, 1](Math.max(0, Math.min(1, ...)), the same guard already used innoncentral-beta.js), since0.5*(1+dx/pi) + sum/picancels twoO(1)terms and can round a few ULPs outside[0, 1]forxfar frommu— a pre-existing characteristic of that formula, only reachable now that largekappano longer immediately overflows toNaN(#1308). That cancellation is now fixed by #1320:_cdfno longer computes0.5*(1+dx/pi) + sum/piat all, replacing the Fourier series entirely with directtanhSinhquadrature of the already cancellation-free_pdfover the tail interval (using thepdf(mu+t) = pdf(mu-t)symmetry to always integrate on the side away from the density's peak atmu), which is monotonic by construction and accurate arbitrarily deep into the tail instead of merely clamped to[0, 1]— e.g.VonMises(0, 730).cdf(-0.357)now returns~4.29e-22(matching an mpmathmp.dps=50reference) instead of the previous2.78e-16cancellation noise that made.cdf(-0.355)come out below.cdf(-0.357)despite-0.355 > -0.357. Existingpdf/cdfprecision-gate values forkappain{0.5, 1, 2, 9, 11, 1000, 1500, 2000}are unchanged within their existing tolerances.