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 e7076f5..568a779 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 corner peak function from Genz (1984) for integration
+  and sensitivity analysis exercises.
 - The one-dimensional sine function from Currin et al. (1988) for metamodeling
   exercise.
 - The two-dimensional, time-dependent (vector-valued) cooling cup model
diff --git a/docs/_toc.yml b/docs/_toc.yml
index 6affa25..e2eca1f 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-corner-peak
+            title: Genz (Corner Peak)
           - file: test-functions/gramacy-1d-sine
             title: Gramacy (2007) 1D Sine
           - file: test-functions/hyper-sphere
diff --git a/docs/fundamentals/integration.md b/docs/fundamentals/integration.md
index e2a9179..3475bb0 100644
--- a/docs/fundamentals/integration.md
+++ b/docs/fundamentals/integration.md
@@ -18,14 +18,15 @@ 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}`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 (Corner Peak) <test-functions:genz-corner-peak>`         |        M        | `GenzCornerPeak()`  |
+|                  {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/sensitivity.md b/docs/fundamentals/sensitivity.md
index 26ee368..84c11c8 100644
--- a/docs/fundamentals/sensitivity.md
+++ b/docs/fundamentals/sensitivity.md
@@ -28,6 +28,7 @@ in the comparison of sensitivity analysis methods.
 | {ref}`Damped Oscillator <test-functions:damped-oscillator>`  |        7        | `DampedOscillator()` |
 |             {ref}`Flood <test-functions:flood>`              |        8        |      `Flood()`       |
 |      {ref}`Friedman (6D) <test-functions:friedman-6d>`       |        6        |    `Friedman6D()`    |
+| {ref}`Genz (Corner Peak) <test-functions:genz-corner-peak>`  |        M        |  `GenzCornerPeak()`  |
 |          {ref}`Ishigami <test-functions:ishigami>`           |        3        |     `Ishigami()`     |
 |        {ref}`Moon (2010) 3D <test-functions:moon3d>`         |        3        |      `Moon3D()`      |
 |   {ref}`Morris et al. (2006) <test-functions:morris2006>`    |        M        |    `Morris2006()`    |
diff --git a/docs/references.bib b/docs/references.bib
index 50d1300..d706a77 100644
--- a/docs/references.bib
+++ b/docs/references.bib
@@ -974,4 +974,29 @@ @TechReport{Currin1988
   doi         = {10.2172/814584},
 }
 
+ 
+@Article{Zhang2014,
+  author    = {Zhang, Xufang and Pandey, Mahesh D.},
+  journal   = {Reliability Engineering &amp; System Safety},
+  title     = {An effective approximation for variance-based global sensitivity analysis},
+  year      = {2014},
+  issn      = {0951-8320},
+  month     = jan,
+  pages     = {164--174},
+  volume    = {121},
+  doi       = {10.1016/j.ress.2013.07.010},
+  publisher = {Elsevier BV},
+}
+
+@InProceedings{Genz1984,
+  author    = {Genz, Alan},
+  booktitle = {Proceeding of International Conference on Tools, Methods and Languages for Scientific and Engineering Computation},
+  title     = {Testing multidimensional integration routines},
+  year      = {1984},
+  pages     = {81--94},
+  publisher = {Elsevier North-Holland, Inc.},
+  isbn      = {0444875700},
+  location  = {Paris, France},
+}
+
 @Comment{jabref-meta: databaseType:bibtex;}


Test patch:
diff --git a/docs/test-functions/available.md b/docs/test-functions/available.md
index fed97e8..c49ff96 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 (Corner Peak) <test-functions:genz-corner-peak>`             |        M        |       `GenzCornerPeak()`        |
 |           {ref}`Gramacy (2007) 1D Sine <test-functions:gramacy-1d-sine>`            |        1        |        `Gramacy1DSine()`        |
 |               {ref}`Hyper-sphere Bound <test-functions:hyper-sphere>`               |        2        |         `HyperSphere()`         |
 |                      {ref}`Ishigami <test-functions:ishigami>`                      |        3        |          `Ishigami()`           |
diff --git a/docs/test-functions/genz-corner-peak.md b/docs/test-functions/genz-corner-peak.md
new file mode 100644
index 0000000..b702aed
--- /dev/null
+++ b/docs/test-functions/genz-corner-peak.md
@@ -0,0 +1,228 @@
+---
+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-corner-peak)=
+# 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.
+The function was introduced in {cite}`Genz1984` to assess the accuracy
+of numerical integration routines.
+The function was featured in {cite}`Zhang2014` as a test function
+for a global sensitivity analysis method.
+
+```{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-corner-peak>` features a prominent
+  peak in one corner of the multidimensional space. (_this function_)
+
+The functions are further characterized by shift and scale parameters.
+While the shift parameter has minimal impact on the integral's value,
+the scale parameter significantly affects
+the difficulty of the integration problem.
+```
+
+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 Corner Peak
+my_fun_1d = uqtf.GenzCornerPeak(input_dimension=1)
+xx_1d = np.linspace(0, 1, 1000)[:, np.newaxis]
+yy_1d = my_fun_1d(xx_1d)
+
+# --- Create 2D data from Genz Corner Peak
+my_fun_2d = uqtf.GenzCornerPeak(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 corner peak")
+
+# 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 corner peak", 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 corner peak", 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);
+```
+
+## Test function instance
+
+To create a default instance of the test function:
+
+```{code-cell} ipython3
+my_testfun = uqtf.GenzCornerPeak()
+```
+
+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.GenzCornerPeak(input_dimension=6)
+```
+
+## Description
+
+The Genz corner peak function as follows:
+
+$$
+\mathcal{M}(\boldsymbol{x}; \boldsymbol{a}) = \left( 1 + \sum_{i = 1}^{M} a_i x_i \right)^{-(M + 1)}
+$$
+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)$ is the $M$-dimensional
+vector of (fixed) scale parameters.
+Further details about these parameters are provided below.
+
+## Probabilistic input
+
+The input specification for the Genz corner peak 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 scale parameters $\boldsymbol{a}$,
+which determine the extent of the corner peaking.
+Larger values of $\boldsymbol{a}$
+increase the prominence of the peak,
+making the integration problem more challenging.
+
+The available parameters for the Genz corner peak function
+are shown in the table below.
+
+```{table} Available parameters of the Genz corner peak function
+:name: genz-corner-peak-parameters
+
+```
+| No. |                    $\boldsymbol{a}$                    |          Keyword          |             Source              |          Remark           |
+|:---:|:------------------------------------------------------:|:-------------------------:|:-------------------------------:|:-------------------------:|
+| 1.  |                $a_1 = \ldots = a_M = 5$                | `Genz1984` <br> (default) |        {cite}`Genz1984`         |            ---            |
+| 2.  | $a_i = 0.02 + 0.03 \times (i - 1),\, i = 1, \ldots, M$ |       `Zhang2014-1`       | {cite}`Zhang2014` (Section 4.4) | Originally, 3 dimensions  |
+| 3.  |       $a_i = 0.01 \times i,\, i = 1, \ldots, M$        |       `Zhang2014-2`       | {cite}`Zhang2014` (Section 4.4) | Originally, 10 dimensions |
+
+The default parameter is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.parameters)
+```
+
+````{note}
+To create an instance of the Genz corner peak function with different built-in
+parameter values, pass the corresponding keyword to the parameter `parameters_id`.
+For example, to use the parameters from {cite}`Zhang2014`,
+type:
+
+```python
+my_testfun = uqtf.GenzCornerPeak(parameters_id="Zhang2014-1")
+```
+````
+
+## 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);
+```
+
+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/src/uqtestfuns/core/uqtestfun_abc.py b/src/uqtestfuns/core/uqtestfun_abc.py
index f255f1c..3253f36 100644
--- a/src/uqtestfuns/core/uqtestfun_abc.py
+++ b/src/uqtestfuns/core/uqtestfun_abc.py
@@ -588,7 +588,7 @@ def _create_prob_input(self, input_id: str, input_dim: int) -> ProbInput:
             marginals = []
             for i in range(input_dim):
                 marginal = raw_data["marginals"][0].copy()
-                marginal["name"] = f"{marginal['name']}{i}"
+                marginal["name"] = f"{marginal['name']}{i+1}"
                 marginals.append(Marginal(**marginal))
 
         # Recast the type to satisfy type checker
diff --git a/src/uqtestfuns/test_functions/__init__.py b/src/uqtestfuns/test_functions/__init__.py
index 0b78891..e6242a3 100644
--- a/src/uqtestfuns/test_functions/__init__.py
+++ b/src/uqtestfuns/test_functions/__init__.py
@@ -20,6 +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
 from .gramacy2007 import Gramacy1DSine
 from .hyper_sphere import HyperSphere
 from .ishigami import Ishigami
@@ -79,6 +80,7 @@
     "Friedman6D",
     "Friedman10D",
     "GaytonHat",
+    "GenzCornerPeak",
     "Gramacy1DSine",
     "HyperSphere",
     "Ishigami",
diff --git a/src/uqtestfuns/test_functions/genz.py b/src/uqtestfuns/test_functions/genz.py
new file mode 100644
index 0000000..ac8f961
--- /dev/null
+++ b/src/uqtestfuns/test_functions/genz.py
@@ -0,0 +1,178 @@
+"""
+Module with implementations of integral test functions from Genz (1984).
+
+Genz [1] introduced six challenging parameterized $M$-dimensional functions
+designed to test the performance of numerical integration routines:
+
+- Corner peak function features a prominent peak in one corner
+  of the multidimensional space.
+
+The functions are further characterized by shift and scale parameters.
+While the shift parameter has minimal impact on the integral's value,
+the scale parameter significantly affects
+the difficulty of the integration problem.
+
+Some of the functions were featured as test functions in sensitivity
+analysis literature, e.g., [2].
+
+References
+----------
+
+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, 2014.
+   DOI: 10.1016/j.ress.2013.07.010
+"""
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import (
+    FunParamSpecs,
+    MarginalSpecs,
+    ProbInputSpecs,
+)
+from uqtestfuns.core.uqtestfun_abc import UQTestFunVarDimABC
+
+__all__ = ["GenzCornerPeak"]
+
+
+MARGINALS_GENZ1984: MarginalSpecs = [
+    {
+        "name": "X",
+        "distribution": "uniform",
+        "parameters": [0.0, 1.0],
+        "description": None,
+    }
+]
+
+COMMON_TAGS = ["integration", "sensitivity"]
+
+
+def _get_aa_genz_1984(input_dimension: int) -> np.ndarray:
+    """Construct a scaling array for the Genz function from [1]."""
+    aa = 5.0 * np.ones(input_dimension)
+
+    return aa
+
+
+def _get_aa_zhang_2014_1(input_dimension: int) -> np.ndarray:
+    """Construct a scaling array for the Genz function from [2] (3D case).
+
+    Notes
+    -----
+    - The parameters were defined only for the corner peak function.
+    - The test function was originally limited to three dimensions; here,
+      the values beyond dimension 3 are extrapolated.
+    """
+    aa = 0.02 + 0.03 * np.arange(input_dimension)
+
+    return aa
+
+
+def _get_aa_zhang_2014_2(input_dimension: int) -> np.ndarray:
+    """Construct a scaling array for the Genz function from [2] (10D case).
+
+    Notes
+    -----
+    - The parameters were defined only for the corner peak function.
+    - The test function was originally limited to ten dimensions; here,
+      the values beyond dimension 10 are extrapolated.
+    """
+    aa = 0.1 * np.arange(1, input_dimension + 1)
+
+    return aa
+
+
+def evaluate_corner_peak(xx: np.ndarray, aa: np.ndarray) -> np.ndarray:
+    """Evaluate the corner peak 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 scale parameters of the Genz family of integrand
+        functions; the larger the value the more difficult the integrations.
+
+    Returns
+    -------
+    np.ndarray
+        The output of the test function evaluated on the input values.
+        The output is a 1-dimensional array of length N.
+    """
+    dim = xx.shape[1]
+
+    yy = (1 + (xx @ aa)) ** (-(dim + 1))
+
+    return yy
+
+
+class GenzCornerPeak(UQTestFunVarDimABC):
+    """A concrete implementation of the corner peak from Genz (1984)."""
+
+    _tags = COMMON_TAGS
+    _description = "Corner peak integrand from Genz (1984)"
+    _available_inputs: ProbInputSpecs = {
+        "Genz1984": {
+            "function_id": "GenzCornerPeak",
+            "description": (
+                "Input specification for the Genz family of integrand"
+            ),
+            "marginals": MARGINALS_GENZ1984,
+            "copulas": None,
+        }
+    }
+    _available_parameters: FunParamSpecs = {
+        "Genz1984": {
+            "function_id": "GenzCornerPeak",
+            "description": (
+                "Parameter set for the corner peak function from Genz (1984)"
+            ),
+            "declared_parameters": [
+                {
+                    "keyword": "aa",
+                    "value": _get_aa_genz_1984,
+                    "type": np.ndarray,
+                    "description": "Scaling parameter",
+                },
+            ],
+        },
+        "Zhang2014-1": {
+            "function_id": "GenzCornerPeak",
+            "description": (
+                "Parameter set for the corner peak function from Zhang and "
+                "Pandey (2014); Section 4.4 (3D case)"
+            ),
+            "declared_parameters": [
+                {
+                    "keyword": "aa",
+                    "value": _get_aa_zhang_2014_1,
+                    "type": np.ndarray,
+                    "description": "Scaling parameter",
+                },
+            ],
+        },
+        "Zhang2014-2": {
+            "function_id": "GenzCornerPeak",
+            "description": (
+                "Parameter set for the corner peak function from Zhang and "
+                "Pandey (2014); Section 4.4 (10D case)"
+            ),
+            "declared_parameters": [
+                {
+                    "keyword": "aa",
+                    "value": _get_aa_zhang_2014_2,
+                    "type": np.ndarray,
+                    "description": "Scaling parameter",
+                },
+            ],
+        },
+    }
+    _default_parameters_id = "Genz1984"
+
+    evaluate = staticmethod(evaluate_corner_peak)  # type: ignore