diff --git a/src/pysatl_core/families/configuration.py b/src/pysatl_core/families/configuration.py index 4670e3b..3bade38 100644 --- a/src/pysatl_core/families/configuration.py +++ b/src/pysatl_core/families/configuration.py @@ -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 ----- @@ -69,6 +70,7 @@ def configure_families_register() -> ParametricFamilyRegister: The global registry of parametric families. """ _configure_normal_family() + _configure_uniform_family() return ParametricFamilyRegister() @@ -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 ---------- @@ -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={ @@ -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() diff --git a/tests/unit/families/test_configuration.py b/tests/unit/families/test_configuration.py index 1da2911..2d9b389 100644 --- a/tests/unit/families/test_configuration.py +++ b/tests/unit/families/test_configuration.py @@ -2,7 +2,7 @@ Tests for Normal Distribution Family Configuration This module tests the functionality of the normal distribution family -defined in config.py, including parameterizations, characteristics, +defined in configuration.py, including parameterizations, characteristics, and sampling. """ @@ -15,35 +15,41 @@ import numpy as np import pytest -from scipy.stats import norm +from scipy.stats import norm, uniform from pysatl_core.distributions.support import ContinuousSupport from pysatl_core.families.configuration import ( NormalExpParametrization, NormalMeanPrecParametrization, NormalMeanStdParametrization, + UniformMeanWidthParametrization, + UniformMinRangeParametrization, + UniformStandardParametrization, configure_families_register, ) from pysatl_core.families.registry import ParametricFamilyRegister from pysatl_core.types import ContinuousSupportShape1D, UnivariateContinuous -class TestNormalFamily: - """Test suite for Normal distribution family.""" +class BaseDistributionTest: + """Based class for all distribution families' tests""" - # Precision for floating point comparisons CALCULATION_PRECISION = 1e-10 + +class TestNormalFamily(BaseDistributionTest): + """Test suite for Normal distribution family.""" + def setup_method(self): """Setup before each test method.""" registry = configure_families_register() - self.normal_family = registry.get("Normal Family") + self.normal_family = registry.get("Normal") self.normal_dist_example = self.normal_family(mu=2.0, sigma=1.5) def test_family_registration(self): """Test that normal family is properly registered.""" - family = ParametricFamilyRegister.get("Normal Family") - assert family.name == "Normal Family" + family = ParametricFamilyRegister.get("Normal") + assert family.name == "Normal" # Check parameterizations expected_parametrizations = {"meanStd", "meanPrec", "exponential"} @@ -54,7 +60,7 @@ def test_mean_var_parametrization_creation(self): """Test creation of distribution with standard parametrization.""" dist = self.normal_family(mu=2.0, sigma=1.5) - assert dist.family_name == "Normal Family" + assert dist.family_name == "Normal" assert dist.distribution_type == UnivariateContinuous params = cast(NormalMeanStdParametrization, dist.parameters) @@ -245,7 +251,6 @@ def test_normal_support(self): dist = self.normal_dist_example assert dist.support is not None - assert isinstance(dist.support, ContinuousSupport) assert dist.support.left == float("-inf") @@ -270,7 +275,7 @@ class TestNormalFamilyEdgeCases: def setup_method(self): """Setup before each test method.""" registry = configure_families_register() - self.normal_family = registry.get("Normal Family") + self.normal_family = registry.get("Normal") self.normal_dist_example = self.normal_family(mu=2.0, sigma=1.5) def test_invalid_parameterization(self): @@ -285,7 +290,6 @@ def test_missing_parameters(self): def test_invalid_probability_ppf(self): """Test PPF with invalid probability values.""" - self.normal_family(mu=0.0, sigma=1.0) ppf = self.normal_dist_example.computation_strategy.query_method( "ppf", self.normal_dist_example ) @@ -299,3 +303,375 @@ def test_invalid_probability_ppf(self): ppf(-0.1) with pytest.raises(ValueError): ppf(1.1) + + +class TestUniformFamily(BaseDistributionTest): + """Test suite for Uniform distribution family.""" + + def setup_method(self): + """Setup before each test method.""" + registry = configure_families_register() + self.uniform_family = registry.get("ContinuousUniform") + self.uniform_dist_example = self.uniform_family(lower_bound=2.0, upper_bound=5.0) + + def test_family_registration(self): + """Test that uniform family is properly registered.""" + family = ParametricFamilyRegister.get("ContinuousUniform") + assert family.name == "ContinuousUniform" + + # Check parameterizations + expected_parametrizations = {"standard", "meanWidth", "minRange"} + assert set(family.parametrization_names) == expected_parametrizations + assert family.base_parametrization_name == "standard" + + def test_standard_parametrization_creation(self): + """Test creation of distribution with standard parametrization.""" + dist = self.uniform_family(lower_bound=2.0, upper_bound=5.0) + + assert dist.family_name == "ContinuousUniform" + assert dist.distribution_type == UnivariateContinuous + + params = cast(UniformStandardParametrization, dist.parameters) + assert params.lower_bound == 2.0 + assert params.upper_bound == 5.0 + assert params.name == "standard" + + def test_mean_width_parametrization_creation(self): + """Test creation of distribution with mean-width parametrization.""" + dist = self.uniform_family(mean=3.5, width=3.0, parametrization_name="meanWidth") + + params = cast(UniformMeanWidthParametrization, dist.parameters) + assert params.mean == 3.5 + assert params.width == 3.0 + assert params.name == "meanWidth" + + def test_min_range_parametrization_creation(self): + """Test creation of distribution with min-range parametrization.""" + dist = self.uniform_family(minimum=2.0, range_val=3.0, parametrization_name="minRange") + + params = cast(UniformMinRangeParametrization, dist.parameters) + assert params.minimum == 2.0 + assert params.range_val == 3.0 + assert params.name == "minRange" + + def test_parametrization_constraints(self): + """Test parameter constraints validation.""" + # lower_bound must be less than upper_bound + with pytest.raises(ValueError, match="lower_bound < upper_bound"): + self.uniform_family(lower_bound=5.0, upper_bound=2.0) + + # width must be positive + with pytest.raises(ValueError, match="width > 0"): + self.uniform_family(mean=3.5, width=0.0, parametrization_name="meanWidth") + + # range_val must be positive + with pytest.raises(ValueError, match="range_val > 0"): + self.uniform_family(minimum=2.0, range_val=0.0, parametrization_name="minRange") + + def test_pdf_calculation(self): + """Test PDF calculation against scipy.stats.uniform.""" + pdf = self.uniform_dist_example.computation_strategy.query_method( + "pdf", self.uniform_dist_example + ) + test_points = [1.0, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0] + + for x in test_points: + # Our implementation + our_pdf = pdf(x) + # Scipy reference + scipy_pdf = uniform.pdf(x, loc=2.0, scale=3.0) + + assert abs(our_pdf - scipy_pdf) < self.CALCULATION_PRECISION + + def test_cdf_calculation(self): + """Test CDF calculation against scipy.stats.uniform.""" + cdf = self.uniform_dist_example.computation_strategy.query_method( + "cdf", self.uniform_dist_example + ) + test_points = [1.0, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0] + + for x in test_points: + our_cdf = cdf(x) + scipy_cdf = uniform.cdf(x, loc=2.0, scale=3.0) + + assert abs(our_cdf - scipy_cdf) < self.CALCULATION_PRECISION + + def test_ppf_calculation(self): + """Test PPF calculation against scipy.stats.uniform.""" + ppf = self.uniform_dist_example.computation_strategy.query_method( + "ppf", self.uniform_dist_example + ) + test_probabilities = [0.0, 0.25, 0.5, 0.75, 1.0] + + for p in test_probabilities: + our_ppf = ppf(p) + scipy_ppf = uniform.ppf(p, loc=2.0, scale=3.0) + + assert abs(our_ppf - scipy_ppf) < self.CALCULATION_PRECISION + + @pytest.mark.parametrize( + "char_func_arg", + [ + -2.0, + -1.0, + 1.0, + 2.0, + ], + ) + def test_characteristic_function(self, char_func_arg): + """Test characteristic function calculation at specific points.""" + char_func = self.uniform_dist_example.computation_strategy.query_method( + "char_func", self.uniform_dist_example + ) + cf_value = char_func(char_func_arg) + + # Analytical formula for characteristic function of uniform distribution + a, b = 2.0, 5.0 + width = b - a + + # φ(t) = (e^{itb} - e^{ita}) / (it(b-a)) + # Re(φ(t)) = (sin(tb) - sin(ta)) / (t(b-a)) + # Im(φ(t)) = -(cos(tb) - cos(ta)) / (t(b-a)) + if abs(char_func_arg) < self.CALCULATION_PRECISION: + expected_real = 1.0 + expected_imag = 0.0 + else: + expected_real = (math.sin(b * char_func_arg) - math.sin(a * char_func_arg)) / ( + char_func_arg * width + ) + expected_imag = -(math.cos(b * char_func_arg) - math.cos(a * char_func_arg)) / ( + char_func_arg * width + ) + + assert abs(cf_value.real - expected_real) < self.CALCULATION_PRECISION + assert abs(cf_value.imag - expected_imag) < self.CALCULATION_PRECISION + + def test_characteristic_function_at_zero(self): + """Test characteristic function at zero returns 1.""" + char_func = self.uniform_dist_example.computation_strategy.query_method( + "char_func", self.uniform_dist_example + ) + + cf_value_zero = char_func(0.0) + assert abs(cf_value_zero.real - 1.0) < self.CALCULATION_PRECISION + assert abs(cf_value_zero.imag) < self.CALCULATION_PRECISION + + cf_value_small = char_func(self.CALCULATION_PRECISION) + assert abs(cf_value_small.real - 1.0) < self.CALCULATION_PRECISION + + cf_value_large = char_func(1000.0) + assert isinstance(cf_value_large, complex) + assert abs(cf_value_large) <= 1 + + def test_moments(self): + """Test moment calculations.""" + dist = self.uniform_dist_example + + # Mean + mean_func = dist.computation_strategy.query_method("mean", dist) + assert abs(mean_func(None) - 3.5) < self.CALCULATION_PRECISION + + # Variance + var_func = dist.computation_strategy.query_method("var", dist) + assert abs(var_func(None) - 0.75) < self.CALCULATION_PRECISION + + # Skewness + skew_func = dist.computation_strategy.query_method("skewness", dist) + assert abs(skew_func(None) - 0.0) < self.CALCULATION_PRECISION + + def test_kurtosis_calculation(self): + """Test kurtosis calculation with excess parameter.""" + kurt_func = self.uniform_dist_example.computation_strategy.query_method( + "kurtosis", self.uniform_dist_example + ) + + raw_kurt = kurt_func(None) + assert abs(raw_kurt - 1.8) < self.CALCULATION_PRECISION + + excess_kurt = kurt_func(None, excess=True) + assert abs(excess_kurt + 1.2) < self.CALCULATION_PRECISION + + raw_kurt_explicit = kurt_func(None, excess=False) + assert abs(raw_kurt_explicit - 1.8) < self.CALCULATION_PRECISION + + @pytest.mark.parametrize( + "parametrization_name, params, expected_lower, expected_upper", + [ + ("standard", {"lower_bound": 2.0, "upper_bound": 5.0}, 2.0, 5.0), + ("meanWidth", {"mean": 3.5, "width": 3.0}, 2.0, 5.0), + ("minRange", {"minimum": 2.0, "range_val": 3.0}, 2.0, 5.0), + ], + ) + def test_parametrization_conversions( + self, parametrization_name, params, expected_lower, expected_upper + ): + """Test conversions between different parameterizations.""" + base_params = cast( + UniformStandardParametrization, + self.uniform_family.to_base( + self.uniform_family.get_parametrization(parametrization_name)(**params) + ), + ) + + assert abs(base_params.lower_bound - expected_lower) < self.CALCULATION_PRECISION + assert abs(base_params.upper_bound - expected_upper) < self.CALCULATION_PRECISION + + def test_analytical_computations_caching(self): + """Test that analytical computations are properly cached.""" + comp = self.uniform_family(lower_bound=0.0, upper_bound=1.0).analytical_computations + + expected_chars = { + "pdf", + "cdf", + "ppf", + "char_func", + "mean", + "var", + "skewness", + "kurtosis", + } + assert set(comp.keys()) == expected_chars + + def test_array_input_support_pdf(self): + """Test that PDF supports array inputs.""" + dist = self.uniform_family(lower_bound=0.0, upper_bound=1.0) + x_array = np.array([-0.5, 0.0, 0.25, 0.5, 0.75, 1.0, 1.5]) + + pdf = dist.computation_strategy.query_method("pdf", dist) + pdf_array = pdf(x_array) + + assert pdf_array.shape == x_array.shape + scipy_pdf = uniform.pdf(x_array, loc=0.0, scale=1.0) + + np.testing.assert_array_almost_equal( + pdf_array, scipy_pdf, decimal=int(-math.log10(self.CALCULATION_PRECISION)) + ) + + def test_array_input_support_cdf(self): + """Test that CDF supports array inputs.""" + dist = self.uniform_family(lower_bound=0.0, upper_bound=1.0) + x_array = np.array([-0.5, 0.0, 0.25, 0.5, 0.75, 1.0, 1.5]) + + cdf = dist.computation_strategy.query_method("cdf", dist) + cdf_array = cdf(x_array) + + assert cdf_array.shape == x_array.shape + scipy_cdf = uniform.cdf(x_array, loc=0.0, scale=1.0) + + np.testing.assert_array_almost_equal( + cdf_array, scipy_cdf, decimal=int(-math.log10(self.CALCULATION_PRECISION)) + ) + + def test_array_input_support_ppf(self): + """Test that PPF supports array inputs.""" + dist = self.uniform_family(lower_bound=0.0, upper_bound=1.0) + p_array = np.array([0.0, 0.25, 0.5, 0.75, 1.0]) + + ppf = dist.computation_strategy.query_method("ppf", dist) + ppf_array = ppf(p_array) + + assert ppf_array.shape == p_array.shape + scipy_ppf = uniform.ppf(p_array, loc=0.0, scale=1.0) + + np.testing.assert_array_almost_equal( + ppf_array, scipy_ppf, decimal=int(-math.log10(self.CALCULATION_PRECISION)) + ) + + def test_uniform_support(self): + """Test that uniform distribution has correct support [lower_bound, upper_bound].""" + dist = self.uniform_dist_example + + assert dist.support is not None + assert isinstance(dist.support, ContinuousSupport) + + assert dist.support.left == 2.0 + assert dist.support.right == 5.0 + assert dist.support.left_closed # [a, b] - inclusive + assert dist.support.right_closed # [a, b] - inclusive + + # Test containment + assert dist.support.contains(2.0) is True # boundary included + assert dist.support.contains(5.0) is True # boundary included + assert dist.support.contains(3.5) is True # inside + assert dist.support.contains(1.9) is False # outside left + assert dist.support.contains(5.1) is False # outside right + + # Test array + test_points = np.array([1.9, 2.0, 3.5, 5.0, 5.1]) + expected = np.array([False, True, True, True, False]) + results = dist.support.contains(test_points) + np.testing.assert_array_equal(results, expected) + + assert dist.support.shape == ContinuousSupportShape1D.BOUNDED_INTERVAL + + +class TestUniformFamilyEdgeCases(BaseDistributionTest): + """Test edge cases and error conditions for uniform distribution.""" + + def setup_method(self): + """Setup before each test method.""" + registry = configure_families_register() + self.uniform_family = registry.get("ContinuousUniform") + self.uniform_dist = self.uniform_family(lower_bound=0.0, upper_bound=1.0) + + def test_invalid_parameterization(self): + """Test error for invalid parameterization name.""" + with pytest.raises(KeyError): + self.uniform_family.distribution( + parametrization_name="invalid_name", lower_bound=0.0, upper_bound=1.0 + ) + + def test_missing_parameters(self): + """Test error for missing required parameters.""" + with pytest.raises(TypeError): + self.uniform_family.distribution(lower_bound=0.0) # Missing upper_bound + + with pytest.raises(TypeError): + self.uniform_family.distribution(upper_bound=1.0) # Missing lower_bound + + def test_invalid_probability_ppf(self): + """Test PPF with invalid probability values.""" + ppf = self.uniform_dist.computation_strategy.query_method("ppf", self.uniform_dist) + + # Test boundaries + assert ppf(0.0) == 0.0 + assert ppf(1.0) == 1.0 + + # Test invalid probabilities + with pytest.raises(ValueError): + ppf(-0.1) + with pytest.raises(ValueError): + ppf(1.1) + + def test_single_value_uniform(self): + """Test uniform distribution with single value (lower_bound == upper_bound).""" + # This should fail validation + with pytest.raises(ValueError, match="lower_bound < upper_bound"): + self.uniform_family(lower_bound=2.0, upper_bound=2.0) + + def test_characteristic_function_edge_cases(self): + """Test characteristic function at edge cases.""" + char_func = self.uniform_dist.computation_strategy.query_method( + "char_func", self.uniform_dist + ) + + # Test with very small t + cf_value_small = char_func(self.CALCULATION_PRECISION) + # Should be close to 1, but may have numerical issues + assert abs(cf_value_small.real - 1.0) < self.CALCULATION_PRECISION + + # Test with large t + cf_value_large = char_func(1000.0) + # Characteristic function should still be a complex number + assert isinstance(cf_value_large, complex) + assert abs(cf_value_large) <= 1.0 # |φ(t)| ≤ 1 for all t + + def test_negative_width(self): + """Test that negative width is rejected.""" + with pytest.raises(ValueError, match="width > 0"): + self.uniform_family(mean=0.0, width=-1.0, parametrization_name="meanWidth") + + def test_negative_range(self): + """Test that negative range is rejected.""" + with pytest.raises(ValueError, match="range_val > 0"): + self.uniform_family(minimum=0.0, range_val=-1.0, parametrization_name="minRange")