Fix modifiedBesselFunction_k at integer order and for large arguments - #2800
Merged
lballabio merged 1 commit intoSep 17, 2026
Merged
Conversation
modifiedBesselFunction_k at integer order and for large arguments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2785.
modifiedBesselFunction_kis built frompi/2 * (I_(-nu) - I_nu) / sin(nu*pi), which is correct analytically but fails in the two regimes described in the issue. This replaces the evaluation scheme in those two regimes and leaves it untouched elsewhere.Large argument
The issue attributes the
x >= 13zeros to cancellation. It is worth being precise, because the mechanism is stronger than that and explains why the failure is abrupt rather than gradual: for|x| >= 13,modifiedBesselFunction_i_impltakes its asymptotic branch, whose coefficients depend onnuonly throughnu*nu, andI<Real>::value()is0.0so thei*exp(i*nu*pi)term drops out.I_(-nu)andI_nuare therefore computed from identical arithmetic and the difference is exactly zero by construction, not merely ill-conditioned.Note this is specific to real arguments. For
std::complexthat term isirather than0, the two values genuinely differ, and the subtraction remains usable, which is why the complex overloads never produced the silent zero.Kis now summed directly from its own asymptotic series, which shares the coefficients already built in theIbranch but without the alternating sign:The existing
weight2LargeXcovers both overloads:exp(-x)unweighted andexp(-2x)weighted, matching the header'sf(x)*exp(-x)contract.The switch stays at 13, matching the
Ibranch. Lowering it to 10 measurably improves real arguments in[10, 13)(atx = 12, about3e-12against4e-7for the subtraction) but costs accuracy for complex arguments near the imaginary axis, where this series is less converged than the difference it would replace. That trade seemed worth leaving out of a bug fix.Integer order
K_nis computed from the integer-order series, A&S 9.6.11, when|nu - round(nu)|is below a threshold.Kis even innu, so negative integers useK_|n|.The issue notes this needs digamma. In fact only
psiat positive integers is required, wherepsi(1) = -gammaandpsi(m) = -gamma + sum_{j<m} 1/j, so no general digamma function is introduced.The threshold is
1e-9, chosen so the change never makes anything worse. Measured againstboost::math::cyl_bessel_katnu = 1 + delta, the crossover where the integer series becomes the more accurate of the two is about2.4e-9atx = 0.5and larger for largerx; sitting below the smallest crossover means the series is only used where it wins. This fixes the removable singularity and its immediate neighbourhood. It does not address the gradual near-integer loss the issue also measures atnu = 0.001, which needs an evaluation uniform innurather than a threshold.Tests
testModifiedBesselFunctionsandtestWeightedModifiedBesselFunctionsboth pass unchanged in substance, but the two weighted checks needed correcting: they computed their expected value by inlining the sameI_(-nu) - I_nuquotient, over loops steppingnufrom-5to5in halves, so at every integer order they compared the broken formula against itself and passed. The first-kind half of those same checks already compares against the publicmodifiedBesselFunction_i; the second-kind half now does the same.This may be relevant to #2432. If a test's reference is the implementation it is testing, a failure there is not evidence about tolerance.
testModifiedBesselFunctionsRegressionCasesis new: eight points fromboost::math, four at integer order and four past the old cliff. It is verified to fail onmasterwith exactly the values in the issue (-2.04e+16atnu = 1,0atx = 13). Its second-kind tolerance scales by|K|rather than bymax(|I|, |K|)as the existing block does, since out thereIis around1e11whileKis around1e-14and a bound taken from the larger accepts anything.Verification
Reproducer from the issue:
k(0, 2)nank(1, 2)-2.04e+16k(0.5, 14)0Sweep against
boost::math::cyl_bessel_k, 144 real points overnuin[-10, 10]andxin[0.1, 40]: worst relative error1.5e-07, atx = 8, which is below the switch and unchanged by this PR. Complex overload on the positive real axis: worst3.5e-11. Full test suite passes.One limitation worth recording: the asymptotic series needs
xlarge relative tonu, so fornuaround 30 atxnear 13 it is only good to about1e-6. That is inherent to the expansion and the existingIbranch shares it, but previously those points returned zero.