Skip to content

ENH: add iv_ratioinv - #211

Open
dschmitz89 wants to merge 6 commits into
scipy:mainfrom
dschmitz89:iv_ratio_inv
Open

ENH: add iv_ratioinv#211
dschmitz89 wants to merge 6 commits into
scipy:mainfrom
dschmitz89:iv_ratio_inv

Conversation

@dschmitz89

@dschmitz89 dschmitz89 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Reference issue

Towards scipy/scipy#20253

What does this implement/fix?

This PR implements the new function iv_ratioinv which solves $r=I_v(x)/I_{v-1}(x)$ for x given $r$ and $v$. This equation occurs in MLE of distributions such as von Mises-Fisher. Currently, an adhoc solution exists in SciPy.

Additional information

The function is a simple derivative free root finding procedure. Tight brackets for the root were taken from
this paper. In case those bounds fail, we fall back to the bracketing routine for monotonic functions. Once, bounds are found we run Chandrupatla's algorithm to find the exact root.

The SciPy issue contains a gradient based approach but I found this one easier to implement and assume that it is more robust as the objective can be near flat in the tails.

AI Generation Disclosure

LLMs helped inverting the bounds equations.

@github-actions github-actions Bot added the Enhancement New feature or request label Jul 10, 2026
@dschmitz89 dschmitz89 changed the title ENH: add iv_ratioinv ENH: add iv_ratioinv Jul 10, 2026
@dschmitz89

Copy link
Copy Markdown
Contributor Author

Cc @fancidev in case you want to take a look

Comment thread include/xsf/iv_ratio.h Outdated
Comment thread include/xsf/iv_ratio.h
// due to precision issues in iv_ratio or iv_ratio_c. This happens especially for very small or large r.
// Fallback: use bracket_root_for_cdf_inversion to find a valid bracket.
// Bracketing parameters taken from gdtrib, only difference: our function is increasing
auto [b_xl, b_xr, b_f_xl, b_f_xr, bracket_status] = detail::bracket_root_for_cdf_inversion(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should be renamed as it works with any monotonic function, not just CDFs.

@dschmitz89

Copy link
Copy Markdown
Contributor Author

@fbourgey Would you have time to review?

@fbourgey
fbourgey self-requested a review August 17, 2026 19:37

@fbourgey fbourgey left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When $v=\frac12$, it appears we have $I_{1/2}(x)/I_{-1/2}(x)=\tanh(x) = r$, hence $x = \text{atanh}(r)$.

Could you add some tests for $v=0.5$, ideally for the extreme tails?

I wonder if std::nextafter(1.0, 0.0) and std::nextafter(0.0, 1.0) pass?

Comment thread include/xsf/iv_ratio.h Outdated
dschmitz89 and others added 2 commits August 20, 2026 08:34
@dschmitz89

dschmitz89 commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

When v = 1 2 , it appears we have I 1 / 2 ( x ) / I − 1 / 2 ( x ) = tanh ⁡ ( x ) = r , hence x = atanh ( r ) .

Could you add some tests for v = 0.5 , ideally for the extreme tails?

I wonder if std::nextafter(1.0, 0.0) and std::nextafter(0.0, 1.0) pass?

They do not unfortunately pass a roundtrip test. I think it is not possible in double precision. Could you take one last look?

Edit: the iv_ratio function itself does not use the analytical method yet, will try that out.

Comment thread include/xsf/iv_ratio.h
Comment on lines +188 to +197
if (v == 0.5) {
// Closed-form solution for v = 0.5: iv_ratio(0.5, x) = tanh(x)
// Since tanh(x) = 2*expit(2*x) - 1 = r, we have expit(2*x) = (1+r)/2
// For r > 0.5, use logit((1+r)/2) for better stability near r=1
// For r <= 0.5, atanh is sufficiently stable
if (r > 0.5) {
return 0.5 * logit((1.0 + r) * 0.5);
} else {
return std::atanh(r);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (v == 0.5) {
// Closed-form solution for v = 0.5: iv_ratio(0.5, x) = tanh(x)
// Since tanh(x) = 2*expit(2*x) - 1 = r, we have expit(2*x) = (1+r)/2
// For r > 0.5, use logit((1+r)/2) for better stability near r=1
// For r <= 0.5, atanh is sufficiently stable
if (r > 0.5) {
return 0.5 * logit((1.0 + r) * 0.5);
} else {
return std::atanh(r);
}
if (v == 0.5) {
return std::atanh(r);
}

would not be enough?

On my machine os-arm64, iv_ratioinv(0.5, nextafter(1.0, 0.0)) gives inf while it should be 18.714973875118524. I think this is because (1+r)*0.5 rounds to 1 in that case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants