From b9539abfee2837f61f2bcd68b7515ea45fc554fc Mon Sep 17 00:00:00 2001 From: connor-krill Date: Fri, 2 Jun 2023 16:25:23 -0400 Subject: [PATCH 01/20] implemented custom pdf, cdf, icdf method for Uniform.py --- src/UQpy/distributions/collection/Uniform.py | 50 ++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/UQpy/distributions/collection/Uniform.py b/src/UQpy/distributions/collection/Uniform.py index bc630f218..bc6464af5 100644 --- a/src/UQpy/distributions/collection/Uniform.py +++ b/src/UQpy/distributions/collection/Uniform.py @@ -1,9 +1,11 @@ from typing import Union +import numpy as np import scipy.stats as stats from beartype import beartype from UQpy.distributions.baseclass import DistributionContinuous1D +from UQpy.utilities.ValidationTypes import NumericArrayLike class Uniform(DistributionContinuous1D): @@ -18,3 +20,51 @@ def __init__( """ super().__init__(loc=loc, scale=scale, ordered_parameters=("loc", "scale")) self._construct_from_scipy(scipy_name=stats.uniform) + self.pdf = self.__probability_density_function + self.cdf = self.__cumulative_distribution_function + self.icdf = self.__inverse_cumulative_distribution_function + + @beartype + def __probability_density_function(self, x: NumericArrayLike) -> np.ndarray: + """Probability Density Function for the uniform distribution + + :param x: + :return: pdf at all points in x + """ + x = np.atleast_1d(x) + loc = self.parameters['loc'] + scale = self.parameters['scale'] + mask = (loc <= x) & (x <= loc + scale) + pdf = np.zeros_like(x) + pdf[mask] = 1 / scale + return pdf + + @beartype + def __cumulative_distribution_function(self, x: NumericArrayLike) -> np.ndarray: + """Cumulative Distribution Function for the Uniform Distribution + + :param x: + """ + x = np.atleast_1d(x) + loc = self.parameters['loc'] + scale = self.parameters['scale'] + cdf = np.zeros_like(x) + middle_mask = (loc < x) & (x < loc + scale) + upper_mask = loc + scale <= x + cdf[middle_mask] = (x[middle_mask] - loc) / scale + cdf[upper_mask] = 1 + return cdf + + def __inverse_cumulative_distribution_function(self, y: NumericArrayLike) -> np.ndarray: + """Inverse cumulative distribution function for uniform distribution + + :param y: + :return: + """ + y = np.atleast_1d(y) + loc = self.parameters['loc'] + scale = self.parameters['scale'] + icdf = np.full(y.shape, np.nan) + mask = (0 <= y) & (y <= 1) + icdf[mask] = loc + (y[mask] * scale) + return icdf From f98071a0b566d88b1459c535b7280c8c0d7e1604 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Fri, 2 Jun 2023 16:25:33 -0400 Subject: [PATCH 02/20] implemented custom pdf, cdf method for Normal.py --- src/UQpy/distributions/collection/Normal.py | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/UQpy/distributions/collection/Normal.py b/src/UQpy/distributions/collection/Normal.py index 5c4403909..2b795862c 100644 --- a/src/UQpy/distributions/collection/Normal.py +++ b/src/UQpy/distributions/collection/Normal.py @@ -1,7 +1,10 @@ from typing import Union +import numpy as np import scipy.stats as stats from beartype import beartype +from scipy.special import erf, erfinv from UQpy.distributions.baseclass import DistributionContinuous1D +from UQpy.utilities.ValidationTypes import NumericArrayLike class Normal(DistributionContinuous1D): @@ -17,3 +20,45 @@ def __init__( """ super().__init__(loc=loc, scale=scale, ordered_parameters=("loc", "scale")) self._construct_from_scipy(scipy_name=stats.norm) + self.pdf = self.__probability_density_function + self.cdf = self.__cumulative_distribution_function + + @beartype + def __probability_density_function(self, x: NumericArrayLike): + """Probability density function for normal distribution + + :param x: + :return: + """ + x = np.atleast_1d(x) + mean = self.parameters['loc'] + standard_deviation = self.parameters['scale'] + normalizing_constant = 1 / (standard_deviation * np.sqrt(2 * np.pi)) + return normalizing_constant * np.exp(-0.5 * ((x - mean) / standard_deviation)**2) + + @beartype + def __cumulative_distribution_function(self, x: NumericArrayLike): + """Cumulative distribution function for the normal distribution defined with the error function + + :param x: + :return: + """ + x = np.atleast_1d(x) + mean = self.parameters['loc'] + standard_deviation = self.parameters['scale'] + erf_input = (x - mean) / (standard_deviation * np.sqrt(2.0)) + return (1.0 + erf(erf_input)) / 2.0 + + @beartype + def __inverse_cumulative_distribution_function(self, y: NumericArrayLike): + """Inverse cumulative distribution function for normal distribution defined with inverse error function + + Note: Currently this implementation is slower than scipy.stats.norm.ppf + :param y: + :return: + """ + y = np.atleast_1d(y) + icdf = np.full(y.shape, np.nan) + mask = (0 <= y) & (y <= 1) + icdf[mask] = np.sqrt(2) * erfinv(2*y[mask] - 1) + return icdf From 8db235f16aab2e8c4b6e6d2875738bff49790999 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Fri, 2 Jun 2023 16:26:08 -0400 Subject: [PATCH 03/20] new validation type for NumericArrayLikes and appropriate tests for new pdf, cdf, icdf methods in Uniform and Normal --- src/UQpy/utilities/ValidationTypes.py | 2 + .../test_distribution_methods.py | 66 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/UQpy/utilities/ValidationTypes.py b/src/UQpy/utilities/ValidationTypes.py index c52c4f095..a699b5a66 100644 --- a/src/UQpy/utilities/ValidationTypes.py +++ b/src/UQpy/utilities/ValidationTypes.py @@ -3,9 +3,11 @@ import numpy as np from beartype.vale import Is + RandomStateType = Union[None, int, np.random.RandomState] PositiveInteger = Annotated[int, Is[lambda number: number > 0]] PositiveFloat = Annotated[float, Is[lambda number: number > 0]] +NumericArrayLike = Union[int, float, list, tuple, np.ndarray] Numpy2DFloatArray = Annotated[ np.ndarray, Is[lambda array: array.ndim == 2 and np.issubdtype(array.dtype, float)], diff --git a/tests/unit_tests/distributions/test_distribution_methods.py b/tests/unit_tests/distributions/test_distribution_methods.py index f1afa352a..ccb653668 100644 --- a/tests/unit_tests/distributions/test_distribution_methods.py +++ b/tests/unit_tests/distributions/test_distribution_methods.py @@ -1,5 +1,8 @@ from UQpy.distributions import * import numpy as np +import pytest +import scipy + # Test all functions for one type of continuous distribution: uniform dist_continuous = Uniform(loc=1., scale=2.) @@ -81,3 +84,66 @@ def test_update_params_copula(): copula = Gumbel(theta=2.) copula.update_parameters(theta=1.) assert copula.get_parameters()['theta'] == 1. + + +# Test function for custom implementation of Uniform PDF and CDF +@pytest.mark.parametrize("value,expected_probability", [ + (-4, 0), + (0, 1), + (0.5, 1), + (1, 1), + (12, 0), + ([-1, 0, 0.5, 1, 2], [0, 1, 1, 1, 0]), + ((-1, 0, 0.5, 1, 2), (0, 1, 1, 1, 0)), + (np.array([-1, 0, 0.5, 1, 2]), np.array([0, 1, 1, 1, 0])) +]) +def test_uniform_pdf(value, expected_probability): + uniform = Uniform() + assert all(uniform.pdf(value) == expected_probability) + + +@pytest.mark.parametrize("value,expected_probability", [ + (-0.1245, 0), + (0.4, 0.4), + (3.14159, 1), + ([-4, 0.2, 0.75, 1, 6], [0, 0.2, 0.75, 1, 1]), + ((-4, 0.2, 0.75, 1, 6), (0, 0.2, 0.75, 1, 1)), + (np.array([-4, 0.2, 0.75, 1, 6]), np.array([0, 0.2, 0.75, 1, 1])), +]) +def test_uniform_cdf(value, expected_probability): + uniform = Uniform() + assert all(uniform.cdf(value) == expected_probability) + + +@pytest.mark.parametrize("probability,expected_value", [ + (-1, np.nan), + (0, 0), + (0.123, 0.123), + (1, 1), + (1.2, np.nan), + ((-0.5, 0, 0.5, 1, 1.5), (np.nan, 0, 0.5, 1, np.nan)), + ([-0.5, 0, 0.5, 1, 1.5], [np.nan, 0, 0.5, 1, np.nan]), + (np.array([-0.5, 0, 0.5, 1, 1.5]), np.array([np.nan, 0, 0.5, 1, np.nan])) +]) +def test_uniform_icdf(probability, expected_value): + uniform = Uniform() + assert np.array_equal(uniform.icdf(probability), np.atleast_1d(expected_value), equal_nan=True) + + +@pytest.mark.parametrize("value", [-3, -2.5, -1, 0, 1, 2.415, 6.168, np.array([-8, -4.13, -1, 0, 2])]) +def test_normal_pdf(value): + normal = Normal() + assert (np.isclose(normal.pdf(value), scipy.stats.norm.pdf(value))).all() + + +@pytest.mark.parametrize("value", [-4, -2.7, -0.4, 0, 0.4321, 2, 8, np.array([-3, -2, 0, 1, 4])]) +def test_normal_cdf(value): + normal = Normal() + assert (np.isclose(normal.cdf(value), scipy.stats.norm.cdf(value))).all() + + +@pytest.mark.parametrize("probability", [-1, 0, 1e-9, 0.15, 0.6626, 1-1e-9, 1, 2, np.array([-1, 0, 0.321123, 1, 5])]) +def test_normal_icdf(probability): + normal = Normal() + expected_value = np.atleast_1d(scipy.stats.norm.ppf(probability)) + assert np.allclose(normal.icdf(probability), expected_value, equal_nan=True) From b3907b9443e3e040812c9e7b84e2ad3cd50b5caf Mon Sep 17 00:00:00 2001 From: connor-krill Date: Fri, 2 Jun 2023 16:26:23 -0400 Subject: [PATCH 04/20] added scratch folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 921e2e7da..1e43d3f25 100644 --- a/.gitignore +++ b/.gitignore @@ -247,3 +247,4 @@ logo/ /python_model_third_party.py /third_party_script.py /docs/source/auto_examples/ +scratch/ \ No newline at end of file From 2f895b4568ea63f28b02be3e5265b1825e642bb5 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Fri, 1 Sep 2023 13:14:23 -0400 Subject: [PATCH 05/20] removed inverse cdf for normal bc it was slow and inaccurate --- src/UQpy/distributions/collection/Normal.py | 14 -------------- .../distributions/test_distribution_methods.py | 7 ------- 2 files changed, 21 deletions(-) diff --git a/src/UQpy/distributions/collection/Normal.py b/src/UQpy/distributions/collection/Normal.py index 2b795862c..cf90c258c 100644 --- a/src/UQpy/distributions/collection/Normal.py +++ b/src/UQpy/distributions/collection/Normal.py @@ -48,17 +48,3 @@ def __cumulative_distribution_function(self, x: NumericArrayLike): standard_deviation = self.parameters['scale'] erf_input = (x - mean) / (standard_deviation * np.sqrt(2.0)) return (1.0 + erf(erf_input)) / 2.0 - - @beartype - def __inverse_cumulative_distribution_function(self, y: NumericArrayLike): - """Inverse cumulative distribution function for normal distribution defined with inverse error function - - Note: Currently this implementation is slower than scipy.stats.norm.ppf - :param y: - :return: - """ - y = np.atleast_1d(y) - icdf = np.full(y.shape, np.nan) - mask = (0 <= y) & (y <= 1) - icdf[mask] = np.sqrt(2) * erfinv(2*y[mask] - 1) - return icdf diff --git a/tests/unit_tests/distributions/test_distribution_methods.py b/tests/unit_tests/distributions/test_distribution_methods.py index ccb653668..8c798e5f2 100644 --- a/tests/unit_tests/distributions/test_distribution_methods.py +++ b/tests/unit_tests/distributions/test_distribution_methods.py @@ -140,10 +140,3 @@ def test_normal_pdf(value): def test_normal_cdf(value): normal = Normal() assert (np.isclose(normal.cdf(value), scipy.stats.norm.cdf(value))).all() - - -@pytest.mark.parametrize("probability", [-1, 0, 1e-9, 0.15, 0.6626, 1-1e-9, 1, 2, np.array([-1, 0, 0.321123, 1, 5])]) -def test_normal_icdf(probability): - normal = Normal() - expected_value = np.atleast_1d(scipy.stats.norm.ppf(probability)) - assert np.allclose(normal.icdf(probability), expected_value, equal_nan=True) From fc0dac79379ec7be60577f168c2862c3c8ab01f9 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Tue, 5 Sep 2023 16:06:04 -0400 Subject: [PATCH 06/20] renamed variable input for consistency and to pass unit tests --- src/UQpy/distributions/collection/Uniform.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/UQpy/distributions/collection/Uniform.py b/src/UQpy/distributions/collection/Uniform.py index bc6464af5..d853fc39c 100644 --- a/src/UQpy/distributions/collection/Uniform.py +++ b/src/UQpy/distributions/collection/Uniform.py @@ -28,7 +28,7 @@ def __init__( def __probability_density_function(self, x: NumericArrayLike) -> np.ndarray: """Probability Density Function for the uniform distribution - :param x: + :param x: Points at which to evaluate the probability density function :return: pdf at all points in x """ x = np.atleast_1d(x) @@ -43,7 +43,7 @@ def __probability_density_function(self, x: NumericArrayLike) -> np.ndarray: def __cumulative_distribution_function(self, x: NumericArrayLike) -> np.ndarray: """Cumulative Distribution Function for the Uniform Distribution - :param x: + :param x: Points at which to evaluate the cumulative distribution function """ x = np.atleast_1d(x) loc = self.parameters['loc'] @@ -55,16 +55,16 @@ def __cumulative_distribution_function(self, x: NumericArrayLike) -> np.ndarray: cdf[upper_mask] = 1 return cdf - def __inverse_cumulative_distribution_function(self, y: NumericArrayLike) -> np.ndarray: + def __inverse_cumulative_distribution_function(self, x: NumericArrayLike) -> np.ndarray: """Inverse cumulative distribution function for uniform distribution - :param y: + :param x: Point at which to evaluate the inverse cumulative distribution function :return: """ - y = np.atleast_1d(y) + x = np.atleast_1d(x) loc = self.parameters['loc'] scale = self.parameters['scale'] - icdf = np.full(y.shape, np.nan) - mask = (0 <= y) & (y <= 1) - icdf[mask] = loc + (y[mask] * scale) + icdf = np.full(x.shape, np.nan) + mask = (0 <= x) & (x <= 1) + icdf[mask] = loc + (x[mask] * scale) return icdf From 8114bcd983affba84bbb98415d9513ff9603973e Mon Sep 17 00:00:00 2001 From: connor-krill Date: Tue, 12 Sep 2023 15:52:13 -0400 Subject: [PATCH 07/20] changed return behavior for floats and ints in custom pdf, cdf, icdf functions --- src/UQpy/distributions/collection/Normal.py | 16 +++++--- src/UQpy/distributions/collection/Uniform.py | 37 +++++++++++-------- .../test_distribution_methods.py | 8 ++-- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/UQpy/distributions/collection/Normal.py b/src/UQpy/distributions/collection/Normal.py index cf90c258c..bac014e40 100644 --- a/src/UQpy/distributions/collection/Normal.py +++ b/src/UQpy/distributions/collection/Normal.py @@ -30,11 +30,14 @@ def __probability_density_function(self, x: NumericArrayLike): :param x: :return: """ - x = np.atleast_1d(x) + x_array = np.atleast_1d(x) mean = self.parameters['loc'] standard_deviation = self.parameters['scale'] normalizing_constant = 1 / (standard_deviation * np.sqrt(2 * np.pi)) - return normalizing_constant * np.exp(-0.5 * ((x - mean) / standard_deviation)**2) + pdf = normalizing_constant * np.exp(-0.5 * ((x_array - mean) / standard_deviation)**2) + if isinstance(x, int) or isinstance(x, float): + return pdf[0] + return pdf @beartype def __cumulative_distribution_function(self, x: NumericArrayLike): @@ -43,8 +46,11 @@ def __cumulative_distribution_function(self, x: NumericArrayLike): :param x: :return: """ - x = np.atleast_1d(x) + x_array = np.atleast_1d(x) mean = self.parameters['loc'] standard_deviation = self.parameters['scale'] - erf_input = (x - mean) / (standard_deviation * np.sqrt(2.0)) - return (1.0 + erf(erf_input)) / 2.0 + erf_input = (x_array - mean) / (standard_deviation * np.sqrt(2.0)) + cdf = (1.0 + erf(erf_input)) / 2.0 + if isinstance(x, int) or isinstance(x, float): + return cdf[0] + return cdf diff --git a/src/UQpy/distributions/collection/Uniform.py b/src/UQpy/distributions/collection/Uniform.py index d853fc39c..94493e090 100644 --- a/src/UQpy/distributions/collection/Uniform.py +++ b/src/UQpy/distributions/collection/Uniform.py @@ -20,51 +20,58 @@ def __init__( """ super().__init__(loc=loc, scale=scale, ordered_parameters=("loc", "scale")) self._construct_from_scipy(scipy_name=stats.uniform) + self.pdf = self.__probability_density_function self.cdf = self.__cumulative_distribution_function self.icdf = self.__inverse_cumulative_distribution_function @beartype - def __probability_density_function(self, x: NumericArrayLike) -> np.ndarray: + def __probability_density_function(self, x: NumericArrayLike): """Probability Density Function for the uniform distribution :param x: Points at which to evaluate the probability density function :return: pdf at all points in x """ - x = np.atleast_1d(x) + x_array = np.atleast_1d(x) loc = self.parameters['loc'] scale = self.parameters['scale'] - mask = (loc <= x) & (x <= loc + scale) - pdf = np.zeros_like(x) + mask = (loc <= x_array) & (x_array <= loc + scale) + pdf = np.zeros_like(x_array) pdf[mask] = 1 / scale + if isinstance(x, int) or isinstance(x, float): + return pdf[0] return pdf @beartype - def __cumulative_distribution_function(self, x: NumericArrayLike) -> np.ndarray: + def __cumulative_distribution_function(self, x: NumericArrayLike): """Cumulative Distribution Function for the Uniform Distribution :param x: Points at which to evaluate the cumulative distribution function """ - x = np.atleast_1d(x) + x_array = np.atleast_1d(x) loc = self.parameters['loc'] scale = self.parameters['scale'] - cdf = np.zeros_like(x) - middle_mask = (loc < x) & (x < loc + scale) - upper_mask = loc + scale <= x - cdf[middle_mask] = (x[middle_mask] - loc) / scale + cdf = np.zeros_like(x_array) + middle_mask = (loc < x_array) & (x_array < loc + scale) + upper_mask = loc + scale <= x_array + cdf[middle_mask] = (x_array[middle_mask] - loc) / scale cdf[upper_mask] = 1 + if isinstance(x, int) or isinstance(x, float): + return cdf[0] return cdf - def __inverse_cumulative_distribution_function(self, x: NumericArrayLike) -> np.ndarray: + def __inverse_cumulative_distribution_function(self, x: NumericArrayLike): """Inverse cumulative distribution function for uniform distribution :param x: Point at which to evaluate the inverse cumulative distribution function :return: """ - x = np.atleast_1d(x) + x_array = np.atleast_1d(x) loc = self.parameters['loc'] scale = self.parameters['scale'] - icdf = np.full(x.shape, np.nan) - mask = (0 <= x) & (x <= 1) - icdf[mask] = loc + (x[mask] * scale) + icdf = np.full(x_array.shape, np.nan) + mask = (0 <= x_array) & (x_array <= 1) + icdf[mask] = loc + (x_array[mask] * scale) + if isinstance(x, int) or isinstance(x, float): + return icdf[0] return icdf diff --git a/tests/unit_tests/distributions/test_distribution_methods.py b/tests/unit_tests/distributions/test_distribution_methods.py index 8c798e5f2..42a405c84 100644 --- a/tests/unit_tests/distributions/test_distribution_methods.py +++ b/tests/unit_tests/distributions/test_distribution_methods.py @@ -99,7 +99,8 @@ def test_update_params_copula(): ]) def test_uniform_pdf(value, expected_probability): uniform = Uniform() - assert all(uniform.pdf(value) == expected_probability) + assert (np.isclose(uniform.pdf(value), expected_probability, equal_nan=True)).all() + # assert all(uniform.pdf(value) == expected_probability) @pytest.mark.parametrize("value,expected_probability", [ @@ -112,7 +113,8 @@ def test_uniform_pdf(value, expected_probability): ]) def test_uniform_cdf(value, expected_probability): uniform = Uniform() - assert all(uniform.cdf(value) == expected_probability) + assert (np.isclose(uniform.cdf(value), expected_probability, equal_nan=True)).all() + # assert all(uniform.cdf(value) == expected_probability) @pytest.mark.parametrize("probability,expected_value", [ @@ -127,7 +129,7 @@ def test_uniform_cdf(value, expected_probability): ]) def test_uniform_icdf(probability, expected_value): uniform = Uniform() - assert np.array_equal(uniform.icdf(probability), np.atleast_1d(expected_value), equal_nan=True) + assert (np.isclose(uniform.icdf(probability), expected_value, equal_nan=True)).all() @pytest.mark.parametrize("value", [-3, -2.5, -1, 0, 1, 2.415, 6.168, np.array([-8, -4.13, -1, 0, 2])]) From d65483bc502855b8ba7ced3be37e408e3279c5bb Mon Sep 17 00:00:00 2001 From: connor-krill Date: Wed, 22 May 2024 13:09:41 -0400 Subject: [PATCH 08/20] improved documentation, imports, and beartype --- src/UQpy/distributions/collection/Normal.py | 35 +++++++++-------- src/UQpy/distributions/collection/Uniform.py | 40 +++++++++++--------- 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/src/UQpy/distributions/collection/Normal.py b/src/UQpy/distributions/collection/Normal.py index bac014e40..815247128 100644 --- a/src/UQpy/distributions/collection/Normal.py +++ b/src/UQpy/distributions/collection/Normal.py @@ -2,14 +2,13 @@ import numpy as np import scipy.stats as stats from beartype import beartype -from scipy.special import erf, erfinv +from scipy.special import erf from UQpy.distributions.baseclass import DistributionContinuous1D from UQpy.utilities.ValidationTypes import NumericArrayLike +@beartype class Normal(DistributionContinuous1D): - - @beartype def __init__( self, loc: Union[None, float, int] = 0.0, scale: Union[None, float, int] = 1.0 ): @@ -23,32 +22,36 @@ def __init__( self.pdf = self.__probability_density_function self.cdf = self.__cumulative_distribution_function - @beartype - def __probability_density_function(self, x: NumericArrayLike): + def __probability_density_function( + self, x: NumericArrayLike + ) -> Union[float, np.ndarray]: """Probability density function for normal distribution - :param x: - :return: + :param x: Points to evaluate pdf at + :return: PDF of ``x`` as defined by :math:`f_x(x)` """ x_array = np.atleast_1d(x) - mean = self.parameters['loc'] - standard_deviation = self.parameters['scale'] + mean = self.parameters["loc"] + standard_deviation = self.parameters["scale"] normalizing_constant = 1 / (standard_deviation * np.sqrt(2 * np.pi)) - pdf = normalizing_constant * np.exp(-0.5 * ((x_array - mean) / standard_deviation)**2) + pdf = normalizing_constant * np.exp( + -0.5 * ((x_array - mean) / standard_deviation) ** 2 + ) if isinstance(x, int) or isinstance(x, float): return pdf[0] return pdf - @beartype - def __cumulative_distribution_function(self, x: NumericArrayLike): + def __cumulative_distribution_function( + self, x: NumericArrayLike + ) -> Union[float, np.ndarray]: """Cumulative distribution function for the normal distribution defined with the error function - :param x: - :return: + :param x: Points to evaluate cdf at + :return: CDF of ``x`` as defined by :math:`F_X(x)` """ x_array = np.atleast_1d(x) - mean = self.parameters['loc'] - standard_deviation = self.parameters['scale'] + mean = self.parameters["loc"] + standard_deviation = self.parameters["scale"] erf_input = (x_array - mean) / (standard_deviation * np.sqrt(2.0)) cdf = (1.0 + erf(erf_input)) / 2.0 if isinstance(x, int) or isinstance(x, float): diff --git a/src/UQpy/distributions/collection/Uniform.py b/src/UQpy/distributions/collection/Uniform.py index 94493e090..d8216118d 100644 --- a/src/UQpy/distributions/collection/Uniform.py +++ b/src/UQpy/distributions/collection/Uniform.py @@ -1,15 +1,13 @@ -from typing import Union - import numpy as np import scipy.stats as stats from beartype import beartype - +from typing import Union from UQpy.distributions.baseclass import DistributionContinuous1D from UQpy.utilities.ValidationTypes import NumericArrayLike +from line_profiler_pycharm import profile - +@beartype class Uniform(DistributionContinuous1D): - @beartype def __init__( self, loc: Union[None, float, int] = 0.0, scale: Union[None, float, int] = 1.0 ): @@ -25,16 +23,18 @@ def __init__( self.cdf = self.__cumulative_distribution_function self.icdf = self.__inverse_cumulative_distribution_function - @beartype - def __probability_density_function(self, x: NumericArrayLike): + @profile + def __probability_density_function( + self, x: NumericArrayLike + ) -> Union[float, np.ndarray]: """Probability Density Function for the uniform distribution :param x: Points at which to evaluate the probability density function :return: pdf at all points in x """ x_array = np.atleast_1d(x) - loc = self.parameters['loc'] - scale = self.parameters['scale'] + loc = self.parameters["loc"] + scale = self.parameters["scale"] mask = (loc <= x_array) & (x_array <= loc + scale) pdf = np.zeros_like(x_array) pdf[mask] = 1 / scale @@ -42,15 +42,18 @@ def __probability_density_function(self, x: NumericArrayLike): return pdf[0] return pdf - @beartype - def __cumulative_distribution_function(self, x: NumericArrayLike): + @profile + def __cumulative_distribution_function( + self, x: NumericArrayLike + ) -> Union[float, np.ndarray]: """Cumulative Distribution Function for the Uniform Distribution :param x: Points at which to evaluate the cumulative distribution function + :return: cdf of ``x`` as defined by :math:`F_X(x)` """ x_array = np.atleast_1d(x) - loc = self.parameters['loc'] - scale = self.parameters['scale'] + loc = self.parameters["loc"] + scale = self.parameters["scale"] cdf = np.zeros_like(x_array) middle_mask = (loc < x_array) & (x_array < loc + scale) upper_mask = loc + scale <= x_array @@ -60,15 +63,18 @@ def __cumulative_distribution_function(self, x: NumericArrayLike): return cdf[0] return cdf - def __inverse_cumulative_distribution_function(self, x: NumericArrayLike): + @profile + def __inverse_cumulative_distribution_function( + self, x: NumericArrayLike + ) -> Union[float, np.ndarray]: """Inverse cumulative distribution function for uniform distribution :param x: Point at which to evaluate the inverse cumulative distribution function - :return: + :return: inverse cdf of ``x`` as defined by :math:`F^{-1}_X(x)` """ x_array = np.atleast_1d(x) - loc = self.parameters['loc'] - scale = self.parameters['scale'] + loc = self.parameters["loc"] + scale = self.parameters["scale"] icdf = np.full(x_array.shape, np.nan) mask = (0 <= x_array) & (x_array <= 1) icdf[mask] = loc + (x_array[mask] * scale) From 7c3bdc6f910753808df5e6f390b2c94ebee07401 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Wed, 22 May 2024 13:10:02 -0400 Subject: [PATCH 09/20] moved tests for custom uniform and normal pdf, cdf, icdf to test_normal.py and test_uniform.py --- .../test_distribution_methods.py | 96 ++++--------------- tests/unit_tests/distributions/test_normal.py | 58 +++++++++++ .../unit_tests/distributions/test_uniform.py | 59 ++++++++++++ 3 files changed, 134 insertions(+), 79 deletions(-) create mode 100644 tests/unit_tests/distributions/test_normal.py create mode 100644 tests/unit_tests/distributions/test_uniform.py diff --git a/tests/unit_tests/distributions/test_distribution_methods.py b/tests/unit_tests/distributions/test_distribution_methods.py index 42a405c84..34dee4d24 100644 --- a/tests/unit_tests/distributions/test_distribution_methods.py +++ b/tests/unit_tests/distributions/test_distribution_methods.py @@ -1,21 +1,19 @@ -from UQpy.distributions import * import numpy as np -import pytest -import scipy +from UQpy.distributions import * # Test all functions for one type of continuous distribution: uniform -dist_continuous = Uniform(loc=1., scale=2.) +dist_continuous = Uniform(loc=1.0, scale=2.0) def test_get_params(): - assert dist_continuous.get_parameters()['loc'] == 1. + assert dist_continuous.get_parameters()["loc"] == 1.0 def test_update_params(): - dist = Uniform(loc=1., scale=2.) - dist.update_parameters(loc=2.) - assert dist.get_parameters()['loc'] == 2. + dist = Uniform(loc=1.0, scale=2.0) + dist.update_parameters(loc=2.0) + assert dist.get_parameters()["loc"] == 2.0 def test_continuous_pdf(): @@ -41,11 +39,11 @@ def test_continuous_rvs(): def test_continuous_fit(): dict_fit = Uniform(loc=None, scale=None).fit(data=[1.5, 2.5, 3.5]) - assert dict_fit == {'loc': 1.5, 'scale': 2.0} + assert dict_fit == {"loc": 1.5, "scale": 2.0} def test_continuous_moments(): - assert dist_continuous.moments(moments2return='m') == 2. + assert dist_continuous.moments(moments2return="m") == 2.0 # Test all functions for one type of discrete distribution: binomial @@ -53,92 +51,32 @@ def test_continuous_moments(): def test_discrete_pmf(): - assert np.round(dist_discrete.pmf(x=2.), 3) == 0.205 + assert np.round(dist_discrete.pmf(x=2.0), 3) == 0.205 def test_discrete_cdf(): - assert np.round(dist_discrete.cdf(x=2.), 3) == 0.942 + assert np.round(dist_discrete.cdf(x=2.0), 3) == 0.942 def test_discrete_log_pmf(): - assert np.round(dist_discrete.log_pmf(x=2.), 3) == -1.586 + assert np.round(dist_discrete.log_pmf(x=2.0), 3) == -1.586 def test_discrete_icdf(): - assert dist_discrete.icdf(0.9) == 2. + assert dist_discrete.icdf(0.9) == 2.0 def test_discrete_rvs(): samples = dist_discrete.rvs(nsamples=2, random_state=123) - assert np.all(np.round(samples, 3) == np.array([1., 0.]).reshape((2, 1))) + assert np.all(np.round(samples, 3) == np.array([1.0, 0.0]).reshape((2, 1))) def test_discrete_moments(): - assert dist_discrete.moments(moments2return='m') == 1. + assert dist_discrete.moments(moments2return="m") == 1.0 # Test functions for Copula - - def test_update_params_copula(): - copula = Gumbel(theta=2.) - copula.update_parameters(theta=1.) - assert copula.get_parameters()['theta'] == 1. - - -# Test function for custom implementation of Uniform PDF and CDF -@pytest.mark.parametrize("value,expected_probability", [ - (-4, 0), - (0, 1), - (0.5, 1), - (1, 1), - (12, 0), - ([-1, 0, 0.5, 1, 2], [0, 1, 1, 1, 0]), - ((-1, 0, 0.5, 1, 2), (0, 1, 1, 1, 0)), - (np.array([-1, 0, 0.5, 1, 2]), np.array([0, 1, 1, 1, 0])) -]) -def test_uniform_pdf(value, expected_probability): - uniform = Uniform() - assert (np.isclose(uniform.pdf(value), expected_probability, equal_nan=True)).all() - # assert all(uniform.pdf(value) == expected_probability) - - -@pytest.mark.parametrize("value,expected_probability", [ - (-0.1245, 0), - (0.4, 0.4), - (3.14159, 1), - ([-4, 0.2, 0.75, 1, 6], [0, 0.2, 0.75, 1, 1]), - ((-4, 0.2, 0.75, 1, 6), (0, 0.2, 0.75, 1, 1)), - (np.array([-4, 0.2, 0.75, 1, 6]), np.array([0, 0.2, 0.75, 1, 1])), -]) -def test_uniform_cdf(value, expected_probability): - uniform = Uniform() - assert (np.isclose(uniform.cdf(value), expected_probability, equal_nan=True)).all() - # assert all(uniform.cdf(value) == expected_probability) - - -@pytest.mark.parametrize("probability,expected_value", [ - (-1, np.nan), - (0, 0), - (0.123, 0.123), - (1, 1), - (1.2, np.nan), - ((-0.5, 0, 0.5, 1, 1.5), (np.nan, 0, 0.5, 1, np.nan)), - ([-0.5, 0, 0.5, 1, 1.5], [np.nan, 0, 0.5, 1, np.nan]), - (np.array([-0.5, 0, 0.5, 1, 1.5]), np.array([np.nan, 0, 0.5, 1, np.nan])) -]) -def test_uniform_icdf(probability, expected_value): - uniform = Uniform() - assert (np.isclose(uniform.icdf(probability), expected_value, equal_nan=True)).all() - - -@pytest.mark.parametrize("value", [-3, -2.5, -1, 0, 1, 2.415, 6.168, np.array([-8, -4.13, -1, 0, 2])]) -def test_normal_pdf(value): - normal = Normal() - assert (np.isclose(normal.pdf(value), scipy.stats.norm.pdf(value))).all() - - -@pytest.mark.parametrize("value", [-4, -2.7, -0.4, 0, 0.4321, 2, 8, np.array([-3, -2, 0, 1, 4])]) -def test_normal_cdf(value): - normal = Normal() - assert (np.isclose(normal.cdf(value), scipy.stats.norm.cdf(value))).all() + copula = Gumbel(theta=2.0) + copula.update_parameters(theta=1.0) + assert copula.get_parameters()["theta"] == 1.0 diff --git a/tests/unit_tests/distributions/test_normal.py b/tests/unit_tests/distributions/test_normal.py new file mode 100644 index 000000000..400bdf58d --- /dev/null +++ b/tests/unit_tests/distributions/test_normal.py @@ -0,0 +1,58 @@ +import numpy as np +from scipy import stats +from UQpy.distributions import Normal +import hypothesis.strategies as st +from hypothesis import given +from hypothesis.extra.numpy import array_shapes + +normal = Normal() +scipy_normal = stats.norm() + + +@given(st.floats(allow_nan=False)) +def test_normal_pdf_float(x): + """Test custom implementation of normal pdf on float inputs. Should return flosa""" + pdf = normal.pdf(x) + scipy_pdf = scipy_normal.pdf(x) + assert isinstance(pdf, float) + assert np.allclose(pdf, scipy_pdf, equal_nan=True) + + +@given(array_shapes(min_dims=1, min_side=1)) +def test_normal_pdf_array(x): + pdf = normal.pdf(x) + scipy_pdf = scipy_normal.pdf(x) + assert isinstance(pdf, np.ndarray) + assert np.allclose(pdf, scipy_pdf, equal_nan=True) + + +@given(st.floats(allow_nan=False)) +def test_normal_cdf_float(x): + pdf = normal.pdf(x) + scipy_pdf = scipy_normal.pdf(x) + assert isinstance(pdf, float) + assert np.allclose(pdf, scipy_pdf, equal_nan=True) + + +@given(array_shapes(min_dims=1, min_side=1)) +def test_normal_cdf_array(x): + pdf = normal.pdf(x) + scipy_pdf = scipy_normal.pdf(x) + assert isinstance(pdf, np.ndarray) + assert np.allclose(pdf, scipy_pdf, equal_nan=True) + + +# @given(st.floats(allow_nan=False)) +# def test_normal_icdf_float(y): +# icdf = normal.icdf(y) +# scipy_icdf = scipy_normal.ppf(y) +# assert isinstance(icdf, float) +# assert np.allclose(icdf, scipy_icdf, equal_nan=True) + + +# @given(array_shapes(min_dims=1, min_side=1)) +# def test_normal_icdf_array(y): +# icdf = normal.icdf(y) +# scipy_icdf = scipy_normal.ppf(y) +# assert isinstance(icdf, np.ndarray) +# assert np.allclose(icdf, scipy_icdf, equal_nan=True) diff --git a/tests/unit_tests/distributions/test_uniform.py b/tests/unit_tests/distributions/test_uniform.py new file mode 100644 index 000000000..6446e7cb2 --- /dev/null +++ b/tests/unit_tests/distributions/test_uniform.py @@ -0,0 +1,59 @@ +# import pytest +import numpy as np +from scipy import stats +from UQpy.distributions import Uniform +import hypothesis.strategies as st +from hypothesis import given +from hypothesis.extra.numpy import array_shapes + +uniform = Uniform() +scipy_uniform = stats.uniform() + + +@given(st.floats(allow_nan=False)) +def test_uniform_pdf_float(x): + """Test custom implementation of uniform pdf on float inputs. Should return flosa""" + pdf = uniform.pdf(x) + scipy_pdf = scipy_uniform.pdf(x) + assert isinstance(pdf, float) + assert np.allclose(pdf, scipy_pdf, equal_nan=True) + + +@given(array_shapes(min_dims=1, min_side=1)) +def test_uniform_pdf_array(x): + pdf = uniform.pdf(x) + scipy_pdf = scipy_uniform.pdf(x) + assert isinstance(pdf, np.ndarray) + assert np.allclose(pdf, scipy_pdf, equal_nan=True) + + +@given(st.floats(allow_nan=False)) +def test_uniform_cdf_float(x): + pdf = uniform.pdf(x) + scipy_pdf = scipy_uniform.pdf(x) + assert isinstance(pdf, float) + assert np.allclose(pdf, scipy_pdf, equal_nan=True) + + +@given(array_shapes(min_dims=1, min_side=1)) +def test_uniform_cdf_array(x): + pdf = uniform.pdf(x) + scipy_pdf = scipy_uniform.pdf(x) + assert isinstance(pdf, np.ndarray) + assert np.allclose(pdf, scipy_pdf, equal_nan=True) + + +@given(st.floats(allow_nan=False)) +def test_uniform_icdf_float(y): + icdf = uniform.icdf(y) + scipy_icdf = scipy_uniform.ppf(y) + assert isinstance(icdf, float) + assert np.allclose(icdf, scipy_icdf, equal_nan=True) + + +@given(array_shapes(min_dims=1, min_side=1)) +def test_uniform_icdf_array(y): + icdf = uniform.icdf(y) + scipy_icdf = scipy_uniform.ppf(y) + assert isinstance(icdf, np.ndarray) + assert np.allclose(icdf, scipy_icdf, equal_nan=True) From c5f178323bea552b3663a88feeae2175caa10255 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Wed, 22 May 2024 13:10:27 -0400 Subject: [PATCH 10/20] cleaned up formatting --- src/UQpy/distributions/collection/Uniform.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/UQpy/distributions/collection/Uniform.py b/src/UQpy/distributions/collection/Uniform.py index d8216118d..4c8c9b86c 100644 --- a/src/UQpy/distributions/collection/Uniform.py +++ b/src/UQpy/distributions/collection/Uniform.py @@ -6,6 +6,7 @@ from UQpy.utilities.ValidationTypes import NumericArrayLike from line_profiler_pycharm import profile + @beartype class Uniform(DistributionContinuous1D): def __init__( From 26ff00e8b85edf059e750b78dc4c8fe397d2f211 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Tue, 28 May 2024 12:00:00 -0400 Subject: [PATCH 11/20] added custom inverse function --- src/UQpy/distributions/collection/Normal.py | 20 ++++++++- tests/unit_tests/distributions/test_normal.py | 42 ++++++++++++------- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/UQpy/distributions/collection/Normal.py b/src/UQpy/distributions/collection/Normal.py index 815247128..c9500add5 100644 --- a/src/UQpy/distributions/collection/Normal.py +++ b/src/UQpy/distributions/collection/Normal.py @@ -2,7 +2,7 @@ import numpy as np import scipy.stats as stats from beartype import beartype -from scipy.special import erf +from scipy.special import erf, erfinv from UQpy.distributions.baseclass import DistributionContinuous1D from UQpy.utilities.ValidationTypes import NumericArrayLike @@ -21,6 +21,7 @@ def __init__( self._construct_from_scipy(scipy_name=stats.norm) self.pdf = self.__probability_density_function self.cdf = self.__cumulative_distribution_function + self.icdf = self.__inverse_cumulative_distribution_function def __probability_density_function( self, x: NumericArrayLike @@ -57,3 +58,20 @@ def __cumulative_distribution_function( if isinstance(x, int) or isinstance(x, float): return cdf[0] return cdf + + def __inverse_cumulative_distribution_function( + self, y: NumericArrayLike + ) -> Union[float, np.ndarray]: + """Compute the inverse CDF for the normal distribution with the inverse error function + + :param y: + :return: + """ + y_array = np.atleast_1d(y) + mean = self.parameters["loc"] + standard_deviation = self.parameters["scale"] + normalized_icdf = erfinv((2 * y_array) - 1) + icdf = (normalized_icdf * standard_deviation * np.sqrt(2.0)) + mean + if isinstance(y, int) or isinstance(y, float): + return icdf[0] + return icdf diff --git a/tests/unit_tests/distributions/test_normal.py b/tests/unit_tests/distributions/test_normal.py index 400bdf58d..cdad46bf7 100644 --- a/tests/unit_tests/distributions/test_normal.py +++ b/tests/unit_tests/distributions/test_normal.py @@ -9,7 +9,7 @@ scipy_normal = stats.norm() -@given(st.floats(allow_nan=False)) +@given(st.floats(allow_nan=True)) def test_normal_pdf_float(x): """Test custom implementation of normal pdf on float inputs. Should return flosa""" pdf = normal.pdf(x) @@ -19,14 +19,15 @@ def test_normal_pdf_float(x): @given(array_shapes(min_dims=1, min_side=1)) -def test_normal_pdf_array(x): +def test_normal_pdf_array(size): + x = np.random.normal(0, 1, size=size) pdf = normal.pdf(x) scipy_pdf = scipy_normal.pdf(x) assert isinstance(pdf, np.ndarray) assert np.allclose(pdf, scipy_pdf, equal_nan=True) -@given(st.floats(allow_nan=False)) +@given(st.floats(allow_nan=True)) def test_normal_cdf_float(x): pdf = normal.pdf(x) scipy_pdf = scipy_normal.pdf(x) @@ -35,24 +36,33 @@ def test_normal_cdf_float(x): @given(array_shapes(min_dims=1, min_side=1)) -def test_normal_cdf_array(x): +def test_normal_cdf_array(size): + x = np.random.normal(0, 1, size=size) pdf = normal.pdf(x) scipy_pdf = scipy_normal.pdf(x) assert isinstance(pdf, np.ndarray) assert np.allclose(pdf, scipy_pdf, equal_nan=True) -# @given(st.floats(allow_nan=False)) -# def test_normal_icdf_float(y): -# icdf = normal.icdf(y) -# scipy_icdf = scipy_normal.ppf(y) -# assert isinstance(icdf, float) -# assert np.allclose(icdf, scipy_icdf, equal_nan=True) +@given(st.floats(min_value=1e-13)) +def test_normal_icdf_float(y): + """Note: icdf deviates from scipy ppf for values of y between 0 and 1e-13""" + icdf = normal.icdf(y) + scipy_icdf = scipy_normal.ppf(y) + assert isinstance(icdf, float) + assert np.allclose(icdf, scipy_icdf, equal_nan=True) -# @given(array_shapes(min_dims=1, min_side=1)) -# def test_normal_icdf_array(y): -# icdf = normal.icdf(y) -# scipy_icdf = scipy_normal.ppf(y) -# assert isinstance(icdf, np.ndarray) -# assert np.allclose(icdf, scipy_icdf, equal_nan=True) +@given(array_shapes(min_dims=1, min_side=1)) +def test_normal_icdf_array(size): + y = np.random.normal(0, 1, size=size) + icdf = normal.icdf(y) + scipy_icdf = scipy_normal.ppf(y) + assert isinstance(icdf, np.ndarray) + assert np.allclose(icdf, scipy_icdf, equal_nan=True) + + +@given(st.floats(allow_nan=False, allow_infinity=False)) +def test_normal_cdf_icdf(x): # FixMe: restrict these values to += 5 standard deviations. + y = normal.icdf(normal.cdf(x)) + assert np.allclose(x, y) From 298ccb6e6f9306c60227167c19ab1e2500193497 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Tue, 28 May 2024 12:00:13 -0400 Subject: [PATCH 12/20] added int as allowed output type --- src/UQpy/distributions/collection/Uniform.py | 25 +++++++++++-------- .../unit_tests/distributions/test_uniform.py | 23 +++++++++++------ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/UQpy/distributions/collection/Uniform.py b/src/UQpy/distributions/collection/Uniform.py index 4c8c9b86c..3fad7bbdb 100644 --- a/src/UQpy/distributions/collection/Uniform.py +++ b/src/UQpy/distributions/collection/Uniform.py @@ -27,7 +27,7 @@ def __init__( @profile def __probability_density_function( self, x: NumericArrayLike - ) -> Union[float, np.ndarray]: + ) -> Union[int, float, np.ndarray]: """Probability Density Function for the uniform distribution :param x: Points at which to evaluate the probability density function @@ -36,17 +36,20 @@ def __probability_density_function( x_array = np.atleast_1d(x) loc = self.parameters["loc"] scale = self.parameters["scale"] + mask_zero_density = (x_array < loc) | (loc + scale < x_array) mask = (loc <= x_array) & (x_array <= loc + scale) - pdf = np.zeros_like(x_array) + pdf = np.full_like(x_array, np.nan) + pdf[mask_zero_density] = 0.0 pdf[mask] = 1 / scale + if isinstance(x, int) or isinstance(x, float): - return pdf[0] + return float(pdf[0]) return pdf @profile def __cumulative_distribution_function( self, x: NumericArrayLike - ) -> Union[float, np.ndarray]: + ) -> Union[int, float, np.ndarray]: """Cumulative Distribution Function for the Uniform Distribution :param x: Points at which to evaluate the cumulative distribution function @@ -55,19 +58,21 @@ def __cumulative_distribution_function( x_array = np.atleast_1d(x) loc = self.parameters["loc"] scale = self.parameters["scale"] - cdf = np.zeros_like(x_array) + cdf = np.full_like(x_array, np.nan) + lower_mask = x_array <= loc middle_mask = (loc < x_array) & (x_array < loc + scale) upper_mask = loc + scale <= x_array + cdf[lower_mask] = 0.0 cdf[middle_mask] = (x_array[middle_mask] - loc) / scale - cdf[upper_mask] = 1 + cdf[upper_mask] = 1.0 if isinstance(x, int) or isinstance(x, float): - return cdf[0] + return float(cdf[0]) return cdf @profile def __inverse_cumulative_distribution_function( self, x: NumericArrayLike - ) -> Union[float, np.ndarray]: + ) -> Union[int, float, np.ndarray]: """Inverse cumulative distribution function for uniform distribution :param x: Point at which to evaluate the inverse cumulative distribution function @@ -76,9 +81,9 @@ def __inverse_cumulative_distribution_function( x_array = np.atleast_1d(x) loc = self.parameters["loc"] scale = self.parameters["scale"] - icdf = np.full(x_array.shape, np.nan) + icdf = np.full_like(x_array, np.nan) mask = (0 <= x_array) & (x_array <= 1) icdf[mask] = loc + (x_array[mask] * scale) if isinstance(x, int) or isinstance(x, float): - return icdf[0] + return float(icdf[0]) return icdf diff --git a/tests/unit_tests/distributions/test_uniform.py b/tests/unit_tests/distributions/test_uniform.py index 6446e7cb2..dac2954cb 100644 --- a/tests/unit_tests/distributions/test_uniform.py +++ b/tests/unit_tests/distributions/test_uniform.py @@ -10,9 +10,9 @@ scipy_uniform = stats.uniform() -@given(st.floats(allow_nan=False)) +@given(st.floats(allow_nan=True)) def test_uniform_pdf_float(x): - """Test custom implementation of uniform pdf on float inputs. Should return flosa""" + """Test custom implementation of uniform pdf on float inputs""" pdf = uniform.pdf(x) scipy_pdf = scipy_uniform.pdf(x) assert isinstance(pdf, float) @@ -20,14 +20,15 @@ def test_uniform_pdf_float(x): @given(array_shapes(min_dims=1, min_side=1)) -def test_uniform_pdf_array(x): +def test_uniform_pdf_array(size): + x = np.random.normal(0, 1, size=size) pdf = uniform.pdf(x) scipy_pdf = scipy_uniform.pdf(x) assert isinstance(pdf, np.ndarray) assert np.allclose(pdf, scipy_pdf, equal_nan=True) -@given(st.floats(allow_nan=False)) +@given(st.floats(allow_nan=True)) def test_uniform_cdf_float(x): pdf = uniform.pdf(x) scipy_pdf = scipy_uniform.pdf(x) @@ -36,14 +37,15 @@ def test_uniform_cdf_float(x): @given(array_shapes(min_dims=1, min_side=1)) -def test_uniform_cdf_array(x): +def test_uniform_cdf_array(size): + x = np.random.normal(0, 1, size=size) pdf = uniform.pdf(x) scipy_pdf = scipy_uniform.pdf(x) assert isinstance(pdf, np.ndarray) assert np.allclose(pdf, scipy_pdf, equal_nan=True) -@given(st.floats(allow_nan=False)) +@given(st.floats(allow_nan=True)) def test_uniform_icdf_float(y): icdf = uniform.icdf(y) scipy_icdf = scipy_uniform.ppf(y) @@ -52,8 +54,15 @@ def test_uniform_icdf_float(y): @given(array_shapes(min_dims=1, min_side=1)) -def test_uniform_icdf_array(y): +def test_uniform_icdf_array(size): + y = np.random.normal(0, 1, size=size) icdf = uniform.icdf(y) scipy_icdf = scipy_uniform.ppf(y) assert isinstance(icdf, np.ndarray) assert np.allclose(icdf, scipy_icdf, equal_nan=True) + + +@given(st.floats(allow_nan=True)) # FixMe: restrict these values between 0 and +def test_uniform_icdf_cdf(x): + y = uniform.icdf(uniform.cdf(x)) + assert np.allclose(x, y, equal_nan=True) From d10e00f2ae5c79cb0c4188ff6a087ceaad9af78a Mon Sep 17 00:00:00 2001 From: connor-krill Date: Tue, 28 May 2024 13:01:12 -0400 Subject: [PATCH 13/20] improved tests --- tests/unit_tests/distributions/test_normal.py | 8 ++++++-- tests/unit_tests/distributions/test_uniform.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/distributions/test_normal.py b/tests/unit_tests/distributions/test_normal.py index cdad46bf7..2664c85e9 100644 --- a/tests/unit_tests/distributions/test_normal.py +++ b/tests/unit_tests/distributions/test_normal.py @@ -62,7 +62,11 @@ def test_normal_icdf_array(size): assert np.allclose(icdf, scipy_icdf, equal_nan=True) -@given(st.floats(allow_nan=False, allow_infinity=False)) -def test_normal_cdf_icdf(x): # FixMe: restrict these values to += 5 standard deviations. +@given(st.floats(-7, 7, allow_nan=False, allow_infinity=False)) +def test_normal_cdf_icdf(x): + """Reconstruct x as x = icdf(cdf(x)) + Note: For +/- 7 standard deviations, UQpy and SciPy accurately reconstruct x. + At 8 standard deviations, both UQpy and scipy.stats.norm() begin to divergence from the correct answer + """ y = normal.icdf(normal.cdf(x)) assert np.allclose(x, y) diff --git a/tests/unit_tests/distributions/test_uniform.py b/tests/unit_tests/distributions/test_uniform.py index dac2954cb..9c9c3a950 100644 --- a/tests/unit_tests/distributions/test_uniform.py +++ b/tests/unit_tests/distributions/test_uniform.py @@ -62,7 +62,8 @@ def test_uniform_icdf_array(size): assert np.allclose(icdf, scipy_icdf, equal_nan=True) -@given(st.floats(allow_nan=True)) # FixMe: restrict these values between 0 and +@given(st.floats(0, 1)) def test_uniform_icdf_cdf(x): + """Reconstruct x as x = icdf(cdf(x))""" y = uniform.icdf(uniform.cdf(x)) assert np.allclose(x, y, equal_nan=True) From fbd3aa7358b2580f26172f9f5372b0c890c77636 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Tue, 15 Oct 2024 15:07:17 -0400 Subject: [PATCH 14/20] added tests for not-a-number and infinity --- tests/unit_tests/distributions/test_normal.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/unit_tests/distributions/test_normal.py b/tests/unit_tests/distributions/test_normal.py index 2664c85e9..d8100621e 100644 --- a/tests/unit_tests/distributions/test_normal.py +++ b/tests/unit_tests/distributions/test_normal.py @@ -1,3 +1,4 @@ +import pytest import numpy as np from scipy import stats from UQpy.distributions import Normal @@ -9,6 +10,17 @@ scipy_normal = stats.norm() +def test_normal_pdf_nan(): + """Consistent with scipy, pdf(NaN)=NaN""" + assert np.isnan(normal.pdf(np.nan)) + + +@pytest.mark.parametrize("test_input,expected", [(np.inf, 0.0), (-np.inf, 0.0)]) +def test_normal_pdf_infinity(test_input, expected): + """Consistent with scipy pdf(inf)=0.0, pdf(-inf)=0.0""" + assert normal.pdf(test_input) == expected + + @given(st.floats(allow_nan=True)) def test_normal_pdf_float(x): """Test custom implementation of normal pdf on float inputs. Should return flosa""" @@ -27,6 +39,17 @@ def test_normal_pdf_array(size): assert np.allclose(pdf, scipy_pdf, equal_nan=True) +def test_normal_cdf_nan(): + """Consistent with scipy, cdf(NaN)=NaN""" + assert np.isnan(normal.cdf(np.nan)) + + +@pytest.mark.parametrize("test_input,expected", [(np.inf, 1.0), (-np.inf, 0.0)]) +def test_normal_cdf_infinity(test_input, expected): + """Consistent with scipy cdf(inf)=1.0, cdf(-inf)=0.0""" + assert normal.cdf(test_input) == expected + + @given(st.floats(allow_nan=True)) def test_normal_cdf_float(x): pdf = normal.pdf(x) @@ -44,6 +67,18 @@ def test_normal_cdf_array(size): assert np.allclose(pdf, scipy_pdf, equal_nan=True) +@pytest.mark.parametrize("test_input", [np.nan, np.inf, -np.inf]) +def test_normal_icdf_nan_infinity(test_input): + """Consistent with SciPy, the icdf of NaN, inf, and -inf are all NaN""" + assert np.isnan(normal.icdf(test_input)) + + +def test_normal_icdf_zero_one(): + """Consistent with SciPy, icdf(0)=-inf and icdf(1)=inf""" + assert normal.icdf(0) == -np.inf + assert normal.icdf(1) == np.inf + + @given(st.floats(min_value=1e-13)) def test_normal_icdf_float(y): """Note: icdf deviates from scipy ppf for values of y between 0 and 1e-13""" From 290a3cf85e35766d1132367027c3d29311693929 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Wed, 5 Mar 2025 14:05:40 -0500 Subject: [PATCH 15/20] added __repr__ method --- src/UQpy/distributions/collection/Beta.py | 9 ++++- src/UQpy/distributions/collection/Binomial.py | 6 +++ src/UQpy/distributions/collection/Cauchy.py | 9 +++++ .../distributions/collection/ChiSquare.py | 8 ++++ .../distributions/collection/Exponential.py | 9 +++++ src/UQpy/distributions/collection/Gamma.py | 8 ++++ .../collection/GeneralizedExtreme.py | 8 ++++ .../collection/InverseGaussian.py | 8 ++++ .../distributions/collection/JointCopula.py | 3 ++ .../collection/JointIndependent.py | 39 ++++++++++++++----- src/UQpy/distributions/collection/Laplace.py | 9 +++++ src/UQpy/distributions/collection/Levy.py | 9 +++++ src/UQpy/distributions/collection/Logistic.py | 9 +++++ .../distributions/collection/Lognormal.py | 8 ++++ src/UQpy/distributions/collection/Maxwell.py | 9 +++++ .../distributions/collection/Multinomial.py | 3 ++ .../collection/MultivariateNormal.py | 23 +++++++++-- src/UQpy/distributions/collection/Normal.py | 14 ++++--- src/UQpy/distributions/collection/Pareto.py | 8 ++++ src/UQpy/distributions/collection/Poisson.py | 6 +++ src/UQpy/distributions/collection/Rayleigh.py | 9 +++++ .../collection/TruncatedNormal.py | 8 ++++ src/UQpy/distributions/collection/Uniform.py | 19 ++++----- src/UQpy/distributions/copulas/Clayton.py | 3 ++ src/UQpy/distributions/copulas/Frank.py | 9 ++++- src/UQpy/distributions/copulas/Gumbel.py | 3 ++ 26 files changed, 223 insertions(+), 33 deletions(-) diff --git a/src/UQpy/distributions/collection/Beta.py b/src/UQpy/distributions/collection/Beta.py index f2b5804c0..92f4d0a70 100644 --- a/src/UQpy/distributions/collection/Beta.py +++ b/src/UQpy/distributions/collection/Beta.py @@ -1,5 +1,4 @@ from typing import Union - import scipy.stats as stats from UQpy.distributions.baseclass import DistributionContinuous1D from beartype import beartype @@ -30,3 +29,11 @@ def __init__( ordered_parameters=("a", "b", "loc", "scale"), ) self._construct_from_scipy(scipy_name=stats.beta) + + def __repr__(self): + s = "{a}, {b}" + if self.parameters["loc"] != 0.0: + s += ", loc={loc}" + if self.parameters["scale"] != 1.0: + s += ", scale={scale}" + return "Beta(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Binomial.py b/src/UQpy/distributions/collection/Binomial.py index af402bcaa..5a4350ecc 100644 --- a/src/UQpy/distributions/collection/Binomial.py +++ b/src/UQpy/distributions/collection/Binomial.py @@ -22,3 +22,9 @@ def __init__( """ super().__init__(n=n, p=p, loc=loc, ordered_parameters=("n", "p", "loc")) self._construct_from_scipy(scipy_name=stats.binom) + + def __repr__(self): + s = "{n}, {p}" + if self.parameters["loc"] != 0.0: + s += ", loc={loc}" + return "Binomial(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Cauchy.py b/src/UQpy/distributions/collection/Cauchy.py index 51ce9b24d..ccc974fe2 100644 --- a/src/UQpy/distributions/collection/Cauchy.py +++ b/src/UQpy/distributions/collection/Cauchy.py @@ -19,3 +19,12 @@ def __init__( """ super().__init__(loc=loc, scale=scale, ordered_parameters=("loc", "scale")) self._construct_from_scipy(scipy_name=stats.cauchy) + + def __repr__(self): + s = [] + if self.parameters["loc"] != 0.0: + s.append("loc={loc}") + if self.parameters["scale"] != 1.0: + s.append("scale={scale}") + s = ", ".join(s) + return "Cauchy(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/ChiSquare.py b/src/UQpy/distributions/collection/ChiSquare.py index dea8bea9d..ccb42eb84 100644 --- a/src/UQpy/distributions/collection/ChiSquare.py +++ b/src/UQpy/distributions/collection/ChiSquare.py @@ -24,3 +24,11 @@ def __init__( df=df, loc=loc, scale=scale, ordered_parameters=("df", "loc", "scale") ) self._construct_from_scipy(scipy_name=stats.chi2) + + def __repr__(self): + s = "{df}" + if self.parameters["loc"] != 0.0: + s += ", loc={loc}" + if self.parameters["scale"] != 1.0: + s += ", scale={scale}" + return "ChiSquare(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Exponential.py b/src/UQpy/distributions/collection/Exponential.py index 7376bfba9..1083d046f 100644 --- a/src/UQpy/distributions/collection/Exponential.py +++ b/src/UQpy/distributions/collection/Exponential.py @@ -19,3 +19,12 @@ def __init__( """ super().__init__(loc=loc, scale=scale, ordered_parameters=("loc", "scale")) self._construct_from_scipy(scipy_name=stats.expon) + + def __repr__(self): + s = [] + if self.parameters["loc"] != 0.0: + s.append("loc={loc}") + if self.parameters["scale"] != 1.0: + s.append("scale={scale}") + s = ", ".join(s) + return "Exponential(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Gamma.py b/src/UQpy/distributions/collection/Gamma.py index 0e5db0744..837c9852f 100644 --- a/src/UQpy/distributions/collection/Gamma.py +++ b/src/UQpy/distributions/collection/Gamma.py @@ -25,3 +25,11 @@ def __init__( a=a, loc=loc, scale=scale, ordered_parameters=("a", "loc", "scale") ) self._construct_from_scipy(scipy_name=stats.gamma) + + def __repr__(self): + s = "{a}" + if self.parameters["loc"] != 0.0: + s += ", loc={loc}" + if self.parameters["scale"] != 1.0: + s += ", scale={scale}" + return "Gamma(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/GeneralizedExtreme.py b/src/UQpy/distributions/collection/GeneralizedExtreme.py index 0982d8e76..2f73de944 100644 --- a/src/UQpy/distributions/collection/GeneralizedExtreme.py +++ b/src/UQpy/distributions/collection/GeneralizedExtreme.py @@ -25,3 +25,11 @@ def __init__( c=c, loc=loc, scale=scale, ordered_parameters=("c", "loc", "scale") ) self._construct_from_scipy(scipy_name=stats.genextreme) + + def __repr__(self): + s = "{c}" + if self.parameters["loc"] != 0.0: + s += ", loc={loc}" + if self.parameters["scale"] != 1.0: + s += ", scale={scale}" + return "GeneralizedExtreme(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/InverseGaussian.py b/src/UQpy/distributions/collection/InverseGaussian.py index d95315c17..4ead87a0d 100644 --- a/src/UQpy/distributions/collection/InverseGaussian.py +++ b/src/UQpy/distributions/collection/InverseGaussian.py @@ -25,3 +25,11 @@ def __init__( mu=mu, loc=loc, scale=scale, ordered_parameters=("mu", "loc", "scale") ) self._construct_from_scipy(scipy_name=stats.invgauss) + + def __repr__(self): + s = "{mu}" + if self.parameters["loc"] != 0.0: + s += ", loc={loc}" + if self.parameters["scale"] != 1.0: + s += ", scale={scale}" + return "InverseGauss(" + s.format(**self.parameters) + ")" \ No newline at end of file diff --git a/src/UQpy/distributions/collection/JointCopula.py b/src/UQpy/distributions/collection/JointCopula.py index ce4cf04d0..0140bdfd8 100644 --- a/src/UQpy/distributions/collection/JointCopula.py +++ b/src/UQpy/distributions/collection/JointCopula.py @@ -165,3 +165,6 @@ def update_parameters(self, **kwargs: dict): self.copula.parameters[key] = value else: self.marginals[int(index)].parameters[key] = value + + def __repr__(self): + return f"JointCopula({self.marginals}, {self.copula})" diff --git a/src/UQpy/distributions/collection/JointIndependent.py b/src/UQpy/distributions/collection/JointIndependent.py index 8614ec737..ab8e9be7c 100644 --- a/src/UQpy/distributions/collection/JointIndependent.py +++ b/src/UQpy/distributions/collection/JointIndependent.py @@ -14,8 +14,8 @@ class JointIndependent(DistributionND): @beartype def __init__( - self, - marginals: Union[list[DistributionContinuous1D], list[DistributionDiscrete1D]], + self, + marginals: Union[list[DistributionContinuous1D], list[DistributionDiscrete1D]], ): """ :param marginals: list of distribution objects that define the marginals. @@ -24,12 +24,20 @@ def __init__( self.ordered_parameters = [] for i, m in enumerate(marginals): self.ordered_parameters.extend( - [key + "_" + str(i) for key in m.ordered_parameters]) + [key + "_" + str(i) for key in m.ordered_parameters] + ) # Check and save the marginals - if not (isinstance(marginals, list) - and all(isinstance(d, (DistributionContinuous1D, DistributionDiscrete1D)) for d in marginals)): - raise ValueError("Input marginals must be a list of Distribution1d objects.") + if not ( + isinstance(marginals, list) + and all( + isinstance(d, (DistributionContinuous1D, DistributionDiscrete1D)) + for d in marginals + ) + ): + raise ValueError( + "Input marginals must be a list of Distribution1d objects." + ) self.marginals = marginals # If all marginals have a method, the joint has it to @@ -70,6 +78,7 @@ def joint_log_pdf(dist, x): self.log_pmf = MethodType(joint_log_pdf, self) if all(hasattr(m, "cdf") for m in self.marginals): + def joint_cdf(dist, x): x = dist.check_x_dimension(x) # Compute cdf of independent marginals @@ -107,8 +116,8 @@ def joint_fit(dist, data): mle_all = {} for ind_m, marg in enumerate(dist.marginals): if any( - param_value is None - for param_value in marg.get_parameters().values() + param_value is None + for param_value in marg.get_parameters().values() ): mle_i = marg.fit(data[:, ind_m]) else: @@ -125,8 +134,15 @@ def joint_fit(dist, data): def joint_moments(dist, moments2return="mvsk"): # Go through all marginals if len(moments2return) == 1: - return np.array([marg.moments(moments2return=moments2return) for marg in dist.marginals]) - moments_ = [np.empty((len(dist.marginals),)) for _ in range(len(moments2return))] + return np.array( + [ + marg.moments(moments2return=moments2return) + for marg in dist.marginals + ] + ) + moments_ = [ + np.empty((len(dist.marginals),)) for _ in range(len(moments2return)) + ] for ind_m, marg in enumerate(dist.marginals): moments_i = marg.moments(moments2return=moments2return) for j in range(len(moments2return)): @@ -174,3 +190,6 @@ def update_parameters(self, **kwargs: dict): key_split = key_indexed.split("_") key, index = "_".join(key_split[:-1]), int(key_split[-1]) self.marginals[index].parameters[key] = value + + def __repr__(self): + return f"JointIndependent({self.marginals})" diff --git a/src/UQpy/distributions/collection/Laplace.py b/src/UQpy/distributions/collection/Laplace.py index 6c1905cea..0de2d5d1f 100644 --- a/src/UQpy/distributions/collection/Laplace.py +++ b/src/UQpy/distributions/collection/Laplace.py @@ -19,3 +19,12 @@ def __init__( """ super().__init__(loc=loc, scale=scale, ordered_parameters=("loc", "scale")) self._construct_from_scipy(scipy_name=stats.laplace) + + def __repr__(self): + s = [] + if self.parameters["loc"] != 0.0: + s.append("loc={loc}") + if self.parameters["scale"] != 1.0: + s.append("scale={scale}") + s = ", ".join(s) + return "Laplace(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Levy.py b/src/UQpy/distributions/collection/Levy.py index b90c4ce10..d55e86951 100644 --- a/src/UQpy/distributions/collection/Levy.py +++ b/src/UQpy/distributions/collection/Levy.py @@ -19,3 +19,12 @@ def __init__( """ super().__init__(loc=loc, scale=scale, ordered_parameters=("loc", "scale")) self._construct_from_scipy(scipy_name=stats.levy) + + def __repr__(self): + s = [] + if self.parameters["loc"] != 0.0: + s.append("loc={loc}") + if self.parameters["scale"] != 1.0: + s.append("scale={scale}") + s = ", ".join(s) + return "Levy(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Logistic.py b/src/UQpy/distributions/collection/Logistic.py index af3bf69c5..8d4b02cc9 100644 --- a/src/UQpy/distributions/collection/Logistic.py +++ b/src/UQpy/distributions/collection/Logistic.py @@ -19,3 +19,12 @@ def __init__( """ super().__init__(loc=loc, scale=scale, ordered_parameters=("loc", "scale")) self._construct_from_scipy(scipy_name=stats.logistic) + + def __repr__(self): + s = [] + if self.parameters["loc"] != 0.0: + s.append("loc={loc}") + if self.parameters["scale"] != 1.0: + s.append("scale={scale}") + s = ", ".join(s) + return "Logistic(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Lognormal.py b/src/UQpy/distributions/collection/Lognormal.py index 98b72746a..11cc82b54 100644 --- a/src/UQpy/distributions/collection/Lognormal.py +++ b/src/UQpy/distributions/collection/Lognormal.py @@ -25,3 +25,11 @@ def __init__( s=s, loc=loc, scale=scale, ordered_parameters=("s", "loc", "scale") ) self._construct_from_scipy(scipy_name=stats.lognorm) + + def __repr__(self): + s = "{s}" + if self.parameters["loc"] != 0.0: + s += ", loc={loc}" + if self.parameters["scale"] != 1.0: + s += ", scale={scale}" + return "Lognormal(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Maxwell.py b/src/UQpy/distributions/collection/Maxwell.py index 444ea4ebd..5b179600b 100644 --- a/src/UQpy/distributions/collection/Maxwell.py +++ b/src/UQpy/distributions/collection/Maxwell.py @@ -19,3 +19,12 @@ def __init__( """ super().__init__(loc=loc, scale=scale, ordered_parameters=("loc", "scale")) self._construct_from_scipy(scipy_name=stats.maxwell) + + def __repr__(self): + s = [] + if self.parameters["loc"] != 0.0: + s.append("loc={loc}") + if self.parameters["scale"] != 1.0: + s.append("scale={scale}") + s = ", ".join(s) + return "Maxwell(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Multinomial.py b/src/UQpy/distributions/collection/Multinomial.py index 77cd43a50..6f4ebc309 100644 --- a/src/UQpy/distributions/collection/Multinomial.py +++ b/src/UQpy/distributions/collection/Multinomial.py @@ -58,3 +58,6 @@ def moments(self, moments2return="mv"): return mean, cov else: raise ValueError('UQpy: moments2return must be "m", "v" or "mv".') + + def __repr__(self): + return "Multinomial({n}, {p})".format(**self.parameters) diff --git a/src/UQpy/distributions/collection/MultivariateNormal.py b/src/UQpy/distributions/collection/MultivariateNormal.py index f6fe1d4b1..efcc3ab33 100644 --- a/src/UQpy/distributions/collection/MultivariateNormal.py +++ b/src/UQpy/distributions/collection/MultivariateNormal.py @@ -25,8 +25,13 @@ def __init__( if isinstance(cov, (int, float)): pass else: - if not (len(np.array(cov).shape) in [1, 2] and all(sh == len(mean) for sh in np.array(cov).shape)): - raise ValueError("Input covariance must be a float or ndarray of appropriate dimensions.") + if not ( + len(np.array(cov).shape) in [1, 2] + and all(sh == len(mean) for sh in np.array(cov).shape) + ): + raise ValueError( + "Input covariance must be a float or ndarray of appropriate dimensions." + ) super().__init__(mean=mean, cov=cov, ordered_parameters=["mean", "cov"]) def cdf(self, x): @@ -44,8 +49,9 @@ def log_pdf(self, x): def rvs(self, nsamples=1, random_state=None): if not (isinstance(nsamples, int) and nsamples >= 1): raise ValueError("Input nsamples must be an integer > 0.") - return stats.multivariate_normal.rvs(size=nsamples, random_state=random_state, **self.parameters - ).reshape((nsamples, -1)) + return stats.multivariate_normal.rvs( + size=nsamples, random_state=random_state, **self.parameters + ).reshape((nsamples, -1)) def fit(self, data): data = self.check_x_dimension(data) @@ -65,3 +71,12 @@ def moments(self, moments2return="mv"): return self.get_parameters()["mean"], self.get_parameters()["cov"] else: raise ValueError('UQpy: moments2return must be "m", "v" or "mv".') + + def __repr__(self): + s = "{mean}" + is_float_or_int = isinstance(self.parameters["cov"], int | float) + if is_float_or_int: + if self.parameters["cov"] == 1.0: + return "MultivariateNormal(" + s.format(**self.parameters) + ")" + s += ", cov={cov}" + return "MultivariateNormal(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Normal.py b/src/UQpy/distributions/collection/Normal.py index c9500add5..553961285 100644 --- a/src/UQpy/distributions/collection/Normal.py +++ b/src/UQpy/distributions/collection/Normal.py @@ -38,8 +38,6 @@ def __probability_density_function( pdf = normalizing_constant * np.exp( -0.5 * ((x_array - mean) / standard_deviation) ** 2 ) - if isinstance(x, int) or isinstance(x, float): - return pdf[0] return pdf def __cumulative_distribution_function( @@ -55,8 +53,6 @@ def __cumulative_distribution_function( standard_deviation = self.parameters["scale"] erf_input = (x_array - mean) / (standard_deviation * np.sqrt(2.0)) cdf = (1.0 + erf(erf_input)) / 2.0 - if isinstance(x, int) or isinstance(x, float): - return cdf[0] return cdf def __inverse_cumulative_distribution_function( @@ -72,6 +68,12 @@ def __inverse_cumulative_distribution_function( standard_deviation = self.parameters["scale"] normalized_icdf = erfinv((2 * y_array) - 1) icdf = (normalized_icdf * standard_deviation * np.sqrt(2.0)) + mean - if isinstance(y, int) or isinstance(y, float): - return icdf[0] return icdf + + def __repr__(self): + s = [] + if self.parameters["loc"] != 0.0: + s.append(f"loc={self.parameters['loc']}") + if self.parameters["scale"] != 1.0: + s.append(f"scale={self.parameters['scale']}") + return "Normal(" + ", ".join(s) + ")" diff --git a/src/UQpy/distributions/collection/Pareto.py b/src/UQpy/distributions/collection/Pareto.py index efd6a4f44..28f9494e8 100644 --- a/src/UQpy/distributions/collection/Pareto.py +++ b/src/UQpy/distributions/collection/Pareto.py @@ -24,3 +24,11 @@ def __init__( b=b, loc=loc, scale=scale, ordered_parameters=("b", "loc", "scale") ) self._construct_from_scipy(scipy_name=stats.pareto) + + def __repr__(self): + s = "{b}" + if self.parameters["loc"] != 0.0: + s += ", loc={loc}" + if self.parameters["scale"] != 1.0: + s += ", scale={scale}" + return "Pareto(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Poisson.py b/src/UQpy/distributions/collection/Poisson.py index 68027cfb8..b0f4163b9 100644 --- a/src/UQpy/distributions/collection/Poisson.py +++ b/src/UQpy/distributions/collection/Poisson.py @@ -17,3 +17,9 @@ def __init__(self, mu: Union[None, float, int], loc: Union[None, float, int] = 0 """ super().__init__(mu=mu, loc=loc, ordered_parameters=("mu", "loc")) self._construct_from_scipy(scipy_name=stats.poisson) + + def __repr__(self): + s = "{mu}" + if self.parameters["loc"] != 0.0: + s += ", loc={loc}" + return "Poisson(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Rayleigh.py b/src/UQpy/distributions/collection/Rayleigh.py index c66b7d255..42ea3453c 100644 --- a/src/UQpy/distributions/collection/Rayleigh.py +++ b/src/UQpy/distributions/collection/Rayleigh.py @@ -18,3 +18,12 @@ def __init__( """ super().__init__(loc=loc, scale=scale, ordered_parameters=("loc", "scale")) self._construct_from_scipy(scipy_name=stats.rayleigh) + + def __repr__(self): + s = [] + if self.parameters["loc"] != 0.0: + s.append("loc={loc}") + if self.parameters["scale"] != 1.0: + s.append("scale={scale}") + s = ", ".join(s) + return "Rayleigh(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/TruncatedNormal.py b/src/UQpy/distributions/collection/TruncatedNormal.py index c862ec469..724d34853 100644 --- a/src/UQpy/distributions/collection/TruncatedNormal.py +++ b/src/UQpy/distributions/collection/TruncatedNormal.py @@ -31,3 +31,11 @@ def __init__( ordered_parameters=("a", "b", "loc", "scale"), ) self._construct_from_scipy(scipy_name=stats.truncnorm) + + def __repr__(self): + s = "{a}, {b}" + if self.parameters["loc"] != 0.0: + s += ", loc={loc}" + if self.parameters["scale"] != 1.0: + s += ", scale={scale}" + return "TruncatedNormal(" + s.format(**self.parameters) + ")" diff --git a/src/UQpy/distributions/collection/Uniform.py b/src/UQpy/distributions/collection/Uniform.py index 3fad7bbdb..fcfbd816e 100644 --- a/src/UQpy/distributions/collection/Uniform.py +++ b/src/UQpy/distributions/collection/Uniform.py @@ -4,7 +4,6 @@ from typing import Union from UQpy.distributions.baseclass import DistributionContinuous1D from UQpy.utilities.ValidationTypes import NumericArrayLike -from line_profiler_pycharm import profile @beartype @@ -24,7 +23,6 @@ def __init__( self.cdf = self.__cumulative_distribution_function self.icdf = self.__inverse_cumulative_distribution_function - @profile def __probability_density_function( self, x: NumericArrayLike ) -> Union[int, float, np.ndarray]: @@ -41,12 +39,8 @@ def __probability_density_function( pdf = np.full_like(x_array, np.nan) pdf[mask_zero_density] = 0.0 pdf[mask] = 1 / scale - - if isinstance(x, int) or isinstance(x, float): - return float(pdf[0]) return pdf - @profile def __cumulative_distribution_function( self, x: NumericArrayLike ) -> Union[int, float, np.ndarray]: @@ -65,11 +59,8 @@ def __cumulative_distribution_function( cdf[lower_mask] = 0.0 cdf[middle_mask] = (x_array[middle_mask] - loc) / scale cdf[upper_mask] = 1.0 - if isinstance(x, int) or isinstance(x, float): - return float(cdf[0]) return cdf - @profile def __inverse_cumulative_distribution_function( self, x: NumericArrayLike ) -> Union[int, float, np.ndarray]: @@ -84,6 +75,12 @@ def __inverse_cumulative_distribution_function( icdf = np.full_like(x_array, np.nan) mask = (0 <= x_array) & (x_array <= 1) icdf[mask] = loc + (x_array[mask] * scale) - if isinstance(x, int) or isinstance(x, float): - return float(icdf[0]) return icdf + + def __repr__(self): + s = [] + if self.parameters["loc"] != 0.0: + s.append(f"loc={self.parameters['loc']}") + if self.parameters["scale"] != 1.0: + s.append(f"scale={self.parameters['scale']}") + return "Uniform(" + ", ".join(s) + ")" diff --git a/src/UQpy/distributions/copulas/Clayton.py b/src/UQpy/distributions/copulas/Clayton.py index 4cee7922a..640f56238 100644 --- a/src/UQpy/distributions/copulas/Clayton.py +++ b/src/UQpy/distributions/copulas/Clayton.py @@ -41,3 +41,6 @@ def extract_data(self, unit_uniform_samples: Numpy2DFloatArray): v = unit_uniform_samples[:, 1] theta = self.parameters["theta"] return theta, u, v + + def __repr__(self): + return f"Clayton({self.parameters['theta']})" diff --git a/src/UQpy/distributions/copulas/Frank.py b/src/UQpy/distributions/copulas/Frank.py index f633d1e7c..f617cd306 100644 --- a/src/UQpy/distributions/copulas/Frank.py +++ b/src/UQpy/distributions/copulas/Frank.py @@ -32,7 +32,11 @@ def evaluate_cdf(self, unit_uniform_samples: Numpy2DFloatArray) -> numpy.ndarray :return: Values of the cdf. """ theta, u, v = self.extract_data(unit_uniform_samples) - tmp_ratio = ((np.exp(-theta * u) - 1.0) * (np.exp(-theta * v) - 1.0) / (np.exp(-theta) - 1.0)) + tmp_ratio = ( + (np.exp(-theta * u) - 1.0) + * (np.exp(-theta * v) - 1.0) + / (np.exp(-theta) - 1.0) + ) cdf_val = -1.0 / theta * np.log(1.0 + tmp_ratio) return cdf_val @@ -41,3 +45,6 @@ def extract_data(self, unit_uniform_samples: Numpy2DFloatArray): v = unit_uniform_samples[:, 1] theta = self.parameters["theta"] return theta, u, v + + def __repr__(self): + return f"Frank({self.parameters['theta']})" diff --git a/src/UQpy/distributions/copulas/Gumbel.py b/src/UQpy/distributions/copulas/Gumbel.py index d84c1634e..3174714f2 100644 --- a/src/UQpy/distributions/copulas/Gumbel.py +++ b/src/UQpy/distributions/copulas/Gumbel.py @@ -73,3 +73,6 @@ def extract_data(self, unit_uniform_samples): v = unit_uniform_samples[:, 1] theta = self.parameters["theta"] return theta, u, v + + def __repr__(self): + return f"Gumbel({self.parameters['theta']})" From bdb68d36c6e239e0edee6037bf286117450786af Mon Sep 17 00:00:00 2001 From: connor-krill Date: Wed, 5 Mar 2025 14:05:56 -0500 Subject: [PATCH 16/20] added tests for __repr__ method --- .../distributions/test_copula_repr.py | 17 +++ .../distributions/test_distribution_repr.py | 129 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 tests/unit_tests/distributions/test_copula_repr.py create mode 100644 tests/unit_tests/distributions/test_distribution_repr.py diff --git a/tests/unit_tests/distributions/test_copula_repr.py b/tests/unit_tests/distributions/test_copula_repr.py new file mode 100644 index 000000000..3f6e436bf --- /dev/null +++ b/tests/unit_tests/distributions/test_copula_repr.py @@ -0,0 +1,17 @@ +import numpy as np +from UQpy.distributions import copulas + + +def test_clayton_repr(): + clayton = copulas.Clayton(1.0) + assert clayton.__repr__() == "Clayton(1.0)" + + +def test_frank_repr(): + frank = copulas.Frank(2.0) + assert frank.__repr__() == "Frank(2.0)" + + +def test_gumbel_repr(): + gumbel = copulas.Gumbel(np.inf) + assert gumbel.__repr__() == "Gumbel(inf)" diff --git a/tests/unit_tests/distributions/test_distribution_repr.py b/tests/unit_tests/distributions/test_distribution_repr.py new file mode 100644 index 000000000..1fcf20207 --- /dev/null +++ b/tests/unit_tests/distributions/test_distribution_repr.py @@ -0,0 +1,129 @@ +from UQpy import distributions + + +def test_beta_repr(): + beta = distributions.Beta(1, 2, 3, 4) + assert beta.__repr__() == "Beta(1, 2, loc=3, scale=4)" + + +def test_binomial_repr(): + binomial = distributions.Binomial(1, 2, 3) + assert binomial.__repr__() == "Binomial(1, 2, loc=3)" + + +def test_cauchy_repr(): + cauchy = distributions.Cauchy(1, 2) + assert cauchy.__repr__() == "Cauchy(loc=1, scale=2)" + + +def test_chi_square_repr(): + chi_square = distributions.ChiSquare(1, 2, 3) + assert chi_square.__repr__() == "ChiSquare(1, loc=2, scale=3)" + + +def test_exponential_repr(): + exponential = distributions.Exponential(1, 2) + assert exponential.__repr__() == "Exponential(loc=1, scale=2)" + + +def test_gamma_repr(): + gamma = distributions.Gamma(1, 2, 3) + assert gamma.__repr__() == "Gamma(1, loc=2, scale=3)" + + +def test_generalized_extreme_repr(): + generalized_extreme = distributions.GeneralizedExtreme(1, 2, 3) + assert generalized_extreme.__repr__() == "GeneralizedExtreme(1, loc=2, scale=3)" + + +def test_inverse_gaussian_repr(): + inverse_gauss = distributions.InverseGauss(1, 2, 3) + assert inverse_gauss.__repr__() == "InverseGauss(1, loc=2, scale=3)" + + +def test_joint_copula_repr(): + joint_copula = distributions.JointCopula( + [distributions.Normal(loc=2.0), distributions.Uniform()], + copula=distributions.copulas.Frank(1.0), + ) + assert ( + joint_copula.__repr__() + == "JointCopula([Normal(loc=2.0), Uniform()], Frank(1.0))" + ) + + +def test_joint_independent_repr(): + joint_independent = distributions.JointIndependent( + [distributions.Beta(1, 2), distributions.Cauchy()] + ) + assert joint_independent.__repr__() == "JointIndependent([Beta(1, 2), Cauchy()])" + + +def test_laplace_repr(): + laplace = distributions.Laplace(1, 2) + assert laplace.__repr__() == "Laplace(loc=1, scale=2)" + + +def test_levy_repr(): + levy = distributions.Levy(1, 2) + assert levy.__repr__() == "Levy(loc=1, scale=2)" + + +def test_logistic_repr(): + logistic = distributions.Logistic(1, 2) + assert logistic.__repr__() == "Logistic(loc=1, scale=2)" + + +def test_lognormal_repr(): + lognormal = distributions.Lognormal(1, 2, 3) + assert lognormal.__repr__() == "Lognormal(1, loc=2, scale=3)" + + +def test_maxwell_repr(): + maxwell = distributions.Maxwell(1, 2) + assert maxwell.__repr__() == "Maxwell(loc=1, scale=2)" + + +def test_multinomial_repr(): + multinomial = distributions.Multinomial(1, [2.0, 3.0]) + assert multinomial.__repr__() == "Multinomial(1, [2.0, 3.0])" + + +def test_multivariate_normal_repr(): + multivariate_normal = distributions.MultivariateNormal( + [1, 2], [[1.0, 0.4], [0.4, 1.0]] + ) + assert ( + multivariate_normal.__repr__() + == "MultivariateNormal([1, 2], cov=[[1.0, 0.4], [0.4, 1.0]])" + ) + + +def test_normal_repr(): + normal = distributions.Normal(1, 2) + assert normal.__repr__() == "Normal(loc=1, scale=2)" + + +def test_pareto_repr(): + pareto = distributions.Pareto(1, 2, 3) + assert pareto.__repr__() == "Pareto(1, loc=2, scale=3)" + + +def test_poisson_repr(): + poisson = distributions.Poisson(1, 2) + assert poisson.__repr__() == "Poisson(1, loc=2)" + + +def test_rayleigh_repr(): + rayleigh = distributions.Rayleigh(1, 2) + assert rayleigh.__repr__() == "Rayleigh(loc=1, scale=2)" + + +def test_truncated_normal_repr(): + truncated_normal = distributions.TruncatedNormal(1, 2, 3, 4) + assert truncated_normal.__repr__() == "TruncatedNormal(1, 2, loc=3, scale=4)" + + +def test_uniform_repr(): + uniform = distributions.Uniform(1, 2) + assert uniform.__repr__() == "Uniform(loc=1, scale=2)" From 525849753ec75430740f5912b92fac9c40833c04 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Wed, 5 Mar 2025 14:06:08 -0500 Subject: [PATCH 17/20] added tests for custom pdf, cdf, icdf methods --- tests/unit_tests/distributions/test_normal.py | 172 ++++++++---------- .../unit_tests/distributions/test_uniform.py | 109 ++++++----- 2 files changed, 133 insertions(+), 148 deletions(-) diff --git a/tests/unit_tests/distributions/test_normal.py b/tests/unit_tests/distributions/test_normal.py index d8100621e..2c96d662b 100644 --- a/tests/unit_tests/distributions/test_normal.py +++ b/tests/unit_tests/distributions/test_normal.py @@ -2,106 +2,80 @@ import numpy as np from scipy import stats from UQpy.distributions import Normal -import hypothesis.strategies as st from hypothesis import given from hypothesis.extra.numpy import array_shapes -normal = Normal() -scipy_normal = stats.norm() - -def test_normal_pdf_nan(): - """Consistent with scipy, pdf(NaN)=NaN""" - assert np.isnan(normal.pdf(np.nan)) - - -@pytest.mark.parametrize("test_input,expected", [(np.inf, 0.0), (-np.inf, 0.0)]) -def test_normal_pdf_infinity(test_input, expected): - """Consistent with scipy pdf(inf)=0.0, pdf(-inf)=0.0""" - assert normal.pdf(test_input) == expected - - -@given(st.floats(allow_nan=True)) -def test_normal_pdf_float(x): - """Test custom implementation of normal pdf on float inputs. Should return flosa""" - pdf = normal.pdf(x) - scipy_pdf = scipy_normal.pdf(x) - assert isinstance(pdf, float) - assert np.allclose(pdf, scipy_pdf, equal_nan=True) - - -@given(array_shapes(min_dims=1, min_side=1)) -def test_normal_pdf_array(size): - x = np.random.normal(0, 1, size=size) - pdf = normal.pdf(x) - scipy_pdf = scipy_normal.pdf(x) - assert isinstance(pdf, np.ndarray) - assert np.allclose(pdf, scipy_pdf, equal_nan=True) - - -def test_normal_cdf_nan(): - """Consistent with scipy, cdf(NaN)=NaN""" - assert np.isnan(normal.cdf(np.nan)) - - -@pytest.mark.parametrize("test_input,expected", [(np.inf, 1.0), (-np.inf, 0.0)]) -def test_normal_cdf_infinity(test_input, expected): - """Consistent with scipy cdf(inf)=1.0, cdf(-inf)=0.0""" - assert normal.cdf(test_input) == expected - - -@given(st.floats(allow_nan=True)) -def test_normal_cdf_float(x): - pdf = normal.pdf(x) - scipy_pdf = scipy_normal.pdf(x) - assert isinstance(pdf, float) - assert np.allclose(pdf, scipy_pdf, equal_nan=True) - - -@given(array_shapes(min_dims=1, min_side=1)) -def test_normal_cdf_array(size): - x = np.random.normal(0, 1, size=size) - pdf = normal.pdf(x) - scipy_pdf = scipy_normal.pdf(x) - assert isinstance(pdf, np.ndarray) - assert np.allclose(pdf, scipy_pdf, equal_nan=True) - - -@pytest.mark.parametrize("test_input", [np.nan, np.inf, -np.inf]) -def test_normal_icdf_nan_infinity(test_input): - """Consistent with SciPy, the icdf of NaN, inf, and -inf are all NaN""" - assert np.isnan(normal.icdf(test_input)) - - -def test_normal_icdf_zero_one(): - """Consistent with SciPy, icdf(0)=-inf and icdf(1)=inf""" - assert normal.icdf(0) == -np.inf - assert normal.icdf(1) == np.inf - - -@given(st.floats(min_value=1e-13)) -def test_normal_icdf_float(y): - """Note: icdf deviates from scipy ppf for values of y between 0 and 1e-13""" - icdf = normal.icdf(y) - scipy_icdf = scipy_normal.ppf(y) - assert isinstance(icdf, float) - assert np.allclose(icdf, scipy_icdf, equal_nan=True) - - -@given(array_shapes(min_dims=1, min_side=1)) -def test_normal_icdf_array(size): - y = np.random.normal(0, 1, size=size) - icdf = normal.icdf(y) - scipy_icdf = scipy_normal.ppf(y) - assert isinstance(icdf, np.ndarray) - assert np.allclose(icdf, scipy_icdf, equal_nan=True) - - -@given(st.floats(-7, 7, allow_nan=False, allow_infinity=False)) -def test_normal_cdf_icdf(x): - """Reconstruct x as x = icdf(cdf(x)) - Note: For +/- 7 standard deviations, UQpy and SciPy accurately reconstruct x. - At 8 standard deviations, both UQpy and scipy.stats.norm() begin to divergence from the correct answer - """ - y = normal.icdf(normal.cdf(x)) - assert np.allclose(x, y) +class TestNormal: + + normal = Normal() + scipy_normal = stats.norm() + + def test_pdf_nan(self): + """Consistent with scipy, pdf(NaN)=NaN""" + assert np.isnan(self.normal.pdf(np.nan)) + + def test_pdf_infinity(self): + """Consistent with scipy pdf(inf)=0.0, pdf(-inf)=0.0""" + assert np.isclose(self.normal.pdf(np.inf), 0.0) + assert np.isclose(self.normal.pdf(-np.inf), 0.0) + + @given(array_shapes(min_dims=1, min_side=1)) + def test_pdf_shape(self, shape): + """Test the output array matches the shape of the input array""" + x = np.zeros(shape) + assert x.shape == self.normal.pdf(x).shape + + def test_pdf_values(self): + """Test if UQpy pdf matches SciPy pdf for x in [-10, 10]""" + x = np.linspace(-10, 10, num=1_000) + assert np.allclose(self.normal.pdf(x), self.scipy_normal.pdf(x)) + + def test_cdf_nan(self): + """Consistent with scipy, cdf(NaN)=NaN""" + assert np.isnan(self.normal.cdf(np.nan)) + + @pytest.mark.parametrize("test_input,expected", [(np.inf, 1.0), (-np.inf, 0.0)]) + def test_cdf_infinity(self, test_input, expected): + """Consistent with scipy cdf(inf)=1.0, cdf(-inf)=0.0""" + assert self.normal.cdf(test_input) == expected + + @given(array_shapes(min_dims=1, min_side=1)) + def test_cdf_shape(self, shape): + """Test if output array matches the shape of the input array""" + x = np.zeros(shape) + assert x.shape == self.normal.cdf(x).shape + + def test_cdf_values(self): + """Test if UQpy cdf matches scipy cdf for x in [-10, 10]""" + x = np.linspace(-10, 10, num=100) + assert np.allclose(self.normal.cdf(x), self.scipy_normal.cdf(x)) + + def test_icdf_nan(self): + """Consistent with scipy, icdf(NaN) = NaN""" + assert np.isnan(self.normal.icdf(np.nan)) + + def test_icdf_infinity(self): + """Consistent with scipy icdf(inf)=Nan and icdf(-inf)=NaN""" + assert np.isnan(self.normal.icdf(np.inf)) + assert np.isnan(self.normal.icdf(-np.inf)) + + def test_icdf_zero_one(self): + """Consistent with scipy, icdf(0)=-inf and icdf(1)=inf""" + assert self.normal.icdf(0) == -np.inf + assert self.normal.icdf(1) == np.inf + + def test_icdf_values(self): + """Test if UQpy icdf matches scipy ppf for y in [-0.1, 1.1]. Note outside [0, 1] both functions return NaN""" + y = np.linspace(-0.1, 1.1, num=100) + assert np.allclose(self.normal.icdf(y), self.scipy_normal.ppf(y), equal_nan=True) + + def test_cdf_icdf_reconstruction(self): + """Reconstruct x as x = icdf(cdf(x)) + Note: For +/- 7 standard deviations, UQpy and SciPy accurately reconstruct x. + At 8 standard deviations, both UQpy and scipy.stats.norm() begin to divergence from the correct answer + """ + x = np.linspace(-7, 7, num=100) + y = self.normal.cdf(x) + x_reconstruction = self.normal.icdf(y) + assert np.allclose(x, x_reconstruction) diff --git a/tests/unit_tests/distributions/test_uniform.py b/tests/unit_tests/distributions/test_uniform.py index 9c9c3a950..3113bc184 100644 --- a/tests/unit_tests/distributions/test_uniform.py +++ b/tests/unit_tests/distributions/test_uniform.py @@ -1,69 +1,80 @@ -# import pytest +import pytest import numpy as np from scipy import stats from UQpy.distributions import Uniform -import hypothesis.strategies as st from hypothesis import given from hypothesis.extra.numpy import array_shapes -uniform = Uniform() -scipy_uniform = stats.uniform() +class TestUniform: -@given(st.floats(allow_nan=True)) -def test_uniform_pdf_float(x): - """Test custom implementation of uniform pdf on float inputs""" - pdf = uniform.pdf(x) - scipy_pdf = scipy_uniform.pdf(x) - assert isinstance(pdf, float) - assert np.allclose(pdf, scipy_pdf, equal_nan=True) + uniform = Uniform() + scipy_uniform = stats.uniform() + def test_pdf_nan(self): + """Consistent with scipy, pdf(NaN)=NaN""" + assert np.isnan(self.uniform.pdf(np.nan)) -@given(array_shapes(min_dims=1, min_side=1)) -def test_uniform_pdf_array(size): - x = np.random.normal(0, 1, size=size) - pdf = uniform.pdf(x) - scipy_pdf = scipy_uniform.pdf(x) - assert isinstance(pdf, np.ndarray) - assert np.allclose(pdf, scipy_pdf, equal_nan=True) + def test_pdf_infinity(self): + """Consistent with scipy, pdf(inf)=0 and pdf(-inf)=0""" + assert np.isclose(self.uniform.pdf(np.inf), 0.0) + assert np.isclose(self.uniform.pdf(-np.inf), 0.0) + @given(array_shapes(min_dims=1, min_side=1)) + def test_pdf_shape(self, shape): + """Test if the output array matches the shape of the input array""" + x = np.zeros(shape) + assert x.shape == self.uniform.pdf(x).shape -@given(st.floats(allow_nan=True)) -def test_uniform_cdf_float(x): - pdf = uniform.pdf(x) - scipy_pdf = scipy_uniform.pdf(x) - assert isinstance(pdf, float) - assert np.allclose(pdf, scipy_pdf, equal_nan=True) + def test_pdf_values(self): + """Test if UQpy pdf matches scipy pdf for x in [-1, 2]""" + x = np.linspace(-1, 2, num=100) + assert np.allclose(self.uniform.pdf(x), self.scipy_uniform.pdf(x)) + def test_cdf_nan(self): + """Consistent with scipy cdf(NaN)=NaN""" + assert np.isnan(self.uniform.cdf(np.nan)) -@given(array_shapes(min_dims=1, min_side=1)) -def test_uniform_cdf_array(size): - x = np.random.normal(0, 1, size=size) - pdf = uniform.pdf(x) - scipy_pdf = scipy_uniform.pdf(x) - assert isinstance(pdf, np.ndarray) - assert np.allclose(pdf, scipy_pdf, equal_nan=True) + @pytest.mark.parametrize("test_input,expected", [(np.inf, 1.0), (-np.inf, 0.0)]) + def test_cdf_infinity(self, test_input, expected): + """Consistent with scipy cdf(inf)=1.0, cdf(-inf)=0.0""" + assert self.uniform.cdf(test_input) == expected + @given(array_shapes(min_dims=1, min_side=1)) + def test_cdf_shape(self, shape): + """Test if the output array matches the shape of the input array""" + x = np.zeros(shape) + assert x.shape == self.uniform.cdf(x).shape -@given(st.floats(allow_nan=True)) -def test_uniform_icdf_float(y): - icdf = uniform.icdf(y) - scipy_icdf = scipy_uniform.ppf(y) - assert isinstance(icdf, float) - assert np.allclose(icdf, scipy_icdf, equal_nan=True) + def test_cdf_values(self): + """Test if UQpy cdf matches scipy cdf on x in [-1, 2]""" + x = np.linspace(-1, 2, num=100) + assert np.allclose(self.uniform.cdf(x), self.scipy_uniform.cdf(x)) + def test_icdf_nan(self): + """Consistent with scipy icdf(NaN)=NaN""" + assert np.isnan(self.uniform.icdf(np.nan)) -@given(array_shapes(min_dims=1, min_side=1)) -def test_uniform_icdf_array(size): - y = np.random.normal(0, 1, size=size) - icdf = uniform.icdf(y) - scipy_icdf = scipy_uniform.ppf(y) - assert isinstance(icdf, np.ndarray) - assert np.allclose(icdf, scipy_icdf, equal_nan=True) + def test_icdf_infinity(self): + """Consistent with scipy icdf(inf)=NaN and icdf(-inf)=NaN""" + assert np.isnan(self.uniform.icdf(np.inf)) + assert np.isnan(self.uniform.icdf(-np.inf)) + @given(array_shapes(min_dims=1, min_side=1)) + def test_icdf_shape(self, shape): + """Test if the output array has the same shape as the input array""" + x = np.zeros(shape) + assert x.shape == self.uniform.icdf(x).shape -@given(st.floats(0, 1)) -def test_uniform_icdf_cdf(x): - """Reconstruct x as x = icdf(cdf(x))""" - y = uniform.icdf(uniform.cdf(x)) - assert np.allclose(x, y, equal_nan=True) + def test_icdf_values(self): + """Test if UQpy icdf matches scipy ppf for y in [-0.1, 1.1]. Note outside of [0, 1] both return NaN""" + y = np.linspace(-0.1, 1.1, num=100) + assert np.allclose( + self.uniform.icdf(y), self.scipy_uniform.ppf(y), equal_nan=True + ) + + def test_cdf_icdf_reconstruction(self): + x = np.linspace(0, 1, num=100) + y = self.uniform.cdf(x) + x_reconstruction = self.uniform.icdf(y) + assert np.allclose(x, x_reconstruction) From ce275bf3e14728da126351485bb72b5e4b82dc14 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Mon, 17 Mar 2025 14:46:42 -0400 Subject: [PATCH 18/20] minor improvements to docstrings --- tests/unit_tests/distributions/test_normal.py | 2 +- tests/unit_tests/distributions/test_uniform.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/distributions/test_normal.py b/tests/unit_tests/distributions/test_normal.py index 2c96d662b..817fe2ea5 100644 --- a/tests/unit_tests/distributions/test_normal.py +++ b/tests/unit_tests/distributions/test_normal.py @@ -56,7 +56,7 @@ def test_icdf_nan(self): assert np.isnan(self.normal.icdf(np.nan)) def test_icdf_infinity(self): - """Consistent with scipy icdf(inf)=Nan and icdf(-inf)=NaN""" + """Consistent with scipy icdf(inf)=NaN and icdf(-inf)=NaN""" assert np.isnan(self.normal.icdf(np.inf)) assert np.isnan(self.normal.icdf(-np.inf)) diff --git a/tests/unit_tests/distributions/test_uniform.py b/tests/unit_tests/distributions/test_uniform.py index 3113bc184..0eea47206 100644 --- a/tests/unit_tests/distributions/test_uniform.py +++ b/tests/unit_tests/distributions/test_uniform.py @@ -74,6 +74,7 @@ def test_icdf_values(self): ) def test_cdf_icdf_reconstruction(self): + """Reconstruct x as x = icdf(cdf(x)). Note that this only works where cdf(x) is invertible on [0,1]""" x = np.linspace(0, 1, num=100) y = self.uniform.cdf(x) x_reconstruction = self.uniform.icdf(y) From 60d56dcd7e91ac06d4709026af27cf14a12a7fd7 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Tue, 18 Mar 2025 16:01:07 -0400 Subject: [PATCH 19/20] fixed how types are handled in `if` statement --- src/UQpy/distributions/collection/MultivariateNormal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UQpy/distributions/collection/MultivariateNormal.py b/src/UQpy/distributions/collection/MultivariateNormal.py index efcc3ab33..7d8098250 100644 --- a/src/UQpy/distributions/collection/MultivariateNormal.py +++ b/src/UQpy/distributions/collection/MultivariateNormal.py @@ -74,7 +74,7 @@ def moments(self, moments2return="mv"): def __repr__(self): s = "{mean}" - is_float_or_int = isinstance(self.parameters["cov"], int | float) + is_float_or_int = isinstance(self.parameters["cov"], (int, float)) if is_float_or_int: if self.parameters["cov"] == 1.0: return "MultivariateNormal(" + s.format(**self.parameters) + ")" From 29fe99aa2f7efd80753cb507a6ce1431a11bb534 Mon Sep 17 00:00:00 2001 From: connor-krill Date: Thu, 29 May 2025 13:51:18 -0400 Subject: [PATCH 20/20] added Triangular distribution to address issue #262 --- .../distributions/collection/Triangular.py | 26 +++++++++++++++++++ src/UQpy/distributions/collection/__init__.py | 1 + 2 files changed, 27 insertions(+) create mode 100644 src/UQpy/distributions/collection/Triangular.py diff --git a/src/UQpy/distributions/collection/Triangular.py b/src/UQpy/distributions/collection/Triangular.py new file mode 100644 index 000000000..c7b007867 --- /dev/null +++ b/src/UQpy/distributions/collection/Triangular.py @@ -0,0 +1,26 @@ +from scipy import stats +from UQpy.distributions.baseclass import DistributionContinuous1D + + +class Triangular(DistributionContinuous1D): + + def __init__(self, c: float, loc: float = 0.0, scale: float = 1.0): + """ + + :param c: Shape parameter between :math:`0 \leq c \leq 1` + :param loc: The non-zero part distribution starts at ``loc``. Default: 0.0 + :param scale: The width of the non-zero part of the distribution. Default: 1.0 + """ + super().__init__( + c=c, loc=loc, scale=scale, ordered_parameters=("c", "loc", "scale") + ) + self._construct_from_scipy(scipy_name=stats.triang) + + def __repr__(self): + s = "c={c}" + if self.parameters["loc"] != 0.0: + s += ", loc={loc}" + if self.parameters["scale"] != 1.0: + s += ", scale={scale}" + s = s.format(**self.parameters) + return f"Triangular({s})" diff --git a/src/UQpy/distributions/collection/__init__.py b/src/UQpy/distributions/collection/__init__.py index 1c4260852..647b7b4e5 100644 --- a/src/UQpy/distributions/collection/__init__.py +++ b/src/UQpy/distributions/collection/__init__.py @@ -18,6 +18,7 @@ from UQpy.distributions.collection.Pareto import Pareto from UQpy.distributions.collection.Poisson import Poisson from UQpy.distributions.collection.Rayleigh import Rayleigh +from UQpy.distributions.collection.Triangular import Triangular from UQpy.distributions.collection.TruncatedNormal import TruncatedNormal from UQpy.distributions.collection.Uniform import Uniform from UQpy.distributions.collection.JointIndependent import JointIndependent