Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make quad integration measures stateless #748

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
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)