From 0ad84e27b46095636ad3470455fad0301aef97a6 Mon Sep 17 00:00:00 2001 From: shivasankar Date: Tue, 3 Mar 2026 16:06:10 +0900 Subject: [PATCH 1/8] create an rv_continuous trait --- src/stamojo/distributions/traits.mojo | 55 +++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/stamojo/distributions/traits.mojo diff --git a/src/stamojo/distributions/traits.mojo b/src/stamojo/distributions/traits.mojo new file mode 100644 index 0000000..af4e606 --- /dev/null +++ b/src/stamojo/distributions/traits.mojo @@ -0,0 +1,55 @@ +trait RVContinuousLike(Copyable, Movable): + """Trait for continuous random variable distributions.""" + + # --- Density functions --------------------------------------------------- + + fn pdf(self, x: Float64, loc: Float64, scale: Float64) -> Float64: + """Probability density function at *x*.""" + ... + + fn logpdf(self, x: Float64, loc: Float64, scale: Float64) -> Float64: + """Natural logarithm of the probability density function at *x*.""" + ... + + # --- Distribution functions ---------------------------------------------- + + fn cdf(self, x: Float64, loc: Float64, scale: Float64) -> Float64: + """Cumulative distribution function P(X ≤ x).""" + ... + + fn logcdf(self, x: Float64, loc: Float64, scale: Float64) -> Float64: + """Natural logarithm of the cumulative distribution function at *x*.""" + ... + + fn sf(self, x: Float64, loc: Float64, scale: Float64) -> Float64: + """Survival function (1 − CDF) at *x*.""" + ... + + fn logsf(self, x: Float64, loc: Float64, scale: Float64) -> Float64: + """Natural logarithm of the survival function at *x*.""" + ... + + fn ppf(self, q: Float64, loc: Float64, scale: Float64) -> Float64: + """Percent point function (inverse of CDF) at *q*.""" + ... + + fn isf(self, q: Float64, loc: Float64, scale: Float64) -> Float64: + """Inverse survival function (inverse of SF) at *q*.""" + ... + + # --- Statistical properties ------------------------------------------------ + fn median(self, loc: Float64, scale: Float64) -> Float64: + """Median of the distribution.""" + ... + + fn mean(self, loc: Float64, scale: Float64) -> Float64: + """Mean of the distribution.""" + ... + + fn var(self, loc: Float64, scale: Float64) -> Float64: + """Variance of the distribution.""" + ... + + fn std(self, loc: Float64, scale: Float64) -> Float64: + """Standard deviation of the distribution.""" + ... From c72364b452a5d7387843b223ba82aebb2ab537d8 Mon Sep 17 00:00:00 2001 From: shivasankar Date: Tue, 3 Mar 2026 16:06:34 +0900 Subject: [PATCH 2/8] implement exponential distribution --- src/stamojo/distributions/exponential.mojo | 228 +++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 src/stamojo/distributions/exponential.mojo diff --git a/src/stamojo/distributions/exponential.mojo b/src/stamojo/distributions/exponential.mojo new file mode 100644 index 0000000..52bf276 --- /dev/null +++ b/src/stamojo/distributions/exponential.mojo @@ -0,0 +1,228 @@ +# ===----------------------------------------------------------------------=== # +# Stamojo - Distributions - Exponential distribution +# Licensed under Apache 2.0 +# ===----------------------------------------------------------------------=== # +"""Exponential distribution. + +Provides the `Exponential` distribution struct with PDF, log-PDF, CDF, survival function, percent-point function (PPF / quantile), and random variate +generation. + +The exponential distribution with rate parameter λ has PDF: + + f(x; λ) = λ exp(−λx), x ≥ 0 +""" + +from math import sqrt, log, lgamma, exp, nan, inf, log1p, expm1 + +from stamojo.distributions.traits import RVContinuousLike + +struct Expon(Copyable, Movable, RVContinuousLike): + """Exponential distribution. + + Represents the exponential distribution, a continuous probability distribution commonly + used to model the time between independent events that occur at a constant average rate. + + The probability density function (PDF) for the standardized exponential distribution is: + f(x) = exp(-x) + for x >= 0. + + This implementation allows shifting and scaling of the distribution using the `loc` (location) and `scale` parameters: + Expon.pdf(x, loc, scale) = (1/scale) * exp(-(x - loc) / scale) + which is equivalent to `Expon.pdf((x - loc) / scale) / scale`. + + The most common parameterization uses the rate parameter λ > 0, where: + f(x; λ) = λ * exp(-λx), for x >= 0 + This is achieved by setting scale = 1/λ and loc = 0. + """ + + # --- Density functions --------------------------------------------------- + + fn pdf(self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + """Probability density function at x for Expon(loc, scale). + + Args: + x: Point at which to evaluate the PDF. + loc: Location (shift) parameter. + scale: Scale parameter. Must be positive. + + Returns: + 0.0 for x < loc. For x >= loc returns (1/scale) * exp(-(x-loc)/scale). + """ + var y = (x - loc) / scale + if y < 0.0: + return 0.0 + return exp(-y) / scale + + fn logpdf(self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + """Natural logarithm of the PDF at x for Expon(loc, scale). + + Args: + x: Point at which to evaluate the log-PDF. + loc: Location (shift) parameter. + scale: Scale parameter (must be > 0). + + Returns: + -∞ for x < loc. For x >= loc returns -((x - loc) / scale) - log(scale). + """ + var y = (x - loc) / scale + if y < 0.0: + return -inf[DType.float64]() + return -y - log(scale) + + # --- Distribution functions ---------------------------------------------- + fn cdf(self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + """Cumulative distribution function P(X <= x) for Expon(loc, scale). + + Args: + x: Value at which to evaluate the CDF. + loc: Location (shift) parameter. + scale: Scale parameter (must be > 0). + + Returns: + 0.0 for x < loc. For x >= loc returns 1 - exp(-(x - loc)/scale). + """ + if x < loc: + return 0.0 + var y = (x - loc) / scale + return -expm1(-y) + + fn logcdf(self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + """Natural logarithm of the CDF P(X <= x) for Expon(loc, scale). + + Args: + x: Value at which to evaluate the log-CDF. + loc: Location (shift) parameter. + scale: Scale parameter (must be > 0). + + Returns: + -∞ for x < loc. For x >= loc returns log(1 - exp(-(x - loc)/scale)). + """ + if x < loc: + return -inf[DType.float64]() + var y = (x - loc) / scale + return log1p(-exp(-y)) + + fn sf(self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + """Survival function P(X > x) for Expon(loc, scale). + + Args: + x: Value at which to evaluate the survival function. + loc: Location (shift) parameter. + scale: Scale parameter (must be > 0). + + Returns: + 1.0 for x < loc. For x >= loc returns exp(-(x - loc)/scale). + """ + if x < loc: + return 1.0 + var y = (x - loc) / scale + return exp(-y) + + fn logsf(self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + """Natural logarithm of the survival function for Expon(loc, scale). + + Args: + x: Value at which to evaluate the log-SF. + loc: Location (shift) parameter. + scale: Scale parameter (must be > 0). + + Returns: + 0.0 for x < loc. For x >= loc returns -(x - loc)/scale. + """ + if x < loc: + return 0.0 + var y = (x - loc) / scale + return -y + + fn ppf(self, q: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + """Percent-point (quantile) function for Expon(loc, scale). + + For 0 <= q < 1: PPF(q) = loc - scale * log(1 - q). + PPF(0) = loc. + PPF(1) = +∞. + + Args: + q: Probability in [0, 1]. + loc: Location (shift) parameter. + scale: Scale parameter (must be > 0). + """ + if q < 0.0 or q > 1.0: + return nan[DType.float64]() + if q == 0.0: + return loc + if q == 1.0: + return inf[DType.float64]() + return loc - scale * log1p(-q) + + fn isf(self, q: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + """Inverse survival function for Expon(loc, scale). + + For 0 < q <= 1: ISF(q) = loc - scale * log(q). + ISF(0) = +∞. + ISF(1) = loc. + + Args: + q: Probability in [0, 1]. + loc: Location (shift) parameter. + scale: Scale parameter (must be > 0). + """ + if q < 0.0 or q > 1.0: + return nan[DType.float64]() + if q == 0.0: + return inf[DType.float64]() + if q == 1.0: + return loc + return loc - scale * log(q) + + # --- Summary statistics -------------------------------------------------- + fn median(self, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + """ + Median of the Expon distribution. + + Args: + loc: Location (shift) parameter. + scale: Scale parameter (must be > 0). + + Returns: + The median value, computed as loc + scale * log(2). + """ + return loc + scale * log(2.0) + + fn mean(self, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + """ + Mean of the Expon distribution. + + Args: + loc: Location (shift) parameter. + scale: Scale parameter (must be > 0). + + Returns: + The mean value, computed as loc + scale. + """ + return loc + scale + + fn var(self, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + """ + Variance of the Expon distribution. + + Args: + loc: Location parameter (unused in variance). + scale: Scale parameter (must be > 0). + + Returns: + The variance value, computed as scale * scale. + """ + return scale * scale + + fn std(self, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + """ + Standard deviation of the Expon distribution. + + Args: + loc: Location parameter (unused in std). + scale: Scale parameter (must be > 0). + + Returns: + The standard deviation value, equal to scale. + """ + return scale From 6eecc2f4e908e90920bad68803432a508c010c68 Mon Sep 17 00:00:00 2001 From: shivasankar Date: Tue, 3 Mar 2026 16:14:03 +0900 Subject: [PATCH 3/8] change var to variance as it's a reserved keyword. --- src/stamojo/distributions/exponential.mojo | 35 ++++++++++++++++------ src/stamojo/distributions/traits.mojo | 2 +- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/stamojo/distributions/exponential.mojo b/src/stamojo/distributions/exponential.mojo index 52bf276..020234e 100644 --- a/src/stamojo/distributions/exponential.mojo +++ b/src/stamojo/distributions/exponential.mojo @@ -16,6 +16,7 @@ from math import sqrt, log, lgamma, exp, nan, inf, log1p, expm1 from stamojo.distributions.traits import RVContinuousLike + struct Expon(Copyable, Movable, RVContinuousLike): """Exponential distribution. @@ -37,7 +38,9 @@ struct Expon(Copyable, Movable, RVContinuousLike): # --- Density functions --------------------------------------------------- - fn pdf(self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn pdf( + self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 + ) -> Float64: """Probability density function at x for Expon(loc, scale). Args: @@ -53,7 +56,9 @@ struct Expon(Copyable, Movable, RVContinuousLike): return 0.0 return exp(-y) / scale - fn logpdf(self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn logpdf( + self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 + ) -> Float64: """Natural logarithm of the PDF at x for Expon(loc, scale). Args: @@ -70,7 +75,9 @@ struct Expon(Copyable, Movable, RVContinuousLike): return -y - log(scale) # --- Distribution functions ---------------------------------------------- - fn cdf(self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn cdf( + self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 + ) -> Float64: """Cumulative distribution function P(X <= x) for Expon(loc, scale). Args: @@ -86,7 +93,9 @@ struct Expon(Copyable, Movable, RVContinuousLike): var y = (x - loc) / scale return -expm1(-y) - fn logcdf(self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn logcdf( + self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 + ) -> Float64: """Natural logarithm of the CDF P(X <= x) for Expon(loc, scale). Args: @@ -102,7 +111,9 @@ struct Expon(Copyable, Movable, RVContinuousLike): var y = (x - loc) / scale return log1p(-exp(-y)) - fn sf(self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn sf( + self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 + ) -> Float64: """Survival function P(X > x) for Expon(loc, scale). Args: @@ -118,7 +129,9 @@ struct Expon(Copyable, Movable, RVContinuousLike): var y = (x - loc) / scale return exp(-y) - fn logsf(self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn logsf( + self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 + ) -> Float64: """Natural logarithm of the survival function for Expon(loc, scale). Args: @@ -134,7 +147,9 @@ struct Expon(Copyable, Movable, RVContinuousLike): var y = (x - loc) / scale return -y - fn ppf(self, q: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn ppf( + self, q: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 + ) -> Float64: """Percent-point (quantile) function for Expon(loc, scale). For 0 <= q < 1: PPF(q) = loc - scale * log(1 - q). @@ -154,7 +169,9 @@ struct Expon(Copyable, Movable, RVContinuousLike): return inf[DType.float64]() return loc - scale * log1p(-q) - fn isf(self, q: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn isf( + self, q: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 + ) -> Float64: """Inverse survival function for Expon(loc, scale). For 0 < q <= 1: ISF(q) = loc - scale * log(q). @@ -201,7 +218,7 @@ struct Expon(Copyable, Movable, RVContinuousLike): """ return loc + scale - fn var(self, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn variance(self, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: """ Variance of the Expon distribution. diff --git a/src/stamojo/distributions/traits.mojo b/src/stamojo/distributions/traits.mojo index af4e606..8f91fdf 100644 --- a/src/stamojo/distributions/traits.mojo +++ b/src/stamojo/distributions/traits.mojo @@ -46,7 +46,7 @@ trait RVContinuousLike(Copyable, Movable): """Mean of the distribution.""" ... - fn var(self, loc: Float64, scale: Float64) -> Float64: + fn variance(self, loc: Float64, scale: Float64) -> Float64: """Variance of the distribution.""" ... From ae5277d121b0be8229ecb2d737e739206a10bb8c Mon Sep 17 00:00:00 2001 From: shivasankar Date: Tue, 3 Mar 2026 16:23:12 +0900 Subject: [PATCH 4/8] add tests for exponential, fix imports --- src/stamojo/distributions/__init__.mojo | 1 + src/stamojo/distributions/exponential.mojo | 5 + tests/test_distributions.mojo | 220 ++++++++++++++++++++- 3 files changed, 223 insertions(+), 3 deletions(-) diff --git a/src/stamojo/distributions/__init__.mojo b/src/stamojo/distributions/__init__.mojo index c124457..e3de41f 100644 --- a/src/stamojo/distributions/__init__.mojo +++ b/src/stamojo/distributions/__init__.mojo @@ -18,3 +18,4 @@ from .normal import Normal from .t import StudentT from .chi2 import ChiSquared from .f import FDist +from .exponential import Expon diff --git a/src/stamojo/distributions/exponential.mojo b/src/stamojo/distributions/exponential.mojo index 020234e..cd6173d 100644 --- a/src/stamojo/distributions/exponential.mojo +++ b/src/stamojo/distributions/exponential.mojo @@ -36,6 +36,11 @@ struct Expon(Copyable, Movable, RVContinuousLike): This is achieved by setting scale = 1/λ and loc = 0. """ + # --- Initialization ------------------------------------------------------- + + fn __init__(out self): + pass + # --- Density functions --------------------------------------------------- fn pdf( diff --git a/tests/test_distributions.mojo b/tests/test_distributions.mojo index ea2ce29..9aab9cd 100644 --- a/tests/test_distributions.mojo +++ b/tests/test_distributions.mojo @@ -4,7 +4,7 @@ # ===----------------------------------------------------------------------=== # """Tests for the distributions subpackage. -Covers Normal, Student's t, Chi-squared, and F distributions. +Covers Normal, Student's t, Chi-squared, F, and Exponential distributions. Each distribution is tested for: - Known analytical values - CDF/PPF round-trip consistency @@ -12,11 +12,11 @@ Each distribution is tested for: - Comparison against scipy.stats (when available) """ -from math import exp +from math import exp, log from python import Python, PythonObject from testing import assert_almost_equal -from stamojo.distributions import Normal, StudentT, ChiSquared, FDist +from stamojo.distributions import Normal, StudentT, ChiSquared, FDist, Expon # ===----------------------------------------------------------------------=== # @@ -313,6 +313,206 @@ fn test_f_scipy() raises: print("✓ test_f_scipy passed") +# ===----------------------------------------------------------------------=== # +# Exponential distribution tests +# ===----------------------------------------------------------------------=== # + + +fn test_expon_pdf() raises: + """Test Exponential PDF at known values.""" + var e = Expon() + # Standard exponential: pdf(0) = 1.0 + assert_almost_equal(e.pdf(0.0), 1.0, atol=1e-15) + # pdf(1) = exp(-1) + assert_almost_equal(e.pdf(1.0), exp(-1.0), atol=1e-15) + # pdf(2) = exp(-2) + assert_almost_equal(e.pdf(2.0), exp(-2.0), atol=1e-15) + # pdf(x < 0) = 0 + assert_almost_equal(e.pdf(-1.0), 0.0, atol=1e-15) + # With scale=2: pdf(x) = (1/2)*exp(-x/2) + assert_almost_equal(e.pdf(0.0, scale=2.0), 0.5, atol=1e-15) + assert_almost_equal(e.pdf(2.0, scale=2.0), 0.5 * exp(-1.0), atol=1e-15) + # With loc=1: pdf(1) = 1.0, pdf(0) = 0.0 + assert_almost_equal(e.pdf(1.0, loc=1.0), 1.0, atol=1e-15) + assert_almost_equal(e.pdf(0.5, loc=1.0), 0.0, atol=1e-15) + print("✓ test_expon_pdf passed") + + +fn test_expon_logpdf() raises: + """Test Exponential log-PDF at known values.""" + var e = Expon() + # logpdf(0) = 0.0 for standard exponential + assert_almost_equal(e.logpdf(0.0), 0.0, atol=1e-15) + # logpdf(1) = -1.0 + assert_almost_equal(e.logpdf(1.0), -1.0, atol=1e-15) + # logpdf(x) = log(pdf(x)) + assert_almost_equal(e.logpdf(2.0), log(e.pdf(2.0)), atol=1e-15) + # With scale=3: logpdf(x) = -x/3 - log(3) + assert_almost_equal(e.logpdf(3.0, scale=3.0), -1.0 - log(3.0), atol=1e-15) + print("✓ test_expon_logpdf passed") + + +fn test_expon_cdf() raises: + """Test Exponential CDF at known values.""" + var e = Expon() + # CDF(0) = 0 + assert_almost_equal(e.cdf(0.0), 0.0, atol=1e-15) + # CDF(1) = 1 - exp(-1) + assert_almost_equal(e.cdf(1.0), 1.0 - exp(-1.0), atol=1e-15) + # CDF(x < 0) = 0 + assert_almost_equal(e.cdf(-1.0), 0.0, atol=1e-15) + # CDF should be monotonically increasing + var c1 = e.cdf(0.5) + var c2 = e.cdf(1.0) + var c3 = e.cdf(5.0) + if not (c1 < c2 and c2 < c3): + raise Error("Expon CDF not monotonically increasing") + # With scale=0.5 (rate=2): CDF(x) = 1 - exp(-2x) + assert_almost_equal(e.cdf(1.0, scale=0.5), 1.0 - exp(-2.0), atol=1e-15) + print("✓ test_expon_cdf passed") + + +fn test_expon_sf() raises: + """Test Exponential survival function: SF(x) = 1 - CDF(x).""" + var e = Expon() + assert_almost_equal(e.sf(0.0), 1.0, atol=1e-15) + assert_almost_equal(e.sf(1.0), exp(-1.0), atol=1e-15) + # CDF + SF = 1 + var xs: List[Float64] = [0.1, 0.5, 1.0, 2.0, 5.0, 10.0] + for i in range(len(xs)): + assert_almost_equal(e.cdf(xs[i]) + e.sf(xs[i]), 1.0, atol=1e-15) + # SF(x < loc) = 1 + assert_almost_equal(e.sf(-1.0), 1.0, atol=1e-15) + print("✓ test_expon_sf passed") + + +fn test_expon_ppf() raises: + """Test Exponential PPF (inverse CDF).""" + var e = Expon() + # PPF(0) = 0 (loc) + assert_almost_equal(e.ppf(0.0), 0.0, atol=1e-15) + # PPF(1 - exp(-1)) = 1 (since CDF(1) = 1 - exp(-1)) + assert_almost_equal(e.ppf(1.0 - exp(-1.0)), 1.0, atol=1e-12) + # PPF(0.5) = ln(2) (median of standard exponential) + assert_almost_equal(e.ppf(0.5), log(2.0), atol=1e-12) + # With loc and scale + assert_almost_equal(e.ppf(0.5, loc=1.0, scale=2.0), 1.0 + 2.0 * log(2.0), atol=1e-12) + print("✓ test_expon_ppf passed") + + +fn test_expon_cdf_ppf_roundtrip() raises: + """Test CDF(PPF(p)) ≈ p for many probability values.""" + var e = Expon() + var ps: List[Float64] = [0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] + for i in range(len(ps)): + var p = ps[i] + assert_almost_equal(e.cdf(e.ppf(p)), p, atol=1e-12) + # Also test with loc and scale + var loc = 2.0 + var scale = 3.0 + for i in range(len(ps)): + var p = ps[i] + assert_almost_equal(e.cdf(e.ppf(p, loc, scale), loc, scale), p, atol=1e-12) + print("✓ test_expon_cdf_ppf_roundtrip passed") + + +fn test_expon_isf() raises: + """Test Exponential ISF (inverse survival function).""" + var e = Expon() + # ISF(1) = loc = 0 + assert_almost_equal(e.isf(1.0), 0.0, atol=1e-15) + # ISF(exp(-1)) = 1 (since SF(1) = exp(-1)) + assert_almost_equal(e.isf(exp(-1.0)), 1.0, atol=1e-12) + # ISF(q) = PPF(1 - q) + var qs: List[Float64] = [0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] + for i in range(len(qs)): + var q = qs[i] + assert_almost_equal(e.isf(q), e.ppf(1.0 - q), atol=1e-12) + print("✓ test_expon_isf passed") + + +fn test_expon_logcdf_logsf() raises: + """Test log-CDF and log-SF against log of CDF and SF.""" + var e = Expon() + var xs: List[Float64] = [0.01, 0.1, 0.5, 1.0, 2.0, 5.0] + for i in range(len(xs)): + var x = xs[i] + assert_almost_equal(e.logcdf(x), log(e.cdf(x)), atol=1e-12) + assert_almost_equal(e.logsf(x), log(e.sf(x)), atol=1e-15) + print("✓ test_expon_logcdf_logsf passed") + + +fn test_expon_stats() raises: + """Test Exponential distribution summary statistics.""" + var e = Expon() + # Standard exponential: mean=1, var=1, std=1, median=ln(2) + assert_almost_equal(e.mean(), 1.0, atol=1e-15) + assert_almost_equal(e.variance(), 1.0, atol=1e-15) + assert_almost_equal(e.std(), 1.0, atol=1e-15) + assert_almost_equal(e.median(), log(2.0), atol=1e-15) + # With loc=2, scale=3: mean=5, var=9, std=3, median=2+3*ln(2) + assert_almost_equal(e.mean(loc=2.0, scale=3.0), 5.0, atol=1e-15) + assert_almost_equal(e.variance(loc=2.0, scale=3.0), 9.0, atol=1e-15) + assert_almost_equal(e.std(loc=2.0, scale=3.0), 3.0, atol=1e-15) + assert_almost_equal(e.median(loc=2.0, scale=3.0), 2.0 + 3.0 * log(2.0), atol=1e-15) + print("✓ test_expon_stats passed") + + +fn test_expon_loc_scale() raises: + """Test Exponential with non-default loc and scale across all functions.""" + var e = Expon() + var loc = 5.0 + var scale = 2.0 + # PDF at loc should be 1/scale + assert_almost_equal(e.pdf(loc, loc, scale), 1.0 / scale, atol=1e-15) + # CDF at loc should be 0 + assert_almost_equal(e.cdf(loc, loc, scale), 0.0, atol=1e-15) + # SF at loc should be 1 + assert_almost_equal(e.sf(loc, loc, scale), 1.0, atol=1e-15) + # CDF(loc + scale) = 1 - exp(-1) + assert_almost_equal(e.cdf(loc + scale, loc, scale), 1.0 - exp(-1.0), atol=1e-15) + print("✓ test_expon_loc_scale passed") + + +fn test_expon_scipy() raises: + """Test Exponential distribution against scipy.stats.expon.""" + var sp = _load_scipy_stats() + if sp is None: + print("test_expon_scipy skipped (scipy not available)") + return + + var e = Expon() + var xs: List[Float64] = [0.0, 0.5, 1.0, 2.0, 5.0, 10.0] + + for i in range(len(xs)): + var x = xs[i] + var sp_pdf = _py_f64(sp.expon.pdf(x)) + var sp_cdf = _py_f64(sp.expon.cdf(x)) + var sp_sf = _py_f64(sp.expon.sf(x)) + assert_almost_equal(e.pdf(x), sp_pdf, atol=1e-12) + assert_almost_equal(e.cdf(x), sp_cdf, atol=1e-12) + assert_almost_equal(e.sf(x), sp_sf, atol=1e-12) + + # Test PPF + var ps: List[Float64] = [0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] + for i in range(len(ps)): + var p = ps[i] + var sp_ppf = _py_f64(sp.expon.ppf(p)) + assert_almost_equal(e.ppf(p), sp_ppf, atol=1e-12) + + # Test with loc and scale + var loc = 2.0 + var scale = 3.0 + for i in range(len(xs)): + var x = xs[i] + loc + var sp_pdf2 = _py_f64(sp.expon.pdf(x, loc, scale)) + var sp_cdf2 = _py_f64(sp.expon.cdf(x, loc, scale)) + assert_almost_equal(e.pdf(x, loc, scale), sp_pdf2, atol=1e-12) + assert_almost_equal(e.cdf(x, loc, scale), sp_cdf2, atol=1e-12) + + print("✓ test_expon_scipy passed") + + # ===----------------------------------------------------------------------=== # # Main test runner # ===----------------------------------------------------------------------=== # @@ -352,6 +552,20 @@ fn main() raises: test_f_ppf() test_f_stats() test_f_scipy() + print() + + # Exponential + test_expon_pdf() + test_expon_logpdf() + test_expon_cdf() + test_expon_sf() + test_expon_ppf() + test_expon_cdf_ppf_roundtrip() + test_expon_isf() + test_expon_logcdf_logsf() + test_expon_stats() + test_expon_loc_scale() + test_expon_scipy() print() print("=== All distribution tests passed ===") From 2a8a0069e2bcfcd0407b97d73ff79367f28374ca Mon Sep 17 00:00:00 2001 From: shivasankar Date: Tue, 3 Mar 2026 16:23:21 +0900 Subject: [PATCH 5/8] Update test_distributions.mojo --- tests/test_distributions.mojo | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_distributions.mojo b/tests/test_distributions.mojo index 9aab9cd..4f6a3f8 100644 --- a/tests/test_distributions.mojo +++ b/tests/test_distributions.mojo @@ -396,7 +396,9 @@ fn test_expon_ppf() raises: # PPF(0.5) = ln(2) (median of standard exponential) assert_almost_equal(e.ppf(0.5), log(2.0), atol=1e-12) # With loc and scale - assert_almost_equal(e.ppf(0.5, loc=1.0, scale=2.0), 1.0 + 2.0 * log(2.0), atol=1e-12) + assert_almost_equal( + e.ppf(0.5, loc=1.0, scale=2.0), 1.0 + 2.0 * log(2.0), atol=1e-12 + ) print("✓ test_expon_ppf passed") @@ -412,7 +414,9 @@ fn test_expon_cdf_ppf_roundtrip() raises: var scale = 3.0 for i in range(len(ps)): var p = ps[i] - assert_almost_equal(e.cdf(e.ppf(p, loc, scale), loc, scale), p, atol=1e-12) + assert_almost_equal( + e.cdf(e.ppf(p, loc, scale), loc, scale), p, atol=1e-12 + ) print("✓ test_expon_cdf_ppf_roundtrip passed") @@ -454,7 +458,9 @@ fn test_expon_stats() raises: assert_almost_equal(e.mean(loc=2.0, scale=3.0), 5.0, atol=1e-15) assert_almost_equal(e.variance(loc=2.0, scale=3.0), 9.0, atol=1e-15) assert_almost_equal(e.std(loc=2.0, scale=3.0), 3.0, atol=1e-15) - assert_almost_equal(e.median(loc=2.0, scale=3.0), 2.0 + 3.0 * log(2.0), atol=1e-15) + assert_almost_equal( + e.median(loc=2.0, scale=3.0), 2.0 + 3.0 * log(2.0), atol=1e-15 + ) print("✓ test_expon_stats passed") @@ -470,7 +476,9 @@ fn test_expon_loc_scale() raises: # SF at loc should be 1 assert_almost_equal(e.sf(loc, loc, scale), 1.0, atol=1e-15) # CDF(loc + scale) = 1 - exp(-1) - assert_almost_equal(e.cdf(loc + scale, loc, scale), 1.0 - exp(-1.0), atol=1e-15) + assert_almost_equal( + e.cdf(loc + scale, loc, scale), 1.0 - exp(-1.0), atol=1e-15 + ) print("✓ test_expon_loc_scale passed") From 0c28515ae38168be74dfa805ebaa4c0ea4e9ce18 Mon Sep 17 00:00:00 2001 From: shivasankar Date: Tue, 3 Mar 2026 20:08:19 +0900 Subject: [PATCH 6/8] update expon api --- src/stamojo/distributions/exponential.mojo | 123 +++++++-------------- src/stamojo/distributions/traits.mojo | 24 ++-- tests/test_distributions.mojo | 56 +++++----- 3 files changed, 80 insertions(+), 123 deletions(-) diff --git a/src/stamojo/distributions/exponential.mojo b/src/stamojo/distributions/exponential.mojo index cd6173d..b211a40 100644 --- a/src/stamojo/distributions/exponential.mojo +++ b/src/stamojo/distributions/exponential.mojo @@ -36,125 +36,106 @@ struct Expon(Copyable, Movable, RVContinuousLike): This is achieved by setting scale = 1/λ and loc = 0. """ + var loc: Float64 + """Location (shift) parameter. Specifies the minimum value of the distribution; all random variates are greater than or equal to `loc`.""" + + var scale: Float64 + """Scale parameter (must be > 0). Controls the spread of the distribution; larger values result in a slower rate of decay.""" + # --- Initialization ------------------------------------------------------- - fn __init__(out self): - pass + fn __init__(out self, loc: Float64 = 0.0, scale: Float64 = 1.0): + self.loc = loc + self.scale = scale # --- Density functions --------------------------------------------------- - fn pdf( - self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 - ) -> Float64: + fn pdf(self, x: Float64) -> Float64: """Probability density function at x for Expon(loc, scale). Args: x: Point at which to evaluate the PDF. - loc: Location (shift) parameter. - scale: Scale parameter. Must be positive. Returns: 0.0 for x < loc. For x >= loc returns (1/scale) * exp(-(x-loc)/scale). """ - var y = (x - loc) / scale + var y = (x - self.loc) / self.scale if y < 0.0: return 0.0 - return exp(-y) / scale + return exp(-y) / self.scale - fn logpdf( - self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 - ) -> Float64: + fn logpdf(self, x: Float64) -> Float64: """Natural logarithm of the PDF at x for Expon(loc, scale). Args: x: Point at which to evaluate the log-PDF. - loc: Location (shift) parameter. - scale: Scale parameter (must be > 0). Returns: -∞ for x < loc. For x >= loc returns -((x - loc) / scale) - log(scale). """ - var y = (x - loc) / scale + var y = (x - self.loc) / self.scale if y < 0.0: return -inf[DType.float64]() - return -y - log(scale) + return -y - log(self.scale) # --- Distribution functions ---------------------------------------------- - fn cdf( - self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 - ) -> Float64: + fn cdf(self, x: Float64) -> Float64: """Cumulative distribution function P(X <= x) for Expon(loc, scale). Args: x: Value at which to evaluate the CDF. - loc: Location (shift) parameter. - scale: Scale parameter (must be > 0). Returns: 0.0 for x < loc. For x >= loc returns 1 - exp(-(x - loc)/scale). """ - if x < loc: + if x < self.loc: return 0.0 - var y = (x - loc) / scale + var y = (x - self.loc) / self.scale return -expm1(-y) - fn logcdf( - self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 - ) -> Float64: + fn logcdf(self, x: Float64) -> Float64: """Natural logarithm of the CDF P(X <= x) for Expon(loc, scale). Args: x: Value at which to evaluate the log-CDF. - loc: Location (shift) parameter. - scale: Scale parameter (must be > 0). Returns: -∞ for x < loc. For x >= loc returns log(1 - exp(-(x - loc)/scale)). """ - if x < loc: + if x < self.loc: return -inf[DType.float64]() - var y = (x - loc) / scale + var y = (x - self.loc) / self.scale return log1p(-exp(-y)) - fn sf( - self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 - ) -> Float64: + fn sf(self, x: Float64) -> Float64: """Survival function P(X > x) for Expon(loc, scale). Args: x: Value at which to evaluate the survival function. - loc: Location (shift) parameter. - scale: Scale parameter (must be > 0). Returns: 1.0 for x < loc. For x >= loc returns exp(-(x - loc)/scale). """ - if x < loc: + if x < self.loc: return 1.0 - var y = (x - loc) / scale + var y = (x - self.loc) / self.scale return exp(-y) - fn logsf( - self, x: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 - ) -> Float64: + fn logsf(self, x: Float64) -> Float64: """Natural logarithm of the survival function for Expon(loc, scale). Args: x: Value at which to evaluate the log-SF. - loc: Location (shift) parameter. - scale: Scale parameter (must be > 0). Returns: 0.0 for x < loc. For x >= loc returns -(x - loc)/scale. """ - if x < loc: + if x < self.loc: return 0.0 - var y = (x - loc) / scale + var y = (x - self.loc) / self.scale return -y - fn ppf( - self, q: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 - ) -> Float64: + fn ppf(self, q: Float64) -> Float64: """Percent-point (quantile) function for Expon(loc, scale). For 0 <= q < 1: PPF(q) = loc - scale * log(1 - q). @@ -163,20 +144,16 @@ struct Expon(Copyable, Movable, RVContinuousLike): Args: q: Probability in [0, 1]. - loc: Location (shift) parameter. - scale: Scale parameter (must be > 0). """ if q < 0.0 or q > 1.0: return nan[DType.float64]() if q == 0.0: - return loc + return self.loc if q == 1.0: return inf[DType.float64]() - return loc - scale * log1p(-q) + return self.loc - self.scale * log1p(-q) - fn isf( - self, q: Float64, loc: Float64 = 0.0, scale: Float64 = 1.0 - ) -> Float64: + fn isf(self, q: Float64) -> Float64: """Inverse survival function for Expon(loc, scale). For 0 < q <= 1: ISF(q) = loc - scale * log(q). @@ -185,66 +162,48 @@ struct Expon(Copyable, Movable, RVContinuousLike): Args: q: Probability in [0, 1]. - loc: Location (shift) parameter. - scale: Scale parameter (must be > 0). """ if q < 0.0 or q > 1.0: return nan[DType.float64]() if q == 0.0: return inf[DType.float64]() if q == 1.0: - return loc - return loc - scale * log(q) + return self.loc + return self.loc - self.scale * log(q) # --- Summary statistics -------------------------------------------------- - fn median(self, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn median(self) -> Float64: """ Median of the Expon distribution. - Args: - loc: Location (shift) parameter. - scale: Scale parameter (must be > 0). - Returns: The median value, computed as loc + scale * log(2). """ - return loc + scale * log(2.0) + return self.loc + self.scale * log(2.0) - fn mean(self, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn mean(self) -> Float64: """ Mean of the Expon distribution. - Args: - loc: Location (shift) parameter. - scale: Scale parameter (must be > 0). - Returns: The mean value, computed as loc + scale. """ - return loc + scale + return self.loc + self.scale - fn variance(self, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn variance(self) -> Float64: """ Variance of the Expon distribution. - Args: - loc: Location parameter (unused in variance). - scale: Scale parameter (must be > 0). - Returns: The variance value, computed as scale * scale. """ - return scale * scale + return self.scale * self.scale - fn std(self, loc: Float64 = 0.0, scale: Float64 = 1.0) -> Float64: + fn std(self) -> Float64: """ Standard deviation of the Expon distribution. - Args: - loc: Location parameter (unused in std). - scale: Scale parameter (must be > 0). - Returns: The standard deviation value, equal to scale. """ - return scale + return self.scale diff --git a/src/stamojo/distributions/traits.mojo b/src/stamojo/distributions/traits.mojo index 8f91fdf..db99b3b 100644 --- a/src/stamojo/distributions/traits.mojo +++ b/src/stamojo/distributions/traits.mojo @@ -3,53 +3,53 @@ trait RVContinuousLike(Copyable, Movable): # --- Density functions --------------------------------------------------- - fn pdf(self, x: Float64, loc: Float64, scale: Float64) -> Float64: + fn pdf(self, x: Float64) -> Float64: """Probability density function at *x*.""" ... - fn logpdf(self, x: Float64, loc: Float64, scale: Float64) -> Float64: + fn logpdf(self, x: Float64) -> Float64: """Natural logarithm of the probability density function at *x*.""" ... # --- Distribution functions ---------------------------------------------- - fn cdf(self, x: Float64, loc: Float64, scale: Float64) -> Float64: + fn cdf(self, x: Float64) -> Float64: """Cumulative distribution function P(X ≤ x).""" ... - fn logcdf(self, x: Float64, loc: Float64, scale: Float64) -> Float64: + fn logcdf(self, x: Float64) -> Float64: """Natural logarithm of the cumulative distribution function at *x*.""" ... - fn sf(self, x: Float64, loc: Float64, scale: Float64) -> Float64: + fn sf(self, x: Float64) -> Float64: """Survival function (1 − CDF) at *x*.""" ... - fn logsf(self, x: Float64, loc: Float64, scale: Float64) -> Float64: + fn logsf(self, x: Float64) -> Float64: """Natural logarithm of the survival function at *x*.""" ... - fn ppf(self, q: Float64, loc: Float64, scale: Float64) -> Float64: + fn ppf(self, q: Float64) -> Float64: """Percent point function (inverse of CDF) at *q*.""" ... - fn isf(self, q: Float64, loc: Float64, scale: Float64) -> Float64: + fn isf(self, q: Float64) -> Float64: """Inverse survival function (inverse of SF) at *q*.""" ... # --- Statistical properties ------------------------------------------------ - fn median(self, loc: Float64, scale: Float64) -> Float64: + fn median(self) -> Float64: """Median of the distribution.""" ... - fn mean(self, loc: Float64, scale: Float64) -> Float64: + fn mean(self) -> Float64: """Mean of the distribution.""" ... - fn variance(self, loc: Float64, scale: Float64) -> Float64: + fn variance(self) -> Float64: """Variance of the distribution.""" ... - fn std(self, loc: Float64, scale: Float64) -> Float64: + fn std(self) -> Float64: """Standard deviation of the distribution.""" ... diff --git a/tests/test_distributions.mojo b/tests/test_distributions.mojo index 4f6a3f8..26694eb 100644 --- a/tests/test_distributions.mojo +++ b/tests/test_distributions.mojo @@ -330,11 +330,13 @@ fn test_expon_pdf() raises: # pdf(x < 0) = 0 assert_almost_equal(e.pdf(-1.0), 0.0, atol=1e-15) # With scale=2: pdf(x) = (1/2)*exp(-x/2) - assert_almost_equal(e.pdf(0.0, scale=2.0), 0.5, atol=1e-15) - assert_almost_equal(e.pdf(2.0, scale=2.0), 0.5 * exp(-1.0), atol=1e-15) + var e2 = Expon(scale=2.0) + assert_almost_equal(e2.pdf(0.0), 0.5, atol=1e-15) + assert_almost_equal(e2.pdf(2.0), 0.5 * exp(-1.0), atol=1e-15) # With loc=1: pdf(1) = 1.0, pdf(0) = 0.0 - assert_almost_equal(e.pdf(1.0, loc=1.0), 1.0, atol=1e-15) - assert_almost_equal(e.pdf(0.5, loc=1.0), 0.0, atol=1e-15) + var e3 = Expon(loc=1.0) + assert_almost_equal(e3.pdf(1.0), 1.0, atol=1e-15) + assert_almost_equal(e3.pdf(0.5), 0.0, atol=1e-15) print("✓ test_expon_pdf passed") @@ -348,7 +350,8 @@ fn test_expon_logpdf() raises: # logpdf(x) = log(pdf(x)) assert_almost_equal(e.logpdf(2.0), log(e.pdf(2.0)), atol=1e-15) # With scale=3: logpdf(x) = -x/3 - log(3) - assert_almost_equal(e.logpdf(3.0, scale=3.0), -1.0 - log(3.0), atol=1e-15) + var e2 = Expon(scale=3.0) + assert_almost_equal(e2.logpdf(3.0), -1.0 - log(3.0), atol=1e-15) print("✓ test_expon_logpdf passed") @@ -368,7 +371,8 @@ fn test_expon_cdf() raises: if not (c1 < c2 and c2 < c3): raise Error("Expon CDF not monotonically increasing") # With scale=0.5 (rate=2): CDF(x) = 1 - exp(-2x) - assert_almost_equal(e.cdf(1.0, scale=0.5), 1.0 - exp(-2.0), atol=1e-15) + var e2 = Expon(scale=0.5) + assert_almost_equal(e2.cdf(1.0), 1.0 - exp(-2.0), atol=1e-15) print("✓ test_expon_cdf passed") @@ -396,9 +400,8 @@ fn test_expon_ppf() raises: # PPF(0.5) = ln(2) (median of standard exponential) assert_almost_equal(e.ppf(0.5), log(2.0), atol=1e-12) # With loc and scale - assert_almost_equal( - e.ppf(0.5, loc=1.0, scale=2.0), 1.0 + 2.0 * log(2.0), atol=1e-12 - ) + var e2 = Expon(loc=1.0, scale=2.0) + assert_almost_equal(e2.ppf(0.5), 1.0 + 2.0 * log(2.0), atol=1e-12) print("✓ test_expon_ppf passed") @@ -410,13 +413,10 @@ fn test_expon_cdf_ppf_roundtrip() raises: var p = ps[i] assert_almost_equal(e.cdf(e.ppf(p)), p, atol=1e-12) # Also test with loc and scale - var loc = 2.0 - var scale = 3.0 + var e2 = Expon(loc=2.0, scale=3.0) for i in range(len(ps)): var p = ps[i] - assert_almost_equal( - e.cdf(e.ppf(p, loc, scale), loc, scale), p, atol=1e-12 - ) + assert_almost_equal(e2.cdf(e2.ppf(p)), p, atol=1e-12) print("✓ test_expon_cdf_ppf_roundtrip passed") @@ -455,30 +455,27 @@ fn test_expon_stats() raises: assert_almost_equal(e.std(), 1.0, atol=1e-15) assert_almost_equal(e.median(), log(2.0), atol=1e-15) # With loc=2, scale=3: mean=5, var=9, std=3, median=2+3*ln(2) - assert_almost_equal(e.mean(loc=2.0, scale=3.0), 5.0, atol=1e-15) - assert_almost_equal(e.variance(loc=2.0, scale=3.0), 9.0, atol=1e-15) - assert_almost_equal(e.std(loc=2.0, scale=3.0), 3.0, atol=1e-15) - assert_almost_equal( - e.median(loc=2.0, scale=3.0), 2.0 + 3.0 * log(2.0), atol=1e-15 - ) + var e2 = Expon(loc=2.0, scale=3.0) + assert_almost_equal(e2.mean(), 5.0, atol=1e-15) + assert_almost_equal(e2.variance(), 9.0, atol=1e-15) + assert_almost_equal(e2.std(), 3.0, atol=1e-15) + assert_almost_equal(e2.median(), 2.0 + 3.0 * log(2.0), atol=1e-15) print("✓ test_expon_stats passed") fn test_expon_loc_scale() raises: """Test Exponential with non-default loc and scale across all functions.""" - var e = Expon() var loc = 5.0 var scale = 2.0 + var e = Expon(loc, scale) # PDF at loc should be 1/scale - assert_almost_equal(e.pdf(loc, loc, scale), 1.0 / scale, atol=1e-15) + assert_almost_equal(e.pdf(loc), 1.0 / scale, atol=1e-15) # CDF at loc should be 0 - assert_almost_equal(e.cdf(loc, loc, scale), 0.0, atol=1e-15) + assert_almost_equal(e.cdf(loc), 0.0, atol=1e-15) # SF at loc should be 1 - assert_almost_equal(e.sf(loc, loc, scale), 1.0, atol=1e-15) + assert_almost_equal(e.sf(loc), 1.0, atol=1e-15) # CDF(loc + scale) = 1 - exp(-1) - assert_almost_equal( - e.cdf(loc + scale, loc, scale), 1.0 - exp(-1.0), atol=1e-15 - ) + assert_almost_equal(e.cdf(loc + scale), 1.0 - exp(-1.0), atol=1e-15) print("✓ test_expon_loc_scale passed") @@ -511,12 +508,13 @@ fn test_expon_scipy() raises: # Test with loc and scale var loc = 2.0 var scale = 3.0 + var e2 = Expon(loc, scale) for i in range(len(xs)): var x = xs[i] + loc var sp_pdf2 = _py_f64(sp.expon.pdf(x, loc, scale)) var sp_cdf2 = _py_f64(sp.expon.cdf(x, loc, scale)) - assert_almost_equal(e.pdf(x, loc, scale), sp_pdf2, atol=1e-12) - assert_almost_equal(e.cdf(x, loc, scale), sp_cdf2, atol=1e-12) + assert_almost_equal(e2.pdf(x), sp_pdf2, atol=1e-12) + assert_almost_equal(e2.cdf(x), sp_cdf2, atol=1e-12) print("✓ test_expon_scipy passed") From 9320fb48307058183d038578c43de5b8aa5980f8 Mon Sep 17 00:00:00 2001 From: shivasankar Date: Tue, 3 Mar 2026 20:10:50 +0900 Subject: [PATCH 7/8] Update exponential.mojo --- src/stamojo/distributions/exponential.mojo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stamojo/distributions/exponential.mojo b/src/stamojo/distributions/exponential.mojo index b211a40..53f7992 100644 --- a/src/stamojo/distributions/exponential.mojo +++ b/src/stamojo/distributions/exponential.mojo @@ -37,10 +37,10 @@ struct Expon(Copyable, Movable, RVContinuousLike): """ var loc: Float64 - """Location (shift) parameter. Specifies the minimum value of the distribution; all random variates are greater than or equal to `loc`.""" + """Location (shift) parameter. Deaults to 0.0. The distribution is supported for x >= loc.""" var scale: Float64 - """Scale parameter (must be > 0). Controls the spread of the distribution; larger values result in a slower rate of decay.""" + """Scale parameter (must be > 0). Defaults to 1.0.""" # --- Initialization ------------------------------------------------------- From 4000cd4d7221167d074154103d43bfe1834c470f Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Wed, 4 Mar 2026 17:12:35 +0100 Subject: [PATCH 8/8] Some changes --- pixi.toml | 2 +- src/stamojo/distributions/__init__.mojo | 11 +- src/stamojo/distributions/exponential.mojo | 119 ++++++++++++--------- src/stamojo/distributions/traits.mojo | 14 ++- tests/test_distributions.mojo | 101 +++++------------ tests/test_hypothesis.mojo | 43 +------- tests/test_special.mojo | 6 -- tests/test_stats.mojo | 19 +--- 8 files changed, 118 insertions(+), 197 deletions(-) diff --git a/pixi.toml b/pixi.toml index 593f6a6..d880187 100644 --- a/pixi.toml +++ b/pixi.toml @@ -1,5 +1,5 @@ [workspace] -authors = ["Yuhao Zhu (朱宇浩) "] +authors = ["MojoMath "] channels = ["https://repo.prefix.dev/modular-community", "https://conda.modular.com/max-nightly", "https://conda.modular.com/max", "conda-forge"] description = "A statistical computing library for Mojo, inspired by scipy.stats and statsmodels" license = "Apache-2.0" diff --git a/src/stamojo/distributions/__init__.mojo b/src/stamojo/distributions/__init__.mojo index e3de41f..56148d5 100644 --- a/src/stamojo/distributions/__init__.mojo +++ b/src/stamojo/distributions/__init__.mojo @@ -8,14 +8,15 @@ This subpackage provides continuous and discrete probability distributions, including PDF/PMF, CDF, quantile (PPF), and random variate generation. Distributions provided: -- `Normal` — Normal (Gaussian) distribution -- `StudentT` — Student's t-distribution -- `ChiSquared` — Chi-squared distribution -- `FDist` — F-distribution (Fisher-Snedecor) +- `Normal` — Normal (Gaussian) distribution +- `StudentT` — Student's t-distribution +- `ChiSquared` — Chi-squared distribution +- `FDist` — F-distribution (Fisher-Snedecor) +- `Exponential` — Exponential distribution """ from .normal import Normal from .t import StudentT from .chi2 import ChiSquared from .f import FDist -from .exponential import Expon +from .exponential import Exponential diff --git a/src/stamojo/distributions/exponential.mojo b/src/stamojo/distributions/exponential.mojo index 53f7992..41445f2 100644 --- a/src/stamojo/distributions/exponential.mojo +++ b/src/stamojo/distributions/exponential.mojo @@ -4,40 +4,46 @@ # ===----------------------------------------------------------------------=== # """Exponential distribution. -Provides the `Exponential` distribution struct with PDF, log-PDF, CDF, survival function, percent-point function (PPF / quantile), and random variate -generation. +Provides the `Exponential` distribution struct with PDF, log-PDF, CDF, +survival function, and percent-point function (PPF / quantile). The exponential distribution with rate parameter λ has PDF: f(x; λ) = λ exp(−λx), x ≥ 0 """ -from math import sqrt, log, lgamma, exp, nan, inf, log1p, expm1 +from math import log, exp, nan, inf, log1p, expm1 -from stamojo.distributions.traits import RVContinuousLike +from stamojo.distributions.traits import ContinuouslyDistributed -struct Expon(Copyable, Movable, RVContinuousLike): +# `ContinuouslyDistributed` trait contains `Copyable` and `Movable` traits +struct Exponential(ContinuouslyDistributed): """Exponential distribution. - Represents the exponential distribution, a continuous probability distribution commonly - used to model the time between independent events that occur at a constant average rate. + Represents the exponential distribution, a continuous probability + distribution commonly used to model the time between independent events + that occur at a constant average rate. - The probability density function (PDF) for the standardized exponential distribution is: - f(x) = exp(-x) - for x >= 0. + The probability density function (PDF) for the standardized exponential + distribution is: - This implementation allows shifting and scaling of the distribution using the `loc` (location) and `scale` parameters: - Expon.pdf(x, loc, scale) = (1/scale) * exp(-(x - loc) / scale) - which is equivalent to `Expon.pdf((x - loc) / scale) / scale`. + f(x) = exp(-x), x ≥ 0 - The most common parameterization uses the rate parameter λ > 0, where: - f(x; λ) = λ * exp(-λx), for x >= 0 - This is achieved by setting scale = 1/λ and loc = 0. + This implementation allows shifting and scaling via `loc` and `scale`:: + + Exponential.pdf(x, loc, scale) = (1/scale) * exp(-(x - loc) / scale) + + The most common parameterization uses the rate parameter λ > 0, where:: + + f(x; λ) = λ * exp(-λx), x ≥ 0 + + This is achieved by setting ``scale = 1/λ`` and ``loc = 0``. """ var loc: Float64 - """Location (shift) parameter. Deaults to 0.0. The distribution is supported for x >= loc.""" + """Location (shift) parameter. Defaults to 0.0. The distribution is + supported for x >= loc.""" var scale: Float64 """Scale parameter (must be > 0). Defaults to 1.0.""" @@ -51,13 +57,13 @@ struct Expon(Copyable, Movable, RVContinuousLike): # --- Density functions --------------------------------------------------- fn pdf(self, x: Float64) -> Float64: - """Probability density function at x for Expon(loc, scale). + """Probability density function at *x*. Args: x: Point at which to evaluate the PDF. Returns: - 0.0 for x < loc. For x >= loc returns (1/scale) * exp(-(x-loc)/scale). + 0.0 for x < loc; (1/scale) * exp(-(x-loc)/scale) otherwise. """ var y = (x - self.loc) / self.scale if y < 0.0: @@ -65,13 +71,13 @@ struct Expon(Copyable, Movable, RVContinuousLike): return exp(-y) / self.scale fn logpdf(self, x: Float64) -> Float64: - """Natural logarithm of the PDF at x for Expon(loc, scale). + """Natural logarithm of the PDF at *x*. Args: x: Point at which to evaluate the log-PDF. Returns: - -∞ for x < loc. For x >= loc returns -((x - loc) / scale) - log(scale). + -∞ for x < loc; -((x - loc) / scale) - log(scale) otherwise. """ var y = (x - self.loc) / self.scale if y < 0.0: @@ -80,13 +86,13 @@ struct Expon(Copyable, Movable, RVContinuousLike): # --- Distribution functions ---------------------------------------------- fn cdf(self, x: Float64) -> Float64: - """Cumulative distribution function P(X <= x) for Expon(loc, scale). + """Cumulative distribution function P(X ≤ x). Args: x: Value at which to evaluate the CDF. Returns: - 0.0 for x < loc. For x >= loc returns 1 - exp(-(x - loc)/scale). + 0.0 for x < loc; 1 - exp(-(x - loc)/scale) otherwise. """ if x < self.loc: return 0.0 @@ -94,27 +100,30 @@ struct Expon(Copyable, Movable, RVContinuousLike): return -expm1(-y) fn logcdf(self, x: Float64) -> Float64: - """Natural logarithm of the CDF P(X <= x) for Expon(loc, scale). + """Natural logarithm of the CDF at *x*. + + Uses ``log(-expm1(-y))`` instead of ``log1p(-exp(-y))`` for better + numerical stability when *x* is close to *loc*. Args: x: Value at which to evaluate the log-CDF. Returns: - -∞ for x < loc. For x >= loc returns log(1 - exp(-(x - loc)/scale)). + -∞ for x < loc; log(1 - exp(-(x - loc)/scale)) otherwise. """ if x < self.loc: return -inf[DType.float64]() var y = (x - self.loc) / self.scale - return log1p(-exp(-y)) + return log(-expm1(-y)) fn sf(self, x: Float64) -> Float64: - """Survival function P(X > x) for Expon(loc, scale). + """Survival function (1 − CDF) at *x*. Args: x: Value at which to evaluate the survival function. Returns: - 1.0 for x < loc. For x >= loc returns exp(-(x - loc)/scale). + 1.0 for x < loc; exp(-(x - loc)/scale) otherwise. """ if x < self.loc: return 1.0 @@ -122,13 +131,13 @@ struct Expon(Copyable, Movable, RVContinuousLike): return exp(-y) fn logsf(self, x: Float64) -> Float64: - """Natural logarithm of the survival function for Expon(loc, scale). + """Natural logarithm of the survival function at *x*. Args: x: Value at which to evaluate the log-SF. Returns: - 0.0 for x < loc. For x >= loc returns -(x - loc)/scale. + 0.0 for x < loc; -(x - loc)/scale otherwise. """ if x < self.loc: return 0.0 @@ -136,14 +145,19 @@ struct Expon(Copyable, Movable, RVContinuousLike): return -y fn ppf(self, q: Float64) -> Float64: - """Percent-point (quantile) function for Expon(loc, scale). - - For 0 <= q < 1: PPF(q) = loc - scale * log(1 - q). - PPF(0) = loc. - PPF(1) = +∞. + """Percent-point (quantile) function (inverse CDF). Args: q: Probability in [0, 1]. + + Returns: + The quantile corresponding to *q*. + + Notes: + + PPF(q) = loc - scale * log(1 - q), 0 <= q < 1, + PPF(0) = loc, + PPF(1) = +∞. """ if q < 0.0 or q > 1.0: return nan[DType.float64]() @@ -154,14 +168,19 @@ struct Expon(Copyable, Movable, RVContinuousLike): return self.loc - self.scale * log1p(-q) fn isf(self, q: Float64) -> Float64: - """Inverse survival function for Expon(loc, scale). - - For 0 < q <= 1: ISF(q) = loc - scale * log(q). - ISF(0) = +∞. - ISF(1) = loc. + """Inverse survival function (inverse SF). Args: q: Probability in [0, 1]. + + Returns: + The value *x* such that SF(x) = *q*. + + Notes: + + ISF(q) = loc - scale * log(q), 0 < q <= 1, + ISF(0) = +∞, + ISF(1) = loc. """ if q < 0.0 or q > 1.0: return nan[DType.float64]() @@ -173,37 +192,33 @@ struct Expon(Copyable, Movable, RVContinuousLike): # --- Summary statistics -------------------------------------------------- fn median(self) -> Float64: - """ - Median of the Expon distribution. + """Median of the distribution: loc + scale * ln(2). Returns: - The median value, computed as loc + scale * log(2). + The median of the distribution. """ return self.loc + self.scale * log(2.0) fn mean(self) -> Float64: - """ - Mean of the Expon distribution. + """Distribution mean: loc + scale. Returns: - The mean value, computed as loc + scale. + The mean of the distribution. """ return self.loc + self.scale fn variance(self) -> Float64: - """ - Variance of the Expon distribution. + """Distribution variance: scale². Returns: - The variance value, computed as scale * scale. + The variance of the distribution. """ return self.scale * self.scale fn std(self) -> Float64: - """ - Standard deviation of the Expon distribution. + """Distribution standard deviation: scale. Returns: - The standard deviation value, equal to scale. + The standard deviation of the distribution. """ return self.scale diff --git a/src/stamojo/distributions/traits.mojo b/src/stamojo/distributions/traits.mojo index db99b3b..a68aabe 100644 --- a/src/stamojo/distributions/traits.mojo +++ b/src/stamojo/distributions/traits.mojo @@ -1,5 +1,12 @@ -trait RVContinuousLike(Copyable, Movable): - """Trait for continuous random variable distributions.""" +# ===----------------------------------------------------------------------=== # +# Stamojo - Distributions - Distribution traits +# Licensed under Apache 2.0 +# ===----------------------------------------------------------------------=== # +"""Traits for probability distributions.""" + + +trait ContinuouslyDistributed(Copyable, Movable): + """Trait for continuous probability distributions.""" # --- Density functions --------------------------------------------------- @@ -37,7 +44,8 @@ trait RVContinuousLike(Copyable, Movable): """Inverse survival function (inverse of SF) at *q*.""" ... - # --- Statistical properties ------------------------------------------------ + # --- Statistical properties ---------------------------------------------- + fn median(self) -> Float64: """Median of the distribution.""" ... diff --git a/tests/test_distributions.mojo b/tests/test_distributions.mojo index 26694eb..50707cc 100644 --- a/tests/test_distributions.mojo +++ b/tests/test_distributions.mojo @@ -14,9 +14,15 @@ Each distribution is tested for: from math import exp, log from python import Python, PythonObject -from testing import assert_almost_equal +from testing import assert_almost_equal, TestSuite -from stamojo.distributions import Normal, StudentT, ChiSquared, FDist, Expon +from stamojo.distributions import ( + Normal, + StudentT, + ChiSquared, + FDist, + Exponential, +) # ===----------------------------------------------------------------------=== # @@ -320,7 +326,7 @@ fn test_f_scipy() raises: fn test_expon_pdf() raises: """Test Exponential PDF at known values.""" - var e = Expon() + var e = Exponential() # Standard exponential: pdf(0) = 1.0 assert_almost_equal(e.pdf(0.0), 1.0, atol=1e-15) # pdf(1) = exp(-1) @@ -330,11 +336,11 @@ fn test_expon_pdf() raises: # pdf(x < 0) = 0 assert_almost_equal(e.pdf(-1.0), 0.0, atol=1e-15) # With scale=2: pdf(x) = (1/2)*exp(-x/2) - var e2 = Expon(scale=2.0) + var e2 = Exponential(scale=2.0) assert_almost_equal(e2.pdf(0.0), 0.5, atol=1e-15) assert_almost_equal(e2.pdf(2.0), 0.5 * exp(-1.0), atol=1e-15) # With loc=1: pdf(1) = 1.0, pdf(0) = 0.0 - var e3 = Expon(loc=1.0) + var e3 = Exponential(loc=1.0) assert_almost_equal(e3.pdf(1.0), 1.0, atol=1e-15) assert_almost_equal(e3.pdf(0.5), 0.0, atol=1e-15) print("✓ test_expon_pdf passed") @@ -342,7 +348,7 @@ fn test_expon_pdf() raises: fn test_expon_logpdf() raises: """Test Exponential log-PDF at known values.""" - var e = Expon() + var e = Exponential() # logpdf(0) = 0.0 for standard exponential assert_almost_equal(e.logpdf(0.0), 0.0, atol=1e-15) # logpdf(1) = -1.0 @@ -350,14 +356,14 @@ fn test_expon_logpdf() raises: # logpdf(x) = log(pdf(x)) assert_almost_equal(e.logpdf(2.0), log(e.pdf(2.0)), atol=1e-15) # With scale=3: logpdf(x) = -x/3 - log(3) - var e2 = Expon(scale=3.0) + var e2 = Exponential(scale=3.0) assert_almost_equal(e2.logpdf(3.0), -1.0 - log(3.0), atol=1e-15) print("✓ test_expon_logpdf passed") fn test_expon_cdf() raises: """Test Exponential CDF at known values.""" - var e = Expon() + var e = Exponential() # CDF(0) = 0 assert_almost_equal(e.cdf(0.0), 0.0, atol=1e-15) # CDF(1) = 1 - exp(-1) @@ -369,16 +375,16 @@ fn test_expon_cdf() raises: var c2 = e.cdf(1.0) var c3 = e.cdf(5.0) if not (c1 < c2 and c2 < c3): - raise Error("Expon CDF not monotonically increasing") + raise Error("Exponential CDF not monotonically increasing") # With scale=0.5 (rate=2): CDF(x) = 1 - exp(-2x) - var e2 = Expon(scale=0.5) + var e2 = Exponential(scale=0.5) assert_almost_equal(e2.cdf(1.0), 1.0 - exp(-2.0), atol=1e-15) print("✓ test_expon_cdf passed") fn test_expon_sf() raises: """Test Exponential survival function: SF(x) = 1 - CDF(x).""" - var e = Expon() + var e = Exponential() assert_almost_equal(e.sf(0.0), 1.0, atol=1e-15) assert_almost_equal(e.sf(1.0), exp(-1.0), atol=1e-15) # CDF + SF = 1 @@ -392,7 +398,7 @@ fn test_expon_sf() raises: fn test_expon_ppf() raises: """Test Exponential PPF (inverse CDF).""" - var e = Expon() + var e = Exponential() # PPF(0) = 0 (loc) assert_almost_equal(e.ppf(0.0), 0.0, atol=1e-15) # PPF(1 - exp(-1)) = 1 (since CDF(1) = 1 - exp(-1)) @@ -400,20 +406,20 @@ fn test_expon_ppf() raises: # PPF(0.5) = ln(2) (median of standard exponential) assert_almost_equal(e.ppf(0.5), log(2.0), atol=1e-12) # With loc and scale - var e2 = Expon(loc=1.0, scale=2.0) + var e2 = Exponential(loc=1.0, scale=2.0) assert_almost_equal(e2.ppf(0.5), 1.0 + 2.0 * log(2.0), atol=1e-12) print("✓ test_expon_ppf passed") fn test_expon_cdf_ppf_roundtrip() raises: """Test CDF(PPF(p)) ≈ p for many probability values.""" - var e = Expon() + var e = Exponential() var ps: List[Float64] = [0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] for i in range(len(ps)): var p = ps[i] assert_almost_equal(e.cdf(e.ppf(p)), p, atol=1e-12) # Also test with loc and scale - var e2 = Expon(loc=2.0, scale=3.0) + var e2 = Exponential(loc=2.0, scale=3.0) for i in range(len(ps)): var p = ps[i] assert_almost_equal(e2.cdf(e2.ppf(p)), p, atol=1e-12) @@ -422,7 +428,7 @@ fn test_expon_cdf_ppf_roundtrip() raises: fn test_expon_isf() raises: """Test Exponential ISF (inverse survival function).""" - var e = Expon() + var e = Exponential() # ISF(1) = loc = 0 assert_almost_equal(e.isf(1.0), 0.0, atol=1e-15) # ISF(exp(-1)) = 1 (since SF(1) = exp(-1)) @@ -437,7 +443,7 @@ fn test_expon_isf() raises: fn test_expon_logcdf_logsf() raises: """Test log-CDF and log-SF against log of CDF and SF.""" - var e = Expon() + var e = Exponential() var xs: List[Float64] = [0.01, 0.1, 0.5, 1.0, 2.0, 5.0] for i in range(len(xs)): var x = xs[i] @@ -448,14 +454,14 @@ fn test_expon_logcdf_logsf() raises: fn test_expon_stats() raises: """Test Exponential distribution summary statistics.""" - var e = Expon() + var e = Exponential() # Standard exponential: mean=1, var=1, std=1, median=ln(2) assert_almost_equal(e.mean(), 1.0, atol=1e-15) assert_almost_equal(e.variance(), 1.0, atol=1e-15) assert_almost_equal(e.std(), 1.0, atol=1e-15) assert_almost_equal(e.median(), log(2.0), atol=1e-15) # With loc=2, scale=3: mean=5, var=9, std=3, median=2+3*ln(2) - var e2 = Expon(loc=2.0, scale=3.0) + var e2 = Exponential(loc=2.0, scale=3.0) assert_almost_equal(e2.mean(), 5.0, atol=1e-15) assert_almost_equal(e2.variance(), 9.0, atol=1e-15) assert_almost_equal(e2.std(), 3.0, atol=1e-15) @@ -467,7 +473,7 @@ fn test_expon_loc_scale() raises: """Test Exponential with non-default loc and scale across all functions.""" var loc = 5.0 var scale = 2.0 - var e = Expon(loc, scale) + var e = Exponential(loc, scale) # PDF at loc should be 1/scale assert_almost_equal(e.pdf(loc), 1.0 / scale, atol=1e-15) # CDF at loc should be 0 @@ -486,7 +492,7 @@ fn test_expon_scipy() raises: print("test_expon_scipy skipped (scipy not available)") return - var e = Expon() + var e = Exponential() var xs: List[Float64] = [0.0, 0.5, 1.0, 2.0, 5.0, 10.0] for i in range(len(xs)): @@ -508,7 +514,7 @@ fn test_expon_scipy() raises: # Test with loc and scale var loc = 2.0 var scale = 3.0 - var e2 = Expon(loc, scale) + var e2 = Exponential(loc, scale) for i in range(len(xs)): var x = xs[i] + loc var sp_pdf2 = _py_f64(sp.expon.pdf(x, loc, scale)) @@ -525,53 +531,4 @@ fn test_expon_scipy() raises: fn main() raises: - print("=== StaMojo: Testing distributions ===") - print() - - # Normal - test_normal_pdf() - test_normal_cdf() - test_normal_ppf() - test_normal_cdf_ppf_roundtrip() - test_normal_sf() - test_normal_stats() - test_normal_scipy() - print() - - # Student's t - test_t_pdf_symmetry() - test_t_cdf() - test_t_ppf() - test_t_stats() - test_t_scipy() - print() - - # Chi-squared - test_chi2_cdf() - test_chi2_ppf() - test_chi2_stats() - test_chi2_scipy() - print() - - # F-distribution - test_f_cdf_boundary() - test_f_ppf() - test_f_stats() - test_f_scipy() - print() - - # Exponential - test_expon_pdf() - test_expon_logpdf() - test_expon_cdf() - test_expon_sf() - test_expon_ppf() - test_expon_cdf_ppf_roundtrip() - test_expon_isf() - test_expon_logcdf_logsf() - test_expon_stats() - test_expon_loc_scale() - test_expon_scipy() - - print() - print("=== All distribution tests passed ===") + TestSuite.discover_tests[__functions_in_module()]().run() diff --git a/tests/test_hypothesis.mojo b/tests/test_hypothesis.mojo index aee96f6..defb741 100644 --- a/tests/test_hypothesis.mojo +++ b/tests/test_hypothesis.mojo @@ -14,7 +14,7 @@ Covers: from math import sqrt from python import Python, PythonObject -from testing import assert_almost_equal +from testing import assert_almost_equal, TestSuite from stamojo.stats import ( ttest_1samp, @@ -486,43 +486,4 @@ fn test_f_oneway_scipy() raises: fn main() raises: - print("=== StaMojo: Testing hypothesis tests & correlation ===") - print() - - # t-tests - test_ttest_1samp_basic() - test_ttest_1samp_no_effect() - test_ttest_1samp_scipy() - test_ttest_ind_welch() - test_ttest_ind_scipy() - test_ttest_rel() - print() - - # Chi-squared tests - test_chi2_gof_fair_die() - test_chi2_ind_basic() - test_chi2_ind_scipy() - print() - - # KS test - test_ks_normal_data() - test_ks_uniform_data() - print() - - # Correlation - test_pearsonr_perfect() - test_pearsonr_negative() - test_pearsonr_scipy() - test_spearmanr_perfect_monotone() - test_spearmanr_scipy() - test_kendalltau_concordant() - test_kendalltau_discordant() - print() - - # ANOVA - test_f_oneway_identical() - test_f_oneway_different() - test_f_oneway_scipy() - - print() - print("=== All hypothesis test & correlation tests passed ===") + TestSuite.discover_tests[__functions_in_module()]().run() diff --git a/tests/test_special.mojo b/tests/test_special.mojo index 929ba28..4610d76 100644 --- a/tests/test_special.mojo +++ b/tests/test_special.mojo @@ -405,10 +405,4 @@ fn test_erfinv_scipy() raises: fn main() raises: - print("=== StaMojo: Testing special functions ===") - print() - TestSuite.discover_tests[__functions_in_module()]().run() - - print() - print("=== All special function tests passed ===") diff --git a/tests/test_stats.mojo b/tests/test_stats.mojo index bf1e57e..6ca44ce 100644 --- a/tests/test_stats.mojo +++ b/tests/test_stats.mojo @@ -10,7 +10,7 @@ with both analytical checks and scipy/numpy comparisons. from math import sqrt from python import Python, PythonObject -from testing import assert_almost_equal +from testing import assert_almost_equal, TestSuite from stamojo.stats import ( mean, @@ -176,19 +176,4 @@ fn test_scipy_comparison() raises: fn main() raises: - print("=== StaMojo: Testing descriptive statistics ===") - print() - - test_mean() - test_variance() - test_std() - test_median_odd() - test_median_even() - test_quantile() - test_skewness_symmetric() - test_kurtosis_uniform() - test_min_max() - test_scipy_comparison() - - print() - print("=== All descriptive statistics tests passed ===") + TestSuite.discover_tests[__functions_in_module()]().run()