Skip to content

Commit 90b5dd3

Browse files
authored
Make quad integration measures stateless (#748)
1 parent d94ff24 commit 90b5dd3

File tree

5 files changed

+94
-44
lines changed

5 files changed

+94
-44
lines changed

src/probnum/quad/_utils.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,11 @@ def as_domain(
5353
"""
5454
if input_dim is not None and input_dim < 1:
5555
raise ValueError(
56-
f"If given, input dimension must be positive. Current value "
57-
f"is ({input_dim})."
56+
f"Input dimension must be positive. Current value is ({input_dim})."
5857
)
5958

6059
if len(domain) != 2:
61-
raise ValueError(f"domain must be of length 2 ({len(domain)}).")
60+
raise ValueError(f"'domain' must be of length 2 ({len(domain)}).")
6261

6362
# Domain limits must have equal dimensions
6463
if np.size(domain[0]) != np.size(domain[1]):
@@ -85,8 +84,8 @@ def as_domain(
8584
# Size of domain and input dimension do not match
8685
if input_dim != domain_dim:
8786
raise ValueError(
88-
"If domain limits are not scalars, their lengths "
89-
"must match the input dimension."
87+
f"If domain limits are not scalars, their lengths "
88+
f"must match the input dimension ({input_dim})."
9089
)
9190
domain_a = domain[0]
9291
domain_b = domain[1]

src/probnum/quad/integration_measures/_integration_measure.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def __call__(self, points: Union[FloatLike, np.ndarray]) -> np.ndarray:
5454
def sample(
5555
self,
5656
n_sample: IntLike,
57-
rng: Optional[np.random.Generator] = np.random.default_rng(),
57+
rng: np.random.Generator,
5858
) -> np.ndarray:
5959
"""Sample ``n_sample`` points from the integration measure.
6060
@@ -63,7 +63,7 @@ def sample(
6363
n_sample
6464
Number of points to be sampled
6565
rng
66-
Random number generator. Optional. Default is `np.random.default_rng()`.
66+
A Random number generator.
6767
6868
Returns
6969
-------

src/probnum/quad/integration_measures/_lebesgue_measure.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def __call__(self, points: np.ndarray) -> np.ndarray:
6565
def sample(
6666
self,
6767
n_sample: IntLike,
68-
rng: Optional[np.random.Generator] = np.random.default_rng(),
68+
rng: np.random.Generator,
6969
) -> np.ndarray:
7070
return self.random_variable.rvs(
7171
size=(n_sample, self.input_dim), random_state=rng

tests/test_quad/test_bq_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010
@pytest.mark.parametrize(
1111
"dom, in_dim",
1212
[
13+
((0, 1), 0), # zero dimension
1314
((0, 1), -2), # negative dimension
1415
((np.zeros(2), np.ones(2)), 3), # length of bounds does not match dimension
1516
((np.zeros(2), np.ones(3)), None), # lower and upper bounds not equal lengths
1617
((np.array([0, 0]), np.array([1, 0])), None), # integration domain is empty
1718
((np.zeros([2, 1]), np.ones([2, 1])), None), # bounds have too many dimensions
1819
((np.zeros([2, 1]), np.ones([2, 1])), 2), # bounds have too many dimensions
20+
((0, 1, 2), 2), # domain has too many elements
21+
((-np.ones(2), np.zeros(2), np.ones(2)), 2), # domain has too many elements
1922
]
2023
)
2124
def test_as_domain_wrong_input(dom, in_dim):

tests/test_quad/test_integration_measure.py

Lines changed: 84 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,25 @@
55

66
from probnum.quad.integration_measures import GaussianMeasure, LebesgueMeasure
77

8+
# Tests shared by all measures start here
89

9-
# Tests for Gaussian measure
10-
def test_gaussian_diagonal_covariance(input_dim: int):
10+
11+
def test_density_call_shape(x, measure):
12+
expected_shape = (x.shape[0],)
13+
assert measure(x).shape == expected_shape
14+
15+
16+
@pytest.mark.parametrize("n_sample", [1, 2, 5])
17+
def test_sample_shape(measure, n_sample, rng):
18+
input_dim = measure.input_dim
19+
res = measure.sample(n_sample=n_sample, rng=rng)
20+
assert res.shape == (n_sample, input_dim)
21+
22+
23+
# Tests for Gaussian measure start here
24+
25+
26+
def test_gaussian_diagonal_covariance(input_dim):
1127
"""Check that diagonal covariance matrices are recognised as diagonal."""
1228
mean = np.full((input_dim,), 0.0)
1329
cov = np.eye(input_dim)
@@ -36,13 +52,6 @@ def test_gaussian_mean_shape_1d(mean, cov):
3652
assert measure.cov.size == 1
3753

3854

39-
@pytest.mark.parametrize("neg_dim", [0, -1, -10, -100])
40-
def test_gaussian_negative_dimension(neg_dim):
41-
"""Make sure that a negative dimension raises ValueError."""
42-
with pytest.raises(ValueError):
43-
GaussianMeasure(0, 1, neg_dim)
44-
45-
4655
def test_gaussian_param_assignment(input_dim: int):
4756
"""Check that diagonal mean and covariance for higher dimensions are extended
4857
correctly."""
@@ -57,15 +66,24 @@ def test_gaussian_param_assignment(input_dim: int):
5766
assert np.array_equal(measure.cov, np.eye(input_dim))
5867

5968

60-
def test_gaussian_scalar():
69+
def test_gaussian_param_assignment_scalar():
6170
"""Check that the 1d Gaussian case works."""
6271
measure = GaussianMeasure(0.5, 1.5)
6372
assert measure.mean == 0.5
6473
assert measure.cov == 1.5
6574

6675

67-
# Tests for Lebesgue measure
68-
def test_lebesgue_dim_correct(input_dim: int):
76+
@pytest.mark.parametrize("wrong_dim", [0, -1, -10, -100])
77+
def test_gaussian_wrong_dimension_raises(wrong_dim):
78+
"""Make sure that a non-positive dimension raises ValueError."""
79+
with pytest.raises(ValueError):
80+
GaussianMeasure(0, 1, wrong_dim)
81+
82+
83+
# Tests for Lebesgue measure start here
84+
85+
86+
def test_lebesgue_input_dim_assignment(input_dim: int):
6987
"""Check that dimensions are handled correctly."""
7088
domain1 = (0.0, 1.87)
7189
measure11 = LebesgueMeasure(domain=domain1)
@@ -82,40 +100,70 @@ def test_lebesgue_dim_correct(input_dim: int):
82100
assert measure22.input_dim == input_dim
83101

84102

85-
@pytest.mark.parametrize("domain_a", [0, np.full((3,), 0), np.full((13,), 0)])
86-
@pytest.mark.parametrize("domain_b", [np.full((4,), 1.2), np.full((14,), 1.2)])
87-
@pytest.mark.parametrize("input_dim", [-10, -2, 0, 2, 12, 122])
88-
def test_lebesgue_dim_incorrect(domain_a, domain_b, input_dim):
89-
"""Check that ValueError is raised if domain limits have mismatching dimensions or
90-
dimension is not positive."""
91-
with pytest.raises(ValueError):
92-
LebesgueMeasure(domain=(domain_a, domain_b), input_dim=input_dim)
93-
94-
95-
def test_lebesgue_normalization(input_dim: int):
103+
def test_lebesgue_normalization_value(input_dim: int):
96104
"""Check that normalization constants are handled properly when not equal to one."""
97105
domain = (0, 2)
98-
measure = LebesgueMeasure(domain=domain, input_dim=input_dim, normalized=True)
99106

107+
# normalized
108+
measure = LebesgueMeasure(domain=domain, input_dim=input_dim, normalized=True)
100109
volume = 2**input_dim
101-
assert measure.normalization_constant == 1 / volume
102-
110+
assert measure.normalization_constant == 1.0 / volume
103111

104-
@pytest.mark.parametrize("domain", [(0, np.Inf), (-np.Inf, 0), (-np.Inf, np.Inf)])
105-
def test_lebesgue_normalization_raises(domain, input_dim: int):
106-
"""Check that exception is raised when normalization is not possible."""
107-
with pytest.raises(ValueError):
108-
LebesgueMeasure(domain=domain, input_dim=input_dim, normalized=True)
112+
# not normalized
113+
measure = LebesgueMeasure(domain=domain, input_dim=input_dim, normalized=False)
114+
assert measure.normalization_constant == 1.0
109115

110116

111-
def test_lebesgue_unnormalized(input_dim: int):
117+
def test_lebesgue_normalization_value_unnormalized(input_dim: int):
112118
"""Check that normalization constants are handled properly when equal to one."""
113119
measure1 = LebesgueMeasure(domain=(0, 1), input_dim=input_dim, normalized=True)
114120
measure2 = LebesgueMeasure(domain=(0, 1), input_dim=input_dim, normalized=False)
121+
122+
assert measure1.normalized
123+
assert not measure2.normalized
115124
assert measure1.normalization_constant == measure2.normalization_constant
116125

117126

118-
# Tests for all integration measures
119-
def test_density_call(x, measure):
120-
expected_shape = (x.shape[0],)
121-
assert measure(x).shape == expected_shape
127+
@pytest.mark.parametrize("wrong_input_dim", [-5, -1, 0])
128+
def test_lebesgue_non_positive_input_dim_raises(wrong_input_dim):
129+
# non positive input dimenions are not allowed
130+
with pytest.raises(ValueError):
131+
LebesgueMeasure(input_dim=wrong_input_dim, domain=(0, 1))
132+
133+
134+
@pytest.mark.parametrize(
135+
"domain",
136+
[
137+
(0.0, np.ones(4)),
138+
(np.ones(4), 0.0),
139+
(np.zeros(4), np.ones(3)),
140+
(np.zeros([4, 1]), np.ones(4)),
141+
(np.zeros(4), np.ones([4, 1])),
142+
(np.zeros([4, 1]), np.ones([4, 1])),
143+
],
144+
)
145+
def test_lebesgue_domain_shape_mismatch_raises(domain):
146+
# left and right bounds of domain are not consistent
147+
with pytest.raises(ValueError):
148+
LebesgueMeasure(domain=domain)
149+
150+
151+
@pytest.mark.parametrize(
152+
"input_dim,domain",
153+
[
154+
(1, (np.zeros(3), np.ones(3))),
155+
(2, (np.zeros(3), np.ones(3))),
156+
(5, (np.zeros(3), np.ones(3))),
157+
],
158+
)
159+
def test_lebesgue_domain_input_dim_mismatch_raises(input_dim, domain):
160+
# input dimension does not agree with domain shapes
161+
with pytest.raises(ValueError):
162+
LebesgueMeasure(input_dim=input_dim, domain=domain)
163+
164+
165+
@pytest.mark.parametrize("domain", [(0, np.Inf), (-np.Inf, 0), (-np.Inf, np.Inf)])
166+
def test_lebesgue_normalization_raises(domain, input_dim: int):
167+
"""Check that exception is raised when normalization is not possible."""
168+
with pytest.raises(ValueError):
169+
LebesgueMeasure(domain=domain, input_dim=input_dim, normalized=True)

0 commit comments

Comments
 (0)