From f657a873724d389f8e6bb3db36b5fec3fb845899 Mon Sep 17 00:00:00 2001
From: Maren Mahsereci <42842079+mmahsereci@users.noreply.github.com>
Date: Fri, 10 Feb 2023 12:18:42 +0100
Subject: [PATCH] Some `quad` toy problems (#746)
---
docs/source/api/problems/zoo.quad.rst | 6 +
docs/source/api/problems/zoo.rst | 5 +
src/probnum/problems/_problems.py | 63 +++---
src/probnum/problems/zoo/quad/__init__.py | 6 +
.../problems/zoo/quad/_emukit_problems.py | 197 ++++++++++++++++++
.../test_zoo/test_quad/test_problems.py | 94 +++++++++
6 files changed, 337 insertions(+), 34 deletions(-)
create mode 100644 docs/source/api/problems/zoo.quad.rst
create mode 100644 src/probnum/problems/zoo/quad/__init__.py
create mode 100644 src/probnum/problems/zoo/quad/_emukit_problems.py
create mode 100644 tests/test_problems/test_zoo/test_quad/test_problems.py
diff --git a/docs/source/api/problems/zoo.quad.rst b/docs/source/api/problems/zoo.quad.rst
new file mode 100644
index 000000000..8b263a767
--- /dev/null
+++ b/docs/source/api/problems/zoo.quad.rst
@@ -0,0 +1,6 @@
+Quadrature
+----------
+
+.. automodapi:: probnum.problems.zoo.quad
+ :no-heading:
+ :headings: "*"
diff --git a/docs/source/api/problems/zoo.rst b/docs/source/api/problems/zoo.rst
index 296dacc14..33047d42b 100644
--- a/docs/source/api/problems/zoo.rst
+++ b/docs/source/api/problems/zoo.rst
@@ -19,3 +19,8 @@ probnum.problems.zoo
:hidden:
zoo.linalg
+
+.. toctree::
+ :hidden:
+
+ zoo.quad
diff --git a/src/probnum/problems/_problems.py b/src/probnum/problems/_problems.py
index 54a2f2398..eb86ef208 100644
--- a/src/probnum/problems/_problems.py
+++ b/src/probnum/problems/_problems.py
@@ -8,7 +8,7 @@
import numpy as np
import scipy.sparse
-from probnum import linops, randvars
+from probnum import linops, quad, randvars
from probnum.typing import FloatLike
@@ -225,51 +225,46 @@ class QuadratureProblem:
Compute the integral
- .. math::
- \int_\Omega f(x) \, \text{d} \mu(x)
+ .. math::
+ \int_\Omega f(x) \, \text{d} \mu(x)
- for a function :math:`f: \Omega \rightarrow \mathbb{R}`.
- For the time being, :math:`\mu` is the Lebesgue measure.
- Solved by quadrature rules in :mod:`probnum.quad`.
+ for a function :math:`f: \Omega \rightarrow \mathbb{R}` w.r.t. the measure
+ :math:`\mu`.
Parameters
----------
- integrand
- Function to be integrated.
- lower_bd
- A number or a vector representing the lower bounds of the integrals.
- upper_bd
- A number or a vector representing the upper bounds of the integrals.
- output_dim
- Output dimension of the integrand.
+ fun
+ Function to be integrated. It needs to accept a shape=(n_eval, input_dim)
+ ``np.ndarray`` and return a shape=(n_eval,) ``np.ndarray``.
+ measure
+ The integration measure.
solution
- Closed form, analytic solution to the problem.
+ Analytic value of the integral or precise numerical solution.
Used for testing and benchmarking.
Examples
--------
>>> import numpy as np
- >>> def integrand(x):
- ... return np.linalg.norm(x)**2
- >>> lower_bd = 0.41
- >>> upper_bd = 4.32
- >>> qp1d = QuadratureProblem(integrand, lower_bd=lower_bd, upper_bd=upper_bd)
- >>> np.round(qp1d.integrand(0.2), 2)
- 0.04
- >>> qp1d.lower_bd
- 0.41
+ >>> from probnum.quad.integration_measures import LebesgueMeasure
+ >>>
+ >>> def fun(x):
+ ... return np.linalg.norm(x, axis=1)**2
>>>
- >>> lower_bd = [0., 0.]
- >>> upper_bd = [1., 1.]
- >>> qp2d = QuadratureProblem(integrand, lower_bd=lower_bd, upper_bd=upper_bd)
- >>> qp2d.upper_bd
- [1.0, 1.0]
+ >>> measure1d = LebesgueMeasure(domain=(0, 1), input_dim=1)
+ >>> qp1d = QuadratureProblem(fun, measure=measure1d, solution=1/3)
+ >>> np.round(qp1d.fun(np.array([[0.2]]))[0], 2)
+ 0.04
+ >>> measure2d = LebesgueMeasure(domain=(0, 1), input_dim=2)
+ >>> qp2d = QuadratureProblem(fun, measure=measure2d, solution=None)
+ >>> np.round(qp2d.fun(np.array([[0.2, 0.2]]))[0], 2)
+ 0.08
"""
- integrand: Callable[[np.ndarray], Union[float, np.ndarray]]
- lower_bd: Union[FloatLike, np.ndarray]
- upper_bd: Union[FloatLike, np.ndarray]
- output_dim: Optional[int] = 1
+ fun: Callable[[np.ndarray], np.ndarray]
+ measure: quad.integration_measures.IntegrationMeasure
# For testing and benchmarking
- solution: Optional[Union[float, np.ndarray, randvars.RandomVariable]] = None
+ solution: Optional[Union[float, np.ndarray, randvars.RandomVariable]]
+
+ def __post_init__(self):
+ self.input_dim = self.measure.input_dim
diff --git a/src/probnum/problems/zoo/quad/__init__.py b/src/probnum/problems/zoo/quad/__init__.py
new file mode 100644
index 000000000..9d0ffd79f
--- /dev/null
+++ b/src/probnum/problems/zoo/quad/__init__.py
@@ -0,0 +1,6 @@
+"""Test problems for numerical integration/ quadrature."""
+
+from ._emukit_problems import circulargaussian2d, hennig1d, hennig2d, sombrero2d
+
+# Public classes and functions. Order is reflected in documentation.
+__all__ = ["circulargaussian2d", "hennig1d", "hennig2d", "sombrero2d"]
diff --git a/src/probnum/problems/zoo/quad/_emukit_problems.py b/src/probnum/problems/zoo/quad/_emukit_problems.py
new file mode 100644
index 000000000..4d32666b4
--- /dev/null
+++ b/src/probnum/problems/zoo/quad/_emukit_problems.py
@@ -0,0 +1,197 @@
+"""Toy integrands from Emukit."""
+
+# The integrands are re-implementations from scratch based on the Emukit docs.
+# There is no guarantee that they are identical to the Emukit implementations.
+
+from typing import Optional
+
+import numpy as np
+
+from probnum.problems import QuadratureProblem
+from probnum.quad.integration_measures import LebesgueMeasure
+from probnum.typing import FloatLike
+
+
+def hennig1d() -> QuadratureProblem:
+ r"""The univariate hennig function integrated wrt the Lebesgue measure. [1]_
+
+ The integrand is
+
+ .. math::
+ f(x) = e^{-x^2 -\sin^2(3x)}
+
+ on the domain :math:`\Omega=[-3, 3]`.
+
+ Returns
+ -------
+ quad_problem:
+ The quadrature problem.
+
+ References
+ ----------
+ .. [1] Emukit docs on `hennig1d `__.
+
+ """ # pylint: disable=line-too-long
+
+ def fun(x):
+ return np.exp(-x[:, 0] ** 2 - np.sin(3.0 * x[:, 0]) ** 2)
+
+ measure = LebesgueMeasure(input_dim=1, domain=(-3, 3))
+ return QuadratureProblem(fun=fun, measure=measure, solution=1.1433287777179366)
+
+
+def hennig2d(c: Optional[np.ndarray] = None) -> QuadratureProblem:
+ r"""The two-dimensional hennig function integrated wrt the Lebesgue measure. [1]_
+
+ The integrand is
+
+ .. math::
+ f(x) = e^{-x^{\intercal}c x -\sin(3\|x\|^2)}
+
+ on the domain :math:`\Omega=[-3, 3]^2`. Above, :math:`c` is the ``c`` parameter.
+
+ Parameters
+ ----------
+ c
+ A positive definite matrix of shape (2, 2). Defaults to [[1, .5], [.5, 1]].
+
+ Returns
+ -------
+ quad_problem:
+ The quadrature problem.
+
+ References
+ ----------
+ .. [1] Emukit docs on `hennig2d `__ .
+
+ """ # pylint: disable=line-too-long
+
+ solution = None
+ if c is None:
+ c = np.array([[1, 0.5], [0.5, 1]])
+ solution = 3.525721820076955
+
+ if c.shape != (2, 2):
+ raise ValueError(f"'c' must be a (2, 2) array. Found shape is {c.shape}.")
+
+ eigvals = np.linalg.eigvals(c)
+ if np.any(eigvals <= 0):
+ raise ValueError("'c' must be positive definite.")
+
+ def fun(x):
+ return np.exp(-np.sum((x @ c) * x, axis=1) - np.sin(3 * np.sum(x**2, axis=1)))
+
+ measure = LebesgueMeasure(input_dim=2, domain=(-3, 3))
+ return QuadratureProblem(fun=fun, measure=measure, solution=solution)
+
+
+def sombrero2d(w: Optional[FloatLike] = None) -> QuadratureProblem:
+ r"""The two-dimensional sombrero function integrated wrt the Lebesgue
+ measure. [1]_
+
+ The integrand is
+
+ .. math::
+ f(x) = \frac{\operatorname{sin}(\pi r w)}{\pi r w}
+
+ on the domain :math:`\Omega=[-3, 3]^2`. Above, :math:`w` is the ``w``
+ parameter and :math:`r=\|x\|` is the norm of the input vector :math:`x`.
+
+ Parameters
+ ----------
+ w
+ The positive frequency parameter. Defaults to 1.0.
+
+ Returns
+ -------
+ quad_problem:
+ The quadrature problem.
+
+ References
+ ----------
+ .. [1] Emukit docs on `sombrero2d `__ .
+
+ """ # pylint: disable=line-too-long
+
+ solution = None
+ if w is None:
+ w = 1.0
+ solution = 0.85225026427372
+
+ if w <= 0:
+ raise ValueError(f"The 'w' parameter must be positive ({w}).")
+
+ w = float(w)
+
+ def fun(x):
+ r_scaled = (np.pi * w) * np.sqrt((x * x).sum(axis=1))
+ f = np.sin(r_scaled) / r_scaled
+ f[np.isnan(f)] = 1.0
+ return f
+
+ measure = LebesgueMeasure(input_dim=2, domain=(-3, 3))
+ return QuadratureProblem(fun=fun, measure=measure, solution=solution)
+
+
+def circulargaussian2d(
+ m: Optional[FloatLike] = None, v: Optional[FloatLike] = None
+) -> QuadratureProblem:
+ r"""The two-dimensional circular Gaussian integrated wrt the Lebesgue
+ measure. [1]_
+
+ The integrand is
+
+ .. math::
+ f(x) = (2\pi v)^{-\frac{1}{2}} r^2 e^{-\frac{(r - m)^2}{2 v}}
+
+ on the domain :math:`\Omega=[-3, 3]^2`. Above, :math:`v` is the ``v``
+ parameter, :math:`m` is the ``m`` parameter and :math:`r = \|x\|` is the
+ norm of the input vector :math:`x`.
+
+ Parameters
+ ----------
+ m
+ The non-negative mean of the circular Gaussian in units of radius.
+ Defaults to 0.0.
+ v
+ The positive variance of the circular Gaussian. Defaults to 1.0.
+
+ Returns
+ -------
+ quad_problem:
+ The quadrature problem.
+
+ References
+ ----------
+ .. [1] Emukit docs on `circulargaussian2d `__ .
+
+ """ # pylint: disable=line-too-long
+
+ _v = 1.0
+ _m = 0.0
+
+ solution = None
+ if m is None and v is None:
+ v, m = _v, _m
+ solution = 4.853275495632483
+
+ if m is None:
+ m = _m
+ if v is None:
+ v = _v
+
+ if m < 0:
+ raise ValueError(f"'m' ({m}) must be non-negative.")
+
+ if v <= 0:
+ raise ValueError(f"'v' ({v}) must be positive.")
+
+ m, v = float(m), float(v)
+
+ def fun(x):
+ r = np.linalg.norm(x, axis=1)
+ rel_square_diff = (r - m) ** 2 / (2.0 * v)
+ return r**2 * np.exp(-rel_square_diff) / np.sqrt(2.0 * np.pi * v)
+
+ measure = LebesgueMeasure(input_dim=2, domain=(-3, 3))
+ return QuadratureProblem(fun=fun, measure=measure, solution=solution)
diff --git a/tests/test_problems/test_zoo/test_quad/test_problems.py b/tests/test_problems/test_zoo/test_quad/test_problems.py
new file mode 100644
index 000000000..59fee2edc
--- /dev/null
+++ b/tests/test_problems/test_zoo/test_quad/test_problems.py
@@ -0,0 +1,94 @@
+"""Tests for quadrature problems."""
+
+import numpy as np
+import pytest
+
+from probnum.problems.zoo.quad import circulargaussian2d, hennig1d, hennig2d, sombrero2d
+from probnum.quad.integration_measures import LebesgueMeasure
+
+
+@pytest.fixture(params=[hennig1d, hennig2d, sombrero2d, circulargaussian2d])
+def problem(request):
+ """quad problems with default values"""
+ return request.param()
+
+
+@pytest.fixture(
+ params=[
+ (hennig2d, dict(c=np.array([[2, 0.1], [0.1, 1]]))),
+ (circulargaussian2d, dict(m=1.0, v=1.5)),
+ (sombrero2d, dict(w=2.0)),
+ ],
+ ids=["hennig2d", "circulargaussian2d", "sombrero2d"],
+)
+def problem_parametrized(request):
+ """quad problems with custom parameters"""
+ return request.param[0](**request.param[1])
+
+
+def test_problem_solution_type(problem):
+ assert isinstance(problem.solution, float)
+
+
+def test_problem_parametrized_solution_type(problem_parametrized):
+ # problems that do not have an analytic solution currently do not provide any.
+ assert problem_parametrized.solution is None
+
+
+@pytest.mark.parametrize("num_dat", [1, 5])
+def test_problem_fun_shapes(problem, num_dat):
+ input_dim = problem.input_dim
+ res = problem.fun(np.ones([num_dat, input_dim]))
+ assert res.shape == (num_dat,)
+
+
+def test_problem_correct_solution_value(problem):
+ s = problem.solution
+ m = problem.measure
+
+ # 1e3 samples for MC estimator.
+ # Test might not work for high dimensions (>5). Testing integrals in high dimensions
+ # is intrinsically hard.
+ if s is not None and isinstance(s, float):
+ x = m.sample(int(1e3), rng=np.random.default_rng(0))
+ f = problem.fun(x)
+ s_test = f.mean()
+ c_test = f.std()
+
+ # scale MC estimator with domain volume for unnormalized Lebesgue measure.
+ if isinstance(m, LebesgueMeasure) and not m.normalized:
+ volume = np.prod(m.domain[1] - m.domain[0])
+ s_test *= volume
+ c_test *= volume
+
+ # Check if integral lies in a 95% confidence interval of the MC estimator.
+ assert s_test - 2 * c_test < s < s_test + 2 * c_test
+
+
+def test_problem_sombrero2d_raises():
+
+ # frequency is negative
+ with pytest.raises(ValueError):
+ sombrero2d(w=-2.0)
+
+
+def test_problem_hennig2d_raises():
+
+ # wrong c shape
+ with pytest.raises(ValueError):
+ hennig2d(c=np.eye(3))
+
+ # c not pos def
+ with pytest.raises(ValueError):
+ hennig2d(c=-np.eye(2))
+
+
+def test_problem_circulargaussian2d_raises():
+
+ # mean m negative
+ with pytest.raises(ValueError):
+ circulargaussian2d(m=-2.0)
+
+ # mean v non-positive
+ with pytest.raises(ValueError):
+ circulargaussian2d(v=0.0)