math: implement discrete and continuous distributions (#2955)

* math: implement discrete and continuous distributions

Implement a comprehensive set of continuous and discrete probability
distributions with support for PDF, CDF, inverse CDF, random sampling,
mean, and variance calculations.

The following distributions are implemented:
* Normal
* Uniform
* Exponential
* Chi-Squared
* F-Distribution
* Student t
* Binomial
* Poisson

* update releasenotes.md

* Formatting

---------

Co-authored-by: Christoffer Lerno <christoffer@aegik.com>
This commit is contained in:
konimarti
2026-02-19 20:09:11 +01:00
committed by GitHub
parent 6b3139940c
commit 8bb974829d
7 changed files with 1718 additions and 0 deletions

View File

@@ -0,0 +1,589 @@
// Copyright (c) 2026 Koni Marti. All rights reserved.
// Use of this source code is governed by the MIT license.
<*
This module provides a comprehensive set of continuous and discrete
probability distributions with support for PDF, CDF, inverse CDF,
random sampling, mean, and variance calculations.
*>
module std::math::distributions;
import std::math::random;
// Distribution interface defining common statistical operations
interface Distribution
{
<* Calculate the mean (expected value) of the distribution *>
fn double mean();
<* Calculate the variance of the distribution *>
fn double variance();
}
interface ContinuousDistribution : Distribution
{
<* Probability density function (PDF) *>
fn double pdf(double x);
<* Cumulative distribution function (CDF) *>
fn double cdf(double x);
<* Inverse cumulative distribution function (quantile function) *>
fn double quantile(double p);
<* Generate a random sample from the distribution *>
fn double random(Random rand);
}
interface DiscreteDistribution : Distribution
{
<* Probability mass function (PMF) *>
fn double pmf(int k);
<* Cumulative distribution function (CDF) *>
fn double cdf(int k);
<* Inverse cumulative distribution function *>
fn int quantile(double p);
<* Generate a random sample from the distribution *>
fn int random(Random rand);
}
<*
Uniform distribution over [a, b]
*>
struct UniformDist (ContinuousDistribution)
{
double a;
double b;
}
<*
@require b > a : "Upper bound must be greater than lower bound."
*>
fn UniformDist uniform(double a, double b)
{
return (UniformDist){ a, b };
}
fn double UniformDist.mean(&self) @dynamic
{
return (self.a + self.b) / 2.0;
}
fn double UniformDist.variance(&self) @dynamic
{
double range = self.b - self.a;
return range * range / 12.0;
}
fn double UniformDist.pdf(&self, double x) @dynamic
{
if (x < self.a || x > self.b) return 0;
return 1.0 / (self.b - self.a);
}
fn double UniformDist.cdf(&self, double x) @dynamic
{
if (x < self.a) return 0.0;
if (x > self.b) return 1.0;
return (x - self.a) / (self.b - self.a);
}
<*
@require p >= 0.0 && p <= 1.0 : "Probability must be between 0 and 1."
*>
fn double UniformDist.quantile(&self, double p) @dynamic
{
return self.a + p * (self.b - self.a);
}
fn double UniformDist.random(&self, Random rand) @dynamic
{
return self.a + random::next_double(rand) * (self.b - self.a);
}
<*
Normal (Gaussian) distribution
*>
struct NormalDist (ContinuousDistribution)
{
double mu;
double sigma;
}
<*
@require sigma > 0.0 : "Standard deviation must be positive"
*>
fn NormalDist normal(double mu = 0.0, double sigma = 1.0)
{
return (NormalDist){ mu, sigma };
}
fn double NormalDist.mean(&self) @dynamic
{
return self.mu;
}
fn double NormalDist.variance(&self) @dynamic
{
return self.sigma * self.sigma;
}
fn double NormalDist.pdf(&self, double x) @dynamic
{
double z = (x - self.mu) / self.sigma;
return math::exp(-0.5 * z * z) / (self.sigma * math::sqrt(2.0 * math::PI));
}
fn double NormalDist.cdf(&self, double x) @dynamic
{
double z = (x - self.mu) / self.sigma;
return math::clamp(0.5 * (1.0 + math::erf(z / math::SQRT2)), 0.0, 1.0);
}
<*
@require p >= 0.0 && p <= 1.0 : "Probability must be between 0 and 1."
*>
fn double NormalDist.quantile(&self, double p) @dynamic
{
double z = inverse_erf(2.0 * p - 1.0) * math::SQRT2;
return self.mu + self.sigma * z;
}
fn double NormalDist.random(&self, Random rand) @dynamic
{
// Box-Muller transform.
double u1 = random::next_double(rand);
double u2 = random::next_double(rand);
double z = math::sqrt(-2.0 * math::ln(u1)) * math::cos(2.0 * math::PI * u2);
return self.mu + self.sigma * z;
}
<*
Exponential distribution
*>
struct ExponentialDist (ContinuousDistribution)
{
double lambda;
}
<*
@require lambda > 0.0 : "Rate parameter must be positive."
*>
fn ExponentialDist exponential(double lambda = 1.0)
{
return (ExponentialDist){ lambda };
}
<*
@require self.lambda > 0.0 : "Rate parameter must be positive."
*>
fn double ExponentialDist.mean(&self) @dynamic
{
return 1.0 / self.lambda;
}
<*
@require self.lambda > 0.0 : "Rate parameter must be positive."
*>
fn double ExponentialDist.variance(&self) @dynamic
{
return 1.0 / (self.lambda * self.lambda);
}
fn double ExponentialDist.pdf(&self, double x) @dynamic
{
if (x < 0.0) return 0.0;
return self.lambda * math::exp(-self.lambda * x);
}
fn double ExponentialDist.cdf(&self, double x) @dynamic
{
if (x < 0.0) return 0.0;
return math::clamp(1.0 - math::exp(-self.lambda * x), 0.0, 1.0);
}
<*
@require p >= 0.0 && p <= 1.0 : "Probability must be between 0 and 1."
@require self.lambda > 0.0 : "Rate parameter must be positive."
*>
fn double ExponentialDist.quantile(&self, double p) @dynamic
{
return -math::ln(1.0 - p) / self.lambda;
}
<*
@require self.lambda > 0.0 : "Rate parameter must be positive."
*>
fn double ExponentialDist.random(&self, Random rand) @dynamic
{
return -math::ln(1.0 - random::next_double(rand)) / self.lambda;
}
<*
Student's t-distribution
*>
struct TDist (ContinuousDistribution)
{
double df;
}
<*
@require df > 0.0 : "Degrees of freedom must be positive."
*>
fn TDist t_distribution(double df)
{
return (TDist){ df };
}
fn double TDist.mean(&self) @dynamic
{
if (self.df <= 1.0) return double.nan;
return 0.0;
}
fn double TDist.variance(&self) @dynamic
{
if (self.df <= 1.0) return double.nan;
if (self.df <= 2.0) return double.inf;
return self.df / (self.df - 2.0);
}
fn double TDist.pdf(&self, double x) @dynamic
{
double v = self.df;
double coef = math::tgamma((v + 1.0) / 2.0) /
(math::sqrt(v * math::PI) * math::tgamma(v / 2.0));
return coef * math::pow(1.0 + x * x / v, -(v + 1.0) / 2.0);
}
fn double TDist.cdf(&self, double x) @dynamic
{
double v = self.df;
if (x == 0.0) return 0.5;
double t = v / (v + x * x);
double a = v / 2.0;
double b = 0.5;
// Using regularized incomplete beta function.
double beta_cdf = incomplete_beta(t, a, b);
double p = x >= 0.0 ? 1.0 - 0.5 * beta_cdf : 0.5 * beta_cdf;
return math::clamp(p, 0.0, 1.0);
}
<*
@require p >= 0.0 && p <= 1.0 : "Probability must be between 0 and 1."
*>
fn double TDist.quantile(&self, double p) @dynamic
{
if (p == 0.5) return 0.0;
double x = (p < 0.5) ? -1.0 : 1.0;
return newton_raphson(self, x, p) ?? double.nan;
}
fn double TDist.random(&self, Random rand) @dynamic
{
// Generate using relationship with normal and chi-squared
NormalDist std_normal = normal(0.0, 1.0);
double z = std_normal.random(rand);
double v = chi_squared_sample(self.df, rand);
return z / math::sqrt(v / self.df);
}
<*
F-distribution
*>
struct FDist (ContinuousDistribution)
{
double d1;
double d2;
}
<*
@require d1 > 0.0 && d2 > 0.0 : "Degrees of freedom must be positive."
*>
fn FDist f_distribution(double d1, double d2)
{
return (FDist){ d1, d2 };
}
fn double FDist.mean(&self) @dynamic
{
if (self.d2 <= 2.0) return double.nan;
return self.d2 / (self.d2 - 2.0);
}
fn double FDist.variance(&self) @dynamic
{
if (self.d2 <= 4.0) return double.nan;
double d1 = self.d1;
double d2 = self.d2;
return 2.0 * d2 * d2 * (d1 + d2 - 2.0) /
(d1 * (d2 - 2.0) * (d2 - 2.0) * (d2 - 4.0));
}
fn double FDist.pdf(&self, double x) @dynamic
{
if (x < 0.0) return 0.0;
double d1 = self.d1;
double d2 = self.d2;
double num = math::pow(d1 * x, d1) * math::pow(d2, d2);
double denom = math::pow(d1 * x + d2, d1 + d2);
double beta_term = x * beta_function(d1 / 2.0, d2 / 2.0);
return math::sqrt(num / denom) / beta_term;
}
fn double FDist.cdf(&self, double x) @dynamic
{
if (x <= 0.0) return 0.0;
double d1 = self.d1;
double d2 = self.d2;
double t = d1 * x / (d1 * x + d2);
double p = incomplete_beta(t, d1 / 2.0, d2 / 2.0);
return math::clamp(p, 0.0, 1.0);
}
<*
@require p >= 0.0 && p <= 1.0 : "Probability must be between 0 and 1."
*>
fn double FDist.quantile(&self, double p) @dynamic
{
return find_quantile(self, 0.0, 1000.0, p);
}
fn double FDist.random(&self, Random rand) @dynamic
{
// Generate using ratio of chi-squared variables.
double u1 = chi_squared_sample(self.d1, rand);
double u2 = chi_squared_sample(self.d2, rand);
return (u1 / self.d1) / (u2 / self.d2);
}
<*
Chi-squared distribution
*>
struct ChiSquaredDist (ContinuousDistribution)
{
double k;
}
<*
@require k > 0.0 : "Degrees of freedom must be positive"
*>
fn ChiSquaredDist chi_squared(double k)
{
return (ChiSquaredDist){ k };
}
fn double ChiSquaredDist.mean(&self) @dynamic
{
return self.k;
}
fn double ChiSquaredDist.variance(&self) @dynamic
{
return 2.0 * self.k;
}
fn double ChiSquaredDist.pdf(&self, double x) @dynamic
{
if (x < 0.0) return 0.0;
if (x == 0.0 && self.k < 2.0) return double.inf;
if (x == 0.0) return 0.0;
double k = self.k;
return math::pow(x, k / 2.0 - 1.0) * math::exp(-x / 2.0) /
(math::pow(2.0, k / 2.0) * math::tgamma(k / 2.0));
}
fn double ChiSquaredDist.cdf(&self, double x) @dynamic
{
if (x <= 0.0) return 0.0;
double p = lower_incomplete_gamma(self.k / 2.0, x / 2.0);
return math::clamp(p, 0.0, 1.0);
}
<*
@require p >= 0.0 && p <= 1.0 : "Probability must be between 0 and 1."
*>
fn double ChiSquaredDist.quantile(&self, double p) @dynamic
{
double low = 0.0;
double high = self.k + 10.0 * math::sqrt(2.0 * self.k);
return find_quantile(self, low, high, p);
}
fn double ChiSquaredDist.random(&self, Random rand) @dynamic
{
return chi_squared_sample(self.k, rand);
}
<*
Binomial distribution
*>
struct BinomialDist (DiscreteDistribution)
{
int n;
double p;
}
<*
@require n >= 0 : "Number of trials must be non-negative."
@require p >= 0.0 && p <= 1.0 : "Probability must be between 0 and 1."
*>
fn BinomialDist binomial(int n, double p)
{
return (BinomialDist){ n, p };
}
fn double BinomialDist.mean(&self) @dynamic
{
return (double)self.n * self.p;
}
fn double BinomialDist.variance(&self) @dynamic
{
return (double)self.n * self.p * (1.0 - self.p);
}
fn double BinomialDist.pmf(&self, int k) @dynamic
{
if (k < 0 || k > self.n) return 0.0;
return binomial_coefficient(self.n, k) *
math::pow(self.p, (double)k) *
math::pow(1.0 - self.p, (double)(self.n - k));
}
fn double BinomialDist.cdf(&self, int k) @dynamic
{
if (k < 0) return 0.0;
if (k >= self.n) return 1.0;
double sum = 0.0;
for (int i = 0; i <= k; i++)
{
sum += self.pmf(i);
}
return sum;
}
<*
@require p >= 0.0 && p <= 1.0 : "Probability must be between 0 and 1."
*>
fn int BinomialDist.quantile(&self, double p) @dynamic
{
double cumulative = 0.0;
for (int k = 0; k <= self.n; k++)
{
cumulative += self.pmf(k);
if (cumulative >= p) return k;
}
return self.n;
}
fn int BinomialDist.random(&self, Random rand) @dynamic
{
// Generate using Bernoulli trials.
int successes = 0;
for (int i = 0; i < self.n; i++)
{
if (random::next_double(rand) < self.p)
{
successes++;
}
}
return successes;
}
<*
Poisson distribution
*>
struct PoissonDist (DiscreteDistribution)
{
double lambda;
}
<*
@require lambda > 0.0 : "Rate parameter must be positive."
*>
fn PoissonDist poisson(double lambda)
{
return (PoissonDist){ lambda };
}
fn double PoissonDist.mean(&self) @dynamic
{
return self.lambda;
}
fn double PoissonDist.variance(&self) @dynamic
{
return self.lambda;
}
fn double PoissonDist.pmf(&self, int k) @dynamic
{
if (k < 0) return 0.0;
return math::exp(-self.lambda + (double)k * math::ln(self.lambda) - ln_factorial(k));
}
fn double PoissonDist.cdf(&self, int k) @dynamic
{
if (k < 0) return 0.0;
double sum = 0.0;
for (int i = 0; i <= k; i++)
{
sum += self.pmf(i);
}
return sum;
}
<*
@require p >= 0.0 && p <= 1.0 : "Probability must be between 0 and 1."
*>
fn int PoissonDist.quantile(&self, double p) @dynamic
{
double cumulative = 0.0;
int k = 0;
while (cumulative < p)
{
cumulative += self.pmf(k);
if (cumulative >= p) return k;
k++;
if (k > 1_000_000) break; // Safety limit
}
return k;
}
fn int PoissonDist.random(&self, Random rand) @dynamic
{
// Knuth's algorithm for small lambda.
if (self.lambda < 30.0)
{
double l = math::exp(-self.lambda);
int k = 0;
double p = 1.0;
do
{
k++;
p *= random::next_double(rand);
} while (p > l);
return (k - 1);
}
else
{
// Use normal approximation for large lambda
NormalDist approx = normal(self.lambda, math::sqrt(self.lambda));
return (int)math::max(0.0, math::round(approx.random(rand)));
}
}

View File

@@ -0,0 +1,407 @@
// Copyright (c) 2026 Koni Marti. All rights reserved.
// Use of this source code is governed by the MIT license.
module std::math::distributions @private;
import std::math::random;
struct ConvergenceControl
{
usz max_iter;
double epsilon;
}
const DEFAULT_CONV = (ConvergenceControl){ 600, 1e-12 };
const RELAXED_CONV = (ConvergenceControl){ 600, 1e-6 };
faultdef NOT_CONVERGED;
<*
Compute binomial coefficient C(n, k)
*>
fn double binomial_coefficient(int n, int k)
{
if (k < 0 || k > n) return 0.0;
if (k == 0 || k == n) return 1.0;
// Use symmetry.
if (k > n - k) k = n - k;
double result = 1.0;
for (int i = 0; i < k; i++)
{
result *= (double)(n - i);
result /= (double)(i + 1);
}
return result;
}
<*
Natural logarithm of factorial.
*>
fn double ln_factorial(int n)
{
if (n < 0) return double.nan;
if (n <= 1) return 0.0;
return math::lgamma((double)(n + 1));
}
<*
Beta function B(a, b)
@require a > 0
@require b > 0
*>
fn double beta_function(double a, double b)
{
return math::exp(math::lgamma(a) + math::lgamma(b) - math::lgamma(a + b));
}
<*
Regularized incomplete beta function I_x(a, b)
Based on https://github.com/codeplea/incbeta/blob/master/incbeta.c
*>
fn double incomplete_beta(double x, double a, double b, ConvergenceControl conv = DEFAULT_CONV)
{
if (x < 0.0 || x > 1.0) return double.nan;
if (x == 0.0) return 0.0;
if (x == 1.0) return 1.0;
if (x > (a + 1.0)/(a + b + 2.0)) return 1.0 - incomplete_beta(1.0 - x, b,a);
const double TINY = 1e-30;
// Find the first part before the continued fraction.
double lbeta_ab = math::lgamma(a) + math::lgamma(b) - math::lgamma(a + b);
double front = math::exp(math::ln(x) * a + math::ln(1.0 - x) * b - lbeta_ab) / a;
// Use Lentz's algorithm to evaluate the continued fraction.
double f = 1.0;
double c = 1.0;
double d = 0.0;
usz m;
for (usz i = 0; i <= conv.max_iter; ++i)
{
m = i/2;
double numerator;
if (i == 0)
{
numerator = 1.0;
}
else if (i % 2 == 0)
{
numerator = (m * (b - m) * x) / ((a + 2.0 * m - 1.0) * (a + 2.0 * m));
}
else
{
numerator = -((a + m) * (a + b + m) * x) / ((a + 2.0 * m) * (a + 2.0 * m + 1));
}
d = 1.0 + numerator * d;
if (math::abs(d) < TINY) d = TINY;
d = 1.0 / d;
c = 1.0 + numerator / c;
if (math::abs(c) < TINY) c = TINY;
double cd = c*d;
f *= cd;
if (math::abs(1.0 - cd) < conv.epsilon) return front * (f - 1.0);
}
return double.nan; // Not convered.
}
<*
Calculates the p-th quantile for a continuous distribution using bisection.
@return? NOT_CONVERGED
*>
fn double? bisection_search(ContinuousDistribution dist, double low, double high, double p,
ConvergenceControl conv = DEFAULT_CONV)
{
// Expand upper bound if needed.
while (dist.cdf(high) < p) high *= 2.0;
// Bisection search.
for (usz i = 0; i < conv.max_iter; i++)
{
double mid = (low + high) * 0.5;
if (high - low < conv.epsilon) return mid;
if (dist.cdf(mid) < p)
{
low = mid;
}
else
{
high = mid;
}
}
return NOT_CONVERGED~;
}
<*
Calculates the p-th quantile for a continuous distribution using Newton-Raphson.
@return? NOT_CONVERGED
*>
fn double? newton_raphson(ContinuousDistribution dist, double x, double p,
ConvergenceControl conv = DEFAULT_CONV)
{
double delta, pdf;
for (usz i = 0; i < conv.max_iter; i++)
{
pdf = dist.pdf(x);
if (pdf < 1e-300) break;
delta = (dist.cdf(x) - p) / pdf;
x -= delta;
if (math::abs(delta) < conv.epsilon) return x;
}
return NOT_CONVERGED~;
}
<*
Calculates the p-th quantile for a continuous distribution.
@require p >= 0.0 && p <= 1.0
@require low < high
*>
fn double find_quantile(ContinuousDistribution dist, double low, double high, double p)
{
double mid = bisection_search(dist, low, high, p, RELAXED_CONV) ?? (low + high) * 0.5;
return newton_raphson(dist, mid, p, DEFAULT_CONV) ?? mid;
}
<*
Generate a chi-squared random sample.
@param k : "Degrees of freedom"
@require k > 0.0
*>
fn double chi_squared_sample(double k, Random rand)
{
// Sum of k squared standard normals.
NormalDist std_normal = normal(0.0, 1.0);
double sum = 0.0;
int k_int = (int)k;
for (int i = 0; i < k_int; i++)
{
double z = std_normal.random(rand);
sum += z * z;
}
// Handle fractional degrees of freedom.
double frac = k - (double)k_int;
if (frac > 0.0)
{
double z = std_normal.random(rand);
sum += frac * z * z;
}
return sum;
}
<*
Inverse of the error function (math::erf).
Based on Golang's math.Erfinv.
*>
fn double inverse_erf(double x)
{
if (x < -1 || x > 1)
{
return double.nan;
}
else if (x == 1.0)
{
return double.inf;
}
else if (x == -1.0)
{
return -double.inf;
}
const double LN2 = 6.931471805599453094172321214581e-1;
const double A0 = 1.1975323115670912564578e0;
const double A1 = 4.7072688112383978012285e1;
const double A2 = 6.9706266534389598238465e2;
const double A3 = 4.8548868893843886794648e3;
const double A4 = 1.6235862515167575384252e4;
const double A5 = 2.3782041382114385731252e4;
const double A6 = 1.1819493347062294404278e4;
const double A7 = 8.8709406962545514830200e2;
const double B0 = 1.0000000000000000000e0;
const double B1 = 4.2313330701600911252e1;
const double B2 = 6.8718700749205790830e2;
const double B3 = 5.3941960214247511077e3;
const double B4 = 2.1213794301586595867e4;
const double B5 = 3.9307895800092710610e4;
const double B6 = 2.8729085735721942674e4;
const double B7 = 5.2264952788528545610e3;
const double C0 = 1.42343711074968357734e0;
const double C1 = 4.63033784615654529590e0;
const double C2 = 5.76949722146069140550e0;
const double C3 = 3.64784832476320460504e0;
const double C4 = 1.27045825245236838258e0;
const double C5 = 2.41780725177450611770e-1;
const double C6 = 2.27238449892691845833e-2;
const double C7 = 7.74545014278341407640e-4;
const double D0 = 1.4142135623730950488016887e0;
const double D1 = 2.9036514445419946173133295e0;
const double D2 = 2.3707661626024532365971225e0;
const double D3 = 9.7547832001787427186894837e-1;
const double D4 = 2.0945065210512749128288442e-1;
const double D5 = 2.1494160384252876777097297e-2;
const double D6 = 7.7441459065157709165577218e-4;
const double D7 = 1.4859850019840355905497876e-9;
const double E0 = 6.65790464350110377720e0;
const double E1 = 5.46378491116411436990e0;
const double E2 = 1.78482653991729133580e0;
const double E3 = 2.96560571828504891230e-1;
const double E4 = 2.65321895265761230930e-2;
const double E5 = 1.24266094738807843860e-3;
const double E6 = 2.71155556874348757815e-5;
const double E7 = 2.01033439929228813265e-7;
const double F0 = 1.414213562373095048801689e0;
const double F1 = 8.482908416595164588112026e-1;
const double F2 = 1.936480946950659106176712e-1;
const double F3 = 2.103693768272068968719679e-2;
const double F4 = 1.112800997078859844711555e-3;
const double F5 = 2.611088405080593625138020e-5;
const double F6 = 2.010321207683943062279931e-7;
const double F7 = 2.891024605872965461538222e-15;
double sign = 1.0;
if (x < 0)
{
x = -x;
sign = -1.0;
}
double ans;
if (x <= 0.85)
{
double r = 0.180625 - 0.25 * x *x;
double z1 = ((((((A7 * r + A6) * r + A5) * r + A4) * r + A3) * r + A2) * r + A1) * r + A0;
double z2 = ((((((B7 * r + B6) * r + B5) * r + B4) * r + B3) * r + B2) * r + B1) * r + B0;
ans = (x * z1) / z2;
}
else
{
double z1, z2;
double r = math::sqrt(LN2 - math::ln(1.0 - x));
if (r <= 5.0)
{
r -= 1.6;
z1 = ((((((C7 * r + C6) * r + C5) * r + C4) * r + C3) * r + C2) * r + C1) * r + C0;
z2 = ((((((D7 * r + D6) * r + D5) * r + D4) * r + D3) * r + D2) * r + D1) * r + D0;
}
else
{
r -= 5.0;
z1 = ((((((E7 * r + E6) * r + E5) * r + E4) * r + E3) * r + E2) * r + E1) * r + E0;
z2 = ((((((F7 * r + F6) * r + F5) * r + F4) * r + F3) * r + F2) * r + F1) * r + F0;
}
ans = z1 / z2;
}
return sign * ans;
}
<*
Regularized Lower incomplete gamma function.
Returns nan when not converged.
@param s : "Shape parameter"
@param x : "Upper limit of integration"
@require s > 0.0 : "s must be positive."
@require x >= 0.0 : "x must be non-negative."
*>
fn double lower_incomplete_gamma(double s, double x)
{
if (x == 0.0) return 0.0;
if (x == double.inf) return 1.0;
// Use series expansion for x < s+1
if (x < s + 1.0)
{
return incomplete_gamma_series_expansion(s, x) ?? double.nan;
}
else
{
return 1.0 - incomplete_gamma_continued_fraction(s, x) ?? double.nan;
}
}
<*
Lower incomplete gamma series expansion.
@return? NOT_CONVERGED
*>
fn double? incomplete_gamma_series_expansion(double s, double x, ConvergenceControl conv = DEFAULT_CONV)
{
double lnpre = s * math::ln(x) - x - math::lgamma(s + 1.0);
if (lnpre < -708.0) return 0.0; // result underflows to zero
double term = 1.0;
double sum = 1.0;
double ap = s;
for (int n = 1; n <= conv.max_iter; n++)
{
ap += 1.0;
term *= x / ap;
sum += term;
if (math::abs(term) < math::abs(sum) * conv.epsilon)
{
return math::exp(lnpre) * sum;
}
}
// Non-convergence.
return NOT_CONVERGED~;
}
<*
Modified Lentz continued fraction.
@return? NOT_CONVERGED
*>
fn double? incomplete_gamma_continued_fraction(double s, double x, ConvergenceControl conv = DEFAULT_CONV)
{
double lnpre = s * math::ln(x) - x - math::lgamma(s);
// Lentz initialisation: fpmin guards against division by zero.
double fpmin = 1e-300;
double b = x + 1.0 - s;
double c = 1.0 / fpmin;
double d = 1.0 / b;
double h = d;
for (int i = 1; i <= conv.max_iter; i++)
{
double an = (double)i * (s - (double)i);
b += 2.0;
d = an * d + b;
if (math::abs(d) < fpmin) d = fpmin;
c = b + an / c;
if (math::abs(c) < fpmin) c = fpmin;
d = 1.0 / d;
double delta = d * c;
h *= delta;
if (math::abs(delta - 1.0) < conv.epsilon)
{
return math::exp(lnpre) * h;
}
}
// Non-convergence.
return NOT_CONVERGED~;
}

View File

@@ -124,6 +124,42 @@ macro sign(x)
$endif
}
<*
@require values::@is_int(x) || values::@is_float(x) : "Expected an integer or floating point value"
*>
macro erf(x)
{
$if $typeof(x) == float:
return _erff(x);
$else
return _erf(x);
$endif
}
<*
@require values::@is_int(x) || values::@is_float(x) : "Expected an integer or floating point value"
*>
macro tgamma(x)
{
$if $typeof(x) == float:
return _tgammaf(x);
$else
return _tgamma(x);
$endif
}
<*
@require values::@is_int(x) || values::@is_float(x) : "Expected an integer or floating point value"
*>
macro lgamma(x)
{
$if $typeof(x) == float:
return _lgammaf(x);
$else
return _lgamma(x);
$endif
}
<*
@require values::@is_int(x) || values::@is_float(x) : "Expected an integer or floating point value"
@require values::@is_int(y) || values::@is_float(y) : "Expected an integer or floating point value"
@@ -1088,6 +1124,14 @@ extern fn float _acoshf(float x) @MathLibc("acoshf");
extern fn float _asinhf(float x) @MathLibc("asinhf");
extern fn float _atanhf(float x) @MathLibc("atanhf");
extern fn double _erf(double x) @MathLibc("erf");
extern fn double _lgamma(double x) @MathLibc("lgamma");
extern fn double _tgamma(double x) @MathLibc("tgamma");
extern fn float _erff(float x) @MathLibc("erf");
extern fn float _lgammaf(float x) @MathLibc("lgammaf");
extern fn float _tgammaf(float x) @MathLibc("tgammaf");
fn double _frexp(double x, int* e)
{

View File

@@ -0,0 +1,77 @@
module std::math::nolibc @if(env::NO_LIBC || $feature(C3_MATH));
<*
Error function for float.
*>
fn float _erff(float x)
{
// Handle special cases.
if (x == 0.0) return 0.0;
if (x == float.inf) return 1.0;
if (x == -float.inf) return -1.0;
// Use symmetry: erf(-x) = -erf(x)
float sign = 1.0;
if (x < 0.0)
{
x = -x;
sign = -1.0;
}
const float P1 = 0.4067420160f;
const float P2 = 0.0072279182f;
const float A1 = 0.3168798904f;
const float A2 = -0.1383293141f;
const float A3 = 1.0868083034f;
const float A4 = -1.1169415512f;
const float A5 = 1.2064490307f;
const float A6 = -0.3931277152f;
const float A7 = 0.0382613542f;
float t = 1.0f / (1.0f + P1 * x + P2 * x * x);
float t2 = t * t;
float sum = A1 * t + A2 * t2 + A3 * t2 * t + A4 * t2 * t2;
sum += A5 * t2 * t2 * t + A6 * t2 * t2 * t2 + A7 * t2 * t2 * t2 * t;
// For intermediate x, use continued fraction.
return sign * (1.0f - sum * math::exp(-x * x));
}
<*
Error function for double.
*>
fn double _erf(double x)
{
// Handle special cases.
if (x == 0.0) return 0.0;
if (x == double.inf) return 1.0;
if (x == -double.inf) return -1.0;
// Use symmetry: erf(-x) = -erf(x)
double sign = 1.0;
if (x < 0.0)
{
x = -x;
sign = -1.0;
}
const double P1 = 0.406742016006509;
const double P2 = 0.0072279182302319;
const double A1 = 0.316879890481381;
const double A2 = -0.138329314150635;
const double A3 = 1.08680830347054;
const double A4 = -1.11694155120396;
const double A5 = 1.20644903073232;
const double A6 = -0.393127715207728;
const double A7 = 0.0382613542530727;
double t = 1.0 / (1.0 + P1 * x + P2 * x * x);
double t2 = t * t;
double sum = A1 * t + A2 * t2 + A3 * t2 * t + A4 * t2 * t2;
sum += A5 * t2 * t2 * t + A6 * t2 * t2 * t2 + A7 * t2 * t2 * t2 * t;
// For intermediate x, use continued fraction.
return sign * (1.0 - sum * math::exp(-x * x));
}

View File

@@ -0,0 +1,70 @@
module std::math::nolibc @if(env::NO_LIBC || $feature(C3_MATH));
// Constants
const double SQRT_PI = 1.7724538509055160272981674833411451;
const double LN_SQRT_2PI = 0.9189385332046727417803297364056176;
<*
Natural logarithm of the gamma function using the Lanczos approximation.
@require x > 0.0 : "x must be positive."
*>
fn double lgamma(double x)
{
// Lanczos approximation coefficients (g = 7, n = 9)
const double[*] LANCZOS_COEF = {
0.99999999999980993,
676.5203681218851,
-1259.1392167224028,
771.32342877765313,
-176.61502916214059,
12.507343278686905,
-0.13857109526572012,
9.9843695780195716e-6,
1.5056327351493116e-7
};
const double LANCZOS_G = 7.0;
// For small x, use the reflection formula.
if (x < 0.5)
{
return math::ln(math::PI) - math::ln(math::sin(math::PI * x)) - lgamma(1.0 - x);
}
// Shift to use approximation around x >= 1.5
x -= 1.0;
double base = x + LANCZOS_G + 0.5;
double sum = LANCZOS_COEF[0];
for (int i = 1; i < 9; i++)
{
sum += LANCZOS_COEF[i] / (x + (double)i);
}
return LN_SQRT_2PI + math::ln(sum) + (x + 0.5) * math::ln(base) - base;
}
<*
Gamma function.
Valid for x > 0 and some negative non-integer values.
*>
fn double tgamma(double x)
{
// Handle special cases.
if (x == 0.0) return double.inf;
// Check for negative integers (poles).
if (x < 0.0 && x == math::floor(x))
{
return double.nan;
}
// For positive values, use exp(lgamma(x)).
if (x > 0.0)
{
return math::exp(lgamma(x));
}
// For negative non-integer values, use the reflection formula.
return math::PI / (math::sin(math::PI * x) * tgamma(1.0 - x));
}