add integrate_1d_gauss_kronrod and integrate_1d_double_exponential#3326
add integrate_1d_gauss_kronrod and integrate_1d_double_exponential#3326avehtari wants to merge 5 commits into
Conversation
|
Would it make sense to add a suffixed alias of the existing integrate_1d as part of this? I think we have done similar in the past when adding e.g. a second kind of algebra solver |
|
If we would add |
|
That would also be good, then. If I recall correctly these functions are also already ready to be variadic at the math level but just aren’t in the language, should we make these new versions variadic from the start? |
|
I' m confident I can get Claude to create |
|
In the existing code (and this PR, looking over it briefly) the _impl function already is variadic. So we would just rename that to drop the _impl, remove the adaptor struct wrapping F, and we should be good to go. I can help with this (and the stanc stuff, since it gets slightly more complicated to add a variadic), but I think it’s worth doing if we’re introducing new names anyway |
|
I can add |
|
Ah, didn't see your latest comment. I can add |
|
Sounds good. As long as claude follows the lead on the existing code, the extra steps to make it variadic should be very easy. The main reason it hasn’t been so far is that we need to come up with a new name for that version, which you’re already doing! |
… into integrate-1d-gauss-kronrod
|
Ok, added |
|
Ok. I will try to review tomorrow, but if not it will take a few days due to the long holiday weekend in the US. If changes are necessary for the signatures to be variadic in Stanc, would you like me to comment on the diff or just go ahead and make them? |
|
Just go ahead and make them so you can test right away |
Closes #3000.
Summary
Adds two new explicit-name 1-D quadrature functions, both wrapping
existing Boost quadrature routines (no new vendored code; Boost 1.87.0
already ships them). They cover disjoint integrand weaknesses and live
side by side with the existing
integrate_1d:integrate_1d(existing)abs_tolintegrate_1d_double_exponential(new)integrate_1dintegrate_1d_gauss_kronrod(new)gauss_kronrod<double, 21>integrate_1d_double_exponentialis a strict superset ofintegrate_1d: same DE dispatch, samexcsemantics, plus two newoptional arguments (
absolute_tolerance,max_refinements).integrate_1d_gauss_kronrodis a new function with the sameStan-facing API shape but adaptive Gauss-Kronrod backend (K21, fixed
order), optional arguments (
absolute_tolerance,max_depth).My use case is integrals of functions which are product of normal
density and a smooth function. Due to the light tails of the normal
density this functions goes to zero in tails and Gauss-Kronrod excels
compare to double exponential. I really needed this for some
experiments I'm running, and
abs_tolwas also required to get rid ofnumerical problems.
Why add these alongside
integrate_1d?endpoints; integrands with near-zero endpoints and a sharp peak
in the middle get undersampled. Gauss-Kronrod distributes nodes across the whole
interval by Legendre weights and picks up such peaks directly.
an explicit
absolute_toleranceargument; the convergence testbecomes
error <= max(rel_tol * L1, abs_tol). Withabsolute_tolerance = 0(default) this reduces to the strictpure-relative test of
integrate_1d. Withabs_tol > 0theuser can escape the pathological regime in which the strict
test is measuring accumulated floating-point round-off against
itself (e.g. failure mode of nested
integrate_1d_* in the deep tail of a Gaussian factor).
per-class refinement / bisection cap (
max_refinementsfor DE,max_depthfor GK) as an optional argument.integrate_1dcurrently uses Boost's per-class defaults implicitly with no way
to override.
Public API
Both functions expose four overloads in Stan-language, all dispatching
to the same C++ implementation in the relevant autodiff layer:
C++ user-facing form for both:
The user-supplied integrand functor signature is unchanged from
integrate_1d.xcis meaningful under DE; under GK it is alwayspassed as
NaN(Boost's gauss_kronrod has no distance-to-boundaryconcept).
Design notes
Gauss-Kronrod
conventions. Polynomial exactness of the K rule at N=21 is
degree 31, comfortable for Gaussian-times-likelihood shapes
that dominate Stan use. Higher N would tighten initial node
spacing on sharply peaked integrands at 1.5x-3x constant cost
on smooth integrals. The two robustness tools we already have
(
absolute_toleranceand informed bound choice) cover thefailure modes we have encountered; exposing N is a sensible
future extension but deliberately out of scope here.
absolute_toleranceargument. Boost's adaptiveGauss-Kronrod accumulates
error += error_localacrossbisection leaves, each carrying a
2 * eps * |K_local|floor.Accumulated round-off scales as
~2^max_depth * eps * L1. Forintegrals whose
L1falls below ~1e-8, the strictpure-relative test measures noise against itself; QUADPACK-style
mixed convergence is the established fix.
in tests (a
endpoint_singularity_throwstest asserts that GKthrows on
1/sqrt(x)and beta-with-small-shapes integrands).Users with such integrands keep using DE.
Double-exponential
absolute_toleranceapplies per piece of the zero-crossingsplit. The existing
integrate_1dworker splits integralsthat cross zero into two pieces (per Boost's exp_sinh docs); the
new convergence test applies independently to each piece. This
is the simplest interpretation of "abs_tol is the absolute error
floor we accept" and the right behaviour when one piece of the
split has near-zero L1.
max_refinementsdefault 15. Matches Boost'stanh_sinhdefault and
integrate_1d_gauss_kronrod'smax_depthforsymmetry. Boost's
exp_sinh/sinh_sinhdefaults are 9; theunified default of 15 here is mildly more conservative on
infinite-interval cases.
xcsemantics preserved. DE computes a meaningfulxc(distance to nearest boundary) and passes it to the user
functor exactly as
integrate_1ddoes. User code thatexploits
xccarries over unchanged.Practical guidance
integrate_1d_double_exponentialintegrate_1d_double_exponentialintegrate_1d_gauss_kronrodabs_tol > 0Implementation layout
The user-facing functor signature is adapted through the existing
integrate_1d_adapter(one adapter, three callers).stanc3 changes will be in a companion PR:
Testing
all passing. Tests for the new DE function are 1:1 clones of
integrate_1d's tests with the function name renamed (the newfunction is a strict superset at
abs_tol = 0); tests for GKare adapted from the same baseline with the endpoint-singular
cases marked as expected-throw and the gradient-endpoint-
log-singular PDFs (Beta, ChiSquare, Gamma, Weibull) omitted.
all four overloads on integrals with known closed forms. The
GK model uses
muas aparameters-block variable toexercise the rev autodiff specialisation across 200 MCMC
draws (returns exactly 1 every time).
integrate_1d_gauss_kronrod: thenested random-effects marginal likelihood from a Student-t hierarchical
linear model (144 observations, 4 chains x 100 draws) produces ELPD
estimates that agree with independent bridge sampling to
0.05 nats per fold at the 99th percentile. The existing
integrate_1dwassilently returning zeros.
Future work
integrate_1d_gauss_kronrod(aswitchover the six validBoost-supported N values). Deliberately out of scope here.
Companion PRs
Will be made after this PR has been approved
self-contained).
functions (deferred until this PR is merged).
Checklist
Stan Development Team. The code is duplication of existing integrate_1d code and there is no additional creativity.
the basic tests are passing
./runTests.py test/unit)make test-headers)make test-math-dependencies)make doxygen)make cpplint)the code is written in idiomatic C++ and changes are documented in the doxygen
the new changes are tested
AI Use Disclosure
The duplication of the existing
integrate_1dcode and tests was made using Claude AI Agent. The actual quadrature algorithm is in Boost library, and the code is just a wrapper to call Boost.