packages feed

hgg-core-0.2.0.0: src/Graphics/Hgg/Math/Special.hs

-- |
-- Module      : Graphics.Hgg.Math.Special
-- Description : Special functions — log-gamma, regularized incomplete beta, beta quantile
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- [日本語]: backend 非依存の数値特殊関数。 確率プロットの厳密 rank-based CI
--   (順序統計量 U_(i) ~ Beta(i, n-i+1)) などで必要になる:
--
--     * 'logGamma'          : ln Γ(x) (Lanczos 近似、 x > 0)
--     * 'regIncompleteBeta' : 正則化不完全ベータ I_x(a,b) (連分数 / Lentz 法)
--     * 'betaQuantile'      : I_x(a,b) = q を満たす x (二分法による逆関数)
--
--   アルゴリズムは Numerical Recipes の @gammln@ / @betai@ / @betacf@ に準ずる。
--   core 内に他の特殊関数 (invNormCdf) は 'Graphics.Hgg.Layout.RangeOf' にあるが、
--   ベータ系はサイズが大きいので本 module に分離する。
-- [English]: Backend-agnostic numerical special functions, needed for
--   example by the exact rank-based confidence intervals of probability
--   plots (order statistics U_(i) ~ Beta(i, n-i+1)):
--
--     * 'logGamma'          : ln Γ(x) (Lanczos approximation, x > 0)
--     * 'regIncompleteBeta' : the regularized incomplete beta function
--       I_x(a,b) (continued fraction / Lentz's method)
--     * 'betaQuantile'      : the x satisfying I_x(a,b) = q (inverse via
--       bisection)
--
--   The algorithms follow Numerical Recipes' @gammln@ / @betai@ / @betacf@.
--   Another special function (invNormCdf) lives in 'Graphics.Hgg.Layout.RangeOf'
--   elsewhere in core, but the beta-related functions are split into this
--   module because of their size.
module Graphics.Hgg.Math.Special
  ( logGamma
  , regIncompleteBeta
  , betaQuantile
  ) where

-- ===========================================================================
-- log-gamma (Lanczos 近似、 g=5 / 6 係数)
-- ===========================================================================

-- | [日本語]: ln Γ(x) (x > 0 を仮定)。 相対誤差 < 2e-10。
--   Numerical Recipes @gammln@ と同一係数 (Lanczos, g=5)。
--   [English]: ln Γ(x), assuming x > 0. Relative error < 2e-10. Uses the
--   same coefficients as Numerical Recipes' @gammln@ (Lanczos, g=5).
logGamma :: Double -> Double
logGamma x =
  let tmp0 = x + 5.5
      tmp  = tmp0 - (x + 0.5) * log tmp0
      ser  = 1.000000000190015
             + sum [ c / (x + fromIntegral j) | (j, c) <- zip [(1 :: Int) ..] cof ]
  in -tmp + log (2.5066282746310005 * ser / x)
  where
    cof = [  76.18009172947146,   -86.50532032941677
          ,  24.01409824083091,    -1.231739572450155
          ,   0.1208650973866179e-2, -0.5395239384953e-5 ]

-- ===========================================================================
-- 正則化不完全ベータ I_x(a,b)
-- ===========================================================================

-- | [日本語]: 正則化不完全ベータ関数 I_x(a,b) = B(x;a,b) / B(a,b) ∈ [0,1]。
--   a,b > 0、 x ∈ [0,1]。 x < (a+1)/(a+b+2) で連分数を直接、 それ以外は
--   対称性 I_x(a,b) = 1 - I_{1-x}(b,a) を使い収束を確保する。
--   [English]: The regularized incomplete beta function
--   I_x(a,b) = B(x;a,b) / B(a,b) ∈ [0,1], for a,b > 0 and x ∈ [0,1]. When
--   x < (a+1)/(a+b+2) the continued fraction is evaluated directly;
--   otherwise the symmetry I_x(a,b) = 1 - I_{1-x}(b,a) is used to ensure
--   convergence.
regIncompleteBeta :: Double -> Double -> Double -> Double
regIncompleteBeta a b x
  | x <= 0    = 0
  | x >= 1    = 1
  | otherwise =
      let bt = exp ( logGamma (a + b) - logGamma a - logGamma b
                     + a * log x + b * log (1 - x) )
      in if x < (a + 1) / (a + b + 2)
           then bt * betacf a b x / a
           else 1 - bt * betacf b a (1 - x) / b

-- | [日本語]: I_x(a,b) の連分数展開 (Lentz の修正法)。 NR @betacf@ と同型。
--   [English]: The continued-fraction expansion of I_x(a,b) (Lentz's
--   modified method). Structurally identical to Numerical Recipes' @betacf@.
betacf :: Double -> Double -> Double -> Double
betacf a b x = go 1 h0 c0 d0
  where
    fpmin = 1e-30
    eps   = 3e-12
    maxit = 300 :: Int
    qab = a + b
    qap = a + 1
    qam = a - 1
    fix v = if abs v < fpmin then fpmin else v
    d0 = 1 / fix (1 - qab * x / qap)
    c0 = 1
    h0 = d0
    go m h c d
      | m > maxit = h
      | abs (del - 1) < eps = h2
      | otherwise = go (m + 1) h2 c2 d2
      where
        m2  = fromIntegral (2 * m) :: Double
        fm  = fromIntegral m :: Double
        -- 偶数ステップ
        aa1 = fm * (b - fm) * x / ((qam + m2) * (a + m2))
        d1  = 1 / fix (1 + aa1 * d)
        c1  = fix (1 + aa1 / c)
        h1  = h * d1 * c1
        -- 奇数ステップ
        aa2 = negate (a + fm) * (qab + fm) * x / ((a + m2) * (qap + m2))
        d2  = 1 / fix (1 + aa2 * d1)
        c2  = fix (1 + aa2 / c1)
        del = d2 * c2
        h2  = h1 * del

-- ===========================================================================
-- ベータ分位点 (I_x(a,b) = q の逆関数)
-- ===========================================================================

-- | [日本語]: I_x(a,b) = q を満たす x ∈ [0,1] を二分法で求める (= Beta(a,b) の
--   q 分位点)。 'regIncompleteBeta' は x について単調増加なので二分法が確実に
--   収束する。 80 反復で区間幅は 2^-80 (≈ 1e-24) になり double 精度では完全収束。
--   [English]: Finds the x ∈ [0,1] satisfying I_x(a,b) = q by bisection (the
--   q-quantile of Beta(a,b)). Since 'regIncompleteBeta' is monotonically
--   increasing in x, bisection is guaranteed to converge. After 80
--   iterations the interval width is 2^-80 (≈ 1e-24), which is full
--   convergence at double precision.
betaQuantile :: Double -> Double -> Double -> Double
betaQuantile q a b
  | q <= 0    = 0
  | q >= 1    = 1
  | otherwise = bisect 0 1 (80 :: Int)
  where
    bisect lo hi n
      | n <= 0    = mid
      | regIncompleteBeta a b mid < q = bisect mid hi (n - 1)
      | otherwise = bisect lo mid (n - 1)
      where mid = (lo + hi) / 2