Source code for pysatl_core.families.builtins.continuous.uniform
"""
Uniform distribution family implementation.
Contains the Uniform family with multiple parameterizations.
"""
from __future__ import annotations
__author__ = "Fedor Myznikov"
__copyright__ = "Copyright (c) 2025 PySATL project"
__license__ = "SPDX-License-Identifier: MIT"
from typing import cast
import numpy as np
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,
)
[docs]
def configure_uniform_family() -> None:
"""
Configure and register the Uniform distribution family.
"""
if ParametricFamilyRegister.contains(FamilyName.CONTINUOUS_UNIFORM):
return
UNIFORM_DOC = """
Uniform (continuous) distribution.
The uniform distribution is a continuous probability distribution where
all intervals of the same length are equally probable. It is defined by
two parameters: lower bound and upper bound.
Probability density function:
f(x) = 1/(upper_bound - lower_bound) for x in [lower_bound, upper_bound], 0 otherwise
The uniform distribution is often used when there is no prior knowledge
about the possible values of a variable, representing maximum uncertainty.
"""
def pdf(parameters: Parametrization, x: NumericArray) -> NumericArray:
"""
Probability density function for uniform distribution.
- For x < lower_bound: returns 0
- For x > upper_bound: returns 0
- Otherwise: returns (1 / (upper_bound - lower_bound))
Parameters
----------
parameters : Parametrization
Distribution parameters object with fields:
- lower_bound: float (lower bound)
- upper_bound: float (upper bound)
x : NumericArray
Points at which to evaluate the probability density function
Returns
-------
NumericArray
Probability density values at points x
"""
parameters = cast(_Standard, parameters)
lower_bound = parameters.lower_bound
upper_bound = parameters.upper_bound
return np.where(
(x >= lower_bound) & (x <= upper_bound), 1.0 / (upper_bound - lower_bound), 0.0
)
def cdf(parameters: Parametrization, x: NumericArray) -> NumericArray:
"""
Cumulative distribution function for uniform distribution.
Uses np.clip for vectorized computation:
- For x < lower_bound: returns 0
- For x > upper_bound: returns 1
Parameters
----------
parameters : Parametrization
Distribution parameters object with fields:
- lower_bound: float (lower bound)
- upper_bound: float (upper bound)
x : NumericArray
Points at which to evaluate the cumulative distribution function
Returns
-------
NumericArray
Probabilities P(X ≤ x) for each point x
"""
parameters = cast(_Standard, parameters)
lower_bound = parameters.lower_bound
upper_bound = parameters.upper_bound
return cast(
NumericArray, np.clip((x - lower_bound) / (upper_bound - lower_bound), 0.0, 1.0)
)
def ppf(parameters: Parametrization, p: NumericArray) -> NumericArray:
"""
Percent point function (inverse CDF) for uniform distribution.
For uniform distribution on [lower_bound, upper_bound]:
- For p = 0: returns lower_bound
- For p = 1: returns upper_bound
- For p in (0, 1): returns lower_bound + p × (upper_bound - lower_bound)
Parameters
----------
parameters : Parametrization
Distribution parameters object with fields:
- lower_bound: float (lower bound)
- upper_bound: float (upper bound)
p : NumericArray
Probability from [0, 1]
Returns
-------
NumericArray
Quantiles corresponding to probabilities p
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(_Standard, parameters)
lower_bound = parameters.lower_bound
upper_bound = parameters.upper_bound
return cast(NumericArray, lower_bound + p * (upper_bound - lower_bound))
def char_func(parameters: Parametrization, t: NumericArray) -> ComplexArray:
"""
Characteristic function of uniform distribution.
Characteristic function formula for uniform distribution on [lower_bound, upper bound]:
φ(t) = sinc((upper bound - lower_bound) * t / 2) *
* exp(i * (lower_bound + upper_bound) * t / 2)
where sinc(x) = sin(πx)/(πx) as defined by numpy.
Parameters
----------
parameters : Parametrization
Distribution parameters object with fields:
- lower_bound: float (lower bound)
- upper_bound: float (upper bound)
t : NumericArray
Points at which to evaluate the characteristic function
Returns
-------
ComplexArray
Characteristic function values at points t
"""
parameters = cast(_Standard, parameters)
lower_bound = parameters.lower_bound
upper_bound = parameters.upper_bound
width = upper_bound - lower_bound
center = (lower_bound + upper_bound) / 2
t_arr = np.asarray(t, dtype=np.float64)
x = width * t_arr / (2 * np.pi)
sinc_val = np.sinc(x)
return cast(ComplexArray, sinc_val * np.exp(1j * center * t_arr))
def lpdf(parameters: Parametrization, x: NumericArray) -> NumericArray:
"""
Logarithm of the probability density function for uniform distribution.
Parameters
----------
parameters : Parametrization
Distribution parameters object with fields:
- lower_bound: float (lower bound)
- upper_bound: float (upper bound)
x : NumericArray
Points at which to evaluate the log-probability density function
Returns
-------
NumericArray
Log-probability density values at points x
For x outside [lower_bound, upper_bound] returns -np.inf
"""
parameters = cast(_Standard, parameters)
a = parameters.lower_bound
b = parameters.upper_bound
return np.where((x >= a) & (x <= b), -np.log(b - a), -np.inf)
def mean_func(parameters: Parametrization) -> float:
"""Mean of uniform distribution."""
parameters = cast(_Standard, parameters)
return (parameters.lower_bound + parameters.upper_bound) / 2
def var_func(parameters: Parametrization) -> float:
"""Variance of uniform distribution."""
parameters = cast(_Standard, parameters)
width = parameters.upper_bound - parameters.lower_bound
return width**2 / 12
def skew_func() -> int:
"""Skewness of uniform distribution (always 0)."""
return 0
def kurt_func(*, excess: bool = False) -> float:
"""Raw or excess kurtosis of uniform distribution.
Parameters
----------
_1 : Parametrization
Needed by architecture parameter
_2 : Any
Needed by architecture parameter
excess : bool
A value defines if there will be raw or excess kurtosis
default is False
Returns
-------
float
Kurtosis value
"""
if not excess:
return 1.8
else:
return -1.2
def _support(parameters: Parametrization) -> ContinuousSupport:
"""Support of uniform distribution"""
parameters = cast(_Standard, parameters.transform_to_base_parametrization())
return ContinuousSupport(
left=parameters.lower_bound,
right=parameters.upper_bound,
left_closed=True,
right_closed=True,
)
def _base_score(parameters: Parametrization, x: NumericArray) -> NumericArray:
"""
Compute the score (gradient of log‑PDF) for the base
parametrization (lower_bound, upper_bound).
For uniform distribution on [a, b] with a < b, log‑PDF is:
log f(x) = -log(b - a) for a ≤ x ≤ b, else -∞.
The gradient with respect to a and b (where defined, i.e., inside the support) is:
∂/∂a log f = 1/(b - a)
∂/∂b log f = -1/(b - a)
For points outside the support, the gradient is set to 0 (since density is zero,
but the score is typically considered undefined; we return 0 for numerical safety).
Parameters
----------
parameters : Parametrization
Base parametrization instance (_Standard) with fields lower_bound, upper_bound.
x : NumericArray
Points at which to evaluate the gradient.
Returns
-------
NumericArray
Gradient array of shape (..., 2) where last axis corresponds to
[d(log f)/d(lower_bound), d(log f)/d(upper_bound)].
"""
params = cast(_Standard, parameters)
a = params.lower_bound
b = params.upper_bound
width = b - a
inside = (x >= a) & (x <= b)
grad_a = np.where(inside, 1.0 / width, 0.0)
grad_b = np.where(inside, -1.0 / width, 0.0)
return np.stack([grad_a, grad_b], axis=-1)
Uniform = ParametricFamily(
name=FamilyName.CONTINUOUS_UNIFORM,
distr_type=UnivariateContinuous,
distr_parametrizations=["standard", "meanWidth", "minRange"],
distr_characteristics={
CharacteristicName.PDF: pdf,
CharacteristicName.CDF: cdf,
CharacteristicName.PPF: ppf,
CharacteristicName.CF: char_func,
CharacteristicName.LPDF: lpdf,
CharacteristicName.MEAN: mean_func,
CharacteristicName.VAR: var_func,
CharacteristicName.SKEW: skew_func,
CharacteristicName.KURT: kurt_func,
},
support_by_parametrization=_support,
base_score=_base_score,
)
Uniform.__doc__ = UNIFORM_DOC
@parametrization(family=Uniform, name="standard")
class _Standard(Parametrization):
"""
Standard parametrization of uniform distribution.
Parameters
----------
lower_bound : float
Lower bound of the distribution
upper_bound : float
Upper bound of the distribution
"""
lower_bound: float
upper_bound: float
@constraint(description="lower_bound < upper_bound")
def check_lower_less_than_upper(self) -> bool:
"""Check that lower bound is less than upper bound."""
return self.lower_bound < self.upper_bound
@parametrization(family=Uniform, name="meanWidth")
class _MeanWidth(Parametrization):
"""
Mean-width parametrization of uniform distribution.
Parameters
----------
mean : float
Mean (center) of the distribution
width : float
Width of the distribution (upper_bound - lower_bound)
"""
mean: float
width: float
@constraint(description="width > 0")
def check_width_positive(self) -> bool:
"""Check that width is positive."""
return self.width > 0
def transform_to_base_parametrization(self) -> Parametrization:
"""
Transform to Standard parametrization.
Returns
-------
Parametrization
Standard parametrization instance
"""
half_width = self.width / 2
return _Standard(lower_bound=self.mean - half_width, upper_bound=self.mean + half_width)
def gradient_transform(self, grad_base: NumericArray) -> NumericArray:
"""
Transform gradient from base parameters (a, b) to (mean, width).
The transformation is:
mean = (a + b) / 2
width = b - a
The Jacobian matrix:
d(mean)/da = 0.5, d(mean)/db = 0.5
d(width)/da = -1, d(width)/db = 1
Hence:
grad_mean = 0.5 * (grad_a + grad_b)
grad_width = -grad_a + grad_b
Parameters
----------
grad_base : NumericArray
Gradient with respect to (a, b), shape (..., 2).
Returns
-------
NumericArray
Gradient with respect to (mean, width), shape (..., 2).
"""
grad_a = grad_base[..., 0]
grad_b = grad_base[..., 1]
grad_mean = 0.5 * (grad_a + grad_b)
grad_width = -grad_a + grad_b
return np.stack([grad_mean, grad_width], axis=-1)
@parametrization(family=Uniform, name="minRange")
class _MinRange(Parametrization):
"""
Minimum-range parametrization of uniform distribution.
Parameters
----------
minimum : float
Minimum value (lower bound)
range_val : float
Range of the distribution (upper_bound - lower_bound)
"""
minimum: float
range_val: float
@constraint(description="range_val > 0")
def check_range_positive(self) -> bool:
"""Check that range is positive."""
return self.range_val > 0
def transform_to_base_parametrization(self) -> Parametrization:
"""
Transform to Standard parametrization.
Returns
-------
Parametrization
Standard parametrization instance
"""
return _Standard(lower_bound=self.minimum, upper_bound=self.minimum + self.range_val)
def gradient_transform(self, grad_base: NumericArray) -> NumericArray:
"""
Transform gradient from base parameters (a, b) to (minimum, range_val).
The transformation is:
minimum = a
range_val = b - a
The Jacobian matrix:
d(minimum)/da = 1, d(minimum)/db = 0
d(range_val)/da = -1, d(range_val)/db = 1
Hence:
grad_minimum = grad_a
grad_range = -grad_a + grad_b
Parameters
----------
grad_base : NumericArray
Gradient with respect to (a, b), shape (..., 2).
Returns
-------
NumericArray
Gradient with respect to (minimum, range_val), shape (..., 2).
"""
grad_a = grad_base[..., 0]
grad_b = grad_base[..., 1]
grad_minimum = grad_a
grad_range = -grad_a + grad_b
return np.stack([grad_minimum, grad_range], axis=-1)
ParametricFamilyRegister.register(Uniform)