Repository: damar-wicaksono/uqtestfuns (language: python)

Issue:
Add the series of test functions from Genz (1984)
The paper by Genz (1984)[^1] contains a series of test functions used in numerical integration exercises. Some of the functions were often revisited in the context of sensitivity analysis (see, for instance, Zhang and Pandey (2014) [^2]).

Implement and include the functions in UQTestFuns:

- [x] Oscillatory (resolved via PR #416)
- [x] Product Peak (resolved via PR #413)
- [x] Corner Peak (resolved via PR #412)
- [x] Gaussian (resolved via PR #415)
- [x] Continuous (resolved via PR #414)
- [x] Discontinuous (resolved via PR #417)

[^1]: A. Genz, “Testing Multidimensional Integration Routines,” in Proc. of International Conference on Tools, Methods and Languages for Scientific and Engineering Computation, USA: Elsevier North-Holland, Inc., 1984, pp. 81–94.
[^2]: X. Zhang and M. D. Pandey, “An effective approximation for variance-based global sensitivity analysis,” Reliability Engineering & System Safety, vol. 121, pp. 164–174, Jan. 2014, doi: [10.1016/j.ress.2013.07.010](https://doi.org/10.1016/j.ress.2013.07.010).


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1816e3c..b3ef7bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+- The M-dimensional oscillatory function from Genz (1984) for integration
+  exercises; one parameter set from the literature is included.
 - The M-dimensional Gaussian function from Genz (1984) for integration
   exercises; one parameter set from the literature is included.
 - The M-dimensional continuous function from Genz (1984) for integration
diff --git a/docs/_toc.yml b/docs/_toc.yml
index 558807f..a21924b 100644
--- a/docs/_toc.yml
+++ b/docs/_toc.yml
@@ -100,6 +100,8 @@ parts:
             title: Genz (Corner Peak)
           - file: test-functions/genz-gaussian
             title: Genz (Gaussian)
+          - file: test-functions/genz-oscillatory
+            title: Genz (Oscillatory)
           - file: test-functions/genz-product-peak
             title: Genz (Product Peak)
           - file: test-functions/gramacy-1d-sine
diff --git a/docs/fundamentals/integration.md b/docs/fundamentals/integration.md
index b938145..4025582 100644
--- a/docs/fundamentals/integration.md
+++ b/docs/fundamentals/integration.md
@@ -18,18 +18,19 @@ kernelspec:
 The table below listed the available test functions typically used
 in the testing and comparison of numerical integration method.
 
-|                                   Name                                    | Input Dimension |     Constructor     |
-|:-------------------------------------------------------------------------:|:---------------:|:-------------------:|
-|       {ref}`Bratley et al. (1992) A <test-functions:bratley1992a>`        |        M        |  `Bratley1992a()`   |
-|       {ref}`Bratley et al. (1992) B <test-functions:bratley1992b>`        |        M        |  `Bratley1992b()`   |
-|       {ref}`Bratley et al. (1992) C <test-functions:bratley1992c>`        |        M        |  `Bratley1992c()`   |
-|       {ref}`Bratley et al. (1992) D <test-functions:bratley1992d>`        |        M        |  `Bratley1992d()`   |
-|         {ref}`Genz (Continuous) <test-functions:genz-continuous>`         |        M        | `GenzContinuous()`  |
-|        {ref}`Genz (Corner Peak) <test-functions:genz-corner-peak>`        |        M        | `GenzCornerPeak()`  |
-|           {ref}`Genz (Gaussian) <test-functions:genz-gaussian>`           |        M        |  `GenzGaussian()`   |
-|       {ref}`Genz (Product Peak) <test-functions:genz-product-peak>`       |        M        | `GenzProductPeak()` |
-|                 {ref}`Sobol'-G <test-functions:sobol-g>`                  |        M        |     `SobolG()`      |
-|           {ref}`Welch et al. (1992) <test-functions:welch1992>`           |       20        |    `Welch1992()`    |
+|                                  Name                                   | Input Dimension |     Constructor      |
+|:-----------------------------------------------------------------------:|:---------------:|:--------------------:|
+|      {ref}`Bratley et al. (1992) A <test-functions:bratley1992a>`       |        M        |   `Bratley1992a()`   |
+|      {ref}`Bratley et al. (1992) B <test-functions:bratley1992b>`       |        M        |   `Bratley1992b()`   |
+|      {ref}`Bratley et al. (1992) C <test-functions:bratley1992c>`       |        M        |   `Bratley1992c()`   |
+|      {ref}`Bratley et al. (1992) D <test-functions:bratley1992d>`       |        M        |   `Bratley1992d()`   |
+|        {ref}`Genz (Continuous) <test-functions:genz-continuous>`        |        M        |  `GenzContinuous()`  |
+|       {ref}`Genz (Corner Peak) <test-functions:genz-corner-peak>`       |        M        |  `GenzCornerPeak()`  |
+|          {ref}`Genz (Gaussian) <test-functions:genz-gaussian>`          |        M        |   `GenzGaussian()`   |
+|       {ref}`Genz (Oscillatory) <test-functions:genz-oscillatory>`       |        M        | `GenzOscillatory()`  |
+|      {ref}`Genz (Product Peak) <test-functions:genz-product-peak>`      |        M        | `GenzProductPeak()`  |
+|                {ref}`Sobol'-G <test-functions:sobol-g>`                 |        M        |      `SobolG()`      |
+|          {ref}`Welch et al. (1992) <test-functions:welch1992>`          |       20        |    `Welch1992()`     |
 
 In a Python terminal, you can list all the available functions relevant
 for metamodeling applications using ``list_functions()`` and filter the results


Test patch:
diff --git a/docs/test-functions/available.md b/docs/test-functions/available.md
index dd38934..683179e 100644
--- a/docs/test-functions/available.md
+++ b/docs/test-functions/available.md
@@ -53,6 +53,7 @@ regardless of their typical applications.
 |              {ref}`Genz (Continuous) <test-functions:genz-continuous>`              |        M        |       `GenzContinuous()`        |
 |             {ref}`Genz (Corner Peak) <test-functions:genz-corner-peak>`             |        M        |       `GenzCornerPeak()`        |
 |                {ref}`Genz (Gaussian) <test-functions:genz-gaussian>`                |        M        |        `GenzGaussian()`         |
+|             {ref}`Genz (Oscillatory) <test-functions:genz-oscillatory>`             |        M        |       `GenzOscillatory()`       |
 |            {ref}`Genz (Product Peak) <test-functions:genz-product-peak>`            |        M        |       `GenzProductPeak()`       |
 |           {ref}`Gramacy (2007) 1D Sine <test-functions:gramacy-1d-sine>`            |        1        |        `Gramacy1DSine()`        |
 |               {ref}`Hyper-sphere Bound <test-functions:hyper-sphere>`               |        2        |         `HyperSphere()`         |
diff --git a/docs/test-functions/genz-continuous.md b/docs/test-functions/genz-continuous.md
index 4abaf61..ab2d7c2 100644
--- a/docs/test-functions/genz-continuous.md
+++ b/docs/test-functions/genz-continuous.md
@@ -98,6 +98,8 @@ Genz {cite}`Genz1984` introduced six challenging
 parameterized $M$-dimensional functions
 designed to test the performance of numerical integration routines:
 
+- {ref}`Oscillatory <test-functions:genz-oscillatory>` function features
+  an oscillating shape in the multidimensional space.
 - {ref}`Product peak <test-functions:genz-product-peak>` function features
   a prominent peak at the center of the multidimensional space.
 - {ref}`Corner peak <test-functions:genz-corner-peak>` function features
@@ -174,7 +176,7 @@ The shape parameters determines the extent of the product peaking;
 Larger values of $\boldsymbol{a}$
 increase the prominence of the peak,
 making the integration problem more challenging.
-The offset parameters, on the other hand, does not affect significantly
+The offset parameters, on the other hand, do not affect significantly
 the difficulty of the problem and can be chosen randomly.
 
 The default parameter is shown below.
diff --git a/docs/test-functions/genz-corner-peak.md b/docs/test-functions/genz-corner-peak.md
index 1a6e56d..c18c2bf 100644
--- a/docs/test-functions/genz-corner-peak.md
+++ b/docs/test-functions/genz-corner-peak.md
@@ -101,6 +101,8 @@ Genz {cite}`Genz1984` introduced six challenging
 parameterized $M$-dimensional functions
 designed to test the performance of numerical integration routines:
 
+- {ref}`Oscillatory <test-functions:genz-oscillatory>` function features
+  an oscillating shape in the multidimensional space.
 - {ref}`Product peak <test-functions:genz-product-peak>` function features
   a prominent peak at the center of the multidimensional space.
 - {ref}`Corner peak <test-functions:genz-corner-peak>` function features
diff --git a/docs/test-functions/genz-gaussian.md b/docs/test-functions/genz-gaussian.md
index 5a856b5..246c57f 100644
--- a/docs/test-functions/genz-gaussian.md
+++ b/docs/test-functions/genz-gaussian.md
@@ -97,6 +97,8 @@ Genz {cite}`Genz1984` introduced six challenging
 parameterized $M$-dimensional functions
 designed to test the performance of numerical integration routines:
 
+- {ref}`Oscillatory <test-functions:genz-oscillatory>` function features
+  an oscillating shape in the multidimensional space.
 - {ref}`Product peak <test-functions:genz-product-peak>` function features
   a prominent peak at the center of the multidimensional space.
 - {ref}`Corner peak <test-functions:genz-corner-peak>` function features
@@ -173,7 +175,7 @@ The shape parameters determines the extent of the product peaking;
 Larger values of $\boldsymbol{a}$
 increase the prominence of the peak,
 making the integration problem more challenging.
-The offset parameters, on the other hand, does not affect significantly
+The offset parameters, on the other hand, do not affect significantly
 the difficulty of the problem and can be chosen randomly.
 
 The default parameter is shown below.
diff --git a/docs/test-functions/genz-oscillatory.md b/docs/test-functions/genz-oscillatory.md
new file mode 100644
index 0000000..563ac74
--- /dev/null
+++ b/docs/test-functions/genz-oscillatory.md
@@ -0,0 +1,220 @@
+---
+jupytext:
+  formats: ipynb,md:myst
+  text_representation:
+    extension: .md
+    format_name: myst
+    format_version: 0.13
+    jupytext_version: 1.14.1
+kernelspec:
+  display_name: Python 3 (ipykernel)
+  language: python
+  name: python3
+---
+
+(test-functions:genz-oscillatory)=
+# Genz Corner Peak Function
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The Genz corner peak function is an $M$-dimensional scalar-valued
+function commonly used to assess the accuracy of numerical
+integration routines.
+It is one of six functions introduced by Genz {cite}`Genz1984`;
+see the box below.
+
+The function features an oscillating shape (cosine)
+in the multidimensional space.
+The plots for one-dimensional and two-dimensional Genz corner peak function
+with the default parameters can be seen below.
+
+```{code-cell} ipython3
+:tags: [remove-input]
+
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+
+# --- Create 1D data from Genz Oscillatory
+my_fun_1d = uqtf.GenzOscillatory(input_dimension=1)
+xx_1d = np.linspace(0, 1, 1000)[:, np.newaxis]
+yy_1d = my_fun_1d(xx_1d)
+
+# --- Create 2D data from Genz Oscillatory
+my_fun_2d = uqtf.GenzOscillatory(input_dimension=2)
+mesh_2d = np.meshgrid(xx_1d, xx_1d)
+xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
+yy_2d = my_fun_2d(xx_2d)
+
+# --- Create two-dimensional plots
+fig = plt.figure(figsize=(15, 5))
+
+# 1D
+axs_1 = plt.subplot(131)
+axs_1.plot(xx_1d, yy_1d, color="#8da0cb")
+axs_1.grid()
+axs_1.set_xlabel("$x$", fontsize=14)
+axs_1.set_ylabel("$\mathcal{M}(x)$", fontsize=14)
+axs_1.set_title("1D Genz oscillatory")
+
+# Surface
+axs_2 = plt.subplot(132, projection='3d')
+axs_2.plot_surface(
+    mesh_2d[0],
+    mesh_2d[1],
+    yy_2d.reshape(1000, 1000).T,
+    cmap="plasma",
+    linewidth=0,
+    antialiased=False,
+    alpha=0.5
+)
+axs_2.set_xlabel("$x_1$", fontsize=14)
+axs_2.set_ylabel("$x_2$", fontsize=14)
+axs_2.set_zlabel("$\mathcal{M}(x_1, x_2)$", fontsize=14)
+axs_2.set_title("Surface plot of 2D Genz oscillatory", fontsize=14)
+
+# Contour
+axs_3 = plt.subplot(133)
+cf = axs_3.contourf(
+    mesh_2d[0], mesh_2d[1], yy_2d.reshape(1000, 1000).T, cmap="plasma"
+)
+axs_3.set_xlabel("$x_1$", fontsize=14)
+axs_3.set_ylabel("$x_2$", fontsize=14)
+axs_3.set_title("Contour plot of 2D Genz oscillatory", fontsize=14)
+divider = make_axes_locatable(axs_3)
+cax = divider.append_axes('right', size='5%', pad=0.05)
+fig.colorbar(cf, cax=cax, orientation='vertical')
+axs_3.axis('scaled')
+
+fig.tight_layout(pad=3.0)
+plt.gcf().set_dpi(150);
+```
+
+```{note}
+Genz {cite}`Genz1984` introduced six challenging
+parameterized $M$-dimensional functions
+designed to test the performance of numerical integration routines:
+
+- {ref}`Oscillatory <test-functions:genz-oscillatory>` function features
+  an oscillating shape in the multidimensional space. (_this function_)
+- {ref}`Product peak <test-functions:genz-product-peak>` function features
+  a prominent peak at the center of the multidimensional space.
+- {ref}`Corner peak <test-functions:genz-corner-peak>` function features
+  a prominent peak in one corner of the multidimensional space.
+- {ref}`Gaussian <test-functions:genz-gaussian>` function features
+  a bell-shaped peak at the center of the multidimensional space.
+- {ref}`Continuous <test-functions:genz-continuous>` function features
+  an exponential decay from the center of the multidimensional space.
+  The function is continuous everywhere, but non-differentiable at the center.
+
+The functions are further characterized by offset (shift) and shape parameters.
+While the offset parameter has minimal impact on the integral's value,
+the shape parameter significantly affects
+the difficulty of the integration problem.
+```
+
+## Test function instance
+
+To create a default instance of the test function:
+
+```{code-cell} ipython3
+my_testfun = uqtf.GenzOscillatory()
+```
+
+Check if it has been correctly instantiated:
+
+```{code-cell} ipython3
+print(my_testfun)
+```
+
+By default, the input dimension is set to $2$[^default_dimension].
+To create an instance with another value of input dimension,
+pass an integer to the parameter `input_dimension` (the first parameter).
+For example, to create an instance of the test function in six dimensions,
+type:
+
+```python
+my_testfun = uqtf.GenzOscillatory(input_dimension=6)
+```
+
+## Description
+
+The Genz oscillatory function is defined as:
+
+$$
+\mathcal{M}(\boldsymbol{x}; \boldsymbol{a}, b) = \cos{\left(2 \pi b + \sum_{i = 1}^M a_i x_i \right)},
+$$
+
+where $\boldsymbol{x} = \left( x_1, \ldots, x_M \right)$
+is the $M$-dimensional vector of input variables;
+$\boldsymbol{a} = \left( a_1, \ldots, a_M \right)$ 
+is $M$-dimensional vector corresponding to the (fixed) shape and parameters;
+and $b$ is the scalar offset parameter.
+Further details about these parameters are provided below.
+
+## Probabilistic input
+
+The input specification for the Genz oscillatory function is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
+```
+
+## Parameters
+
+The parameters of the Genz Gaussian function consists
+of the vector of shape (scale) parameters $\boldsymbol{a}$
+and the scalar of offset parameter $b$. 
+
+The shape parameters determines the extent of the product peaking;
+Larger values of $\boldsymbol{a}$
+increase the prominence of the peak,
+making the integration problem more challenging.
+The offset parameter, on the other hand, does not affect significantly
+the difficulty of the problem and can be chosen randomly.
+
+The default parameter is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.parameters)
+```
+
+## Reference results
+
+This section provides several reference results of typical UQ analyses involving
+the test function.
+
+### Sample histogram
+
+Shown below is the histogram of the output based on $100'000$ random points:
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+my_testfun.prob_input.reset_rng(42)
+xx_test = my_testfun.prob_input.get_sample(100000)
+yy_test = my_testfun(xx_test)
+
+plt.hist(yy_test, bins="auto", color="#8da0cb");
+plt.grid();
+plt.ylabel("Counts [-]");
+plt.xlabel("$\mathcal{M}(\mathbf{X})$");
+plt.gcf().set_dpi(150);
+```
+
+Notice that the values are indeed mostly zeros.
+
+## References
+
+```{bibliography}
+:filter: docname in docnames
+```
+
+[^default_dimension]: This default dimension applies to all variable dimension
+test functions. It will be used if the `input_dimension` argument is not given.
\ No newline at end of file
diff --git a/docs/test-functions/genz-product-peak.md b/docs/test-functions/genz-product-peak.md
index d4d574c..319ea9d 100644
--- a/docs/test-functions/genz-product-peak.md
+++ b/docs/test-functions/genz-product-peak.md
@@ -96,6 +96,8 @@ Genz {cite}`Genz1984` introduced six challenging
 parameterized $M$-dimensional functions
 designed to test the performance of numerical integration routines:
 
+- {ref}`Oscillatory <test-functions:genz-oscillatory>` function features
+  an oscillating shape in the multidimensional space.
 - {ref}`Product peak <test-functions:genz-product-peak>` function features
   a prominent peak at the center of the multidimensional space.
   (_this function_)
@@ -172,7 +174,7 @@ The shape parameters determines the extent of the product peaking;
 Larger values of $\boldsymbol{a}$
 increase the prominence of the peak,
 making the integration problem more challenging.
-The offset parameters, on the other hand, does not affect significantly
+The offset parameters, on the other hand, do not affect significantly
 the difficulty of the problem and can be chosen randomly.
 
 The default parameter is shown below.
diff --git a/src/uqtestfuns/test_functions/__init__.py b/src/uqtestfuns/test_functions/__init__.py
index 7b8cbda..31689c2 100644
--- a/src/uqtestfuns/test_functions/__init__.py
+++ b/src/uqtestfuns/test_functions/__init__.py
@@ -20,7 +20,13 @@
 from .franke import Franke1, Franke2, Franke3, Franke4, Franke5, Franke6
 from .friedman import Friedman6D, Friedman10D
 from .gayton_hat import GaytonHat
-from .genz import GenzContinuous, GenzCornerPeak, GenzGaussian, GenzProductPeak
+from .genz import (
+    GenzContinuous,
+    GenzCornerPeak,
+    GenzGaussian,
+    GenzOscillatory,
+    GenzProductPeak,
+)
 from .gramacy2007 import Gramacy1DSine
 from .hyper_sphere import HyperSphere
 from .ishigami import Ishigami
@@ -83,6 +89,7 @@
     "GenzContinuous",
     "GenzCornerPeak",
     "GenzGaussian",
+    "GenzOscillatory",
     "GenzProductPeak",
     "Gramacy1DSine",
     "HyperSphere",
diff --git a/src/uqtestfuns/test_functions/genz.py b/src/uqtestfuns/test_functions/genz.py
index 3adedf6..2b2453e 100644
--- a/src/uqtestfuns/test_functions/genz.py
+++ b/src/uqtestfuns/test_functions/genz.py
@@ -43,6 +43,7 @@
     "GenzContinuous",
     "GenzCornerPeak",
     "GenzGaussian",
+    "GenzOscillatory",
     "GenzProductPeak",
 ]
 
@@ -99,6 +100,76 @@ def _get_aa_zhang_2014_2(input_dimension: int) -> np.ndarray:
     return aa
 
 
+def evaluate_oscillatory(
+    xx: np.ndarray, aa: np.ndarray, b: float
+) -> np.ndarray:
+    """Evaluate the oscillatory function on a set of input values.
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        M-Dimensional input values given by an N-by-M array where
+        N is the number of input values.
+    aa : np.ndarray
+        The vector of shape parameters of the Genz family of integrand
+        functions; the larger the value the more difficult the integrations.
+    b : float
+        The scalar shift parameter of the Genz family of integrand.
+
+    Returns
+    -------
+    np.ndarray
+        The output of the test function evaluated on the input values.
+        The output is a 1-dimensional array of length N.
+    """
+    yy = np.cos(2 * np.pi * b + np.sum(xx * aa, axis=1))
+
+    return yy
+
+
+class GenzOscillatory(UQTestFunVarDimABC):
+    """A concrete implementation of the Genz oscillatory function."""
+
+    _tags = ["integration"]
+    _description = "Oscillatory integrand from Genz (1984)"
+    _available_inputs: ProbInputSpecs = {
+        "Genz1984": {
+            "function_id": "GenzOscillatory",
+            "description": (
+                "Input specification for the Genz family of integrand"
+            ),
+            "marginals": MARGINALS_GENZ1984,
+            "copulas": None,
+        }
+    }
+    _available_parameters: FunParamSpecs = {
+        "Genz1984": {
+            "function_id": "GenzOscillatory",
+            "description": (
+                "Parameter set for the oscillatory function from Genz (1984);"
+                " constant shape and offset values"
+            ),
+            "declared_parameters": [
+                {
+                    "keyword": "aa",
+                    "value": _get_aa_genz_1984,
+                    "type": np.ndarray,
+                    "description": "Shape parameter",
+                },
+                {
+                    "keyword": "b",
+                    "value": 0.5,
+                    "type": float,
+                    "description": "Offset parameter",
+                },
+            ],
+        },
+    }
+    _default_parameters_id = "Genz1984"
+
+    evaluate = staticmethod(evaluate_oscillatory)  # type: ignore
+
+
 def evaluate_corner_peak(xx: np.ndarray, aa: np.ndarray) -> np.ndarray:
     """Evaluate the corner peak function on a set of input values.