diff --git a/src/probnum/quad/_utils.py b/src/probnum/quad/_utils.py index 4e7c5a1a5..5e682aec3 100644 --- a/src/probnum/quad/_utils.py +++ b/src/probnum/quad/_utils.py @@ -53,12 +53,11 @@ def as_domain( """ if input_dim is not None and input_dim < 1: raise ValueError( - f"If given, input dimension must be positive. Current value " - f"is ({input_dim})." + f"Input dimension must be positive. Current value is ({input_dim})." ) if len(domain) != 2: - raise ValueError(f"domain must be of length 2 ({len(domain)}).") + raise ValueError(f"'domain' must be of length 2 ({len(domain)}).") # Domain limits must have equal dimensions if np.size(domain[0]) != np.size(domain[1]): @@ -85,8 +84,8 @@ def as_domain( # Size of domain and input dimension do not match if input_dim != domain_dim: raise ValueError( - "If domain limits are not scalars, their lengths " - "must match the input dimension." + f"If domain limits are not scalars, their lengths " + f"must match the input dimension ({input_dim})." ) domain_a = domain[0] domain_b = domain[1] diff --git a/src/probnum/quad/integration_measures/_integration_measure.py b/src/probnum/quad/integration_measures/_integration_measure.py index 2bf8c0410..e8e7f9da4 100644 --- a/src/probnum/quad/integration_measures/_integration_measure.py +++ b/src/probnum/quad/integration_measures/_integration_measure.py @@ -54,7 +54,7 @@ def __call__(self, points: Union[FloatLike, np.ndarray]) -> np.ndarray: def sample( self, n_sample: IntLike, - rng: Optional[np.random.Generator] = np.random.default_rng(), + rng: np.random.Generator, ) -> np.ndarray: """Sample ``n_sample`` points from the integration measure. @@ -63,7 +63,7 @@ def sample( n_sample Number of points to be sampled rng - Random number generator. Optional. Default is `np.random.default_rng()`. + A Random number generator. Returns ------- diff --git a/src/probnum/quad/integration_measures/_lebesgue_measure.py b/src/probnum/quad/integration_measures/_lebesgue_measure.py index 0d96da846..27a5bed70 100644 --- a/src/probnum/quad/integration_measures/_lebesgue_measure.py +++ b/src/probnum/quad/integration_measures/_lebesgue_measure.py @@ -65,7 +65,7 @@ def __call__(self, points: np.ndarray) -> np.ndarray: def sample( self, n_sample: IntLike, - rng: Optional[np.random.Generator] = np.random.default_rng(), + rng: np.random.Generator, ) -> np.ndarray: return self.random_variable.rvs( size=(n_sample, self.input_dim), random_state=rng diff --git a/tests/test_quad/test_bq_utils.py b/tests/test_quad/test_bq_utils.py index 9fd145bb9..60728b06e 100644 --- a/tests/test_quad/test_bq_utils.py +++ b/tests/test_quad/test_bq_utils.py @@ -10,12 +10,15 @@ @pytest.mark.parametrize( "dom, in_dim", [ + ((0, 1), 0), # zero dimension ((0, 1), -2), # negative dimension ((np.zeros(2), np.ones(2)), 3), # length of bounds does not match dimension ((np.zeros(2), np.ones(3)), None), # lower and upper bounds not equal lengths ((np.array([0, 0]), np.array([1, 0])), None), # integration domain is empty ((np.zeros([2, 1]), np.ones([2, 1])), None), # bounds have too many dimensions ((np.zeros([2, 1]), np.ones([2, 1])), 2), # bounds have too many dimensions + ((0, 1, 2), 2), # domain has too many elements + ((-np.ones(2), np.zeros(2), np.ones(2)), 2), # domain has too many elements ] ) def test_as_domain_wrong_input(dom, in_dim): diff --git a/tests/test_quad/test_integration_measure.py b/tests/test_quad/test_integration_measure.py index 7596a2167..6b1e8351e 100644 --- a/tests/test_quad/test_integration_measure.py +++ b/tests/test_quad/test_integration_measure.py @@ -5,9 +5,25 @@ from probnum.quad.integration_measures import GaussianMeasure, LebesgueMeasure +# Tests shared by all measures start here -# Tests for Gaussian measure -def test_gaussian_diagonal_covariance(input_dim: int): + +def test_density_call_shape(x, measure): + expected_shape = (x.shape[0],) + assert measure(x).shape == expected_shape + + +@pytest.mark.parametrize("n_sample", [1, 2, 5]) +def test_sample_shape(measure, n_sample, rng): + input_dim = measure.input_dim + res = measure.sample(n_sample=n_sample, rng=rng) + assert res.shape == (n_sample, input_dim) + + +# Tests for Gaussian measure start here + + +def test_gaussian_diagonal_covariance(input_dim): """Check that diagonal covariance matrices are recognised as diagonal.""" mean = np.full((input_dim,), 0.0) cov = np.eye(input_dim) @@ -36,13 +52,6 @@ def test_gaussian_mean_shape_1d(mean, cov): assert measure.cov.size == 1 -@pytest.mark.parametrize("neg_dim", [0, -1, -10, -100]) -def test_gaussian_negative_dimension(neg_dim): - """Make sure that a negative dimension raises ValueError.""" - with pytest.raises(ValueError): - GaussianMeasure(0, 1, neg_dim) - - def test_gaussian_param_assignment(input_dim: int): """Check that diagonal mean and covariance for higher dimensions are extended correctly.""" @@ -57,15 +66,24 @@ def test_gaussian_param_assignment(input_dim: int): assert np.array_equal(measure.cov, np.eye(input_dim)) -def test_gaussian_scalar(): +def test_gaussian_param_assignment_scalar(): """Check that the 1d Gaussian case works.""" measure = GaussianMeasure(0.5, 1.5) assert measure.mean == 0.5 assert measure.cov == 1.5 -# Tests for Lebesgue measure -def test_lebesgue_dim_correct(input_dim: int): +@pytest.mark.parametrize("wrong_dim", [0, -1, -10, -100]) +def test_gaussian_wrong_dimension_raises(wrong_dim): + """Make sure that a non-positive dimension raises ValueError.""" + with pytest.raises(ValueError): + GaussianMeasure(0, 1, wrong_dim) + + +# Tests for Lebesgue measure start here + + +def test_lebesgue_input_dim_assignment(input_dim: int): """Check that dimensions are handled correctly.""" domain1 = (0.0, 1.87) measure11 = LebesgueMeasure(domain=domain1) @@ -82,40 +100,70 @@ def test_lebesgue_dim_correct(input_dim: int): assert measure22.input_dim == input_dim -@pytest.mark.parametrize("domain_a", [0, np.full((3,), 0), np.full((13,), 0)]) -@pytest.mark.parametrize("domain_b", [np.full((4,), 1.2), np.full((14,), 1.2)]) -@pytest.mark.parametrize("input_dim", [-10, -2, 0, 2, 12, 122]) -def test_lebesgue_dim_incorrect(domain_a, domain_b, input_dim): - """Check that ValueError is raised if domain limits have mismatching dimensions or - dimension is not positive.""" - with pytest.raises(ValueError): - LebesgueMeasure(domain=(domain_a, domain_b), input_dim=input_dim) - - -def test_lebesgue_normalization(input_dim: int): +def test_lebesgue_normalization_value(input_dim: int): """Check that normalization constants are handled properly when not equal to one.""" domain = (0, 2) - measure = LebesgueMeasure(domain=domain, input_dim=input_dim, normalized=True) + # normalized + measure = LebesgueMeasure(domain=domain, input_dim=input_dim, normalized=True) volume = 2**input_dim - assert measure.normalization_constant == 1 / volume - + assert measure.normalization_constant == 1.0 / volume -@pytest.mark.parametrize("domain", [(0, np.Inf), (-np.Inf, 0), (-np.Inf, np.Inf)]) -def test_lebesgue_normalization_raises(domain, input_dim: int): - """Check that exception is raised when normalization is not possible.""" - with pytest.raises(ValueError): - LebesgueMeasure(domain=domain, input_dim=input_dim, normalized=True) + # not normalized + measure = LebesgueMeasure(domain=domain, input_dim=input_dim, normalized=False) + assert measure.normalization_constant == 1.0 -def test_lebesgue_unnormalized(input_dim: int): +def test_lebesgue_normalization_value_unnormalized(input_dim: int): """Check that normalization constants are handled properly when equal to one.""" measure1 = LebesgueMeasure(domain=(0, 1), input_dim=input_dim, normalized=True) measure2 = LebesgueMeasure(domain=(0, 1), input_dim=input_dim, normalized=False) + + assert measure1.normalized + assert not measure2.normalized assert measure1.normalization_constant == measure2.normalization_constant -# Tests for all integration measures -def test_density_call(x, measure): - expected_shape = (x.shape[0],) - assert measure(x).shape == expected_shape +@pytest.mark.parametrize("wrong_input_dim", [-5, -1, 0]) +def test_lebesgue_non_positive_input_dim_raises(wrong_input_dim): + # non positive input dimenions are not allowed + with pytest.raises(ValueError): + LebesgueMeasure(input_dim=wrong_input_dim, domain=(0, 1)) + + +@pytest.mark.parametrize( + "domain", + [ + (0.0, np.ones(4)), + (np.ones(4), 0.0), + (np.zeros(4), np.ones(3)), + (np.zeros([4, 1]), np.ones(4)), + (np.zeros(4), np.ones([4, 1])), + (np.zeros([4, 1]), np.ones([4, 1])), + ], +) +def test_lebesgue_domain_shape_mismatch_raises(domain): + # left and right bounds of domain are not consistent + with pytest.raises(ValueError): + LebesgueMeasure(domain=domain) + + +@pytest.mark.parametrize( + "input_dim,domain", + [ + (1, (np.zeros(3), np.ones(3))), + (2, (np.zeros(3), np.ones(3))), + (5, (np.zeros(3), np.ones(3))), + ], +) +def test_lebesgue_domain_input_dim_mismatch_raises(input_dim, domain): + # input dimension does not agree with domain shapes + with pytest.raises(ValueError): + LebesgueMeasure(input_dim=input_dim, domain=domain) + + +@pytest.mark.parametrize("domain", [(0, np.Inf), (-np.Inf, 0), (-np.Inf, np.Inf)]) +def test_lebesgue_normalization_raises(domain, input_dim: int): + """Check that exception is raised when normalization is not possible.""" + with pytest.raises(ValueError): + LebesgueMeasure(domain=domain, input_dim=input_dim, normalized=True)