Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
332 changes: 330 additions & 2 deletions src/pysatl_core/families/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
This module defines and configures parametric distribution families for the PySATL library:

- :class:`Normal Family` — Gaussian distribution with multiple parameterizations.
- :class:`Uniform Family` — Gaussian distribution with multiple parameterizations.

Notes
-----
Expand Down Expand Up @@ -69,6 +70,7 @@ def configure_families_register() -> ParametricFamilyRegister:
The global registry of parametric families.
"""
_configure_normal_family()
_configure_uniform_family()
return ParametricFamilyRegister()


Expand Down Expand Up @@ -323,7 +325,7 @@ def skew_func(_1: Parametrization, _2: Any) -> int:
return 0

def kurt_func(_1: Parametrization, _2: Any, excess: bool = False) -> int:
"""Raw or excess kurtosis of normal distribution (always 3).
"""Raw or excess kurtosis of normal distribution.

Parameters
----------
Expand All @@ -344,10 +346,11 @@ def kurt_func(_1: Parametrization, _2: Any, excess: bool = False) -> int:
return 0

def _normal_support(_: Parametrization) -> ContinuousSupport:
"""Support of normal distribution"""
return ContinuousSupport()

Normal = ParametricFamily(
name="Normal Family",
name="Normal",
distr_type=UnivariateContinuous,
distr_parametrizations=["meanStd", "meanPrec", "exponential"],
distr_characteristics={
Expand All @@ -372,6 +375,331 @@ def _normal_support(_: Parametrization) -> ContinuousSupport:
ParametricFamilyRegister.register(Normal)


@dataclass
class UniformStandardParametrization(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


@dataclass
class UniformMeanWidthParametrization(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 UniformStandardParametrization(
lower_bound=self.mean - half_width, upper_bound=self.mean + half_width
)


@dataclass
class UniformMinRangeParametrization(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 UniformStandardParametrization(
lower_bound=self.minimum, upper_bound=self.minimum + self.range_val
)


def _configure_uniform_family() -> None:
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 uniform_pdf(
parameters: Parametrization, x: npt.NDArray[np.float64]
) -> npt.NDArray[np.float64]:
"""
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 : npt.NDArray[np.float64]
Points at which to evaluate the probability density function

Returns
-------
npt.NDArray[np.float64]
Probability density values at points x
"""
parameters = cast(UniformStandardParametrization, 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 uniform_cdf(
parameters: Parametrization, x: npt.NDArray[np.float64]
) -> npt.NDArray[np.float64]:
"""
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 : npt.NDArray[np.float64]
Points at which to evaluate the cumulative distribution function

Returns
-------
npt.NDArray[np.float64]
Probabilities P(X ≤ x) for each point x
"""
parameters = cast(UniformStandardParametrization, parameters)

lower_bound = parameters.lower_bound
upper_bound = parameters.upper_bound

return np.clip((x - lower_bound) / (upper_bound - lower_bound), 0.0, 1.0)

def uniform_ppf(
parameters: Parametrization, p: npt.NDArray[np.float64]
) -> npt.NDArray[np.float64]:
"""
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 : npt.NDArray[np.float64]
Probability from [0, 1]

Returns
-------
npt.NDArray[np.float64]
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(UniformStandardParametrization, parameters)
lower_bound = parameters.lower_bound
upper_bound = parameters.upper_bound

return cast(npt.NDArray[np.float64], lower_bound + p * (upper_bound - lower_bound))

def uniform_char_func(
parameters: Parametrization, t: npt.NDArray[np.float64]
) -> npt.NDArray[np.complex128]:
"""
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 : npt.NDArray[np.float64]
Points at which to evaluate the characteristic function

Returns
-------
npt.NDArray[np.complex128]
Characteristic function values at points t
"""
parameters = cast(UniformStandardParametrization, 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(npt.NDArray[np.complex128], sinc_val * np.exp(1j * center * t_arr))

def mean_func(parameters: Parametrization, _: Any) -> float:
"""Mean of uniform distribution."""
parameters = cast(UniformStandardParametrization, parameters)
return (parameters.lower_bound + parameters.upper_bound) / 2

def var_func(parameters: Parametrization, _: Any) -> float:
"""Variance of uniform distribution."""
parameters = cast(UniformStandardParametrization, parameters)
width = parameters.upper_bound - parameters.lower_bound
return width**2 / 12

def skew_func(_1: Parametrization, _2: Any) -> int:
"""Skewness of uniform distribution (always 0)."""
return 0

def kurt_func(_1: Parametrization, _2: Any, 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 _uniform_support(parameters: Parametrization) -> ContinuousSupport:
"""Support of uniform distribution"""
parameters = cast(
UniformStandardParametrization, parameters.transform_to_base_parametrization()
)
return ContinuousSupport(
left=parameters.lower_bound,
right=parameters.upper_bound,
left_closed=True,
right_closed=True,
)

Uniform = ParametricFamily(
name="ContinuousUniform",
distr_type=UnivariateContinuous,
distr_parametrizations=["standard", "meanWidth", "minRange"],
distr_characteristics={
PDF: uniform_pdf,
CDF: uniform_cdf,
PPF: uniform_ppf,
CF: uniform_char_func,
MEAN: mean_func,
VAR: var_func,
SKEW: skew_func,
KURT: kurt_func,
},
sampling_strategy=DefaultSamplingUnivariateStrategy(),
support_by_parametrization=_uniform_support,
)
Uniform.__doc__ = UNIFORM_DOC

parametrization(family=Uniform, name="standard")(UniformStandardParametrization)
parametrization(family=Uniform, name="meanWidth")(UniformMeanWidthParametrization)
parametrization(family=Uniform, name="minRange")(UniformMinRangeParametrization)

ParametricFamilyRegister.register(Uniform)


def reset_families_register() -> None:
configure_families_register.cache_clear()
ParametricFamilyRegister._reset()
Loading