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 7112345..0b5a771 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 continuous function from Genz (1984) for integration
+  exercises; one parameter set from the literature is included.
 - The M-dimensional product peak function from Genz (1984) for integration
   exercises; one parameter set from the literature is included.
 - The M-dimensional corner peak function from Genz (1984) for integration
diff --git a/docs/_toc.yml b/docs/_toc.yml
index 74c6d2b..b5ff945 100644
--- a/docs/_toc.yml
+++ b/docs/_toc.yml
@@ -94,6 +94,8 @@ parts:
             title: Friedman (10D)
           - file: test-functions/gayton-hat
             title: Gayton Hat
+          - file: test-functions/genz-continuous
+            title: Genz (Continuous)
           - file: test-functions/genz-corner-peak
             title: Genz (Corner Peak)
           - file: test-functions/genz-product-peak
diff --git a/docs/fundamentals/integration.md b/docs/fundamentals/integration.md
index 4e0fd32..3cbbc0c 100644
--- a/docs/fundamentals/integration.md
+++ b/docs/fundamentals/integration.md
@@ -18,16 +18,17 @@ 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 (Corner Peak) <test-functions:genz-corner-peak>`  |        M        |  `GenzCornerPeak()`  |
-| {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 (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
diff --git a/docs/fundamentals/metamodeling.md b/docs/fundamentals/metamodeling.md
index 29011b3..813290a 100644
--- a/docs/fundamentals/metamodeling.md
+++ b/docs/fundamentals/metamodeling.md
@@ -39,6 +39,7 @@ in the comparison of metamodeling approaches.
 |             {ref}`(6th) Franke <test-functions:franke-6>`              |        2        |      `Franke6()`       |
 |           {ref}`Friedman (6D) <test-functions:friedman-6d>`            |        6        |     `Friedman6D()`     |
 |          {ref}`Friedman (10D) <test-functions:friedman-10d>`           |       10        |    `Friedman10D()`     |
+|      {ref}`Genz (Corner Peak) <test-functions:genz-corner-peak>`       |        M        |   `GenzCornerPeak()`   |
 |     {ref}`Gramacy (2007) 1D Sine <test-functions:gramacy-1d-sine>`     |        1        |   `Gramacy1DSine()`    |
 | {ref}`Lim et al. (2002) Non-Polynomial <test-functions:lim-non-poly>`  |        2        |     `LimNonPoly()`     |
 |     {ref}`Lim et al. (2002) Polynomial <test-functions:lim-poly>`      |        2        |      `LimPoly()`       |


Test patch:
diff --git a/docs/test-functions/available.md b/docs/test-functions/available.md
index 24df6d4..959fc38 100644
--- a/docs/test-functions/available.md
+++ b/docs/test-functions/available.md
@@ -50,6 +50,7 @@ regardless of their typical applications.
 |                  {ref}`Friedman (6D) <test-functions:friedman-6d>`                  |        6        |         `Friedman6D()`          |
 |                 {ref}`Friedman (10D) <test-functions:friedman-10d>`                 |       10        |         `Friedman10D()`         |
 |                    {ref}`Gayton Hat <test-functions:gayton-hat>`                    |        2        |          `GaytonHat()`          |
+|              {ref}`Genz (Continuous) <test-functions:genz-continuous>`              |        M        |       `GenzContinuous()`        |
 |             {ref}`Genz (Corner Peak) <test-functions:genz-corner-peak>`             |        M        |       `GenzCornerPeak()`        |
 |            {ref}`Genz (Product Peak) <test-functions:genz-product-peak>`            |        M        |       `GenzProductPeak()`       |
 |           {ref}`Gramacy (2007) 1D Sine <test-functions:gramacy-1d-sine>`            |        1        |        `Gramacy1DSine()`        |
diff --git a/docs/test-functions/genz-continuous.md b/docs/test-functions/genz-continuous.md
new file mode 100644
index 0000000..ee3caaa
--- /dev/null
+++ b/docs/test-functions/genz-continuous.md
@@ -0,0 +1,216 @@
+---
+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-continuous)=
+# Genz Continuous Function
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The Genz continuous 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 exponential decay from the center
+of the multidimensional space. The function is continuous everywhere,
+but non-differentiable at the center.
+The plots for one-dimensional and two-dimensional Genz continuous 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 Continuous
+my_fun_1d = uqtf.GenzContinuous(input_dimension=1)
+xx_1d = np.linspace(0, 1, 1000)[:, np.newaxis]
+yy_1d = my_fun_1d(xx_1d)
+
+# --- Create 2D data from Genz Continuous
+my_fun_2d = uqtf.GenzContinuous(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 continuous")
+
+# 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 continuous", 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 continuous", 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}`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}`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.
+  (_this function_)
+
+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.GenzContinuous()
+```
+
+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.GenzContinuous(input_dimension=6)
+```
+
+## Description
+
+The Genz continuous is defined as:
+
+$$
+\mathcal{M}(\boldsymbol{x}; \boldsymbol{a}, \boldsymbol{b}) = \exp{\left[ - \sum_{i = 1}^M a_i \lvert x_i - b_i \rvert \right]},
+$$
+
+where $\boldsymbol{x} = \left( x_1, \ldots, x_M \right)$
+is the $M$-dimensional vector of input variables;
+and $\boldsymbol{a} = \left( a_1, \ldots, a_M \right)$ 
+$\boldsymbol{b} = \left( b_1, \ldots, b_M \right)$ are $M$-dimensional
+vectors corresponding to the (fixed) shape and offset parameters, respectively.
+Further details about these parameters are provided below.
+
+## Probabilistic input
+
+The input specification for the Genz continuous function is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
+```
+
+## Parameters
+
+The parameters of the Genz corner peak function consists
+of the vector of shape (scale) parameters $\boldsymbol{a}$
+and the vector of offset parameters $\boldsymbol{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 parameters, 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, color="#8da0cb");
+plt.grid();
+plt.ylabel("Counts [-]");
+plt.xlabel("$\mathcal{M}(\mathbf{X})$");
+plt.gcf().set_dpi(150);
+```
+
+## 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-corner-peak.md b/docs/test-functions/genz-corner-peak.md
index a56f8e4..c2ff01a 100644
--- a/docs/test-functions/genz-corner-peak.md
+++ b/docs/test-functions/genz-corner-peak.md
@@ -31,22 +31,8 @@ The function was later featured in {cite}`Zhang2014` as a test function
 for a global sensitivity analysis method and in {cite}`Jakeman2015` for
 metamodeling applications.
 
-```{note}
-Genz {cite}`Genz1984` introduced six challenging
-parameterized $M$-dimensional functions
-designed to test the performance of numerical integration routines:
-
-- {ref}`Corner peak <test-functions:genz-product-peak>` features a prominent
-  peak in the center of the multidimensional space.
-- {ref}`Corner peak <test-functions:genz-corner-peak>` features a prominent
-  peak in one corner of the multidimensional space. (_this function_)
-
-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.
-```
-
+The function features a prominent peak in one corner of the multidimensional
+space.
 The plots for one-dimensional and two-dimensional Genz corner peak function
 with the default parameters can be seen below.
 
@@ -110,6 +96,26 @@ 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}`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.
+  (_this function_)
+- {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:
diff --git a/docs/test-functions/genz-product-peak.md b/docs/test-functions/genz-product-peak.md
index 99c9acd..ec176e7 100644
--- a/docs/test-functions/genz-product-peak.md
+++ b/docs/test-functions/genz-product-peak.md
@@ -27,22 +27,7 @@ integration routines.
 It is one of six functions introduced by Genz {cite}`Genz1984`;
 see the box below.
 
-```{note}
-Genz {cite}`Genz1984` introduced six challenging
-parameterized $M$-dimensional functions
-designed to test the performance of numerical integration routines:
-
-- {ref}`Corner peak <test-functions:genz-product-peak>` features a prominent
-  peak in the center of the multidimensional space. (_this function_)
-- {ref}`Corner peak <test-functions:genz-corner-peak>` features a prominent
-  peak in one corner of the multidimensional space.
-
-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.
-```
-
+The function features a peak at the center of the multidimensional space.
 The plots for one-dimensional and two-dimensional Genz product peak function
 with the default parameters can be seen below.
 
@@ -106,6 +91,26 @@ 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}`Product peak <test-functions:genz-product-peak>` function features
+  a prominent peak at the center of the multidimensional space.
+  (_this function_)
+- {ref}`Corner peak <test-functions:genz-corner-peak>` function features
+  a prominent peak in one corner 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:
diff --git a/src/uqtestfuns/test_functions/__init__.py b/src/uqtestfuns/test_functions/__init__.py
index a82b2ee..d35370e 100644
--- a/src/uqtestfuns/test_functions/__init__.py
+++ b/src/uqtestfuns/test_functions/__init__.py
@@ -20,7 +20,7 @@
 from .franke import Franke1, Franke2, Franke3, Franke4, Franke5, Franke6
 from .friedman import Friedman6D, Friedman10D
 from .gayton_hat import GaytonHat
-from .genz import GenzCornerPeak, GenzProductPeak
+from .genz import GenzContinuous, GenzCornerPeak, GenzProductPeak
 from .gramacy2007 import Gramacy1DSine
 from .hyper_sphere import HyperSphere
 from .ishigami import Ishigami
@@ -80,6 +80,7 @@
     "Friedman6D",
     "Friedman10D",
     "GaytonHat",
+    "GenzContinuous",
     "GenzCornerPeak",
     "GenzProductPeak",
     "Gramacy1DSine",
diff --git a/src/uqtestfuns/test_functions/genz.py b/src/uqtestfuns/test_functions/genz.py
index a2e70e6..a03c13f 100644
--- a/src/uqtestfuns/test_functions/genz.py
+++ b/src/uqtestfuns/test_functions/genz.py
@@ -39,7 +39,7 @@
 )
 from uqtestfuns.core.uqtestfun_abc import UQTestFunVarDimABC
 
-__all__ = ["GenzCornerPeak", "GenzProductPeak"]
+__all__ = ["GenzContinuous", "GenzCornerPeak", "GenzProductPeak"]
 
 
 MARGINALS_GENZ1984: MarginalSpecs = [
@@ -258,3 +258,79 @@ class GenzProductPeak(UQTestFunVarDimABC):
     _default_parameters_id = "Genz1984"
 
     evaluate = staticmethod(evaluate_product_peak)  # type: ignore
+
+
+def evaluate_continuous(
+    xx: np.ndarray, aa: np.ndarray, bb: np.ndarray
+) -> np.ndarray:
+    """Compute the continuous 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.
+        The length of the vector must M.
+    bb : np.ndarray
+        The vector of offset parameters of the Genz family of integrand
+        functions; the values do not alter significantly the difficulty of
+        the integration problem.
+        The length of the vector must be M.
+
+    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.exp(-1 * np.sum(np.abs(xx - bb) * aa, axis=1))
+
+    return yy
+
+
+class GenzContinuous(UQTestFunVarDimABC):
+    """A concrete implementation of the Genz continuous function."""
+
+    _tags = ["integration"]
+    _description = (
+        "Continuous (but non-differentiable) integrand from Genz (1984)"
+    )
+    _available_inputs: ProbInputSpecs = {
+        "Genz1984": {
+            "function_id": "GenzContinuous",
+            "description": (
+                "Input specification for the Genz family of integrand"
+            ),
+            "marginals": MARGINALS_GENZ1984,
+            "copulas": None,
+        }
+    }
+    _available_parameters: FunParamSpecs = {
+        "Genz1984": {
+            "function_id": "GenzContinuous",
+            "description": (
+                "Parameter set for the continuous 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": "bb",
+                    "value": _get_bb_genz_1984,
+                    "type": np.ndarray,
+                    "description": "Offset parameter",
+                },
+            ],
+        },
+    }
+    _default_parameters_id = "Genz1984"
+
+    evaluate = staticmethod(evaluate_continuous)  # type: ignore