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 15 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 |
|
@avehtari I updated the signatures and posted stan-dev/stanc3#1626. Do you mind updating the tests for the new signatures (variadic rather than forcing the use of the 3 arrays)? I'm guessing claude would be decent at it |
| // Gauss-Kronrod does not pass a distance-to-boundary; the user functor | ||
| // still takes (x, xc) for signature compatibility with integrate_1d, but | ||
| // xc is unused here. | ||
| auto f_wrap = [&f](double x) { return f(x, NOT_A_NUMBER); }; | ||
|
|
There was a problem hiding this comment.
Is signature compatibility really worth a useless argument? I guess it is nice for people who are switching back and forth?
|
Jenkins Console Log Machine informationNo LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 20.04.3 LTS Release: 20.04 Codename: focalCPU: G++: Clang: |
|
I updated the PR text to mention the variadic API |
Closes #3000.
EDIT 2026-05-22: PR text updated to note variadic API
Summary
Adds two new explicit-name 1-D quadrature functions, both wrapping
existing Boost quadrature routines. 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 behavioural superset ofintegrate_1dat default settings: same DE dispatch, samexcsemantics, plus two new optional arguments (
absolute_tolerance,max_refinements) and the variadic-args API (see Public API below).integrate_1d_gauss_kronrodis a new function with the samevariadic-args API as
integrate_1d_double_exponential, an adaptiveGauss-Kronrod backend (K21, fixed order), and the same 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.
variadic arguments following the existing ODE-functions.
Public API
Each new function exposes two overloads in Stan-language, mirroring
the
ode_rk45/ode_rk45_tolconvention: a default-tolerance formand a
_tolform that takes the three tolerance / iteration knobsexplicitly. Both forms are variadic: any number of arguments of
any Stan type follow the fixed prefix, and are forwarded to the
integrand functor.
C++ user-facing form for both:
The integrand functor signature is variadic, matching the call:
This differs from
integrate_1d, whose user functor is forced into afixed
(x, xc, array[] real theta, array[] real x_r, array[] int x_i)shape; callers had to pack parameters and data into those three
containers. The variadic form lets users pass
real,int,vector,matrix,array[]of any of these, etc., directly.xcis meaningfulunder DE; under GK it is always passed as
NaN(Boost's gauss_kronrodhas no distance-to-boundary concept).
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
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.
both overloads on integrals with known closed forms. The GK
model uses
muas aparameters-block variable to exercise therev 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.
Companion PRs
functions (will be made after this PR has been approved).
Release notes
Two new explicit-name Boost 1D adaptive quadrature functions integrate_1d_double_exponential and integrate_1d_gauss_kronrod with three tolerance / max iterations arguments and variadic form.
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 were made using Claude AI Agent. The actual quadrature algorithm is in Boost library, and the code is just a wrapper to call Boost. Variadic code changes by @WardBrian, and test file function calls modified to follow new API by Claude.