From 7e6167c16820b75e63b060b9f1fad53b17593eef Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Tue, 29 Nov 2022 11:10:07 +0200 Subject: [PATCH 01/34] Add Kaiyu's original MLBQ implementation in probnum --- src/probnum/quad/_bayesquad.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 6dd889989..6cbe47b98 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -284,6 +284,36 @@ def bayesquad_from_data( return integral_belief, info +def Probnum_mlbq(L, X, Y, kernel=None, domain=None, measure=None): + """ + L: (int) the number of levels + X: (list of np.ndarray) shape (L, n_eval, input_dim) + Y: (list of np.ndarray) – shape=(L, n_eval,) – Function evaluations at nodes + kernel: None Defaults to the ExpQuad kernel. (list of kernels) + domain=None,The integration domain. Contains lower and upper bound as scalar or np.ndarray. + measure=None, The integration measure. Defaults to the Lebesgue measure. + """ + # #scale_estimation='mle', cannot add + # #jitter=None default=1e-8 list of floats, cannot add + # if jitter is None: + # jitter = [1e-8]*L + + if kernel is None: + kernel = [None] * (L + 1) + + mean = np.zeros(L + 1) + var = np.zeros(L + 1) + + for l in range(L + 1): + mv, info = pn.quad.bayesquad_from_data( + nodes=X[l], fun_evals=Y[l], kernel=kernel[l], domain=domain, measure=measure + ) + mean[l] = mv.mean + var[l] = mv.var + + return np.sum(mean), np.sum(var), mean, var + + def _check_domain_measure_compatibility( input_dim: IntLike, domain: Optional[DomainLike], From 349b7494017ff0e6447bfd1b098e3ce163061982 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Thu, 1 Dec 2022 14:17:56 +0200 Subject: [PATCH 02/34] Some edits to MLBQ --- src/probnum/quad/__init__.py | 3 ++- src/probnum/quad/_bayesquad.py | 25 +++++++++++-------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/probnum/quad/__init__.py b/src/probnum/quad/__init__.py index dc8d59280..429edeb91 100644 --- a/src/probnum/quad/__init__.py +++ b/src/probnum/quad/__init__.py @@ -7,12 +7,13 @@ """ from . import integration_measures, kernel_embeddings, solvers -from ._bayesquad import bayesquad, bayesquad_from_data +from ._bayesquad import bayesquad, bayesquad_from_data, multilevel_bayesquad # Public classes and functions. Order is reflected in documentation. __all__ = [ "bayesquad", "bayesquad_from_data", + "multilevel_bayesquad", ] # Set correct module paths. Corrects links and module paths in documentation. diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 6cbe47b98..c67730490 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -284,28 +284,25 @@ def bayesquad_from_data( return integral_belief, info -def Probnum_mlbq(L, X, Y, kernel=None, domain=None, measure=None): - """ - L: (int) the number of levels - X: (list of np.ndarray) shape (L, n_eval, input_dim) - Y: (list of np.ndarray) – shape=(L, n_eval,) – Function evaluations at nodes - kernel: None Defaults to the ExpQuad kernel. (list of kernels) - domain=None,The integration domain. Contains lower and upper bound as scalar or np.ndarray. - measure=None, The integration measure. Defaults to the Lebesgue measure. - """ - # #scale_estimation='mle', cannot add - # #jitter=None default=1e-8 list of floats, cannot add - # if jitter is None: - # jitter = [1e-8]*L +def multilevel_bayesquad( + nodes: np.ndarray, + fun_evals: np.ndarray, + kernel: Optional[np.ndarray[Kernel]] = None, + measure: Optional[IntegrationMeasure] = None, + domain: Optional[DomainLike] = None, + options: Optional[dict] = None, +) -> Tuple[Normal, BQIterInfo]: + L = 1 if kernel is None: kernel = [None] * (L + 1) mean = np.zeros(L + 1) var = np.zeros(L + 1) + X = 0 for l in range(L + 1): - mv, info = pn.quad.bayesquad_from_data( + mv, info = bayesquad_from_data( nodes=X[l], fun_evals=Y[l], kernel=kernel[l], domain=domain, measure=measure ) mean[l] = mv.mean From 15e4e44d3469cd9e779b842e107739e9427fec1f Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Fri, 2 Dec 2022 16:15:44 +0200 Subject: [PATCH 03/34] Write a proper version of MLBQ --- src/probnum/quad/__init__.py | 4 +-- src/probnum/quad/_bayesquad.py | 50 +++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/probnum/quad/__init__.py b/src/probnum/quad/__init__.py index 429edeb91..84173f3b7 100644 --- a/src/probnum/quad/__init__.py +++ b/src/probnum/quad/__init__.py @@ -7,13 +7,13 @@ """ from . import integration_measures, kernel_embeddings, solvers -from ._bayesquad import bayesquad, bayesquad_from_data, multilevel_bayesquad +from ._bayesquad import bayesquad, bayesquad_from_data, bayesquad_multilevel # Public classes and functions. Order is reflected in documentation. __all__ = [ "bayesquad", "bayesquad_from_data", - "multilevel_bayesquad", + "bayesquad_multilevel", ] # Set correct module paths. Corrects links and module paths in documentation. diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index c67730490..d68e64904 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -8,7 +8,7 @@ """ from __future__ import annotations -from typing import Callable, Optional, Tuple +from typing import Callable, Optional, Tuple, Union import warnings import numpy as np @@ -284,31 +284,37 @@ def bayesquad_from_data( return integral_belief, info -def multilevel_bayesquad( - nodes: np.ndarray, - fun_evals: np.ndarray, - kernel: Optional[np.ndarray[Kernel]] = None, - measure: Optional[IntegrationMeasure] = None, - domain: Optional[DomainLike] = None, - options: Optional[dict] = None, -) -> Tuple[Normal, BQIterInfo]: - - L = 1 - if kernel is None: - kernel = [None] * (L + 1) +def bayesquad_multilevel( + nodes: Tuple[np.ndarray, ...], + fun_diff_evals: Tuple[np.ndarray, ...], + kernels: Optional[Union[Kernel, Tuple[Kernel, ...]]] = None, + measure: Optional[IntegrationMeasure] = None, + domain: Optional[DomainLike] = None, + options: Optional[dict] = None, +) -> Tuple[Normal, Tuple[BQIterInfo, ...]]: - mean = np.zeros(L + 1) - var = np.zeros(L + 1) - X = 0 + max_level = len(nodes) + if len(fun_diff_evals) != max_level or len(kernels) != max_level: + raise ValueError( + "You must provide an equal number of kernels, vectors of " + "function evaluations and sets of nodes." + ) - for l in range(L + 1): - mv, info = bayesquad_from_data( - nodes=X[l], fun_evals=Y[l], kernel=kernel[l], domain=domain, measure=measure + integer_belief = Normal(mean=0.0, cov=0.0) + infos = () + for l in range(max_level): + integer_belief_l, info_l = bayesquad_from_data( + nodes=nodes[l], + fun_evals=fun_diff_evals[l], + kernel=kernels[l], + measure=measure, + domain=domain, + options=options, ) - mean[l] = mv.mean - var[l] = mv.var + integer_belief += integer_belief_l + infos += (info_l,) - return np.sum(mean), np.sum(var), mean, var + return integer_belief, infos def _check_domain_measure_compatibility( From bdd27535c80a113d50da12a8c67fed9483761eae Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Mon, 5 Dec 2022 21:27:07 +0200 Subject: [PATCH 04/34] Add some input handling a write docstring --- src/probnum/quad/_bayesquad.py | 85 ++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 4 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index d68e64904..7f96b7185 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -287,14 +287,91 @@ def bayesquad_from_data( def bayesquad_multilevel( nodes: Tuple[np.ndarray, ...], fun_diff_evals: Tuple[np.ndarray, ...], - kernels: Optional[Union[Kernel, Tuple[Kernel, ...]]] = None, + kernels: Optional[Tuple[Kernel, ...]] = None, measure: Optional[IntegrationMeasure] = None, domain: Optional[DomainLike] = None, options: Optional[dict] = None, ) -> Tuple[Normal, Tuple[BQIterInfo, ...]]: + r"""Infer the value of an integral from a given set of nodes and function + evaluations. + + Parameters + ---------- + nodes + *shape=(n_level,)* -- Tuple containing the locations for each level at which + the functionn evaluations are available as ``fun_diff_evals``. Each element + must be a shape=(n_eval, input_dim) ``np.ndarray``. If a tuple containing only + one element is provided, it is inferred that the same nodes ``nodes[0]`` are + used on every level. + fun_diff_evals + *shape=(n_level,)* --- Tuple containing the evaluations of :math:`f_l - f_{l-1}` + for each level at the nodes provided in ``nodes``. Each element must be a + shape=(n_eval,) ``np.ndarray``. The zeroth element contains the evaluations of + :math:`f_0`. + kernels + Tuple containing the kernels used for the GP model at each level. If a tuple + containing only one element is provided, it is inferred that the same kernel + ``kernels[0]`` is used on every level. Defaults to the ``ExpQuad`` kernel. + measure + The integration measure. Defaults to the Lebesgue measure. + domain + The integration domain. Contains lower and upper bound as scalar or + ``np.ndarray``. Obsolete if ``measure`` is given. + options + A dictionary with the following optional solver settings + + scale_estimation : Optional[str] + Estimation method to use to compute the scale parameter. Used + independently on each level. Defaults to 'mle'. Options are + + ============================== ======= + Maximum likelihood estimation ``mle`` + ============================== ======= + + jitter : Optional[FloatLike] + Non-negative jitter to numerically stabilise kernel matrix + inversion. Same jitter is used on each level. Defaults to 1e-8. + + Returns + ------- + integral : + The integral belief subject to the provided measure or domain. + infos : + Information on the performance of the method for each level. + + Raises + ------ + ValueError + If ``nodes``, ``fun_diff_evals`` or ``kernels`` have different lengths. + + Warns + ----- + UserWarning + When ``domain`` is given but not used. + + See Also + -------- + bayesquad : Computes the integral using an acquisition policy. + bayesquad_from_data : Computes the integral :math:`F` using a given dataset of + nodes and function evaluations. + + Examples + -------- + >>> import numpy as np + >>> domain = (0, 1) + >>> nodes = np.linspace(0, 1, 15)[:, None] + >>> fun_evals = nodes.reshape(-1, ) + >>> F, info = bayesquad_from_data(nodes=nodes, fun_evals=fun_evals, domain=domain) + >>> print(F.mean) + 0.5 + """ - max_level = len(nodes) - if len(fun_diff_evals) != max_level or len(kernels) != max_level: + n_level = np.max([len(nodes), len(fun_diff_evals), len(kernels)]) + if len(nodes) == 1: + nodes = n_level * (nodes[0],) + if len(kernels) == 1: + kernels = n_level * (kernels[0],) + if len(fun_diff_evals) != n_level or len(kernels) != n_level: raise ValueError( "You must provide an equal number of kernels, vectors of " "function evaluations and sets of nodes." @@ -302,7 +379,7 @@ def bayesquad_multilevel( integer_belief = Normal(mean=0.0, cov=0.0) infos = () - for l in range(max_level): + for l in range(n_level): integer_belief_l, info_l = bayesquad_from_data( nodes=nodes[l], fun_evals=fun_diff_evals[l], From 4a1cea9d4d561b1f44b6e27785c32c47be6b2f66 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Mon, 5 Dec 2022 21:44:33 +0200 Subject: [PATCH 05/34] Add multilevel example for docstring --- src/probnum/quad/_bayesquad.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 7f96b7185..66ee56df6 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -156,11 +156,19 @@ def bayesquad( >>> input_dim = 1 >>> domain = (0, 1) - >>> def fun(x): - ... return x.reshape(-1, ) - >>> F, info = bayesquad(fun, input_dim, domain=domain, rng=np.random.default_rng(0)) + >>> n_level = 6 + >>> def fun(x, l): + ... return x.reshape(-1, ) / (l + 1.0) + >>> nodes = () + >>> fun_diff_evals = () + >>> for l in range(n_level): + ... n_l = 2*l + 1 + ... nodes += (np.reshape(np.linspace(0, 1, n_l), (n_l, input_dim)),) + ... fun_diff_evals += (np.reshape(fun(nodes[l], l), (n_l,)),) + >>> F, info = bayesquad_multilevel(nodes=nodes, fun_diff_evals=fun_diff_evals, + ... domain=domain) >>> print(F.mean) - 0.5 + 0.7252421350019139 """ input_dim, domain, measure = _check_domain_measure_compatibility( @@ -366,6 +374,8 @@ def bayesquad_multilevel( 0.5 """ + if kernels is None: + kernels = (None,) n_level = np.max([len(nodes), len(fun_diff_evals), len(kernels)]) if len(nodes) == 1: nodes = n_level * (nodes[0],) From b5f8c1a7f649e63eaa8b0c18b5c412f9b271c2ba Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Mon, 5 Dec 2022 21:55:48 +0200 Subject: [PATCH 06/34] Put the example to the correct place --- src/probnum/quad/_bayesquad.py | 35 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 66ee56df6..9f3d857d2 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -153,22 +153,13 @@ def bayesquad( Examples -------- >>> import numpy as np - >>> input_dim = 1 >>> domain = (0, 1) - >>> n_level = 6 - >>> def fun(x, l): - ... return x.reshape(-1, ) / (l + 1.0) - >>> nodes = () - >>> fun_diff_evals = () - >>> for l in range(n_level): - ... n_l = 2*l + 1 - ... nodes += (np.reshape(np.linspace(0, 1, n_l), (n_l, input_dim)),) - ... fun_diff_evals += (np.reshape(fun(nodes[l], l), (n_l,)),) - >>> F, info = bayesquad_multilevel(nodes=nodes, fun_diff_evals=fun_diff_evals, - ... domain=domain) + >>> def fun(x): + ... return x.reshape(-1, ) + >>> F, info = bayesquad(fun, input_dim, domain=domain, rng=np.random.default_rng(0)) >>> print(F.mean) - 0.7252421350019139 + 0.5 """ input_dim, domain, measure = _check_domain_measure_compatibility( @@ -366,12 +357,22 @@ def bayesquad_multilevel( Examples -------- >>> import numpy as np + + >>> input_dim = 1 >>> domain = (0, 1) - >>> nodes = np.linspace(0, 1, 15)[:, None] - >>> fun_evals = nodes.reshape(-1, ) - >>> F, info = bayesquad_from_data(nodes=nodes, fun_evals=fun_evals, domain=domain) + >>> n_level = 6 + >>> def fun(x, l): + ... return x.reshape(-1, ) / (l + 1.0) + >>> nodes = () + >>> fun_diff_evals = () + >>> for l in range(n_level): + ... n_l = 2*l + 1 + ... nodes += (np.reshape(np.linspace(0, 1, n_l), (n_l, input_dim)),) + ... fun_diff_evals += (np.reshape(fun(nodes[l], l), (n_l,)),) + >>> F, info = bayesquad_multilevel(nodes=nodes, fun_diff_evals=fun_diff_evals, + ... domain=domain) >>> print(F.mean) - 0.5 + 0.7252421350019139 """ if kernels is None: From c3ea2a24174dc27a7eee2356ec70e93da0d519d4 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Mon, 5 Dec 2022 22:27:30 +0200 Subject: [PATCH 07/34] Add two basic multilevel BQ tests --- tests/test_quad/test_bayesquad/test_bq.py | 72 ++++++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index 79900fe5d..65301276b 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -4,8 +4,8 @@ import pytest from scipy.integrate import quad as scipyquad -from probnum.quad import bayesquad, bayesquad_from_data -from probnum.quad.integration_measures import LebesgueMeasure +from probnum.quad import bayesquad, bayesquad_from_data, bayesquad_multilevel +from probnum.quad.integration_measures import LebesgueMeasure, GaussianMeasure from probnum.quad.kernel_embeddings import KernelEmbedding from probnum.randvars import Normal @@ -219,3 +219,71 @@ def test_zero_function_gives_zero_variance_with_mle(rng): ) assert bq_integral1.var == 0.0 assert bq_integral2.var == 0.0 + + +def test_multilevel_bq_equals_bq_with_trivial_data_1d(): + """Test that multilevel BQ equals BQ when all but one level are given non-zero + function evaluations for 1D data.""" + input_dim = 1 + n_level = 5 + domain = (0, 3.3) + nodes = () + for l in range(n_level): + n_l = 2 * l + 1 + nodes += (np.reshape(np.linspace(0, 1, n_l), (n_l, input_dim)),) + for i in range(n_level): + jitter = 1e-5 * (i + 1.0) + fun_diff_evals = [np.zeros(shape=(len(xs),)) for xs in nodes] + fun_evals = np.reshape(nodes[i] ** (2 + 0.3 * l) + 1.2, (len(nodes[i]),)) + fun_diff_evals[i] = fun_evals + mlbq_integral, _ = bayesquad_multilevel( + nodes=nodes, + fun_diff_evals=fun_diff_evals, + domain=domain, + options=dict(jitter=jitter), + ) + bq_integral, _ = bayesquad_from_data( + nodes=nodes[i], + fun_evals=fun_evals, + domain=domain, + options=dict(jitter=jitter), + ) + assert mlbq_integral.mean == bq_integral.mean + assert mlbq_integral.cov == bq_integral.cov + + +def test_multilevel_bq_equals_bq_with_trivial_data_2d(): + """Test that multilevel BQ equals BQ when all but one level are given non-zero + function evaluations for 2D data.""" + input_dim = 2 + n_level = 5 + measure = GaussianMeasure(np.full((input_dim,), 0.2), cov=0.6 * np.eye(input_dim)) + nodes = () + for l in range(n_level): + n_gh_l = l + 1 # Be very careful about increasing this too much + nodes_l, _ = gauss_hermite_tensor( + n_points=n_gh_l, input_dim=input_dim, mean=measure.mean, cov=measure.cov + ) + nodes += (nodes_l,) + for i in range(n_level): + jitter = 1e-5 * (i + 1.0) + fun_diff_evals = [np.zeros(shape=(len(xs),)) for xs in nodes] + fun_evals = np.reshape( + np.sin(nodes[i][:, 0] * l) + (l + 1.0) * np.cos(nodes[i][:, 1]), + (len(nodes[i]),), + ) + fun_diff_evals[i] = fun_evals + mlbq_integral, _ = bayesquad_multilevel( + nodes=nodes, + fun_diff_evals=fun_diff_evals, + measure=measure, + options=dict(jitter=jitter), + ) + bq_integral, _ = bayesquad_from_data( + nodes=nodes[i], + fun_evals=fun_evals, + measure=measure, + options=dict(jitter=jitter), + ) + assert mlbq_integral.mean == bq_integral.mean + assert mlbq_integral.cov == bq_integral.cov From 06d67304e364d4d8ccc0c74471de1e2b5bd7efba Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Tue, 6 Dec 2022 15:05:40 +0200 Subject: [PATCH 08/34] Add input handling test for multilevel BQ --- tests/test_quad/test_bayesquad/test_bq.py | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index 65301276b..9f29e4e54 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -221,6 +221,61 @@ def test_zero_function_gives_zero_variance_with_mle(rng): assert bq_integral2.var == 0.0 +def test_multilevel_bq_input_handling(kernel, measure, rng): + """Test that inputs to multilevel BQ are handled properly.""" + n_level = 3 + fun_diff_evals_1 = () + ns_1 = (3, 7, 2) + for l in range(n_level): + fun_diff_evals_1 += (np.zeros((ns_1[l],)),) + # Only one kernel + kernels_1 = (kernel,) + nodes_full = () + for l in range(n_level): + nodes_full += (measure.sample(n_sample=ns_1[l], rng=rng),) + F, _ = bayesquad_multilevel( + nodes=nodes_full, + fun_diff_evals=fun_diff_evals_1, + kernels=kernels_1, + measure=measure, + ) + # Only one set of nodes + ns_2 = (7, 7, 7) + fun_diff_evals_2 = n_level * (np.zeros((ns_2[0],)),) + kernels_full = n_level * (kernel,) + nodes_1 = (measure.sample(n_sample=ns_2[0], rng=rng),) + F, _ = bayesquad_multilevel( + nodes=nodes_1, + fun_diff_evals=fun_diff_evals_2, + kernels=kernels_full, + measure=measure, + ) + # Only one kernel and one set of nodes + F, _ = bayesquad_multilevel( + nodes=nodes_1, + fun_diff_evals=fun_diff_evals_2, + kernels=kernels_1, + measure=measure, + ) + # Wrong number inputs should throw error + kernels_2 = (kernel, kernel) + with pytest.raises(ValueError): + F, _ = bayesquad_multilevel( + nodes=nodes_1, + fun_diff_evals=fun_diff_evals_2, + kernels=kernels_2, + measure=measure, + ) + nodes_2 = (nodes_full[0], nodes_full[1]) + with pytest.raises(ValueError): + F, _ = bayesquad_multilevel( + nodes=nodes_2, + fun_diff_evals=fun_diff_evals_2, + kernels=kernels_2, + measure=measure, + ) + + def test_multilevel_bq_equals_bq_with_trivial_data_1d(): """Test that multilevel BQ equals BQ when all but one level are given non-zero function evaluations for 1D data.""" From 7a83c1d8445d42c35f39e33a43f35da28450f46f Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Thu, 8 Dec 2022 23:35:37 +0200 Subject: [PATCH 09/34] Documentation for multilevel BQ --- src/probnum/quad/_bayesquad.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 9f3d857d2..3ee49d1ae 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -291,8 +291,19 @@ def bayesquad_multilevel( domain: Optional[DomainLike] = None, options: Optional[dict] = None, ) -> Tuple[Normal, Tuple[BQIterInfo, ...]]: - r"""Infer the value of an integral from a given set of nodes and function - evaluations. + r"""Infer the value of an integral from given sets of nodes and function + evaluations using a multilevel method. + + In multilevel Bayesian quadrature, the integral :math:`\int_\Omega f(x) d \mu(x)` + is (approximately) decomposed as a telescoping sum over :math:`L+1` levels: + + .. math:: \int_\Omega f(x) d \mu(x) \approx :math:`\int_\Omega f_0(x) d + \mu(x)` + \sum_{l=1}^L :math:`\int_\Omega [f_l(x) - f_{l-1}(x)] d \mu(x)`, + + where :math:`f_l` is an increasingly accurate but also increasingly expensive + approximation to :math:`f`. Bayesian quadrature is subsequently applied to + independently infer each of the :math:`L+1` integrals and the outputs are summed + to infer :math:`\int_\Omega f(x) d \mu(x)`. Parameters ---------- @@ -354,6 +365,10 @@ def bayesquad_multilevel( bayesquad_from_data : Computes the integral :math:`F` using a given dataset of nodes and function evaluations. + References + ---------- + .. [1] Li, K., et al., Multilevel Bayesian quadrature, *arXiv:2210.08329*, 2022. + Examples -------- >>> import numpy as np From 945aaa5ea533a6da1eff65ddef57e224f504d3f9 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Thu, 8 Dec 2022 23:37:15 +0200 Subject: [PATCH 10/34] Some linting --- src/probnum/quad/_bayesquad.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 3ee49d1ae..76ce511d4 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -8,7 +8,7 @@ """ from __future__ import annotations -from typing import Callable, Optional, Tuple, Union +from typing import Callable, Optional, Tuple import warnings import numpy as np @@ -405,11 +405,11 @@ def bayesquad_multilevel( integer_belief = Normal(mean=0.0, cov=0.0) infos = () - for l in range(n_level): + for level in range(n_level): integer_belief_l, info_l = bayesquad_from_data( - nodes=nodes[l], - fun_evals=fun_diff_evals[l], - kernel=kernels[l], + nodes=nodes[level], + fun_evals=fun_diff_evals[level], + kernel=kernels[level], measure=measure, domain=domain, options=options, From 17d0047212c09ab70ade99c364c26153f2389b31 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Fri, 9 Dec 2022 16:00:06 +0200 Subject: [PATCH 11/34] Fix formattin of a ref --- src/probnum/quad/_bayesquad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 76ce511d4..ac48599f8 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -144,7 +144,7 @@ def bayesquad( References ---------- .. [1] Briol, F.-X., et al., Probabilistic integration: A role in statistical - computation?, *Statistical Science 34.1*, 2019, 1-22, 2019 + computation?, *Statistical Science 34.1*, 2019, 1-22. .. [2] Rasmussen, C. E., and Z. Ghahramani, Bayesian Monte Carlo, *Advances in Neural Information Processing Systems*, 2003, 505-512. .. [3] Mckay et al., A Comparison of Three Methods for Selecting Values of Input From 7e1707234e54aad57bb8389726ee3df3fb19c384 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Sat, 11 Feb 2023 16:33:26 +0200 Subject: [PATCH 12/34] Update src/probnum/quad/_bayesquad.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- src/probnum/quad/_bayesquad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index c882d4b44..aaef8c1af 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -323,7 +323,7 @@ def bayesquad_multilevel( Parameters ---------- nodes - *shape=(n_level,)* -- Tuple containing the locations for each level at which + Tuple of length :math:`L+1` containing the locations for each level at which the functionn evaluations are available as ``fun_diff_evals``. Each element must be a shape=(n_eval, input_dim) ``np.ndarray``. If a tuple containing only one element is provided, it is inferred that the same nodes ``nodes[0]`` are From c495965a07512d7a327b52f311f401356d33ae7d Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Sat, 11 Feb 2023 16:33:56 +0200 Subject: [PATCH 13/34] Update src/probnum/quad/_bayesquad.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- src/probnum/quad/_bayesquad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index aaef8c1af..2374bb9bf 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -312,8 +312,8 @@ def bayesquad_multilevel( In multilevel Bayesian quadrature, the integral :math:`\int_\Omega f(x) d \mu(x)` is (approximately) decomposed as a telescoping sum over :math:`L+1` levels: - .. math:: \int_\Omega f(x) d \mu(x) \approx :math:`\int_\Omega f_0(x) d - \mu(x)` + \sum_{l=1}^L :math:`\int_\Omega [f_l(x) - f_{l-1}(x)] d \mu(x)`, + .. math:: \int_\Omega f(x) d \mu(x) \approx \int_\Omega f_0(x) d + \mu(x) + \sum_{l=1}^L \int_\Omega [f_l(x) - f_{l-1}(x)] d \mu(x), where :math:`f_l` is an increasingly accurate but also increasingly expensive approximation to :math:`f`. Bayesian quadrature is subsequently applied to From 288cde9302c7ecfdc1dcf39efded6071e3caa7b8 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Sat, 11 Feb 2023 16:34:03 +0200 Subject: [PATCH 14/34] Update src/probnum/quad/_bayesquad.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- src/probnum/quad/_bayesquad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 2374bb9bf..267c51406 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -318,7 +318,7 @@ def bayesquad_multilevel( where :math:`f_l` is an increasingly accurate but also increasingly expensive approximation to :math:`f`. Bayesian quadrature is subsequently applied to independently infer each of the :math:`L+1` integrals and the outputs are summed - to infer :math:`\int_\Omega f(x) d \mu(x)`. + to infer :math:`\int_\Omega f(x) d \mu(x)`. [1]_ Parameters ---------- From 02153caa73fde55d89ee8f26f5e03ad3370e1329 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Sat, 11 Feb 2023 16:34:28 +0200 Subject: [PATCH 15/34] Update src/probnum/quad/_bayesquad.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- src/probnum/quad/_bayesquad.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 267c51406..7b0d8aeb9 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -374,12 +374,6 @@ def bayesquad_multilevel( UserWarning When ``domain`` is given but not used. - See Also - -------- - bayesquad : Computes the integral using an acquisition policy. - bayesquad_from_data : Computes the integral :math:`F` using a given dataset of - nodes and function evaluations. - References ---------- .. [1] Li, K., et al., Multilevel Bayesian quadrature, *arXiv:2210.08329*, 2022. From 763e4bcb552355ca97f50c09415151c154e77cf3 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Sun, 12 Feb 2023 14:58:38 +0200 Subject: [PATCH 16/34] Fix things pointed out in review --- src/probnum/quad/__init__.py | 5 +-- src/probnum/quad/_bayesquad.py | 29 +++++++++--------- tests/test_quad/test_bayesquad/test_bq.py | 37 +++++++++++++---------- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/probnum/quad/__init__.py b/src/probnum/quad/__init__.py index 84173f3b7..bbaf1426d 100644 --- a/src/probnum/quad/__init__.py +++ b/src/probnum/quad/__init__.py @@ -7,15 +7,16 @@ """ from . import integration_measures, kernel_embeddings, solvers -from ._bayesquad import bayesquad, bayesquad_from_data, bayesquad_multilevel +from ._bayesquad import bayesquad, bayesquad_from_data, multilevel_bayesquad_from_data # Public classes and functions. Order is reflected in documentation. __all__ = [ "bayesquad", "bayesquad_from_data", - "bayesquad_multilevel", + "multilevel_bayesquad_from_data", ] # Set correct module paths. Corrects links and module paths in documentation. bayesquad.__module__ = "probnum.quad" bayesquad_from_data.__module__ = "probnum.quad" +multilevel_bayesquad_from_data.__module__ = "probnum.quad" diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 7b0d8aeb9..4d6d858f6 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -298,7 +298,7 @@ def bayesquad_from_data( return integral_belief, info -def bayesquad_multilevel( +def multilevel_bayesquad_from_data( nodes: Tuple[np.ndarray, ...], fun_diff_evals: Tuple[np.ndarray, ...], kernels: Optional[Tuple[Kernel, ...]] = None, @@ -316,9 +316,12 @@ def bayesquad_multilevel( \mu(x) + \sum_{l=1}^L \int_\Omega [f_l(x) - f_{l-1}(x)] d \mu(x), where :math:`f_l` is an increasingly accurate but also increasingly expensive - approximation to :math:`f`. Bayesian quadrature is subsequently applied to - independently infer each of the :math:`L+1` integrals and the outputs are summed - to infer :math:`\int_\Omega f(x) d \mu(x)`. [1]_ + approximation to :math:`f`. It is not necessary that the highest level approximation + :math:`f_L` be equal to :math:`f`. + + Bayesian quadrature is subsequently applied to independently infer each of the + :math:`L+1` integrals and the outputs are summed to infer + :math:`\int_\Omega f(x) d \mu(x)`. [1]_ Parameters ---------- @@ -334,9 +337,8 @@ def bayesquad_multilevel( shape=(n_eval,) ``np.ndarray``. The zeroth element contains the evaluations of :math:`f_0`. kernels - Tuple containing the kernels used for the GP model at each level. If a tuple - containing only one element is provided, it is inferred that the same kernel - ``kernels[0]`` is used on every level. Defaults to the ``ExpQuad`` kernel. + Tuple containing the kernels used for the GP model at each level. The user must + input all kernels. Defaults to the ``ExpQuad`` kernel for each level. measure The integration measure. Defaults to the Lebesgue measure. domain @@ -376,7 +378,7 @@ def bayesquad_multilevel( References ---------- - .. [1] Li, K., et al., Multilevel Bayesian quadrature, *arXiv:2210.08329*, 2022. + .. [1] Li, K., et al., Multilevel Bayesian quadrature, AISTATS, 2023. Examples -------- @@ -393,19 +395,18 @@ def bayesquad_multilevel( ... n_l = 2*l + 1 ... nodes += (np.reshape(np.linspace(0, 1, n_l), (n_l, input_dim)),) ... fun_diff_evals += (np.reshape(fun(nodes[l], l), (n_l,)),) - >>> F, info = bayesquad_multilevel(nodes=nodes, fun_diff_evals=fun_diff_evals, - ... domain=domain) + >>> F, info = multilevel_bayesquad_from_data(nodes=nodes, + ... fun_diff_evals=fun_diff_evals, + ... domain=domain) >>> print(F.mean) 0.7252421350019139 """ + n_level = len(fun_diff_evals) if kernels is None: - kernels = (None,) - n_level = np.max([len(nodes), len(fun_diff_evals), len(kernels)]) + kernels = n_level * (None,) if len(nodes) == 1: nodes = n_level * (nodes[0],) - if len(kernels) == 1: - kernels = n_level * (kernels[0],) if len(fun_diff_evals) != n_level or len(kernels) != n_level: raise ValueError( "You must provide an equal number of kernels, vectors of " diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index 9f29e4e54..b60d1b319 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -4,7 +4,7 @@ import pytest from scipy.integrate import quad as scipyquad -from probnum.quad import bayesquad, bayesquad_from_data, bayesquad_multilevel +from probnum.quad import bayesquad, bayesquad_from_data, multilevel_bayesquad_from_data from probnum.quad.integration_measures import LebesgueMeasure, GaussianMeasure from probnum.quad.kernel_embeddings import KernelEmbedding from probnum.randvars import Normal @@ -88,8 +88,8 @@ def test_integral_values_x2_gaussian(kernel, measure, input_dim, scale_estimatio """Test numerical integration of x**2 in higher dimensions.""" # pylint: disable=invalid-name c = np.linspace(0.1, 2.2, input_dim) - fun = lambda x: np.sum(c * x**2, 1) - true_integral = np.sum(c * (measure.mean**2 + np.diag(measure.cov))) + fun = lambda x: np.sum(c * x ** 2, 1) + true_integral = np.sum(c * (measure.mean ** 2 + np.diag(measure.cov))) n_gh = 8 # Be very careful about increasing this - yields huge kernel matrices nodes, _ = gauss_hermite_tensor( n_points=n_gh, input_dim=input_dim, mean=measure.mean, cov=measure.cov @@ -221,46 +221,51 @@ def test_zero_function_gives_zero_variance_with_mle(rng): assert bq_integral2.var == 0.0 -def test_multilevel_bq_input_handling(kernel, measure, rng): +def test_multilevel_bayesquad_from_data_input_handling(kernel, measure, rng): """Test that inputs to multilevel BQ are handled properly.""" n_level = 3 fun_diff_evals_1 = () ns_1 = (3, 7, 2) for l in range(n_level): fun_diff_evals_1 += (np.zeros((ns_1[l],)),) - # Only one kernel - kernels_1 = (kernel,) + # No kernels given so defaults to ExpQuad nodes_full = () for l in range(n_level): nodes_full += (measure.sample(n_sample=ns_1[l], rng=rng),) - F, _ = bayesquad_multilevel( + F, infos = multilevel_bayesquad_from_data( nodes=nodes_full, fun_diff_evals=fun_diff_evals_1, - kernels=kernels_1, measure=measure, ) + assert isinstance(F, Normal) + assert len(infos) == n_level # Only one set of nodes + kernels_1 = n_level * (kernel,) ns_2 = (7, 7, 7) fun_diff_evals_2 = n_level * (np.zeros((ns_2[0],)),) kernels_full = n_level * (kernel,) nodes_1 = (measure.sample(n_sample=ns_2[0], rng=rng),) - F, _ = bayesquad_multilevel( + F, infos = multilevel_bayesquad_from_data( nodes=nodes_1, fun_diff_evals=fun_diff_evals_2, kernels=kernels_full, measure=measure, ) + assert isinstance(F, Normal) + assert len(infos) == n_level # Only one kernel and one set of nodes - F, _ = bayesquad_multilevel( + F, infos = multilevel_bayesquad_from_data( nodes=nodes_1, fun_diff_evals=fun_diff_evals_2, kernels=kernels_1, measure=measure, ) + assert isinstance(F, Normal) + assert len(infos) == n_level # Wrong number inputs should throw error kernels_2 = (kernel, kernel) with pytest.raises(ValueError): - F, _ = bayesquad_multilevel( + _, _ = multilevel_bayesquad_from_data( nodes=nodes_1, fun_diff_evals=fun_diff_evals_2, kernels=kernels_2, @@ -268,7 +273,7 @@ def test_multilevel_bq_input_handling(kernel, measure, rng): ) nodes_2 = (nodes_full[0], nodes_full[1]) with pytest.raises(ValueError): - F, _ = bayesquad_multilevel( + _, _ = multilevel_bayesquad_from_data( nodes=nodes_2, fun_diff_evals=fun_diff_evals_2, kernels=kernels_2, @@ -276,7 +281,7 @@ def test_multilevel_bq_input_handling(kernel, measure, rng): ) -def test_multilevel_bq_equals_bq_with_trivial_data_1d(): +def test_multilevel_bayesquad_from_data_equals_bq_with_trivial_data_1d(): """Test that multilevel BQ equals BQ when all but one level are given non-zero function evaluations for 1D data.""" input_dim = 1 @@ -291,7 +296,7 @@ def test_multilevel_bq_equals_bq_with_trivial_data_1d(): fun_diff_evals = [np.zeros(shape=(len(xs),)) for xs in nodes] fun_evals = np.reshape(nodes[i] ** (2 + 0.3 * l) + 1.2, (len(nodes[i]),)) fun_diff_evals[i] = fun_evals - mlbq_integral, _ = bayesquad_multilevel( + mlbq_integral, _ = multilevel_bayesquad_from_data( nodes=nodes, fun_diff_evals=fun_diff_evals, domain=domain, @@ -307,7 +312,7 @@ def test_multilevel_bq_equals_bq_with_trivial_data_1d(): assert mlbq_integral.cov == bq_integral.cov -def test_multilevel_bq_equals_bq_with_trivial_data_2d(): +def test_multilevel_bayesquad_from_data_equals_bq_with_trivial_data_2d(): """Test that multilevel BQ equals BQ when all but one level are given non-zero function evaluations for 2D data.""" input_dim = 2 @@ -328,7 +333,7 @@ def test_multilevel_bq_equals_bq_with_trivial_data_2d(): (len(nodes[i]),), ) fun_diff_evals[i] = fun_evals - mlbq_integral, _ = bayesquad_multilevel( + mlbq_integral, _ = multilevel_bayesquad_from_data( nodes=nodes, fun_diff_evals=fun_diff_evals, measure=measure, From 9c0d9216c9a6cb53987636c42ace94a857ecd31d Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Sun, 12 Feb 2023 15:16:36 +0200 Subject: [PATCH 17/34] Try to fix some failed checks --- src/probnum/quad/_bayesquad.py | 6 +++--- tests/test_quad/test_bayesquad/test_bq.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 4d6d858f6..4ef7a6b9b 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -395,9 +395,9 @@ def multilevel_bayesquad_from_data( ... n_l = 2*l + 1 ... nodes += (np.reshape(np.linspace(0, 1, n_l), (n_l, input_dim)),) ... fun_diff_evals += (np.reshape(fun(nodes[l], l), (n_l,)),) - >>> F, info = multilevel_bayesquad_from_data(nodes=nodes, - ... fun_diff_evals=fun_diff_evals, - ... domain=domain) + >>> F, infos = multilevel_bayesquad_from_data(nodes=nodes, + ... fun_diff_evals=fun_diff_evals, + ... domain=domain) >>> print(F.mean) 0.7252421350019139 """ diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index b60d1b319..dba235709 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -5,7 +5,7 @@ from scipy.integrate import quad as scipyquad from probnum.quad import bayesquad, bayesquad_from_data, multilevel_bayesquad_from_data -from probnum.quad.integration_measures import LebesgueMeasure, GaussianMeasure +from probnum.quad.integration_measures import GaussianMeasure, LebesgueMeasure from probnum.quad.kernel_embeddings import KernelEmbedding from probnum.randvars import Normal @@ -88,8 +88,8 @@ def test_integral_values_x2_gaussian(kernel, measure, input_dim, scale_estimatio """Test numerical integration of x**2 in higher dimensions.""" # pylint: disable=invalid-name c = np.linspace(0.1, 2.2, input_dim) - fun = lambda x: np.sum(c * x ** 2, 1) - true_integral = np.sum(c * (measure.mean ** 2 + np.diag(measure.cov))) + fun = lambda x: np.sum(c * x**2, 1) + true_integral = np.sum(c * (measure.mean**2 + np.diag(measure.cov))) n_gh = 8 # Be very careful about increasing this - yields huge kernel matrices nodes, _ = gauss_hermite_tensor( n_points=n_gh, input_dim=input_dim, mean=measure.mean, cov=measure.cov From e7032677cc11edd0233efd94b9e36d9a7d68899f Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:40:22 +0200 Subject: [PATCH 18/34] Update src/probnum/quad/_bayesquad.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- src/probnum/quad/_bayesquad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 4ef7a6b9b..9075fa76e 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -332,7 +332,7 @@ def multilevel_bayesquad_from_data( one element is provided, it is inferred that the same nodes ``nodes[0]`` are used on every level. fun_diff_evals - *shape=(n_level,)* --- Tuple containing the evaluations of :math:`f_l - f_{l-1}` + Tuple of length :math:`L+1` containing the evaluations of :math:`f_l - f_{l-1}` for each level at the nodes provided in ``nodes``. Each element must be a shape=(n_eval,) ``np.ndarray``. The zeroth element contains the evaluations of :math:`f_0`. From c65ff73fc19a96aef41a0a0108182518ceb64cbf Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:44:54 +0200 Subject: [PATCH 19/34] Tuple length documentation --- src/probnum/quad/_bayesquad.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 9075fa76e..213c6909c 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -337,8 +337,9 @@ def multilevel_bayesquad_from_data( shape=(n_eval,) ``np.ndarray``. The zeroth element contains the evaluations of :math:`f_0`. kernels - Tuple containing the kernels used for the GP model at each level. The user must - input all kernels. Defaults to the ``ExpQuad`` kernel for each level. + Tuple of length :math:`L+1` containing the kernels used for the GP model at each + level. The user must input all kernels. Defaults to the ``ExpQuad`` kernel for + each level. measure The integration measure. Defaults to the Lebesgue measure. domain @@ -435,7 +436,6 @@ def _check_domain_measure_compatibility( domain: Optional[DomainLike], measure: Optional[IntegrationMeasure], ) -> Tuple[int, Optional[DomainType], IntegrationMeasure]: - input_dim = int(input_dim) # Neither domain nor measure given From c060fce0c1a88215d9e1cd50b7dcde814906aa1a Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:46:04 +0200 Subject: [PATCH 20/34] Update src/probnum/quad/_bayesquad.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- src/probnum/quad/_bayesquad.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 9075fa76e..2e81b8c84 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -407,10 +407,11 @@ def multilevel_bayesquad_from_data( kernels = n_level * (None,) if len(nodes) == 1: nodes = n_level * (nodes[0],) - if len(fun_diff_evals) != n_level or len(kernels) != n_level: + if not (len(nodes) == len(fun_diff_evals) == len(kernels)): raise ValueError( - "You must provide an equal number of kernels, vectors of " - "function evaluations and sets of nodes." + f"You must provide an equal number of kernels ({(len(kernels))}), " + f"vectors of function evaluations ({len(fun_diff_evals)}) " + f"and sets of nodes ({len(nodes)})." ) integer_belief = Normal(mean=0.0, cov=0.0) From f788737638e7d79033de2690109c936b475d43f9 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:47:07 +0200 Subject: [PATCH 21/34] Update src/probnum/quad/_bayesquad.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- src/probnum/quad/_bayesquad.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 2e81b8c84..75b728afe 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -337,8 +337,9 @@ def multilevel_bayesquad_from_data( shape=(n_eval,) ``np.ndarray``. The zeroth element contains the evaluations of :math:`f_0`. kernels - Tuple containing the kernels used for the GP model at each level. The user must - input all kernels. Defaults to the ``ExpQuad`` kernel for each level. + Tuple of length :math:`L+1` containing the kernels used for the GP model at each + level. See **Notes** for further details. Defaults to the ``ExpQuad`` kernel for + each level. measure The integration measure. Defaults to the Lebesgue measure. domain From 31ce405c15d8caa8e483e3c208f8395eede6c030 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:47:58 +0200 Subject: [PATCH 22/34] Update src/probnum/quad/_bayesquad.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- src/probnum/quad/_bayesquad.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 75b728afe..d4830ae38 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -377,6 +377,11 @@ def multilevel_bayesquad_from_data( UserWarning When ``domain`` is given but not used. + Notes + ----- + The tuple of kernels provided by the ``kernels`` parameter must contain distinct + kernel instances, i.e., ``kernels[i] is kernel[j]`` must return ``False`` for any + :math:`i\neq j`. References ---------- .. [1] Li, K., et al., Multilevel Bayesian quadrature, AISTATS, 2023. From 9597e800bb72e2d6e0ab6e5d99cb28584fc74656 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:48:18 +0200 Subject: [PATCH 23/34] Update src/probnum/quad/_bayesquad.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- src/probnum/quad/_bayesquad.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index d4830ae38..201ae3e46 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -389,7 +389,6 @@ def multilevel_bayesquad_from_data( Examples -------- >>> import numpy as np - >>> input_dim = 1 >>> domain = (0, 1) >>> n_level = 6 From 4ea2ccfc941672700021919013e8bb7959f6c021 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:50:04 +0200 Subject: [PATCH 24/34] Update tests/test_quad/test_bayesquad/test_bq.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- tests/test_quad/test_bayesquad/test_bq.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index dba235709..7463fb3ae 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -288,9 +288,7 @@ def test_multilevel_bayesquad_from_data_equals_bq_with_trivial_data_1d(): n_level = 5 domain = (0, 3.3) nodes = () - for l in range(n_level): - n_l = 2 * l + 1 - nodes += (np.reshape(np.linspace(0, 1, n_l), (n_l, input_dim)),) + nodes = [np.linspace(0, 1, 2 * l + 1)[:, None] for l in range(n_level)] for i in range(n_level): jitter = 1e-5 * (i + 1.0) fun_diff_evals = [np.zeros(shape=(len(xs),)) for xs in nodes] From c9d405aa92b06d7f0fc5c0ba02f683ab2b6c0bc6 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:50:44 +0200 Subject: [PATCH 25/34] Update tests/test_quad/test_bayesquad/test_bq.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- tests/test_quad/test_bayesquad/test_bq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index 7463fb3ae..27949c996 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -292,7 +292,7 @@ def test_multilevel_bayesquad_from_data_equals_bq_with_trivial_data_1d(): for i in range(n_level): jitter = 1e-5 * (i + 1.0) fun_diff_evals = [np.zeros(shape=(len(xs),)) for xs in nodes] - fun_evals = np.reshape(nodes[i] ** (2 + 0.3 * l) + 1.2, (len(nodes[i]),)) + fun_evals = nodes[i][:, 0] ** (2 + 0.3 * i) + 1.2 fun_diff_evals[i] = fun_evals mlbq_integral, _ = multilevel_bayesquad_from_data( nodes=nodes, From 5cb3a7e76f088c617e4865e68c35539c5e52b04f Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:52:22 +0200 Subject: [PATCH 26/34] Update tests/test_quad/test_bayesquad/test_bq.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- tests/test_quad/test_bayesquad/test_bq.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index 27949c996..4bb56a95c 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -316,12 +316,8 @@ def test_multilevel_bayesquad_from_data_equals_bq_with_trivial_data_2d(): input_dim = 2 n_level = 5 measure = GaussianMeasure(np.full((input_dim,), 0.2), cov=0.6 * np.eye(input_dim)) - nodes = () - for l in range(n_level): - n_gh_l = l + 1 # Be very careful about increasing this too much - nodes_l, _ = gauss_hermite_tensor( - n_points=n_gh_l, input_dim=input_dim, mean=measure.mean, cov=measure.cov - ) + _gh = gauss_hermite_tensor + nodes = [_gh(l + 1, input_dim, measure.mean, measure.cov)[0] for l in range(n_level)] nodes += (nodes_l,) for i in range(n_level): jitter = 1e-5 * (i + 1.0) From 366972cbefe958fd23110d5d942b9153ef97cc0e Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:52:42 +0200 Subject: [PATCH 27/34] Update tests/test_quad/test_bayesquad/test_bq.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- tests/test_quad/test_bayesquad/test_bq.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index 4bb56a95c..fc6e1c3c2 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -322,10 +322,7 @@ def test_multilevel_bayesquad_from_data_equals_bq_with_trivial_data_2d(): for i in range(n_level): jitter = 1e-5 * (i + 1.0) fun_diff_evals = [np.zeros(shape=(len(xs),)) for xs in nodes] - fun_evals = np.reshape( - np.sin(nodes[i][:, 0] * l) + (l + 1.0) * np.cos(nodes[i][:, 1]), - (len(nodes[i]),), - ) + fun_evals = np.sin(nodes[i][:, 0] * i) + (i + 1.0) * np.cos(nodes[i][:, 1]) fun_diff_evals[i] = fun_evals mlbq_integral, _ = multilevel_bayesquad_from_data( nodes=nodes, From 24856c8837724c274377587ca2e8a175e27a83a2 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:52:55 +0200 Subject: [PATCH 28/34] Update tests/test_quad/test_bayesquad/test_bq.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- tests/test_quad/test_bayesquad/test_bq.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index fc6e1c3c2..762fa938a 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -325,8 +325,8 @@ def test_multilevel_bayesquad_from_data_equals_bq_with_trivial_data_2d(): fun_evals = np.sin(nodes[i][:, 0] * i) + (i + 1.0) * np.cos(nodes[i][:, 1]) fun_diff_evals[i] = fun_evals mlbq_integral, _ = multilevel_bayesquad_from_data( - nodes=nodes, - fun_diff_evals=fun_diff_evals, + nodes=tuple(nodes), + fun_diff_evals=tuple(fun_diff_evals), measure=measure, options=dict(jitter=jitter), ) From 45f9c52db5d4b9a58aceca9a2cc4c1f4733f3891 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:53:07 +0200 Subject: [PATCH 29/34] Update tests/test_quad/test_bayesquad/test_bq.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- tests/test_quad/test_bayesquad/test_bq.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index 762fa938a..912aab874 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -295,8 +295,8 @@ def test_multilevel_bayesquad_from_data_equals_bq_with_trivial_data_1d(): fun_evals = nodes[i][:, 0] ** (2 + 0.3 * i) + 1.2 fun_diff_evals[i] = fun_evals mlbq_integral, _ = multilevel_bayesquad_from_data( - nodes=nodes, - fun_diff_evals=fun_diff_evals, + nodes=tuple(nodes), + fun_diff_evals=tuple(fun_diff_evals), domain=domain, options=dict(jitter=jitter), ) From c56604d86efbb0b5d5a1ffe1d593c1c0e013e668 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 19:53:26 +0200 Subject: [PATCH 30/34] Update tests/test_quad/test_bayesquad/test_bq.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- tests/test_quad/test_bayesquad/test_bq.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index 912aab874..b647d0915 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -224,14 +224,10 @@ def test_zero_function_gives_zero_variance_with_mle(rng): def test_multilevel_bayesquad_from_data_input_handling(kernel, measure, rng): """Test that inputs to multilevel BQ are handled properly.""" n_level = 3 - fun_diff_evals_1 = () ns_1 = (3, 7, 2) - for l in range(n_level): - fun_diff_evals_1 += (np.zeros((ns_1[l],)),) - # No kernels given so defaults to ExpQuad - nodes_full = () - for l in range(n_level): - nodes_full += (measure.sample(n_sample=ns_1[l], rng=rng),) + fun_diff_evals_1 = tuple([np.zeros(ns_1[l]) for l in range(n_level)]) + nodes_full = tuple([measure.sample((ns_1[l]), rng=rng) for l in range(n_level)]) + F, infos = multilevel_bayesquad_from_data( nodes=nodes_full, fun_diff_evals=fun_diff_evals_1, From 39645f985791d8b97ef55a762fba0db845a609ed Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 20:04:54 +0200 Subject: [PATCH 31/34] Update tests/test_quad/test_bayesquad/test_bq.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- tests/test_quad/test_bayesquad/test_bq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index b647d0915..fdb287f0c 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -221,7 +221,7 @@ def test_zero_function_gives_zero_variance_with_mle(rng): assert bq_integral2.var == 0.0 -def test_multilevel_bayesquad_from_data_input_handling(kernel, measure, rng): +def test_multilevel_bayesquad_from_data_output_types_and_shapes(kernel, measure, rng): """Test that inputs to multilevel BQ are handled properly.""" n_level = 3 ns_1 = (3, 7, 2) From a67506b4ebda558fc8aacd37e803c8313ecf0d67 Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 20:06:55 +0200 Subject: [PATCH 32/34] Update src/probnum/quad/_bayesquad.py Co-authored-by: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com> --- src/probnum/quad/_bayesquad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 201ae3e46..28ef33d24 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -403,8 +403,8 @@ def multilevel_bayesquad_from_data( >>> F, infos = multilevel_bayesquad_from_data(nodes=nodes, ... fun_diff_evals=fun_diff_evals, ... domain=domain) - >>> print(F.mean) - 0.7252421350019139 + >>> print(np.round(F.mean, 4)) + 0.7252 """ n_level = len(fun_diff_evals) From 7258d32574a87fb7b13dbef660116e87e1873eaf Mon Sep 17 00:00:00 2001 From: Toni Karvonen Date: Wed, 15 Feb 2023 20:33:14 +0200 Subject: [PATCH 33/34] Try to make pylint happy --- tests/test_quad/test_bayesquad/test_bq.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index aff3a5541..77ef52d4c 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -226,8 +226,8 @@ def test_multilevel_bayesquad_from_data_output_types_and_shapes(kernel, measure, """Test that inputs to multilevel BQ are handled properly.""" n_level = 3 ns_1 = (3, 7, 2) - fun_diff_evals_1 = tuple([np.zeros(ns_1[l]) for l in range(n_level)]) - nodes_full = tuple([measure.sample((ns_1[l]), rng=rng) for l in range(n_level)]) + fun_diff_evals_1 = tuple(np.zeros(ns_1[l]) for l in range(n_level)) + nodes_full = tuple(measure.sample((ns_1[l]), rng=rng) for l in range(n_level)) F, infos = multilevel_bayesquad_from_data( nodes=nodes_full, @@ -266,7 +266,7 @@ def test_multilevel_bayesquad_from_data_wrong_inputs(kernel, measure, rng): kernels=kernels, measure=measure, ) - nodes_2 = tuple([measure.sample((ns[l]), rng=rng) for l in range(2)]) + nodes_2 = tuple(measure.sample((ns[l]), rng=rng) for l in range(2)) with pytest.raises(ValueError): _, _ = multilevel_bayesquad_from_data( nodes=nodes_2, From be1fa1086d2088a060d08d27e490c36259a58f78 Mon Sep 17 00:00:00 2001 From: Maren Mahsereci Date: Thu, 16 Feb 2023 12:34:43 +0100 Subject: [PATCH 34/34] some more minor changes --- src/probnum/quad/_bayesquad.py | 3 +- tests/test_quad/test_bayesquad/test_bq.py | 91 +++++++++++++++-------- 2 files changed, 62 insertions(+), 32 deletions(-) diff --git a/src/probnum/quad/_bayesquad.py b/src/probnum/quad/_bayesquad.py index 67ee9daac..c888fcbfc 100644 --- a/src/probnum/quad/_bayesquad.py +++ b/src/probnum/quad/_bayesquad.py @@ -332,7 +332,7 @@ def multilevel_bayesquad_from_data( one element is provided, it is inferred that the same nodes ``nodes[0]`` are used on every level. fun_diff_evals - Tuple of length :math:`L+1` containing the evaluations of :math:`f_l - f_{l-1}` + Tuple of length :math:`L+1` containing the evaluations of :math:`f_l - f_{l-1}` for each level at the nodes provided in ``nodes``. Each element must be a shape=(n_eval,) ``np.ndarray``. The zeroth element contains the evaluations of :math:`f_0`. @@ -382,6 +382,7 @@ def multilevel_bayesquad_from_data( The tuple of kernels provided by the ``kernels`` parameter must contain distinct kernel instances, i.e., ``kernels[i] is kernel[j]`` must return ``False`` for any :math:`i\neq j`. + References ---------- .. [1] Li, K., et al., Multilevel Bayesian quadrature, AISTATS, 2023. diff --git a/tests/test_quad/test_bayesquad/test_bq.py b/tests/test_quad/test_bayesquad/test_bq.py index 77ef52d4c..5365e68da 100644 --- a/tests/test_quad/test_bayesquad/test_bq.py +++ b/tests/test_quad/test_bayesquad/test_bq.py @@ -223,53 +223,84 @@ def test_zero_function_gives_zero_variance_with_mle(rng): def test_multilevel_bayesquad_from_data_output_types_and_shapes(kernel, measure, rng): - """Test that inputs to multilevel BQ are handled properly.""" - n_level = 3 + """Test correct output for different inputs to multilevel BQ.""" + + # full set of nodes ns_1 = (3, 7, 2) - fun_diff_evals_1 = tuple(np.zeros(ns_1[l]) for l in range(n_level)) - nodes_full = tuple(measure.sample((ns_1[l]), rng=rng) for l in range(n_level)) + n_level_1 = len(ns_1) + fun_diff_evals_1 = tuple(np.zeros(ns_1[l]) for l in range(n_level_1)) + nodes_full = tuple(measure.sample((ns_1[l]), rng=rng) for l in range(n_level_1)) + # i) default kernel F, infos = multilevel_bayesquad_from_data( nodes=nodes_full, fun_diff_evals=fun_diff_evals_1, measure=measure, ) assert isinstance(F, Normal) - assert len(infos) == n_level - # Only one set of nodes - kernels_1 = tuple(copy.deepcopy(kernel) for l in range(n_level)) - ns_2 = (7, 7, 7) - fun_diff_evals_2 = n_level * (np.zeros((ns_2[0],)),) - kernels_full = n_level * (kernel,) + assert len(infos) == n_level_1 + + # ii) full kernel list + kernels_full_1 = tuple(copy.deepcopy(kernel) for _ in range(n_level_1)) + F, infos = multilevel_bayesquad_from_data( + nodes=nodes_full, + fun_diff_evals=fun_diff_evals_1, + kernels=kernels_full_1, + measure=measure, + ) + assert isinstance(F, Normal) + assert len(infos) == n_level_1 + + # one set of nodes + n_level_2 = 3 + ns_2 = n_level_2 * (7,) + fun_diff_evals_2 = tuple(np.zeros(ns_2[l]) for l in range(n_level_2)) nodes_1 = (measure.sample(n_sample=ns_2[0], rng=rng),) + + # i) default kernel + F, infos = multilevel_bayesquad_from_data( + nodes=nodes_1, + fun_diff_evals=fun_diff_evals_2, + measure=measure, + ) + assert isinstance(F, Normal) + assert len(infos) == n_level_2 + + # ii) full kernel list + kernels_full_2 = tuple(copy.deepcopy(kernel) for _ in range(n_level_2)) F, infos = multilevel_bayesquad_from_data( nodes=nodes_1, fun_diff_evals=fun_diff_evals_2, - kernels=kernels_full, + kernels=kernels_full_2, measure=measure, ) assert isinstance(F, Normal) - assert len(infos) == n_level + assert len(infos) == n_level_2 def test_multilevel_bayesquad_from_data_wrong_inputs(kernel, measure, rng): """Tests that wrong number inputs to multilevel BQ throw errors.""" - n_level = 5 ns = (3, 7, 11) - nodes_1 = (measure.sample(n_sample=ns[0], rng=rng),) - fun_diff_evals = n_level * (np.zeros((ns[0],)),) - kernels = (kernel, kernel) + n_level = len(ns) + fun_diff_evals = tuple(np.zeros(ns[l]) for l in range(n_level)) + + # number of nodes does not match the number of fun evals + wrong_n_nodes = 2 + nodes_2 = tuple(measure.sample((ns[l]), rng=rng) for l in range(wrong_n_nodes)) with pytest.raises(ValueError): - _, _ = multilevel_bayesquad_from_data( - nodes=nodes_1, + multilevel_bayesquad_from_data( + nodes=nodes_2, fun_diff_evals=fun_diff_evals, - kernels=kernels, measure=measure, ) - nodes_2 = tuple(measure.sample((ns[l]), rng=rng) for l in range(2)) + + # number of kernels does not match number of fun evals + wrong_n_kernels = 2 + kernels = tuple(copy.deepcopy(kernel) for _ in range(wrong_n_kernels)) + nodes_1 = (measure.sample(n_sample=ns[0], rng=rng),) with pytest.raises(ValueError): - _, _ = multilevel_bayesquad_from_data( - nodes=nodes_2, + multilevel_bayesquad_from_data( + nodes=nodes_1, fun_diff_evals=fun_diff_evals, kernels=kernels, measure=measure, @@ -279,18 +310,16 @@ def test_multilevel_bayesquad_from_data_wrong_inputs(kernel, measure, rng): def test_multilevel_bayesquad_from_data_equals_bq_with_trivial_data_1d(): """Test that multilevel BQ equals BQ when all but one level are given non-zero function evaluations for 1D data.""" - input_dim = 1 n_level = 5 domain = (0, 3.3) - nodes = () - nodes = [np.linspace(0, 1, 2 * l + 1)[:, None] for l in range(n_level)] + nodes = tuple(np.linspace(0, 1, 2 * l + 1)[:, None] for l in range(n_level)) for i in range(n_level): jitter = 1e-5 * (i + 1.0) - fun_diff_evals = [np.zeros(shape=(len(xs),)) for xs in nodes] fun_evals = nodes[i][:, 0] ** (2 + 0.3 * i) + 1.2 + fun_diff_evals = [np.zeros(shape=(len(xs),)) for xs in nodes] fun_diff_evals[i] = fun_evals mlbq_integral, _ = multilevel_bayesquad_from_data( - nodes=tuple(nodes), + nodes=nodes, fun_diff_evals=tuple(fun_diff_evals), domain=domain, options=dict(jitter=jitter), @@ -312,16 +341,16 @@ def test_multilevel_bayesquad_from_data_equals_bq_with_trivial_data_2d(): n_level = 5 measure = GaussianMeasure(np.full((input_dim,), 0.2), cov=0.6 * np.eye(input_dim)) _gh = gauss_hermite_tensor - nodes = [ + nodes = tuple( _gh(l + 1, input_dim, measure.mean, measure.cov)[0] for l in range(n_level) - ] + ) for i in range(n_level): jitter = 1e-5 * (i + 1.0) - fun_diff_evals = [np.zeros(shape=(len(xs),)) for xs in nodes] fun_evals = np.sin(nodes[i][:, 0] * i) + (i + 1.0) * np.cos(nodes[i][:, 1]) + fun_diff_evals = [np.zeros(shape=(len(xs),)) for xs in nodes] fun_diff_evals[i] = fun_evals mlbq_integral, _ = multilevel_bayesquad_from_data( - nodes=tuple(nodes), + nodes=nodes, fun_diff_evals=tuple(fun_diff_evals), measure=measure, options=dict(jitter=jitter),