Source code for pysatl_core.families.builtins.continuous.normal

"""
Normal distribution family implementation.

Contains the Normal family with multiple parameterizations.
"""

from __future__ import annotations

__author__ = "Fedor Myznikov"
__copyright__ = "Copyright (c) 2025 PySATL project"
__license__ = "SPDX-License-Identifier: MIT"

import math
from typing import TYPE_CHECKING, cast

import numpy as np
from scipy.special import erf, erfinv

from pysatl_core.distributions.support import ContinuousSupport
from pysatl_core.families.parametric_family import ParametricFamily
from pysatl_core.families.parametrizations import (
    Parametrization,
    constraint,
    parametrization,
)
from pysatl_core.families.registry import ParametricFamilyRegister
from pysatl_core.types import (
    CharacteristicName,
    ComplexArray,
    FamilyName,
    NumericArray,
    UnivariateContinuous,
)

if TYPE_CHECKING:
    from typing import Any


[docs] def configure_normal_family() -> None: """ Configure and register the Normal distribution family. """ if ParametricFamilyRegister.contains(FamilyName.NORMAL): return NORMAL_DOC = """ Normal (Gaussian) distribution. The normal distribution is a continuous probability distribution characterized by its bell-shaped curve. It is symmetric about its mean and is defined by two parameters: mean (μ) and standard deviation (σ). Probability density function: f(x) = 1/(σ√(2π)) * exp(-(x-μ)²/(2σ²)) The normal distribution is widely used in statistics, natural sciences, and social sciences as a simple model for complex random phenomena. """ def pdf(parameters: Parametrization, x: NumericArray) -> NumericArray: """ Probability density function for normal distribution. Parameters ---------- parameters : Parametrization () Distribution parameters object with fields: - mu: float (mean) - sigma: float (standard deviation) x : NumericArray Points at which to evaluate the probability density function Returns ------- NumericArray Probability density values at points x """ parameters = cast(_MeanStd, parameters) sigma = parameters.sigma mu = parameters.mu coefficient = 1.0 / (sigma * np.sqrt(2 * np.pi)) exponent = -((x - mu) ** 2) / (2 * sigma**2) return cast(NumericArray, coefficient * np.exp(exponent)) def cdf(parameters: Parametrization, x: NumericArray) -> NumericArray: """ Cumulative distribution function for normal distribution. Parameters ---------- parameters : Parametrization Distribution parameters object with fields: - mu: float (mean) - sigma: float (standard deviation) x : NumericArray Points at which to evaluate the cumulative distribution function Returns ------- NumericArray Probabilities P(X ≤ x) for each point x """ parameters = cast(_MeanStd, parameters) z = (x - parameters.mu) / (parameters.sigma * np.sqrt(2)) return cast(NumericArray, 0.5 * (1 + erf(z))) def ppf(parameters: Parametrization, p: NumericArray) -> NumericArray: """ Percent point function (inverse CDF) for normal distribution. Parameters ---------- parameters : Parametrization Distribution parameters object with fields: - mu: float (mean) - sigma: float (standard deviation) p : NumericArray Probability from [0, 1] Returns ------- NumericArray Quantiles corresponding to probabilities p If p[i] is 0 or 1, then the result[i] is -inf and inf correspondingly Raises ------ ValueError If probability is outside [0, 1] """ if np.any((p < 0) | (p > 1)): raise ValueError("Probability must be in [0, 1]") parameters = cast(_MeanStd, parameters) result = cast( NumericArray, parameters.mu + parameters.sigma * np.sqrt(2) * erfinv(2 * p - 1), ) return result def char_func(parameters: Parametrization, t: NumericArray) -> ComplexArray: """ Characteristic function of normal distribution. Parameters ---------- parameters : Parametrization Distribution parameters object with fields: - mu: float (mean) - sigma: float (standard deviation) x : NumericArray Points at which to evaluate the characteristic function Returns ------- ComplexArray Characteristic function values at points x """ parameters = cast(_MeanStd, parameters) sigma = parameters.sigma mu = parameters.mu return cast(ComplexArray, np.exp(1j * mu * t - 0.5 * (sigma**2) * (t**2))) def mean_func(parameters: Parametrization, _: Any) -> float: """Mean of normal distribution.""" parameters = cast(_MeanStd, parameters) return parameters.mu def var_func(parameters: Parametrization, _: Any) -> float: """Variance of normal distribution.""" parameters = cast(_MeanStd, parameters) return parameters.sigma**2 def skew_func(_1: Parametrization, _2: Any) -> int: """Skewness of normal distribution (always 0).""" return 0 def kurt_func(_1: Parametrization, _2: Any, excess: bool = False) -> int: """Raw or excess kurtosis of normal distribution. Parameters ---------- _1 : Parametrization Needed by architecture parameter excess : bool A value defines if there will be raw or excess kurtosis default is False Returns ------- int Kurtosis value """ if not excess: return 3 else: return 0 def _support(_: Parametrization) -> ContinuousSupport: """Support of normal distribution""" return ContinuousSupport() Normal = ParametricFamily( name=FamilyName.NORMAL, distr_type=UnivariateContinuous, distr_parametrizations=["meanStd", "meanPrec", "exponential"], distr_characteristics={ CharacteristicName.PDF: pdf, CharacteristicName.CDF: cdf, CharacteristicName.PPF: ppf, CharacteristicName.CF: char_func, CharacteristicName.MEAN: mean_func, CharacteristicName.VAR: var_func, CharacteristicName.SKEW: skew_func, CharacteristicName.KURT: kurt_func, }, support_by_parametrization=_support, ) Normal.__doc__ = NORMAL_DOC @parametrization(family=Normal, name="meanStd") class _MeanStd(Parametrization): """ Standard parametrization of normal distribution. Parameters ---------- mu : float Mean of the distribution sigma : float Standard deviation of the distribution """ mu: float sigma: float @constraint(description="sigma > 0") def check_sigma_positive(self) -> bool: """Check that standard deviation is positive.""" return self.sigma > 0 @parametrization(family=Normal, name="meanPrec") class _MeanPrec(Parametrization): """ Mean-precision parametrization of normal distribution. Parameters ---------- mu : float Mean of the distribution tau : float Precision parameter (inverse variance) """ mu: float tau: float @constraint(description="tau > 0") def check_tau_positive(self) -> bool: """Check that precision parameter is positive.""" return self.tau > 0 def transform_to_base_parametrization(self) -> Parametrization: """ Transform to Standard parametrization. Returns ------- Parametrization Standard parametrization instance """ sigma = math.sqrt(1 / self.tau) return _MeanStd(mu=self.mu, sigma=sigma) @parametrization(family=Normal, name="exponential") class _Exp(Parametrization): """ Exponential family parametrization of normal distribution. Uses the form: y = exp(a*x² + b*x + c) Parameters ---------- a : float Quadratic term coefficient in exponential form b : float Linear term coefficient in exponential form """ a: float b: float @property def c(self) -> float: """ Calculate the normalization constant c. Returns ------- float Normalization constant """ return (self.b**2) / (4 * self.a) - (1 / 2) * math.log(math.pi / (-self.a)) @constraint(description="a < 0") def check_a_negative(self) -> bool: """Check that quadratic term coefficient is negative.""" return self.a < 0 def transform_to_base_parametrization(self) -> Parametrization: """ Transform to Standard parametrization. Returns ------- Parametrization Standard parametrization instance """ mu = -self.b / (2 * self.a) sigma = math.sqrt(-1 / (2 * self.a)) return _MeanStd(mu=mu, sigma=sigma) ParametricFamilyRegister.register(Normal)