Skip to content

Commit

Permalink
Make quad integration measures stateless (#748)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmahsereci committed Dec 9, 2022
1 parent d94ff24 commit 90b5dd3
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 44 deletions.
9 changes: 4 additions & 5 deletions src/probnum/quad/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand All @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions src/probnum/quad/integration_measures/_integration_measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
-------
Expand Down
2 changes: 1 addition & 1 deletion src/probnum/quad/integration_measures/_lebesgue_measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions tests/test_quad/test_bq_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
120 changes: 84 additions & 36 deletions tests/test_quad/test_integration_measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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."""
Expand All @@ -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)
Expand All @@ -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)

0 comments on commit 90b5dd3

Please sign in to comment.