From 1455083165a2fc5a11af152287cfb13ea06a0884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luiz=20Desu=C3=B3=20N?= Date: Sat, 19 Oct 2024 16:36:04 -0300 Subject: [PATCH] test: add tests for usual cases of auxiliary and data transform functions --- tests/test_banquo.py | 259 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 244 insertions(+), 15 deletions(-) diff --git a/tests/test_banquo.py b/tests/test_banquo.py index 0bb0f56..a8363ec 100644 --- a/tests/test_banquo.py +++ b/tests/test_banquo.py @@ -9,17 +9,31 @@ from typing import no_type_check import numpy as nxp # ! from numpy import array_api as nxp not working +import pytest from hypothesis import given, settings +from hypothesis import strategies as st +from hypothesis.extra.array_api import make_strategies_namespace from scipy.stats import multivariate_normal, norm from banquo import ( + DataMaxExceedsSupportUpperBoundError, + DataMinExceedsSupportLowerBoundError, + DataRangeExceedsSupportBoundError, array, chol2inv, + divide_ns, + homographic_ns, + minmax_normalization, multi_normal_cholesky_copula_lpdf, + multiply_ns, normalize_covariance, + std_ns, ) -from .hypothesis_arrays_strategy import spd_square_matrix_builder_float64 +from .hypothesis_arrays_strategy import FLOAT64, spd_square_matrix_builder_float64 + + +xps = make_strategies_namespace(nxp) ############################################################################### @@ -31,52 +45,267 @@ ############################################################################### -# Tests for chol2inv ######################################################### +# Tests for auxiliary functions ############################################## ############################################################################### @no_type_check @settings(deadline=None) -@given(X=spd_matrix_builder_float64) -def test_chol2inv_equals_inverting_spd_matrix_float64(X: array): +@given(x=spd_matrix_builder_float64) +def test_chol2inv_equals_inverting_spd_matrix_float64(x: array) -> None: """Test if :func:`chol2inv` equals directly inverting a SPD matrix. Parameters ---------- - X : array + x : array SPD matrix. """ - X_inv = nxp.linalg.inv(X) - spd_chol = nxp.linalg.cholesky(X) - X_chol2inv = chol2inv(spd_chol) - assert nxp.allclose(X_inv, X_chol2inv) + x_inv = nxp.linalg.inv(x) + spd_chol = nxp.linalg.cholesky(x) + x_chol2inv = chol2inv(spd_chol) + assert nxp.allclose(x_inv, x_chol2inv) + + +@no_type_check +@settings(deadline=None) +@given( + xps.arrays( + dtype=FLOAT64, + shape=(10,), + elements=st.floats(allow_infinity=False, allow_nan=False), + unique=True, + ) +) +def test_std_ns_not_inf_float64(x: array) -> None: + """Test if :func:`std_ns` is close to finite entries std. + + Parameters + ---------- + x : array + Elements to calculate std. + """ + res = nxp.std(x) + x_robust = std_ns(x) + res_not_inf = ~nxp.isinf(res) + if res_not_inf: + assert nxp.isclose(x_robust, res) + + +@no_type_check +@settings(deadline=None) +@given( + xps.arrays( + dtype=FLOAT64, + shape=(10,), + elements=st.floats(allow_infinity=False, allow_nan=False), + unique=True, + ), + xps.arrays( + dtype=FLOAT64, + shape=(10,), + elements=st.floats(allow_infinity=False, allow_nan=False), + unique=True, + ), +) +def test_divide_ns_not_inf_float64(x1: array, x2: array) -> None: + """Test if :func:`divide_ns` is close to finite entries element-wise division. + + Parameters + ---------- + x1 : array + Numerator. + x2 : array + Denominator. + """ + res = x1 / x2 + div_robust = divide_ns(x1, x2) + res_not_inf = ~nxp.isinf(res) + res_not_nan = ~nxp.isnan(res) + assert nxp.allclose( + res[res_not_inf & res_not_nan], div_robust[res_not_inf & res_not_nan] + ) + + +@no_type_check +@settings(deadline=None) +@given( + xps.arrays( + dtype=FLOAT64, + shape=(10,), + elements=st.floats(allow_infinity=False, allow_nan=False), + unique=True, + ), + xps.arrays( + dtype=FLOAT64, + shape=(10,), + elements=st.floats(allow_infinity=False, allow_nan=False), + unique=True, + ), +) +def test_multiply_ns_not_inf_float64(x1: array, x2: array) -> None: + """Test if :func:`multiply_ns` is close to finite entries multiplication. + + Parameters + ---------- + x1 : array + Factor. + x2 : array + Factor. + """ + res = x1 * x2 + mul_robust = multiply_ns(x1, x2) + res_not_inf = ~nxp.isinf(res) + assert nxp.allclose(res[res_not_inf], mul_robust[res_not_inf]) + + +@no_type_check +@settings(deadline=None) +@given( + xps.arrays( + dtype=FLOAT64, + shape=(10,), + elements=st.floats(allow_infinity=False, allow_nan=False), + unique=True, + ) +) +def test_homographic_ns_float64(x: array) -> None: + r"""Test if :func:`homographic_ns` is close to :math:`1/(1+x)`. + + Parameters + ---------- + x : array + Elements to perform homographic transform. + """ + res = 1 / (1 + x) + homographic = homographic_ns(x) + res_not_inf = ~nxp.isinf(res) + assert nxp.allclose(res[res_not_inf], homographic[res_not_inf]) + + +############################################################################### +# Test data transform ######################################################### +############################################################################### + + +@no_type_check +@settings(deadline=None) +@given( + xps.arrays( + dtype=FLOAT64, + shape=(10,), + elements=st.floats(allow_infinity=False, allow_nan=False), + unique=True, + ) +) +def test_minmax_normalization_default_support_float64(x: array) -> None: + """Test :func:`minmax_normalization` with default support. + + Parameters + ---------- + x : array + Elements to be transformed. + """ + x_transf = minmax_normalization(x) + + x_transfmin = nxp.min(x_transf) + x_transfmax = nxp.max(x_transf) + + # For some unknown reason, in some cases, hypothesis is saying + # x_transf is nan, when it actually is not + if ~nxp.isnan(x_transfmin): + assert nxp.isclose(x_transfmin, 0) or (x_transfmin > 0) + + if ~nxp.isnan(x_transfmax): + assert nxp.isclose(x_transfmax, 1) or (x_transfmax < 1) + + +@no_type_check +@settings(deadline=None) +@given( + xps.arrays( + dtype=FLOAT64, + shape=(10,), + elements=st.floats(allow_infinity=False, allow_nan=False), + unique=True, + ), + xps.arrays( + dtype=FLOAT64, + shape=(2,), + elements=st.floats(allow_nan=False), + unique=True, + ), +) +def test_minmax_normalization_any_support_float64(x: array, support: array) -> None: + """Test :func:`minmax_normalization` with any support. + + Parameters + ---------- + x : array + Elements to be transformed. + support : array + Two-elements array containing the lower and upper bounds + for the elements. + """ + + support = nxp.sort(support) + + x_range = nxp.asarray((nxp.min(x), nxp.max(x))) + + condition_lower: bool = x_range[0] < support[0] + condition_upper: bool = x_range[1] > support[1] + + # Check if data range exceeds support's boundary + if condition_lower and condition_upper: + with pytest.raises(DataRangeExceedsSupportBoundError): + x_transf = minmax_normalization(x, support=support) + # Check if data minimum exceeds support's lower bound + elif x_range[0] < support[0]: + with pytest.raises(DataMinExceedsSupportLowerBoundError): + x_transf = minmax_normalization(x, support=support) + # Check if data maximum exceeds support's upper bound + elif x_range[1] > support[1]: + with pytest.raises(DataMaxExceedsSupportUpperBoundError): + x_transf = minmax_normalization(x, support=support) + else: + x_transf = minmax_normalization(x, support=support) + + x_transfmin = nxp.min(x_transf) + x_transfmax = nxp.max(x_transf) + + # For some unknown reason, in some cases, hypothesis is saying + # x_transf is nan, when it actually is not + if ~nxp.isnan(x_transfmin): + assert nxp.isclose(x_transfmin, 0) or (x_transfmin > 0) + + if ~nxp.isnan(x_transfmax): + assert nxp.isclose(x_transfmax, 1) or (x_transfmax < 1) ############################################################################### -# Tests for multi_normal_cholesky_copula_lpdf ################################ +# Tests for copula functions ################################################# ############################################################################### @no_type_check @settings(deadline=None) -@given(X=spd_matrix_builder_float64) +@given(x=spd_matrix_builder_float64) def test_multi_normal_cholesky_copula_lpdf_equals_multivariate_gaussian_float64( - X: array, -): + x: array, +) -> None: """Test for :func:`multi_normal_cholesky_copula_lpdf`. This test compares the Gaussian copula combined with Gaussian marginal distributions and a multivariate normal distribution lpdf. Parameters ---------- - X : array + x : array SPD matrix. """ rng = nxp.random.default_rng() # Transform covariance into a correlation matrix - corr = normalize_covariance(X) + corr = normalize_covariance(x) # Get the Cholesky factor of corr matrix corr_chol = nxp.linalg.cholesky(corr)