Skip to content

Fixed the issue by adding a fallback to bissection#2066

Closed
Cedriq1astaken wants to merge 9 commits into
JuliaStats:masterfrom
Cedriq1astaken:issue_2061
Closed

Fixed the issue by adding a fallback to bissection#2066
Cedriq1astaken wants to merge 9 commits into
JuliaStats:masterfrom
Cedriq1astaken:issue_2061

Conversation

@Cedriq1astaken

@Cedriq1astaken Cedriq1astaken commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

close #2061

Issue

The issue described by jherekhealy occurs in the quantile_newton method when dealing with extreme value near 0 or 1 (more often near 1). It fails to converge to a value due to floating point error and the default tolerance being too small resulting in either an infinite loop as it oscillates between values, or converging very slowly.

Fix

I modified quantile_newton, cquantile_newton, invlogcdf_newton, invlogccdf_newton by adding:

  • a new parameter maxiter (which by default = 10_000), which defines a maximum iteration before it falls back
  • A cycle detector that detects whenever a 2-cycles occurs

When either cases occur, quantile_newton falls back to quantile_bisect

Test

I wrote a Monte Carlo simulation to test extreme values near 1, [0.9999; 0.999999999999]

Here are the results for 1,000,000 samples at tol = 1e-12:

quantile_newton (no fallback)
  converged              859955   86.00%
  oscillation            140045   14.00%

quantile_newton (with bisection fallback)
  converged              1000000  100.00%

The test shows that the fallback correctly detects the 2-cycles and converges successfully every time instead of infinitely looping.

AI Assistance Disclosure

The debugging strategy, proposed fix, and interpretation of the numerical issue were developed by the author. An AI coding tool was used to help draft and refine parts of the implementation, tests, and PR text. All AI-assisted changes were reviewed, edited, and validated by the author before submission.

@codecov-commenter

codecov-commenter commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.75%. Comparing base (03ddda7) to head (7820dc3).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2066      +/-   ##
==========================================
+ Coverage   86.63%   86.75%   +0.11%     
==========================================
  Files         149      149              
  Lines        8921     8887      -34     
==========================================
- Hits         7729     7710      -19     
+ Misses       1192     1177      -15     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@andreasnoack

Copy link
Copy Markdown
Member

The solution here is too crude. The more fundamental problem is that the stopping rule doesn't take into account the granularity of the floating point numbers around the solution. Try to ask the AI to consider that and about how this is traditionally handled in quantile functions. I also think we should have a max_inter, but it should be something like 50 or 100, and if reached we should at least warn and possibly error. Ideally, you'd never hit it.

- Add magnitude-aware tolerance floor to prevent chasing floating-point granularity
- Reduce max_iter to 50 (proper stopping rule should rarely need it)
- Add clear warnings for non-convergence with fallback to bisection
- Detect 2-cycles explicitly as sign of ill-conditioning
@Cedriq1astaken

Copy link
Copy Markdown
Contributor Author

Hi, thanks for the comment,
I've done what you suggested and the AI suggested in response to scale stopping rule with the magnitude of x,
and add a granularity floor using cbrt(eps)^2 to avoid chasing precision we can't achieve.

Also dropped max_iter to 50 and added a fallback to bisection with warning when things don't converge cleanly.
Is that alright?

@devmotion

Copy link
Copy Markdown
Member

I think we should use Roots.jl. This would also easily allow us to use better bracketing root solvers such as ITP instead of vanilla bisection.

@Cedriq1astaken

Copy link
Copy Markdown
Contributor Author

Okay that makes sense., I switched the fallback to use Roots.jl with ITP instead.

@devmotion

Copy link
Copy Markdown
Member

My point was to generally make use of Roots. Being able to use ITP instead of vanilla bisection is just one benefit. Generally, the algorithms and numerical corner cases should be much better battle tested in Roots.

@Cedriq1astaken

Copy link
Copy Markdown
Contributor Author

Just to make sure I understand: are you suggesting the we replace the internal implementation of quantile_newton to just call Roots.jl instead of using the custom loop?"

@devmotion

Copy link
Copy Markdown
Member

Yes, that's my suggestion. We would tune only the options if the default ones are not appropriate.

Cedriq1astaken and others added 2 commits June 16, 2026 02:17
Delegates core root-finding logic in quantile_newton functions to Roots.find_zero using Newton(). Upgrades quantile_bisect to use the ITP() bracketing solver as suggested. Retains precision-based xrtol tolerance scaling.
@Cedriq1astaken

Copy link
Copy Markdown
Contributor Author

Okay, I've now refactored the core logic of quantile_newton and the others to use Roots.jl and I upgraded the quantile_bisect to use ITP too.

@devmotion

Copy link
Copy Markdown
Member

Before going through the code, can you add all currently failing quantile-related examples, such as #2061, #1611, #1571, #1999, #1807, #1869 (maybe there are more), as tests? Then it would be clearer whether the fixes are sufficient and we would avoid silent regressions in these cases.

@Cedriq1astaken

Copy link
Copy Markdown
Contributor Author

I added regression tests for the six quantile-related issues you listed. I found a couple of other quantile-related issues like #1415, but they don't seem related to the convergence issues this PR focuses on. Should I fix and include those too?

devmotion added a commit that referenced this pull request Jun 19, 2026
Building on #2066, simplify the quantile solvers to rely on Roots'
default tolerances instead of hand-tuned options:

- `quantile_bisect` delegates to `find_zero(g, (lx, rx), ITP())`. ITP's
  relative `xrtol = eps` converges even for brackets far from zero, where
  the previous absolute `cbrt(eps)^2` tolerance fell below the
  floating-point spacing and looped forever (#1611, #1807).
- The four `_newton` functions delegate to
  `find_zero((F, f), x, Newton(), ITP())`. Roots switches to the ITP
  bracketing method as soon as Newton brackets the root, which resolves
  the oscillation/stalling seen for extreme quantiles (#1571, #1898,
  #2061).

The `tol`/`max_iters` parameters and the explicit tolerance floors are
dropped: Roots' Newton defaults combine an absolute (`xatol = eps`) and a
relative (`xrtol = eps`) x-tolerance, so they converge both near `p ≈ 1`
and for roots near zero without bespoke floors.

`quantile_bisect` keeps only the exact `rx == lx` degenerate guard
(#1501); the approximate "collapsed bracket" handling is dropped, as a
non-bracketing interval is a separate concern from the solver choice.

Tests use `isapprox`'s type-appropriate defaults rather than fixed
tolerances, and are split one regression per testset. Adds coverage for
#1898 and a `Float32` round-trip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
devmotion added a commit that referenced this pull request Jun 19, 2026
Building on #2066, simplify the quantile solvers to rely on Roots'
default tolerances instead of hand-tuned options:

- `quantile_bisect` delegates to `find_zero(g, (lx, rx), ITP())`. ITP's
  relative `xrtol = eps` converges even for brackets far from zero, where
  the previous absolute `cbrt(eps)^2` tolerance fell below the
  floating-point spacing and looped forever (#1611, #1807).
- The four `_newton` functions delegate to
  `find_zero((F, f), x, Newton(), ITP())`. Roots switches to the ITP
  bracketing method as soon as Newton brackets the root, which resolves
  the oscillation/stalling seen for extreme quantiles (#1571, #1898,
  #2061).

The `tol`/`max_iters` parameters and the explicit tolerance floors are
dropped: Roots' Newton defaults combine an absolute (`xatol = eps`) and a
relative (`xrtol = eps`) x-tolerance, so they converge both near `p ≈ 1`
and for roots near zero without bespoke floors.

`quantile_bisect` keeps only the exact `rx == lx` degenerate guard
(#1501); the approximate "collapsed bracket" handling is dropped, as a
non-bracketing interval is a separate concern from the solver choice.

Tests use `isapprox`'s type-appropriate defaults rather than fixed
tolerances, and are split one regression per testset. Adds coverage for
#1898 and a `Float32` round-trip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
devmotion added a commit that referenced this pull request Jun 25, 2026
* Fixed the issue by adding a fallback to bissection

* Improve quantile_newton stopping criterion

- Add magnitude-aware tolerance floor to prevent chasing floating-point granularity
- Reduce max_iter to 50 (proper stopping rule should rarely need it)
- Add clear warnings for non-convergence with fallback to bisection
- Detect 2-cycles explicitly as sign of ill-conditioning

* Now uses ITP from Roots.jl for Newton quantile fallback

* Refactor quantile algorithms to use Roots.jl

Delegates core root-finding logic in quantile_newton functions to Roots.find_zero using Newton(). Upgrades quantile_bisect to use the ITP() bracketing solver as suggested. Retains precision-based xrtol tolerance scaling.

* - `quantile_bisect` now uses isapprox to check whether  lx and rx are equal
- the `chernoff` distribution now uses `quantile_newton` for none precomputed quantiles
- Added test cases for the following issues (#1571, #1611, #1807, #1869, #1999, #2061)

* Fix quantile bisection convergence for collapsed brackets

* Handle equal quantile bisection endpoints

* Use Roots defaults with a bracketing fallback for quantile root-finding

Building on #2066, simplify the quantile solvers to rely on Roots'
default tolerances instead of hand-tuned options:

- `quantile_bisect` delegates to `find_zero(g, (lx, rx), ITP())`. ITP's
  relative `xrtol = eps` converges even for brackets far from zero, where
  the previous absolute `cbrt(eps)^2` tolerance fell below the
  floating-point spacing and looped forever (#1611, #1807).
- The four `_newton` functions delegate to
  `find_zero((F, f), x, Newton(), ITP())`. Roots switches to the ITP
  bracketing method as soon as Newton brackets the root, which resolves
  the oscillation/stalling seen for extreme quantiles (#1571, #1898,
  #2061).

The `tol`/`max_iters` parameters and the explicit tolerance floors are
dropped: Roots' Newton defaults combine an absolute (`xatol = eps`) and a
relative (`xrtol = eps`) x-tolerance, so they converge both near `p ≈ 1`
and for roots near zero without bespoke floors.

`quantile_bisect` keeps only the exact `rx == lx` degenerate guard
(#1501); the approximate "collapsed bracket" handling is dropped, as a
non-bracketing interval is a separate concern from the solver choice.

Tests use `isapprox`'s type-appropriate defaults rather than fixed
tolerances, and are split one regression per testset. Adds coverage for
#1898 and a `Float32` round-trip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Bump version from 0.25.127 to 0.25.128

---------

Co-authored-by: cedri <desinorcedric3@gmail.com>
Co-authored-by: Cedriq1astaken <113813505+Cedriq1astaken@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@devmotion

Copy link
Copy Markdown
Member

Completed by #2070

@devmotion devmotion closed this Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

quantile(InverseGaussian(mu), timeval) infinite loop?

4 participants