Skip to main content

r/interpreter/native/
rmath.rs

1#![allow(clippy::not_unsafe_ptr_arg_deref)]
2//! Rmath — Statistical distribution functions and special math functions.
3//!
4//! Implements the R C API mathematical functions declared in `Rmath.h`.
5//! These are `extern "C"` functions resolved by package `.so` files at load time.
6//!
7//! Core special functions:
8//! - Regularized incomplete gamma function (lower P and upper Q)
9//! - Regularized incomplete beta function
10//! - Polygamma functions (digamma, trigamma, etc.)
11//!
12//! Distribution functions follow R's convention:
13//! - `d*(x, params..., give_log)` — density (PDF), optionally log
14//! - `p*(x, params..., lower_tail, log_p)` — distribution (CDF)
15//! - `q*(p, params..., lower_tail, log_p)` — quantile (inverse CDF)
16//! - `r*(params...)` — random variate
17
18use std::os::raw::c_int;
19
20// region: Constants
21
22const LN_SQRT_2PI: f64 = 0.918_938_533_204_672_8;
23const DBL_EPSILON: f64 = f64::EPSILON;
24
25fn r_finite(x: f64) -> bool {
26    x.is_finite()
27}
28
29/// Call the thread-local RNG from runtime.rs
30fn unif_rand() -> f64 {
31    super::runtime::unif_rand()
32}
33
34// endregion
35
36// region: Special functions
37
38/// Regularized lower incomplete gamma function P(a, x) = γ(a,x) / Γ(a).
39/// Uses series expansion for x < a+1, continued fraction otherwise.
40pub fn pgamma_raw(a: f64, x: f64) -> f64 {
41    if x <= 0.0 {
42        return 0.0;
43    }
44    if x.is_infinite() {
45        return 1.0;
46    }
47    if a <= 0.0 {
48        return 1.0;
49    }
50
51    if x < a + 1.0 {
52        // Series representation
53        gamma_series(a, x)
54    } else {
55        // Continued fraction representation
56        1.0 - gamma_cf(a, x)
57    }
58}
59
60/// Regularized upper incomplete gamma function Q(a, x) = 1 - P(a, x).
61pub fn qgamma_raw(a: f64, x: f64) -> f64 {
62    1.0 - pgamma_raw(a, x)
63}
64
65/// Series expansion for the regularized incomplete gamma function.
66fn gamma_series(a: f64, x: f64) -> f64 {
67    let ln_gamma_a = libm::lgamma(a);
68    let mut sum = 1.0 / a;
69    let mut term = 1.0 / a;
70    for n in 1..200 {
71        term *= x / (a + n as f64);
72        sum += term;
73        if term.abs() < sum.abs() * DBL_EPSILON {
74            break;
75        }
76    }
77    sum * (-x + a * x.ln() - ln_gamma_a).exp()
78}
79
80/// Continued fraction for the upper incomplete gamma function.
81fn gamma_cf(a: f64, x: f64) -> f64 {
82    let ln_gamma_a = libm::lgamma(a);
83    // Modified Lentz's method
84    let mut c = 1e-30_f64;
85    let mut d = 1.0 / (x + 1.0 - a);
86    let mut f = d;
87
88    for i in 1..200 {
89        let an = -(i as f64) * (i as f64 - a);
90        let bn = x + 2.0 * i as f64 + 1.0 - a;
91        d = bn + an * d;
92        if d.abs() < 1e-30 {
93            d = 1e-30;
94        }
95        c = bn + an / c;
96        if c.abs() < 1e-30 {
97            c = 1e-30;
98        }
99        d = 1.0 / d;
100        let delta = c * d;
101        f *= delta;
102        if (delta - 1.0).abs() < DBL_EPSILON {
103            break;
104        }
105    }
106    f * (-x + a * x.ln() - ln_gamma_a).exp()
107}
108
109/// Regularized incomplete beta function I_x(a, b).
110/// Uses continued fraction (Lentz's method).
111pub fn pbeta_raw(x: f64, a: f64, b: f64) -> f64 {
112    if x <= 0.0 {
113        return 0.0;
114    }
115    if x >= 1.0 {
116        return 1.0;
117    }
118    if a <= 0.0 || b <= 0.0 {
119        return f64::NAN;
120    }
121
122    // Use the symmetry relation for numerical stability:
123    // I_x(a,b) = 1 - I_{1-x}(b,a)
124    if x > (a + 1.0) / (a + b + 2.0) {
125        return 1.0 - pbeta_raw(1.0 - x, b, a);
126    }
127
128    let ln_beta = libm::lgamma(a) + libm::lgamma(b) - libm::lgamma(a + b);
129    let front = (a * x.ln() + b * (1.0 - x).ln() - ln_beta).exp() / a;
130
131    // Lentz's continued fraction
132    let mut f = 1.0 + beta_cf_term(1, a, b, x);
133    if !f.is_finite() || f == 0.0 {
134        f = 1e-30;
135    }
136    let mut c = f;
137    let mut d = 1.0;
138
139    for m in 2..200 {
140        let term = beta_cf_term(m, a, b, x);
141        d = 1.0 + term * d;
142        if d.abs() < 1e-30 {
143            d = 1e-30;
144        }
145        c = 1.0 + term / c;
146        if c.abs() < 1e-30 {
147            c = 1e-30;
148        }
149        d = 1.0 / d;
150        let delta = c * d;
151        f *= delta;
152        if (delta - 1.0).abs() < 3.0 * DBL_EPSILON {
153            break;
154        }
155    }
156
157    front * f
158}
159
160/// Terms for the continued fraction expansion of the incomplete beta function.
161fn beta_cf_term(m: i32, a: f64, b: f64, x: f64) -> f64 {
162    let m_f = m as f64;
163    if m % 2 == 0 {
164        let k = m_f / 2.0;
165        (k * (b - k) * x) / ((a + 2.0 * k - 1.0) * (a + 2.0 * k))
166    } else {
167        let k = (m_f - 1.0) / 2.0;
168        -((a + k) * (a + b + k) * x) / ((a + 2.0 * k) * (a + 2.0 * k + 1.0))
169    }
170}
171
172/// Trigamma function ψ₁(x) = d²/dx² ln Γ(x).
173pub fn trigamma(x: f64) -> f64 {
174    let mut x = x;
175    let mut result = 0.0;
176    // Shift to large x
177    while x < 6.0 {
178        result += 1.0 / (x * x);
179        x += 1.0;
180    }
181    // Asymptotic series
182    let x2 = 1.0 / (x * x);
183    result
184        + 1.0 / x
185        + x2 / 2.0
186        + x2 / x * (1.0 / 6.0 - x2 * (1.0 / 30.0 - x2 * (1.0 / 42.0 - x2 / 30.0)))
187}
188
189/// Tetragamma function ψ₂(x).
190pub fn tetragamma(x: f64) -> f64 {
191    let mut x = x;
192    let mut result = 0.0;
193    while x < 6.0 {
194        result -= 2.0 / (x * x * x);
195        x += 1.0;
196    }
197    let x2 = 1.0 / (x * x);
198    result - 1.0 / (x * x) - 1.0 / (x * x * x)
199        + x2 * x2 * (-1.0 / 3.0 + x2 * (1.0 / 5.0 - x2 * (1.0 / 7.0)))
200}
201
202/// Pentagamma function ψ₃(x).
203pub fn pentagamma(x: f64) -> f64 {
204    let mut x = x;
205    let mut result = 0.0;
206    while x < 6.0 {
207        result += 6.0 / (x * x * x * x);
208        x += 1.0;
209    }
210    let x2 = 1.0 / (x * x);
211    let x3 = x2 / x;
212    result + 2.0 * x3 + 3.0 * x2 * x2 + x2 * x3 * (2.0 + x2 * (-2.0 + x2 * (3.0)))
213}
214
215/// Psigamma: the m-th derivative of the digamma function.
216pub fn psigamma_fn(x: f64, deriv: f64) -> f64 {
217    let n = deriv as i32;
218    match n {
219        0 => digamma_fn(x),
220        1 => trigamma(x),
221        2 => tetragamma(x),
222        3 => pentagamma(x),
223        _ => f64::NAN,
224    }
225}
226
227/// Digamma function ψ(x) = d/dx ln Γ(x).
228pub fn digamma_fn(x: f64) -> f64 {
229    let mut x = x;
230    let mut result = 0.0;
231    while x < 6.0 {
232        result -= 1.0 / x;
233        x += 1.0;
234    }
235    result += x.ln() - 0.5 / x;
236    let x2 = 1.0 / (x * x);
237    result - x2 * (1.0 / 12.0 - x2 * (1.0 / 120.0 - x2 * (1.0 / 252.0 - x2 / 240.0)))
238}
239
240/// Beta function B(a,b) = Γ(a)Γ(b)/Γ(a+b).
241pub fn beta_fn(a: f64, b: f64) -> f64 {
242    (libm::lgamma(a) + libm::lgamma(b) - libm::lgamma(a + b)).exp()
243}
244
245/// Log-beta function ln B(a,b).
246pub fn lbeta_fn(a: f64, b: f64) -> f64 {
247    libm::lgamma(a) + libm::lgamma(b) - libm::lgamma(a + b)
248}
249
250/// Binomial coefficient choose(n, k).
251pub fn choose_fn(n: f64, k: f64) -> f64 {
252    if k < 0.0 || k > n {
253        return 0.0;
254    }
255    if k == 0.0 || k == n {
256        return 1.0;
257    }
258    // Use lgamma for large values
259    (libm::lgamma(n + 1.0) - libm::lgamma(k + 1.0) - libm::lgamma(n - k + 1.0)).exp()
260}
261
262/// Log of binomial coefficient.
263pub fn lchoose_fn(n: f64, k: f64) -> f64 {
264    if k < 0.0 || k > n {
265        return f64::NEG_INFINITY;
266    }
267    if k == 0.0 || k == n {
268        return 0.0;
269    }
270    libm::lgamma(n + 1.0) - libm::lgamma(k + 1.0) - libm::lgamma(n - k + 1.0)
271}
272
273/// log(1+x) - x, accurate for small x.
274pub fn log1pmx_fn(x: f64) -> f64 {
275    libm::log1p(x) - x
276}
277
278/// lgamma(1+a) for small a, using series expansion.
279pub fn lgamma1p_fn(a: f64) -> f64 {
280    libm::lgamma(1.0 + a)
281}
282
283/// log(exp(lx) + exp(ly)), computed in log-space for numerical stability.
284pub fn logspace_add_fn(lx: f64, ly: f64) -> f64 {
285    if lx > ly {
286        lx + libm::log1p((ly - lx).exp())
287    } else {
288        ly + libm::log1p((lx - ly).exp())
289    }
290}
291
292/// log(exp(lx) - exp(ly)), computed in log-space. Requires lx >= ly.
293pub fn logspace_sub_fn(lx: f64, ly: f64) -> f64 {
294    lx + libm::log1p(-(ly - lx).exp())
295}
296
297// endregion
298
299// region: Distribution helper
300
301/// Apply lower_tail and log_p transforms to a CDF value.
302fn p_transform(p: f64, lower_tail: c_int, log_p: c_int) -> f64 {
303    let p = if lower_tail != 0 { p } else { 1.0 - p };
304    if log_p != 0 {
305        p.ln()
306    } else {
307        p
308    }
309}
310
311/// Decode p from log_p / lower_tail for quantile functions.
312fn q_decode(p: f64, lower_tail: c_int, log_p: c_int) -> f64 {
313    let p = if log_p != 0 { p.exp() } else { p };
314    if lower_tail != 0 {
315        p
316    } else {
317        1.0 - p
318    }
319}
320
321/// Apply give_log to a density value.
322fn d_log(d: f64, give_log: c_int) -> f64 {
323    if give_log != 0 {
324        d.ln()
325    } else {
326        d
327    }
328}
329
330// endregion
331
332// region: Normal distribution
333
334#[no_mangle]
335pub extern "C" fn Rf_dnorm4(x: f64, mu: f64, sigma: f64, give_log: c_int) -> f64 {
336    if !r_finite(sigma) || sigma < 0.0 {
337        return f64::NAN;
338    }
339    if sigma == 0.0 {
340        return if x == mu {
341            f64::INFINITY
342        } else {
343            d_log(0.0, give_log)
344        };
345    }
346    let z = (x - mu) / sigma;
347    let d = (-0.5 * z * z - LN_SQRT_2PI - sigma.ln()).exp();
348    d_log(d, give_log)
349}
350
351#[no_mangle]
352pub extern "C" fn Rf_pnorm5(x: f64, mu: f64, sigma: f64, lower_tail: c_int, log_p: c_int) -> f64 {
353    if !r_finite(sigma) || sigma < 0.0 {
354        return f64::NAN;
355    }
356    if sigma == 0.0 {
357        return p_transform(if x < mu { 0.0 } else { 1.0 }, lower_tail, log_p);
358    }
359    let z = (x - mu) / sigma;
360    let p = 0.5 * libm::erfc(-z / std::f64::consts::SQRT_2);
361    p_transform(p, lower_tail, log_p)
362}
363
364#[no_mangle]
365pub extern "C" fn Rf_qnorm5(p: f64, mu: f64, sigma: f64, lower_tail: c_int, log_p: c_int) -> f64 {
366    if !r_finite(sigma) || sigma < 0.0 {
367        return f64::NAN;
368    }
369    let p = q_decode(p, lower_tail, log_p);
370    if !(0.0..=1.0).contains(&p) {
371        return f64::NAN;
372    }
373    if p == 0.0 {
374        return f64::NEG_INFINITY;
375    }
376    if p == 1.0 {
377        return f64::INFINITY;
378    }
379    // Rational approximation (Abramowitz & Stegun 26.2.23, refined by Peter Acklam)
380    mu + sigma * qnorm_standard(p)
381}
382
383/// Standard normal quantile function (inverse Φ).
384#[allow(clippy::excessive_precision)]
385fn qnorm_standard(p: f64) -> f64 {
386    // Rational approximation by Peter J. Acklam
387    const A: [f64; 6] = [
388        -3.969683028665376e+01,
389        2.209460984245205e+02,
390        -2.759285104469687e+02,
391        1.383577518672690e+02,
392        -3.066479806614716e+01,
393        2.506628277459239e+00,
394    ];
395    const B: [f64; 5] = [
396        -5.447609879822406e+01,
397        1.615858368580409e+02,
398        -1.556989798598866e+02,
399        6.680131188771972e+01,
400        -1.328068155288572e+01,
401    ];
402    const C: [f64; 6] = [
403        -7.784894002430293e-03,
404        -3.223964580411365e-01,
405        -2.400758277161838e+00,
406        -2.549732539343734e+00,
407        4.374664141464968e+00,
408        2.938163982698783e+00,
409    ];
410    const D: [f64; 4] = [
411        7.784695709041462e-03,
412        3.224671290700398e-01,
413        2.445134137142996e+00,
414        3.754408661907416e+00,
415    ];
416
417    const P_LOW: f64 = 0.02425;
418    const P_HIGH: f64 = 1.0 - P_LOW;
419
420    if p < P_LOW {
421        let q = (-2.0 * p.ln()).sqrt();
422        (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
423            / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
424    } else if p <= P_HIGH {
425        let q = p - 0.5;
426        let r = q * q;
427        (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
428            / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
429    } else {
430        let q = (-2.0 * (1.0 - p).ln()).sqrt();
431        -(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
432            / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
433    }
434}
435
436#[no_mangle]
437pub extern "C" fn Rf_rnorm(mu: f64, sigma: f64) -> f64 {
438    // Box-Muller using the interpreter's RNG would be ideal, but for the C API
439    // we fall back to a simple approach using unif_rand.
440    let u1 = unif_rand();
441    let u2 = unif_rand();
442    let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
443    mu + sigma * z
444}
445
446#[no_mangle]
447pub extern "C" fn Rf_pnorm_both(x: f64, cum: *mut f64, ccum: *mut f64, _lt: c_int, _lg: c_int) {
448    let p = 0.5 * libm::erfc(-x / std::f64::consts::SQRT_2);
449    if !cum.is_null() {
450        unsafe {
451            *cum = p;
452        }
453    }
454    if !ccum.is_null() {
455        unsafe {
456            *ccum = 1.0 - p;
457        }
458    }
459}
460
461// endregion
462
463// region: Uniform distribution
464
465#[no_mangle]
466pub extern "C" fn Rf_dunif(x: f64, a: f64, b: f64, give_log: c_int) -> f64 {
467    if b <= a {
468        return f64::NAN;
469    }
470    let d = if x < a || x > b { 0.0 } else { 1.0 / (b - a) };
471    d_log(d, give_log)
472}
473
474#[no_mangle]
475pub extern "C" fn Rf_punif(x: f64, a: f64, b: f64, lower_tail: c_int, log_p: c_int) -> f64 {
476    if b <= a {
477        return f64::NAN;
478    }
479    let p = if x <= a {
480        0.0
481    } else if x >= b {
482        1.0
483    } else {
484        (x - a) / (b - a)
485    };
486    p_transform(p, lower_tail, log_p)
487}
488
489#[no_mangle]
490pub extern "C" fn Rf_qunif(p: f64, a: f64, b: f64, lower_tail: c_int, log_p: c_int) -> f64 {
491    if b <= a {
492        return f64::NAN;
493    }
494    let p = q_decode(p, lower_tail, log_p);
495    if !(0.0..=1.0).contains(&p) {
496        return f64::NAN;
497    }
498    a + p * (b - a)
499}
500
501#[no_mangle]
502pub extern "C" fn Rf_runif(a: f64, b: f64) -> f64 {
503    let u = unif_rand();
504    a + u * (b - a)
505}
506
507// endregion
508
509// region: Exponential distribution
510
511#[no_mangle]
512pub extern "C" fn Rf_dexp(x: f64, scale: f64, give_log: c_int) -> f64 {
513    if scale <= 0.0 {
514        return f64::NAN;
515    }
516    let d = if x < 0.0 {
517        0.0
518    } else {
519        (-x / scale).exp() / scale
520    };
521    d_log(d, give_log)
522}
523
524#[no_mangle]
525pub extern "C" fn Rf_pexp(x: f64, scale: f64, lower_tail: c_int, log_p: c_int) -> f64 {
526    if scale <= 0.0 {
527        return f64::NAN;
528    }
529    let p = if x <= 0.0 {
530        0.0
531    } else {
532        -libm::expm1(-x / scale)
533    };
534    p_transform(p, lower_tail, log_p)
535}
536
537#[no_mangle]
538pub extern "C" fn Rf_qexp(p: f64, scale: f64, lower_tail: c_int, log_p: c_int) -> f64 {
539    if scale <= 0.0 {
540        return f64::NAN;
541    }
542    let p = q_decode(p, lower_tail, log_p);
543    if !(0.0..=1.0).contains(&p) {
544        return f64::NAN;
545    }
546    -scale * libm::log1p(-p)
547}
548
549#[no_mangle]
550pub extern "C" fn Rf_rexp(scale: f64) -> f64 {
551    let u = unif_rand();
552    -scale * u.ln()
553}
554
555// endregion
556
557// region: Gamma distribution
558
559#[no_mangle]
560pub extern "C" fn Rf_dgamma(x: f64, shape: f64, scale: f64, give_log: c_int) -> f64 {
561    if shape <= 0.0 || scale <= 0.0 {
562        return f64::NAN;
563    }
564    if x < 0.0 {
565        return d_log(0.0, give_log);
566    }
567    if x == 0.0 {
568        if shape < 1.0 {
569            return f64::INFINITY;
570        }
571        if shape == 1.0 {
572            return d_log(1.0 / scale, give_log);
573        }
574        return d_log(0.0, give_log);
575    }
576    let log_d = (shape - 1.0) * x.ln() - x / scale - shape * scale.ln() - libm::lgamma(shape);
577    if give_log != 0 {
578        log_d
579    } else {
580        log_d.exp()
581    }
582}
583
584#[no_mangle]
585pub extern "C" fn Rf_pgamma(
586    x: f64,
587    shape: f64,
588    scale: f64,
589    lower_tail: c_int,
590    log_p: c_int,
591) -> f64 {
592    if shape <= 0.0 || scale <= 0.0 {
593        return f64::NAN;
594    }
595    let p = pgamma_raw(shape, x / scale);
596    p_transform(p, lower_tail, log_p)
597}
598
599#[no_mangle]
600pub extern "C" fn Rf_qgamma(
601    p: f64,
602    shape: f64,
603    scale: f64,
604    lower_tail: c_int,
605    log_p: c_int,
606) -> f64 {
607    if shape <= 0.0 || scale <= 0.0 {
608        return f64::NAN;
609    }
610    let p = q_decode(p, lower_tail, log_p);
611    if !(0.0..=1.0).contains(&p) {
612        return f64::NAN;
613    }
614    if p == 0.0 {
615        return 0.0;
616    }
617    if p == 1.0 {
618        return f64::INFINITY;
619    }
620    // Newton's method starting from Wilson-Hilferty approximation
621    let z = qnorm_standard(p);
622    let mut x = shape * (1.0 - 1.0 / (9.0 * shape) + z / (3.0 * shape.sqrt())).powi(3);
623    if x <= 0.0 {
624        x = DBL_EPSILON;
625    }
626    for _ in 0..50 {
627        let px = pgamma_raw(shape, x);
628        let dx = Rf_dgamma(x, shape, 1.0, 0);
629        if dx <= 0.0 {
630            break;
631        }
632        let delta = (px - p) / dx;
633        x -= delta;
634        if x <= 0.0 {
635            x = DBL_EPSILON;
636        }
637        if delta.abs() < x * 1e-12 {
638            break;
639        }
640    }
641    x * scale
642}
643
644#[no_mangle]
645pub extern "C" fn Rf_rgamma(shape: f64, scale: f64) -> f64 {
646    // Marsaglia and Tsang's method for shape >= 1
647    if shape <= 0.0 || scale <= 0.0 {
648        return f64::NAN;
649    }
650    let (d_shape, boost) = if shape < 1.0 {
651        (shape + 1.0, true)
652    } else {
653        (shape, false)
654    };
655    let d = d_shape - 1.0 / 3.0;
656    let c = 1.0 / (9.0 * d).sqrt();
657    let x = loop {
658        let (z, v) = loop {
659            let z = Rf_rnorm(0.0, 1.0);
660            let v = 1.0 + c * z;
661            if v > 0.0 {
662                break (z, v * v * v);
663            }
664        };
665        let u = unif_rand();
666        if u < 1.0 - 0.0331 * (z * z) * (z * z) {
667            break d * v;
668        }
669        if u.ln() < 0.5 * z * z + d * (1.0 - v + v.ln()) {
670            break d * v;
671        }
672    };
673    if boost {
674        let u = unif_rand();
675        x * u.powf(1.0 / shape) * scale
676    } else {
677        x * scale
678    }
679}
680
681// endregion
682
683// region: Beta distribution
684
685#[no_mangle]
686pub extern "C" fn Rf_dbeta(x: f64, a: f64, b: f64, give_log: c_int) -> f64 {
687    if a <= 0.0 || b <= 0.0 {
688        return f64::NAN;
689    }
690    if !(0.0..=1.0).contains(&x) {
691        return d_log(0.0, give_log);
692    }
693    if x == 0.0 {
694        if a < 1.0 {
695            return f64::INFINITY;
696        }
697        if a == 1.0 {
698            return d_log(b, give_log);
699        }
700        return d_log(0.0, give_log);
701    }
702    if x == 1.0 {
703        if b < 1.0 {
704            return f64::INFINITY;
705        }
706        if b == 1.0 {
707            return d_log(a, give_log);
708        }
709        return d_log(0.0, give_log);
710    }
711    let log_d = (a - 1.0) * x.ln() + (b - 1.0) * (1.0 - x).ln() - lbeta_fn(a, b);
712    if give_log != 0 {
713        log_d
714    } else {
715        log_d.exp()
716    }
717}
718
719#[no_mangle]
720pub extern "C" fn Rf_pbeta(x: f64, a: f64, b: f64, lower_tail: c_int, log_p: c_int) -> f64 {
721    if a <= 0.0 || b <= 0.0 {
722        return f64::NAN;
723    }
724    let p = pbeta_raw(x, a, b);
725    p_transform(p, lower_tail, log_p)
726}
727
728#[no_mangle]
729pub extern "C" fn Rf_qbeta(p: f64, a: f64, b: f64, lower_tail: c_int, log_p: c_int) -> f64 {
730    if a <= 0.0 || b <= 0.0 {
731        return f64::NAN;
732    }
733    let p = q_decode(p, lower_tail, log_p);
734    if !(0.0..=1.0).contains(&p) {
735        return f64::NAN;
736    }
737    if p == 0.0 {
738        return 0.0;
739    }
740    if p == 1.0 {
741        return 1.0;
742    }
743    // Newton's method
744    let mut x = p; // initial guess
745    for _ in 0..100 {
746        let px = pbeta_raw(x, a, b);
747        let dx = Rf_dbeta(x, a, b, 0);
748        if dx <= 0.0 || !dx.is_finite() {
749            break;
750        }
751        let delta = (px - p) / dx;
752        x -= delta;
753        x = x.clamp(DBL_EPSILON, 1.0 - DBL_EPSILON);
754        if delta.abs() < x * 1e-12 {
755            break;
756        }
757    }
758    x
759}
760
761#[no_mangle]
762pub extern "C" fn Rf_rbeta(a: f64, b: f64) -> f64 {
763    let x = Rf_rgamma(a, 1.0);
764    let y = Rf_rgamma(b, 1.0);
765    x / (x + y)
766}
767
768// endregion
769
770// region: Chi-squared distribution (special case of Gamma)
771
772#[no_mangle]
773pub extern "C" fn Rf_dchisq(x: f64, df: f64, give_log: c_int) -> f64 {
774    Rf_dgamma(x, df / 2.0, 2.0, give_log)
775}
776
777#[no_mangle]
778pub extern "C" fn Rf_pchisq(x: f64, df: f64, lower_tail: c_int, log_p: c_int) -> f64 {
779    Rf_pgamma(x, df / 2.0, 2.0, lower_tail, log_p)
780}
781
782#[no_mangle]
783pub extern "C" fn Rf_qchisq(p: f64, df: f64, lower_tail: c_int, log_p: c_int) -> f64 {
784    Rf_qgamma(p, df / 2.0, 2.0, lower_tail, log_p)
785}
786
787#[no_mangle]
788pub extern "C" fn Rf_rchisq(df: f64) -> f64 {
789    Rf_rgamma(df / 2.0, 2.0)
790}
791
792// Non-central chi-squared
793#[no_mangle]
794pub extern "C" fn Rf_dnchisq(x: f64, df: f64, ncp: f64, give_log: c_int) -> f64 {
795    if df <= 0.0 || ncp < 0.0 {
796        return f64::NAN;
797    }
798    if x < 0.0 {
799        return d_log(0.0, give_log);
800    }
801    // Approximate: sum of Poisson-weighted chi-squared densities
802    let lambda = ncp / 2.0;
803    let max_terms = 100.min((lambda + 20.0 * lambda.sqrt()) as usize + 10);
804    let mut sum = 0.0;
805    let mut poisson_weight = (-lambda).exp();
806    for j in 0..max_terms {
807        let chi_df = df + 2.0 * j as f64;
808        let chi_dens = Rf_dgamma(x, chi_df / 2.0, 2.0, 0);
809        sum += poisson_weight * chi_dens;
810        poisson_weight *= lambda / (j as f64 + 1.0);
811    }
812    d_log(sum, give_log)
813}
814
815#[no_mangle]
816pub extern "C" fn Rf_pnchisq(x: f64, df: f64, ncp: f64, lower_tail: c_int, log_p: c_int) -> f64 {
817    if df <= 0.0 || ncp < 0.0 {
818        return f64::NAN;
819    }
820    if x <= 0.0 {
821        return p_transform(0.0, lower_tail, log_p);
822    }
823    let lambda = ncp / 2.0;
824    let max_terms = 100.min((lambda + 20.0 * lambda.sqrt()) as usize + 10);
825    let mut sum = 0.0;
826    let mut poisson_weight = (-lambda).exp();
827    for j in 0..max_terms {
828        let chi_df = df + 2.0 * j as f64;
829        let chi_cdf = pgamma_raw(chi_df / 2.0, x / 2.0);
830        sum += poisson_weight * chi_cdf;
831        poisson_weight *= lambda / (j as f64 + 1.0);
832    }
833    p_transform(sum, lower_tail, log_p)
834}
835
836#[no_mangle]
837pub extern "C" fn Rf_qnchisq(p: f64, df: f64, ncp: f64, lower_tail: c_int, log_p: c_int) -> f64 {
838    let p = q_decode(p, lower_tail, log_p);
839    if !(0.0..=1.0).contains(&p) || df <= 0.0 || ncp < 0.0 {
840        return f64::NAN;
841    }
842    // Newton's method starting from chi-squared quantile
843    let mut x = Rf_qchisq(p, df + ncp, 1, 0);
844    if x <= 0.0 {
845        x = df + ncp;
846    }
847    for _ in 0..50 {
848        let px = Rf_pnchisq(x, df, ncp, 1, 0);
849        let dx = Rf_dnchisq(x, df, ncp, 0);
850        if dx <= 0.0 {
851            break;
852        }
853        let delta = (px - p) / dx;
854        x -= delta;
855        if x <= 0.0 {
856            x = DBL_EPSILON;
857        }
858        if delta.abs() < x * 1e-12 {
859            break;
860        }
861    }
862    x
863}
864
865#[no_mangle]
866pub extern "C" fn Rf_rnchisq(df: f64, ncp: f64) -> f64 {
867    // Sum of squared normals
868    if ncp == 0.0 {
869        return Rf_rchisq(df);
870    }
871    let z = Rf_rnorm(ncp.sqrt(), 1.0);
872    z * z + Rf_rchisq(df - 1.0)
873}
874
875// endregion
876
877// region: Student t distribution
878
879#[no_mangle]
880pub extern "C" fn Rf_dt(x: f64, df: f64, give_log: c_int) -> f64 {
881    if df <= 0.0 {
882        return f64::NAN;
883    }
884    let log_d = libm::lgamma((df + 1.0) / 2.0)
885        - libm::lgamma(df / 2.0)
886        - 0.5 * (df * std::f64::consts::PI).ln()
887        - ((df + 1.0) / 2.0) * (1.0 + x * x / df).ln();
888    if give_log != 0 {
889        log_d
890    } else {
891        log_d.exp()
892    }
893}
894
895#[no_mangle]
896pub extern "C" fn Rf_pt(x: f64, df: f64, lower_tail: c_int, log_p: c_int) -> f64 {
897    if df <= 0.0 {
898        return f64::NAN;
899    }
900    // Use incomplete beta: P(t <= x | df) = I_{df/(df+x²)}(df/2, 1/2) when x < 0
901    let t2 = x * x;
902    let p = pbeta_raw(df / (df + t2), df / 2.0, 0.5);
903    let p = if x <= 0.0 { p / 2.0 } else { 1.0 - p / 2.0 };
904    p_transform(p, lower_tail, log_p)
905}
906
907#[no_mangle]
908pub extern "C" fn Rf_qt(p: f64, df: f64, lower_tail: c_int, log_p: c_int) -> f64 {
909    if df <= 0.0 {
910        return f64::NAN;
911    }
912    let p = q_decode(p, lower_tail, log_p);
913    if !(0.0..=1.0).contains(&p) {
914        return f64::NAN;
915    }
916    if p == 0.0 {
917        return f64::NEG_INFINITY;
918    }
919    if p == 1.0 {
920        return f64::INFINITY;
921    }
922    if p == 0.5 {
923        return 0.0;
924    }
925    // Newton's method from normal approximation
926    let mut x = qnorm_standard(p);
927    for _ in 0..50 {
928        let px = Rf_pt(x, df, 1, 0);
929        let dx = Rf_dt(x, df, 0);
930        if dx <= 0.0 {
931            break;
932        }
933        let delta = (px - p) / dx;
934        x -= delta;
935        if delta.abs() < x.abs() * 1e-12 {
936            break;
937        }
938    }
939    x
940}
941
942#[no_mangle]
943pub extern "C" fn Rf_rt(df: f64) -> f64 {
944    Rf_rnorm(0.0, 1.0) / (Rf_rchisq(df) / df).sqrt()
945}
946
947// endregion
948
949// region: F distribution
950
951#[no_mangle]
952pub extern "C" fn Rf_df(x: f64, df1: f64, df2: f64, give_log: c_int) -> f64 {
953    if df1 <= 0.0 || df2 <= 0.0 {
954        return f64::NAN;
955    }
956    if x < 0.0 {
957        return d_log(0.0, give_log);
958    }
959    if x == 0.0 {
960        if df1 < 2.0 {
961            return f64::INFINITY;
962        }
963        if df1 == 2.0 {
964            return d_log(1.0, give_log);
965        }
966        return d_log(0.0, give_log);
967    }
968    let log_d = (df1 / 2.0) * (df1 / df2).ln() + (df1 / 2.0 - 1.0) * x.ln()
969        - lbeta_fn(df1 / 2.0, df2 / 2.0)
970        - ((df1 + df2) / 2.0) * (1.0 + df1 * x / df2).ln();
971    if give_log != 0 {
972        log_d
973    } else {
974        log_d.exp()
975    }
976}
977
978#[no_mangle]
979pub extern "C" fn Rf_pf(x: f64, df1: f64, df2: f64, lower_tail: c_int, log_p: c_int) -> f64 {
980    if df1 <= 0.0 || df2 <= 0.0 {
981        return f64::NAN;
982    }
983    if x <= 0.0 {
984        return p_transform(0.0, lower_tail, log_p);
985    }
986    let v = df1 * x / (df1 * x + df2);
987    let p = pbeta_raw(v, df1 / 2.0, df2 / 2.0);
988    p_transform(p, lower_tail, log_p)
989}
990
991#[no_mangle]
992pub extern "C" fn Rf_qf(p: f64, df1: f64, df2: f64, lower_tail: c_int, log_p: c_int) -> f64 {
993    if df1 <= 0.0 || df2 <= 0.0 {
994        return f64::NAN;
995    }
996    let p = q_decode(p, lower_tail, log_p);
997    if !(0.0..=1.0).contains(&p) {
998        return f64::NAN;
999    }
1000    if p == 0.0 {
1001        return 0.0;
1002    }
1003    if p == 1.0 {
1004        return f64::INFINITY;
1005    }
1006    // Use qbeta: if X ~ Beta(a,b) then X/(1-X)*b/a ~ F(2a,2b)
1007    let bq = Rf_qbeta(p, df1 / 2.0, df2 / 2.0, 1, 0);
1008    (df2 * bq) / (df1 * (1.0 - bq))
1009}
1010
1011#[no_mangle]
1012pub extern "C" fn Rf_rf(df1: f64, df2: f64) -> f64 {
1013    (Rf_rchisq(df1) / df1) / (Rf_rchisq(df2) / df2)
1014}
1015
1016// endregion
1017
1018// region: Lognormal distribution
1019
1020#[no_mangle]
1021pub extern "C" fn Rf_dlnorm(x: f64, meanlog: f64, sdlog: f64, give_log: c_int) -> f64 {
1022    if sdlog <= 0.0 {
1023        return f64::NAN;
1024    }
1025    if x <= 0.0 {
1026        return d_log(0.0, give_log);
1027    }
1028    let z = (x.ln() - meanlog) / sdlog;
1029    let log_d = -0.5 * z * z - LN_SQRT_2PI - sdlog.ln() - x.ln();
1030    if give_log != 0 {
1031        log_d
1032    } else {
1033        log_d.exp()
1034    }
1035}
1036
1037#[no_mangle]
1038pub extern "C" fn Rf_plnorm(
1039    x: f64,
1040    meanlog: f64,
1041    sdlog: f64,
1042    lower_tail: c_int,
1043    log_p: c_int,
1044) -> f64 {
1045    if sdlog <= 0.0 {
1046        return f64::NAN;
1047    }
1048    if x <= 0.0 {
1049        return p_transform(0.0, lower_tail, log_p);
1050    }
1051    Rf_pnorm5(x.ln(), meanlog, sdlog, lower_tail, log_p)
1052}
1053
1054#[no_mangle]
1055pub extern "C" fn Rf_qlnorm(
1056    p: f64,
1057    meanlog: f64,
1058    sdlog: f64,
1059    lower_tail: c_int,
1060    log_p: c_int,
1061) -> f64 {
1062    Rf_qnorm5(p, meanlog, sdlog, lower_tail, log_p).exp()
1063}
1064
1065#[no_mangle]
1066pub extern "C" fn Rf_rlnorm(meanlog: f64, sdlog: f64) -> f64 {
1067    Rf_rnorm(meanlog, sdlog).exp()
1068}
1069
1070// endregion
1071
1072// region: Cauchy distribution
1073
1074#[no_mangle]
1075pub extern "C" fn Rf_dcauchy(x: f64, location: f64, scale: f64, give_log: c_int) -> f64 {
1076    if scale <= 0.0 {
1077        return f64::NAN;
1078    }
1079    let z = (x - location) / scale;
1080    let d = 1.0 / (std::f64::consts::PI * scale * (1.0 + z * z));
1081    d_log(d, give_log)
1082}
1083
1084#[no_mangle]
1085pub extern "C" fn Rf_pcauchy(
1086    x: f64,
1087    location: f64,
1088    scale: f64,
1089    lower_tail: c_int,
1090    log_p: c_int,
1091) -> f64 {
1092    if scale <= 0.0 {
1093        return f64::NAN;
1094    }
1095    let p = 0.5 + libm::atan((x - location) / scale) / std::f64::consts::PI;
1096    p_transform(p, lower_tail, log_p)
1097}
1098
1099#[no_mangle]
1100pub extern "C" fn Rf_qcauchy(
1101    p: f64,
1102    location: f64,
1103    scale: f64,
1104    lower_tail: c_int,
1105    log_p: c_int,
1106) -> f64 {
1107    if scale <= 0.0 {
1108        return f64::NAN;
1109    }
1110    let p = q_decode(p, lower_tail, log_p);
1111    if !(0.0..=1.0).contains(&p) {
1112        return f64::NAN;
1113    }
1114    location + scale * libm::tan(std::f64::consts::PI * (p - 0.5))
1115}
1116
1117#[no_mangle]
1118pub extern "C" fn Rf_rcauchy(location: f64, scale: f64) -> f64 {
1119    let u = unif_rand();
1120    location + scale * libm::tan(std::f64::consts::PI * (u - 0.5))
1121}
1122
1123// endregion
1124
1125// region: Weibull distribution
1126
1127#[no_mangle]
1128pub extern "C" fn Rf_dweibull(x: f64, shape: f64, scale: f64, give_log: c_int) -> f64 {
1129    if shape <= 0.0 || scale <= 0.0 {
1130        return f64::NAN;
1131    }
1132    if x < 0.0 {
1133        return d_log(0.0, give_log);
1134    }
1135    if x == 0.0 {
1136        if shape < 1.0 {
1137            return f64::INFINITY;
1138        }
1139        if shape == 1.0 {
1140            return d_log(1.0 / scale, give_log);
1141        }
1142        return d_log(0.0, give_log);
1143    }
1144    let z = x / scale;
1145    let log_d = (shape - 1.0) * z.ln() + shape.ln() - shape * scale.ln() - z.powf(shape);
1146    if give_log != 0 {
1147        log_d
1148    } else {
1149        log_d.exp()
1150    }
1151}
1152
1153#[no_mangle]
1154pub extern "C" fn Rf_pweibull(
1155    x: f64,
1156    shape: f64,
1157    scale: f64,
1158    lower_tail: c_int,
1159    log_p: c_int,
1160) -> f64 {
1161    if shape <= 0.0 || scale <= 0.0 {
1162        return f64::NAN;
1163    }
1164    if x <= 0.0 {
1165        return p_transform(0.0, lower_tail, log_p);
1166    }
1167    let p = -libm::expm1(-(x / scale).powf(shape));
1168    p_transform(p, lower_tail, log_p)
1169}
1170
1171#[no_mangle]
1172pub extern "C" fn Rf_qweibull(
1173    p: f64,
1174    shape: f64,
1175    scale: f64,
1176    lower_tail: c_int,
1177    log_p: c_int,
1178) -> f64 {
1179    if shape <= 0.0 || scale <= 0.0 {
1180        return f64::NAN;
1181    }
1182    let p = q_decode(p, lower_tail, log_p);
1183    if !(0.0..=1.0).contains(&p) {
1184        return f64::NAN;
1185    }
1186    scale * (-libm::log1p(-p)).powf(1.0 / shape)
1187}
1188
1189#[no_mangle]
1190pub extern "C" fn Rf_rweibull(shape: f64, scale: f64) -> f64 {
1191    let u = unif_rand();
1192    scale * (-u.ln()).powf(1.0 / shape)
1193}
1194
1195// endregion
1196
1197// region: Logistic distribution
1198
1199#[no_mangle]
1200pub extern "C" fn Rf_dlogis(x: f64, location: f64, scale: f64, give_log: c_int) -> f64 {
1201    if scale <= 0.0 {
1202        return f64::NAN;
1203    }
1204    let z = (x - location) / scale;
1205    let e = (-z.abs()).exp();
1206    let d = e / (scale * (1.0 + e) * (1.0 + e));
1207    d_log(d, give_log)
1208}
1209
1210#[no_mangle]
1211pub extern "C" fn Rf_plogis(
1212    x: f64,
1213    location: f64,
1214    scale: f64,
1215    lower_tail: c_int,
1216    log_p: c_int,
1217) -> f64 {
1218    if scale <= 0.0 {
1219        return f64::NAN;
1220    }
1221    let z = (x - location) / scale;
1222    let p = 1.0 / (1.0 + (-z).exp());
1223    p_transform(p, lower_tail, log_p)
1224}
1225
1226#[no_mangle]
1227pub extern "C" fn Rf_qlogis(
1228    p: f64,
1229    location: f64,
1230    scale: f64,
1231    lower_tail: c_int,
1232    log_p: c_int,
1233) -> f64 {
1234    if scale <= 0.0 {
1235        return f64::NAN;
1236    }
1237    let p = q_decode(p, lower_tail, log_p);
1238    if !(0.0..=1.0).contains(&p) {
1239        return f64::NAN;
1240    }
1241    location + scale * (p / (1.0 - p)).ln()
1242}
1243
1244#[no_mangle]
1245pub extern "C" fn Rf_rlogis(location: f64, scale: f64) -> f64 {
1246    let u = unif_rand();
1247    location + scale * (u / (1.0 - u)).ln()
1248}
1249
1250// endregion
1251
1252// region: Poisson distribution
1253
1254#[no_mangle]
1255pub extern "C" fn Rf_dpois(x: f64, lambda: f64, give_log: c_int) -> f64 {
1256    if lambda < 0.0 {
1257        return f64::NAN;
1258    }
1259    if x < 0.0 || x != x.floor() {
1260        return d_log(0.0, give_log);
1261    }
1262    let k = x as i64;
1263    let log_d = k as f64 * lambda.ln() - lambda - libm::lgamma(x + 1.0);
1264    if give_log != 0 {
1265        log_d
1266    } else {
1267        log_d.exp()
1268    }
1269}
1270
1271#[no_mangle]
1272pub extern "C" fn Rf_ppois(x: f64, lambda: f64, lower_tail: c_int, log_p: c_int) -> f64 {
1273    if lambda < 0.0 {
1274        return f64::NAN;
1275    }
1276    if x < 0.0 {
1277        return p_transform(0.0, lower_tail, log_p);
1278    }
1279    let k = x.floor();
1280    // P(X <= k) = Q(k+1, lambda) = upper regularized incomplete gamma
1281    let p = qgamma_raw(k + 1.0, lambda);
1282    p_transform(p, lower_tail, log_p)
1283}
1284
1285#[no_mangle]
1286pub extern "C" fn Rf_qpois(p: f64, lambda: f64, lower_tail: c_int, log_p: c_int) -> f64 {
1287    if lambda < 0.0 {
1288        return f64::NAN;
1289    }
1290    let p = q_decode(p, lower_tail, log_p);
1291    if !(0.0..=1.0).contains(&p) {
1292        return f64::NAN;
1293    }
1294    if p == 0.0 {
1295        return 0.0;
1296    }
1297    if p == 1.0 {
1298        return f64::INFINITY;
1299    }
1300    // Search: start from mean, step by 1
1301    let mut k = (lambda + 0.5).floor();
1302    loop {
1303        let pk = Rf_ppois(k, lambda, 1, 0);
1304        if pk >= p {
1305            break;
1306        }
1307        k += 1.0;
1308        if k > 1e15 {
1309            return f64::INFINITY;
1310        }
1311    }
1312    // Step back to find exact quantile
1313    while k > 0.0 {
1314        let pk = Rf_ppois(k - 1.0, lambda, 1, 0);
1315        if pk < p {
1316            break;
1317        }
1318        k -= 1.0;
1319    }
1320    k
1321}
1322
1323#[no_mangle]
1324pub extern "C" fn Rf_rpois(lambda: f64) -> f64 {
1325    if lambda <= 0.0 {
1326        return 0.0;
1327    }
1328    // Knuth's algorithm for small lambda
1329    if lambda < 30.0 {
1330        let l = (-lambda).exp();
1331        let mut k = 0.0;
1332        let mut p = 1.0;
1333        loop {
1334            k += 1.0;
1335            p *= unif_rand();
1336            if p <= l {
1337                return k - 1.0;
1338            }
1339        }
1340    }
1341    // For large lambda, use rejection method based on normal approximation
1342    loop {
1343        let x = Rf_rnorm(lambda, lambda.sqrt());
1344        let k = (x + 0.5).floor();
1345        if k >= 0.0 {
1346            return k;
1347        }
1348    }
1349}
1350
1351// endregion
1352
1353// region: Binomial distribution
1354
1355#[no_mangle]
1356pub extern "C" fn Rf_dbinom(x: f64, n: f64, p: f64, give_log: c_int) -> f64 {
1357    if n < 0.0 || !(0.0..=1.0).contains(&p) || n != n.floor() {
1358        return f64::NAN;
1359    }
1360    if x < 0.0 || x > n || x != x.floor() {
1361        return d_log(0.0, give_log);
1362    }
1363    let log_d = lchoose_fn(n, x) + x * p.ln() + (n - x) * (1.0 - p).ln();
1364    if give_log != 0 {
1365        log_d
1366    } else {
1367        log_d.exp()
1368    }
1369}
1370
1371#[no_mangle]
1372pub extern "C" fn Rf_pbinom(x: f64, n: f64, p: f64, lower_tail: c_int, log_p: c_int) -> f64 {
1373    if n < 0.0 || !(0.0..=1.0).contains(&p) || n != n.floor() {
1374        return f64::NAN;
1375    }
1376    if x < 0.0 {
1377        return p_transform(0.0, lower_tail, log_p);
1378    }
1379    if x >= n {
1380        return p_transform(1.0, lower_tail, log_p);
1381    }
1382    let k = x.floor();
1383    // P(X <= k) = I_{1-p}(n-k, k+1) (regularized incomplete beta)
1384    let cdf = pbeta_raw(1.0 - p, n - k, k + 1.0);
1385    p_transform(cdf, lower_tail, log_p)
1386}
1387
1388#[no_mangle]
1389pub extern "C" fn Rf_qbinom(p: f64, n: f64, prob: f64, lower_tail: c_int, log_p: c_int) -> f64 {
1390    if n < 0.0 || !(0.0..=1.0).contains(&prob) || n != n.floor() {
1391        return f64::NAN;
1392    }
1393    let p = q_decode(p, lower_tail, log_p);
1394    if !(0.0..=1.0).contains(&p) {
1395        return f64::NAN;
1396    }
1397    if p == 0.0 {
1398        return 0.0;
1399    }
1400    if p == 1.0 {
1401        return n;
1402    }
1403    // Start near the mean and search
1404    let mu = n * prob;
1405    let mut k = mu.floor();
1406    loop {
1407        let pk = Rf_pbinom(k, n, prob, 1, 0);
1408        if pk >= p {
1409            break;
1410        }
1411        k += 1.0;
1412        if k > n {
1413            return n;
1414        }
1415    }
1416    while k > 0.0 {
1417        let pk = Rf_pbinom(k - 1.0, n, prob, 1, 0);
1418        if pk < p {
1419            break;
1420        }
1421        k -= 1.0;
1422    }
1423    k
1424}
1425
1426#[no_mangle]
1427pub extern "C" fn Rf_rbinom(n: f64, p: f64) -> f64 {
1428    if n <= 0.0 || p <= 0.0 {
1429        return 0.0;
1430    }
1431    if p >= 1.0 {
1432        return n;
1433    }
1434    let ni = n as i64;
1435    if ni <= 20 {
1436        // Direct method for small n
1437        let mut x = 0i64;
1438        for _ in 0..ni {
1439            if unif_rand() < p {
1440                x += 1;
1441            }
1442        }
1443        return x as f64;
1444    }
1445    // Normal approximation for large n
1446    let x = Rf_rnorm(n * p, (n * p * (1.0 - p)).sqrt());
1447    x.round().clamp(0.0, n)
1448}
1449
1450#[no_mangle]
1451pub extern "C" fn rmultinom(_n: c_int, _prob: *mut f64, _k: c_int, _rn: *mut c_int) {
1452    // stub — multinomial sampling not yet implemented
1453}
1454
1455// endregion
1456
1457// region: Geometric distribution
1458
1459#[no_mangle]
1460pub extern "C" fn Rf_dgeom(x: f64, p: f64, give_log: c_int) -> f64 {
1461    if p <= 0.0 || p > 1.0 {
1462        return f64::NAN;
1463    }
1464    if x < 0.0 || x != x.floor() {
1465        return d_log(0.0, give_log);
1466    }
1467    let log_d = p.ln() + x * (1.0 - p).ln();
1468    if give_log != 0 {
1469        log_d
1470    } else {
1471        log_d.exp()
1472    }
1473}
1474
1475#[no_mangle]
1476pub extern "C" fn Rf_pgeom(x: f64, p: f64, lower_tail: c_int, log_p: c_int) -> f64 {
1477    if p <= 0.0 || p > 1.0 {
1478        return f64::NAN;
1479    }
1480    if x < 0.0 {
1481        return p_transform(0.0, lower_tail, log_p);
1482    }
1483    let k = x.floor();
1484    let cdf = 1.0 - (1.0 - p).powf(k + 1.0);
1485    p_transform(cdf, lower_tail, log_p)
1486}
1487
1488#[no_mangle]
1489pub extern "C" fn Rf_qgeom(p: f64, prob: f64, lower_tail: c_int, log_p: c_int) -> f64 {
1490    if prob <= 0.0 || prob > 1.0 {
1491        return f64::NAN;
1492    }
1493    let p = q_decode(p, lower_tail, log_p);
1494    if !(0.0..=1.0).contains(&p) {
1495        return f64::NAN;
1496    }
1497    if p == 0.0 {
1498        return 0.0;
1499    }
1500    if p == 1.0 {
1501        return f64::INFINITY;
1502    }
1503    // Exact: ceil(log(1-p) / log(1-prob)) - 1
1504    (libm::log1p(-p) / libm::log1p(-prob)).ceil() - 1.0
1505}
1506
1507#[no_mangle]
1508pub extern "C" fn Rf_rgeom(p: f64) -> f64 {
1509    let u = unif_rand();
1510    (u.ln() / libm::log1p(-p)).floor()
1511}
1512
1513// endregion
1514
1515// region: Hypergeometric distribution
1516
1517#[no_mangle]
1518pub extern "C" fn Rf_dhyper(x: f64, r: f64, b: f64, n: f64, give_log: c_int) -> f64 {
1519    if r < 0.0 || b < 0.0 || n < 0.0 {
1520        return f64::NAN;
1521    }
1522    if x < 0.0 || x != x.floor() || x > r || x > n || n - x > b {
1523        return d_log(0.0, give_log);
1524    }
1525    let log_d = lchoose_fn(r, x) + lchoose_fn(b, n - x) - lchoose_fn(r + b, n);
1526    if give_log != 0 {
1527        log_d
1528    } else {
1529        log_d.exp()
1530    }
1531}
1532
1533#[no_mangle]
1534pub extern "C" fn Rf_phyper(
1535    x: f64,
1536    r: f64,
1537    b: f64,
1538    n: f64,
1539    lower_tail: c_int,
1540    log_p: c_int,
1541) -> f64 {
1542    if r < 0.0 || b < 0.0 || n < 0.0 {
1543        return f64::NAN;
1544    }
1545    if x < 0.0 {
1546        return p_transform(0.0, lower_tail, log_p);
1547    }
1548    let k = x.floor();
1549    let lo = 0.0_f64.max(n - b);
1550    let hi = r.min(n);
1551    if k >= hi {
1552        return p_transform(1.0, lower_tail, log_p);
1553    }
1554    let mut sum = 0.0;
1555    let mut i = lo;
1556    while i <= k {
1557        sum += Rf_dhyper(i, r, b, n, 0);
1558        i += 1.0;
1559    }
1560    p_transform(sum, lower_tail, log_p)
1561}
1562
1563#[no_mangle]
1564pub extern "C" fn Rf_qhyper(
1565    p: f64,
1566    r: f64,
1567    b: f64,
1568    n: f64,
1569    lower_tail: c_int,
1570    log_p: c_int,
1571) -> f64 {
1572    let p = q_decode(p, lower_tail, log_p);
1573    if !(0.0..=1.0).contains(&p) || r < 0.0 || b < 0.0 || n < 0.0 {
1574        return f64::NAN;
1575    }
1576    let lo = 0.0_f64.max(n - b);
1577    let hi = r.min(n);
1578    let mut k = lo;
1579    let mut cum = 0.0;
1580    while k <= hi {
1581        cum += Rf_dhyper(k, r, b, n, 0);
1582        if cum >= p {
1583            return k;
1584        }
1585        k += 1.0;
1586    }
1587    hi
1588}
1589
1590#[no_mangle]
1591pub extern "C" fn Rf_rhyper(r: f64, b: f64, n: f64) -> f64 {
1592    // Simple urn sampling for small populations
1593    let total = r + b;
1594    if total <= 0.0 || n <= 0.0 {
1595        return 0.0;
1596    }
1597    let mut whites = r;
1598    let mut remaining = total;
1599    let mut drawn = 0.0;
1600    let ni = n.min(total) as i64;
1601    for _ in 0..ni {
1602        let u = unif_rand();
1603        if u < whites / remaining {
1604            drawn += 1.0;
1605            whites -= 1.0;
1606        }
1607        remaining -= 1.0;
1608    }
1609    drawn
1610}
1611
1612// endregion
1613
1614// region: Negative binomial distribution
1615
1616#[no_mangle]
1617pub extern "C" fn Rf_dnbinom(x: f64, size: f64, prob: f64, give_log: c_int) -> f64 {
1618    if size <= 0.0 || prob <= 0.0 || prob > 1.0 {
1619        return f64::NAN;
1620    }
1621    if x < 0.0 || x != x.floor() {
1622        return d_log(0.0, give_log);
1623    }
1624    let log_d = lchoose_fn(x + size - 1.0, x) + size * prob.ln() + x * (1.0 - prob).ln();
1625    if give_log != 0 {
1626        log_d
1627    } else {
1628        log_d.exp()
1629    }
1630}
1631
1632#[no_mangle]
1633pub extern "C" fn Rf_pnbinom(x: f64, size: f64, prob: f64, lower_tail: c_int, log_p: c_int) -> f64 {
1634    if size <= 0.0 || prob <= 0.0 || prob > 1.0 {
1635        return f64::NAN;
1636    }
1637    if x < 0.0 {
1638        return p_transform(0.0, lower_tail, log_p);
1639    }
1640    let k = x.floor();
1641    // P(X <= k) = I_p(size, k+1)
1642    let p = pbeta_raw(prob, size, k + 1.0);
1643    p_transform(p, lower_tail, log_p)
1644}
1645
1646#[no_mangle]
1647pub extern "C" fn Rf_qnbinom(p: f64, size: f64, prob: f64, lower_tail: c_int, log_p: c_int) -> f64 {
1648    if size <= 0.0 || prob <= 0.0 || prob > 1.0 {
1649        return f64::NAN;
1650    }
1651    let p = q_decode(p, lower_tail, log_p);
1652    if !(0.0..=1.0).contains(&p) {
1653        return f64::NAN;
1654    }
1655    if p == 0.0 {
1656        return 0.0;
1657    }
1658    if p == 1.0 {
1659        return f64::INFINITY;
1660    }
1661    let mu = size * (1.0 - prob) / prob;
1662    let mut k = mu.floor();
1663    loop {
1664        let pk = Rf_pnbinom(k, size, prob, 1, 0);
1665        if pk >= p {
1666            break;
1667        }
1668        k += 1.0;
1669        if k > 1e15 {
1670            return f64::INFINITY;
1671        }
1672    }
1673    while k > 0.0 {
1674        let pk = Rf_pnbinom(k - 1.0, size, prob, 1, 0);
1675        if pk < p {
1676            break;
1677        }
1678        k -= 1.0;
1679    }
1680    k
1681}
1682
1683#[no_mangle]
1684pub extern "C" fn Rf_rnbinom(size: f64, prob: f64) -> f64 {
1685    let rate = (1.0 - prob) / prob;
1686    let g = Rf_rgamma(size, rate);
1687    Rf_rpois(g)
1688}
1689
1690// NegBinom mu parameterization
1691#[no_mangle]
1692pub extern "C" fn Rf_dnbinom_mu(x: f64, size: f64, mu: f64, give_log: c_int) -> f64 {
1693    if size <= 0.0 || mu < 0.0 {
1694        return f64::NAN;
1695    }
1696    let prob = size / (size + mu);
1697    Rf_dnbinom(x, size, prob, give_log)
1698}
1699
1700#[no_mangle]
1701pub extern "C" fn Rf_pnbinom_mu(
1702    x: f64,
1703    size: f64,
1704    mu: f64,
1705    lower_tail: c_int,
1706    log_p: c_int,
1707) -> f64 {
1708    if size <= 0.0 || mu < 0.0 {
1709        return f64::NAN;
1710    }
1711    let prob = size / (size + mu);
1712    Rf_pnbinom(x, size, prob, lower_tail, log_p)
1713}
1714
1715#[no_mangle]
1716pub extern "C" fn Rf_qnbinom_mu(
1717    x: f64,
1718    size: f64,
1719    mu: f64,
1720    lower_tail: c_int,
1721    log_p: c_int,
1722) -> f64 {
1723    if size <= 0.0 || mu < 0.0 {
1724        return f64::NAN;
1725    }
1726    let prob = size / (size + mu);
1727    Rf_qnbinom(x, size, prob, lower_tail, log_p)
1728}
1729
1730// endregion
1731
1732// region: Non-central Beta distribution
1733
1734#[no_mangle]
1735pub extern "C" fn Rf_dnbeta(x: f64, a: f64, b: f64, ncp: f64, give_log: c_int) -> f64 {
1736    if a <= 0.0 || b <= 0.0 || ncp < 0.0 {
1737        return f64::NAN;
1738    }
1739    if ncp == 0.0 {
1740        return Rf_dbeta(x, a, b, give_log);
1741    }
1742    // Poisson mixture of central beta densities
1743    let lambda = ncp / 2.0;
1744    let max_terms = 100.min((lambda + 20.0 * lambda.sqrt()) as usize + 10);
1745    let mut sum = 0.0;
1746    let mut pw = (-lambda).exp();
1747    for j in 0..max_terms {
1748        sum += pw * Rf_dbeta(x, a + j as f64, b, 0);
1749        pw *= lambda / (j as f64 + 1.0);
1750    }
1751    d_log(sum, give_log)
1752}
1753
1754#[no_mangle]
1755pub extern "C" fn Rf_pnbeta(
1756    x: f64,
1757    a: f64,
1758    b: f64,
1759    ncp: f64,
1760    lower_tail: c_int,
1761    log_p: c_int,
1762) -> f64 {
1763    if a <= 0.0 || b <= 0.0 || ncp < 0.0 {
1764        return f64::NAN;
1765    }
1766    if ncp == 0.0 {
1767        return Rf_pbeta(x, a, b, lower_tail, log_p);
1768    }
1769    let lambda = ncp / 2.0;
1770    let max_terms = 100.min((lambda + 20.0 * lambda.sqrt()) as usize + 10);
1771    let mut sum = 0.0;
1772    let mut pw = (-lambda).exp();
1773    for j in 0..max_terms {
1774        sum += pw * pbeta_raw(x, a + j as f64, b);
1775        pw *= lambda / (j as f64 + 1.0);
1776    }
1777    p_transform(sum, lower_tail, log_p)
1778}
1779
1780#[no_mangle]
1781pub extern "C" fn Rf_qnbeta(
1782    p: f64,
1783    a: f64,
1784    b: f64,
1785    ncp: f64,
1786    lower_tail: c_int,
1787    log_p: c_int,
1788) -> f64 {
1789    if a <= 0.0 || b <= 0.0 || ncp < 0.0 {
1790        return f64::NAN;
1791    }
1792    let p = q_decode(p, lower_tail, log_p);
1793    if !(0.0..=1.0).contains(&p) {
1794        return f64::NAN;
1795    }
1796    // Newton's method
1797    let mut x = Rf_qbeta(p, a, b, 1, 0);
1798    for _ in 0..50 {
1799        let px = Rf_pnbeta(x, a, b, ncp, 1, 0);
1800        let dx = Rf_dnbeta(x, a, b, ncp, 0);
1801        if dx <= 0.0 {
1802            break;
1803        }
1804        let delta = (px - p) / dx;
1805        x -= delta;
1806        x = x.clamp(DBL_EPSILON, 1.0 - DBL_EPSILON);
1807        if delta.abs() < x * 1e-12 {
1808            break;
1809        }
1810    }
1811    x
1812}
1813
1814// endregion
1815
1816// region: Non-central F distribution
1817
1818#[no_mangle]
1819pub extern "C" fn Rf_dnf(x: f64, df1: f64, df2: f64, ncp: f64, give_log: c_int) -> f64 {
1820    if df1 <= 0.0 || df2 <= 0.0 || ncp < 0.0 {
1821        return f64::NAN;
1822    }
1823    if x <= 0.0 {
1824        return d_log(0.0, give_log);
1825    }
1826    // Transform: if X ~ F'(df1,df2,ncp) then Y = df1*X/(df1*X+df2) ~ Beta'(df1/2,df2/2,ncp)
1827    let y = df1 * x / (df1 * x + df2);
1828    let dy_dx = df1 * df2 / ((df1 * x + df2) * (df1 * x + df2));
1829    let d = Rf_dnbeta(y, df1 / 2.0, df2 / 2.0, ncp, 0) * dy_dx;
1830    d_log(d, give_log)
1831}
1832
1833#[no_mangle]
1834pub extern "C" fn Rf_pnf(
1835    x: f64,
1836    df1: f64,
1837    df2: f64,
1838    ncp: f64,
1839    lower_tail: c_int,
1840    log_p: c_int,
1841) -> f64 {
1842    if df1 <= 0.0 || df2 <= 0.0 || ncp < 0.0 {
1843        return f64::NAN;
1844    }
1845    if x <= 0.0 {
1846        return p_transform(0.0, lower_tail, log_p);
1847    }
1848    let y = df1 * x / (df1 * x + df2);
1849    Rf_pnbeta(y, df1 / 2.0, df2 / 2.0, ncp, lower_tail, log_p)
1850}
1851
1852#[no_mangle]
1853pub extern "C" fn Rf_qnf(
1854    p: f64,
1855    df1: f64,
1856    df2: f64,
1857    ncp: f64,
1858    lower_tail: c_int,
1859    log_p: c_int,
1860) -> f64 {
1861    if df1 <= 0.0 || df2 <= 0.0 || ncp < 0.0 {
1862        return f64::NAN;
1863    }
1864    let bq = Rf_qnbeta(p, df1 / 2.0, df2 / 2.0, ncp, lower_tail, log_p);
1865    (df2 * bq) / (df1 * (1.0 - bq))
1866}
1867
1868// endregion
1869
1870// region: Non-central t distribution
1871
1872#[no_mangle]
1873pub extern "C" fn Rf_dnt(x: f64, df: f64, ncp: f64, give_log: c_int) -> f64 {
1874    if df <= 0.0 {
1875        return f64::NAN;
1876    }
1877    if ncp == 0.0 {
1878        return Rf_dt(x, df, give_log);
1879    }
1880    // Numerical approximation via finite difference
1881    let h = 1e-7 * (1.0 + x.abs());
1882    let d = (Rf_pnt(x + h, df, ncp, 1, 0) - Rf_pnt(x - h, df, ncp, 1, 0)) / (2.0 * h);
1883    if d < 0.0 {
1884        return d_log(0.0, give_log);
1885    }
1886    d_log(d, give_log)
1887}
1888
1889#[no_mangle]
1890pub extern "C" fn Rf_pnt(x: f64, df: f64, ncp: f64, lower_tail: c_int, log_p: c_int) -> f64 {
1891    if df <= 0.0 {
1892        return f64::NAN;
1893    }
1894    if ncp == 0.0 {
1895        return Rf_pt(x, df, lower_tail, log_p);
1896    }
1897    // Normal approximation with correction for non-centrality
1898    let z = x * (1.0 - 1.0 / (4.0 * df)).sqrt() - ncp;
1899    let p = Rf_pnorm5(z, 0.0, 1.0, 1, 0);
1900    p_transform(p, lower_tail, log_p)
1901}
1902
1903#[no_mangle]
1904pub extern "C" fn Rf_qnt(p: f64, df: f64, ncp: f64, lower_tail: c_int, log_p: c_int) -> f64 {
1905    if df <= 0.0 {
1906        return f64::NAN;
1907    }
1908    let p = q_decode(p, lower_tail, log_p);
1909    if !(0.0..=1.0).contains(&p) {
1910        return f64::NAN;
1911    }
1912    // Newton's method from normal approximation
1913    let mut x = qnorm_standard(p) + ncp;
1914    for _ in 0..50 {
1915        let px = Rf_pnt(x, df, ncp, 1, 0);
1916        let dx = Rf_dnt(x, df, ncp, 0);
1917        if dx <= 0.0 || !dx.is_finite() {
1918            break;
1919        }
1920        let delta = (px - p) / dx;
1921        x -= delta;
1922        if delta.abs() < x.abs() * 1e-10 {
1923            break;
1924        }
1925    }
1926    x
1927}
1928
1929// endregion
1930
1931// region: Studentized range distribution
1932
1933#[no_mangle]
1934pub extern "C" fn Rf_ptukey(_q: f64, _rr: f64, _cc: f64, _df: f64, _lt: c_int, _lg: c_int) -> f64 {
1935    // Studentized range requires complex numerical integration — stub for now
1936    f64::NAN
1937}
1938
1939#[no_mangle]
1940pub extern "C" fn Rf_qtukey(_p: f64, _rr: f64, _cc: f64, _df: f64, _lt: c_int, _lg: c_int) -> f64 {
1941    f64::NAN
1942}
1943
1944// endregion
1945
1946// region: Wilcoxon rank sum distribution
1947
1948#[no_mangle]
1949pub extern "C" fn Rf_dwilcox(x: f64, m: f64, n: f64, give_log: c_int) -> f64 {
1950    if m < 0.0 || n < 0.0 {
1951        return f64::NAN;
1952    }
1953    if x < 0.0 || x != x.floor() || x > m * n {
1954        return d_log(0.0, give_log);
1955    }
1956    // Count via normal approximation for large m*n
1957    let total = choose_fn(m + n, n);
1958    if total == 0.0 {
1959        return d_log(0.0, give_log);
1960    }
1961    // Exact count for small cases using recursion would be expensive;
1962    // use normal approximation
1963    let mu = m * n / 2.0;
1964    let sigma = (m * n * (m + n + 1.0) / 12.0).sqrt();
1965    if sigma <= 0.0 {
1966        return d_log(if x == mu { 1.0 } else { 0.0 }, give_log);
1967    }
1968    // Continuity-corrected normal approximation
1969    Rf_dnorm4(x, mu, sigma, give_log)
1970}
1971
1972#[no_mangle]
1973pub extern "C" fn Rf_pwilcox(x: f64, m: f64, n: f64, lower_tail: c_int, log_p: c_int) -> f64 {
1974    if m < 0.0 || n < 0.0 {
1975        return f64::NAN;
1976    }
1977    let mu = m * n / 2.0;
1978    let sigma = (m * n * (m + n + 1.0) / 12.0).sqrt();
1979    if sigma <= 0.0 {
1980        return p_transform(if x >= mu { 1.0 } else { 0.0 }, lower_tail, log_p);
1981    }
1982    // Normal approximation with continuity correction
1983    Rf_pnorm5(x + 0.5, mu, sigma, lower_tail, log_p)
1984}
1985
1986#[no_mangle]
1987pub extern "C" fn Rf_qwilcox(p: f64, m: f64, n: f64, lower_tail: c_int, log_p: c_int) -> f64 {
1988    if m < 0.0 || n < 0.0 {
1989        return f64::NAN;
1990    }
1991    let mu = m * n / 2.0;
1992    let sigma = (m * n * (m + n + 1.0) / 12.0).sqrt();
1993    if sigma <= 0.0 {
1994        return mu;
1995    }
1996    let q = Rf_qnorm5(p, mu, sigma, lower_tail, log_p);
1997    q.round().clamp(0.0, m * n)
1998}
1999
2000#[no_mangle]
2001pub extern "C" fn Rf_rwilcox(m: f64, n: f64) -> f64 {
2002    // Simple simulation: sum of ranks
2003    let total = (m + n) as usize;
2004    let mi = m as usize;
2005    // Fisher-Yates sample of mi items from 1..=total
2006    let mut sum = 0.0;
2007    let mut remaining = total;
2008    let mut need = mi;
2009    for rank in 1..=total {
2010        let u = unif_rand();
2011        if (u * remaining as f64) < need as f64 {
2012            sum += rank as f64;
2013            need -= 1;
2014            if need == 0 {
2015                break;
2016            }
2017        }
2018        remaining -= 1;
2019    }
2020    // Wilcoxon rank sum = sum_of_ranks - m*(m+1)/2
2021    sum - m * (m + 1.0) / 2.0
2022}
2023
2024// endregion
2025
2026// region: Wilcoxon signed rank distribution
2027
2028#[no_mangle]
2029pub extern "C" fn Rf_dsignrank(x: f64, n: f64, give_log: c_int) -> f64 {
2030    if n < 0.0 {
2031        return f64::NAN;
2032    }
2033    let mu = n * (n + 1.0) / 4.0;
2034    let sigma = (n * (n + 1.0) * (2.0 * n + 1.0) / 24.0).sqrt();
2035    if sigma <= 0.0 {
2036        return d_log(if x == mu { 1.0 } else { 0.0 }, give_log);
2037    }
2038    Rf_dnorm4(x, mu, sigma, give_log)
2039}
2040
2041#[no_mangle]
2042pub extern "C" fn Rf_psignrank(x: f64, n: f64, lower_tail: c_int, log_p: c_int) -> f64 {
2043    if n < 0.0 {
2044        return f64::NAN;
2045    }
2046    let mu = n * (n + 1.0) / 4.0;
2047    let sigma = (n * (n + 1.0) * (2.0 * n + 1.0) / 24.0).sqrt();
2048    if sigma <= 0.0 {
2049        return p_transform(if x >= mu { 1.0 } else { 0.0 }, lower_tail, log_p);
2050    }
2051    Rf_pnorm5(x + 0.5, mu, sigma, lower_tail, log_p)
2052}
2053
2054#[no_mangle]
2055pub extern "C" fn Rf_qsignrank(p: f64, n: f64, lower_tail: c_int, log_p: c_int) -> f64 {
2056    if n < 0.0 {
2057        return f64::NAN;
2058    }
2059    let mu = n * (n + 1.0) / 4.0;
2060    let sigma = (n * (n + 1.0) * (2.0 * n + 1.0) / 24.0).sqrt();
2061    if sigma <= 0.0 {
2062        return mu;
2063    }
2064    let q = Rf_qnorm5(p, mu, sigma, lower_tail, log_p);
2065    q.round().clamp(0.0, n * (n + 1.0) / 2.0)
2066}
2067
2068#[no_mangle]
2069pub extern "C" fn Rf_rsignrank(n: f64) -> f64 {
2070    let ni = n as i64;
2071    let mut sum = 0.0;
2072    for i in 1..=ni {
2073        if unif_rand() > 0.5 {
2074            sum += i as f64;
2075        }
2076    }
2077    sum
2078}
2079
2080// endregion
2081
2082// region: Bessel functions
2083
2084#[no_mangle]
2085pub extern "C" fn Rf_bessel_j(x: f64, alpha: f64) -> f64 {
2086    if alpha == 0.0 {
2087        return libm::j0(x);
2088    }
2089    if alpha == 1.0 {
2090        return libm::j1(x);
2091    }
2092    if alpha == alpha.floor() && alpha >= 0.0 {
2093        return libm::jn(alpha as i32, x);
2094    }
2095    // For non-integer orders, use series expansion
2096    bessel_j_series(x, alpha)
2097}
2098
2099#[no_mangle]
2100pub extern "C" fn Rf_bessel_y(x: f64, alpha: f64) -> f64 {
2101    if x <= 0.0 {
2102        return f64::NEG_INFINITY;
2103    }
2104    if alpha == 0.0 {
2105        return libm::y0(x);
2106    }
2107    if alpha == 1.0 {
2108        return libm::y1(x);
2109    }
2110    if alpha == alpha.floor() && alpha >= 0.0 {
2111        return libm::yn(alpha as i32, x);
2112    }
2113    // Y_alpha = (J_alpha * cos(alpha*pi) - J_{-alpha}) / sin(alpha*pi)
2114    let pi_a = alpha * std::f64::consts::PI;
2115    (Rf_bessel_j(x, alpha) * pi_a.cos() - Rf_bessel_j(x, -alpha)) / pi_a.sin()
2116}
2117
2118#[no_mangle]
2119pub extern "C" fn Rf_bessel_i(x: f64, alpha: f64, expo: f64) -> f64 {
2120    // Modified Bessel I_alpha(x), optionally scaled by exp(-|x|)
2121    let val = bessel_i_series(x, alpha);
2122    if expo != 1.0 {
2123        val
2124    } else {
2125        val * (-x.abs()).exp()
2126    }
2127}
2128
2129#[no_mangle]
2130pub extern "C" fn Rf_bessel_k(x: f64, alpha: f64, expo: f64) -> f64 {
2131    if x <= 0.0 {
2132        return f64::INFINITY;
2133    }
2134    // K_alpha = pi/2 * (I_{-alpha} - I_alpha) / sin(alpha*pi)
2135    // For integer alpha, use limit
2136    let val = if (alpha - alpha.round()).abs() < 1e-15 {
2137        // Integer order: use K_n via recursion
2138        bessel_k_int(x, alpha.round() as i32)
2139    } else {
2140        let pi_a = alpha * std::f64::consts::PI;
2141        std::f64::consts::PI / 2.0 * (bessel_i_series(x, -alpha) - bessel_i_series(x, alpha))
2142            / pi_a.sin()
2143    };
2144    if expo != 1.0 {
2145        val
2146    } else {
2147        val * x.exp()
2148    }
2149}
2150
2151fn bessel_j_series(x: f64, alpha: f64) -> f64 {
2152    let half_x = x / 2.0;
2153    let mut term = half_x.powf(alpha) / libm::tgamma(alpha + 1.0);
2154    let mut sum = term;
2155    let x2_neg = -half_x * half_x;
2156    for m in 1..100 {
2157        term *= x2_neg / (m as f64 * (alpha + m as f64));
2158        sum += term;
2159        if term.abs() < sum.abs() * DBL_EPSILON {
2160            break;
2161        }
2162    }
2163    sum
2164}
2165
2166fn bessel_i_series(x: f64, alpha: f64) -> f64 {
2167    let half_x = x / 2.0;
2168    let mut term = half_x.powf(alpha) / libm::tgamma(alpha + 1.0);
2169    let mut sum = term;
2170    let x2 = half_x * half_x;
2171    for m in 1..100 {
2172        term *= x2 / (m as f64 * (alpha + m as f64));
2173        sum += term;
2174        if term.abs() < sum.abs() * DBL_EPSILON {
2175            break;
2176        }
2177    }
2178    sum
2179}
2180
2181fn bessel_k_int(x: f64, n: i32) -> f64 {
2182    // K_0 and K_1 from asymptotic or series, then recurse
2183    if n == 0 {
2184        return bessel_k0(x);
2185    }
2186    if n == 1 {
2187        return bessel_k1(x);
2188    }
2189    let mut km1 = bessel_k0(x);
2190    let mut k = bessel_k1(x);
2191    for i in 1..n.unsigned_abs() {
2192        let kp1 = km1 + 2.0 * i as f64 / x * k;
2193        km1 = k;
2194        k = kp1;
2195    }
2196    k
2197}
2198
2199fn bessel_k0(x: f64) -> f64 {
2200    if x <= 2.0 {
2201        let y = x * x / 4.0;
2202        -x.ln() * bessel_i_series(x, 0.0)
2203            + (-0.57721566
2204                + y * (0.42278420
2205                    + y * (0.23069756 + y * (0.03488590 + y * (0.00262698 + y * 0.00010750)))))
2206    } else {
2207        let y = 2.0 / x;
2208        ((-x).exp() / x.sqrt())
2209            * (1.25331414
2210                + y * (-0.07832358
2211                    + y * (0.02189568 + y * (-0.01062446 + y * (0.00587872 + y * (-0.00251540))))))
2212    }
2213}
2214
2215fn bessel_k1(x: f64) -> f64 {
2216    if x <= 2.0 {
2217        let y = x * x / 4.0;
2218        x.ln() * bessel_i_series(x, 1.0)
2219            + (1.0 / x)
2220                * (1.0
2221                    + y * (0.15443144
2222                        + y * (-0.67278579
2223                            + y * (-0.18156897 + y * (-0.01919402 + y * (-0.00110404))))))
2224    } else {
2225        let y = 2.0 / x;
2226        ((-x).exp() / x.sqrt())
2227            * (1.25331414
2228                + y * (0.23498619
2229                    + y * (-0.03655620 + y * (0.01504268 + y * (-0.00780353 + y * 0.00325614)))))
2230    }
2231}
2232
2233// _ex variants: store result in output buffer and return it
2234#[no_mangle]
2235pub extern "C" fn Rf_bessel_i_ex(x: f64, alpha: f64, expo: f64, bi: *mut f64) -> f64 {
2236    let val = Rf_bessel_i(x, alpha, expo);
2237    if !bi.is_null() {
2238        unsafe {
2239            *bi = val;
2240        }
2241    }
2242    val
2243}
2244
2245#[no_mangle]
2246pub extern "C" fn Rf_bessel_j_ex(x: f64, alpha: f64, bj: *mut f64) -> f64 {
2247    let val = Rf_bessel_j(x, alpha);
2248    if !bj.is_null() {
2249        unsafe {
2250            *bj = val;
2251        }
2252    }
2253    val
2254}
2255
2256#[no_mangle]
2257pub extern "C" fn Rf_bessel_k_ex(x: f64, alpha: f64, expo: f64, bk: *mut f64) -> f64 {
2258    let val = Rf_bessel_k(x, alpha, expo);
2259    if !bk.is_null() {
2260        unsafe {
2261            *bk = val;
2262        }
2263    }
2264    val
2265}
2266
2267#[no_mangle]
2268pub extern "C" fn Rf_bessel_y_ex(x: f64, alpha: f64, by: *mut f64) -> f64 {
2269    let val = Rf_bessel_y(x, alpha);
2270    if !by.is_null() {
2271        unsafe {
2272            *by = val;
2273        }
2274    }
2275    val
2276}
2277
2278#[no_mangle]
2279pub extern "C" fn Rf_hypot(a: f64, b: f64) -> f64 {
2280    a.hypot(b)
2281}
2282
2283// endregion