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

Issue:
Add an implementation of the Robot Arm function
The robot arm function models the distance between an origin and the end tip of a four-segment robot arm.
The function is 8-dimensional, scalar-valued function which appears in [^1] for metamodeling purposes.

Consider a four-segment robot arm; the segments have lengths $L_1$, $L_2$, $L_3$, and $L_4$.
The shoulder of the robot arm is at the origin in the $(x, y)$-plane.
The end of the robot arm is given at the following position:

$$
\begin{aligned}
x_e & = \sum_{i = 1}^4 L_i \cos{\left( \sum_{j = 1}^i \theta_j \right) } \\
y_e & = \sum_{i = 1}^4 L_i \sin{\left( \sum_{j = 1}^i \theta_j \right) }
\end{aligned}
$$

where $\theta_1$ is the angle form by the first segment and the horizontal coordinate axis of the plane,
$\theta_2, \theta_3, \theta_4$ are the angles form between segment $1$, $2$, and $3$ with the previous segment, respectively.

The quantity of interest of the function is the distance between the origin and the end of the robot arm, that is

$$
\mathcal{M}(\boldsymbol{x}) = (x_e^2 + y_e^2)^{0.5},
$$

where $\boldsymbol{x} = \left( L_1, L_2, L_3, L_4, \theta_1, \theta_2, \theta_3, \theta_4 \right)$ is the 8-dimensional vector of input variables.
$L_i \sim \mathcal{U}(0, 1)$ and $\theta_i \sim \mathcal{U}(0, 2 \pi)$ for $i = 1, \ldots, 4$.

[^1]: J. An and A. Owen, “Quasi-regression,” *Journal of Complexity*, vol. 17, no. 4, pp. 588–607, Dec. 2001, doi: [10.1006/jcom.2001.0588](https://doi.org/10.1006/jcom.2001.0588).



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index de1bd12..4ed9b51 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Added
+
+- The 8-dimensional robot arm function for metamodeling exercises.
+
 ## [0.5.0] - 2024-11-18
 
 ### Added
diff --git a/docs/_toc.yml b/docs/_toc.yml
index 14c14aa..028eb9a 100644
--- a/docs/_toc.yml
+++ b/docs/_toc.yml
@@ -122,6 +122,8 @@ parts:
             title: Piston Simulation
           - file: test-functions/portfolio-3d
             title: Simple Portfolio Model
+          - file: test-functions/robot-arm
+            title: Robot Arm
           - file: test-functions/rs-circular-bar
             title: RS - Circular Bar
           - file: test-functions/rs-quadratic
diff --git a/docs/fundamentals/metamodeling.md b/docs/fundamentals/metamodeling.md
index 9580e7e..605d21e 100644
--- a/docs/fundamentals/metamodeling.md
+++ b/docs/fundamentals/metamodeling.md
@@ -48,6 +48,7 @@ in the comparison of metamodeling approaches.
 |      {ref}`Oakley & O'Hagan (2002) 1D <test-functions:oakley-1d>`      |        1        |      `Oakley1D()`      |
 |            {ref}`OTL Circuit <test-functions:otl-circuit>`             |     6 / 20      |     `OTLCircuit()`     |
 |            {ref}`Piston Simulation <test-functions:piston>`            |     7 / 20      |       `Piston()`       |
+|              {ref}`Robot Arm <test-functions:robot-arm>`               |        8        |      `RobotArm()`      |
 |          {ref}`Solar Cell Model <test-functions:solar-cell>`           |        5        |     `SolarCell()`      |
 |                 {ref}`Sulfur <test-functions:sulfur>`                  |        9        |       `Sulfur()`       |
 |    {ref}`Undamped Oscillator <test-functions:undamped-oscillator>`     |        6        | `UndampedOscillator()` |
diff --git a/docs/references.bib b/docs/references.bib
index d6ad70e..d97ef2e 100644
--- a/docs/references.bib
+++ b/docs/references.bib
@@ -942,4 +942,15 @@ @Article{Constantine2015
   doi     = {10.1002/sam.11281},
 }
 
+@Article{An2001,
+  author  = {An, Jian and Owen, Art},
+  journal = {Journal of Complexity},
+  title   = {Quasi-regression},
+  year    = {2001},
+  number  = {4},
+  pages   = {588--607},
+  volume  = {17},
+  doi     = {10.1006/jcom.2001.0588},
+}
+
 @Comment{jabref-meta: databaseType:bibtex;}


Test patch:
diff --git a/docs/test-functions/available.md b/docs/test-functions/available.md
index ed4d56f..7734c90 100644
--- a/docs/test-functions/available.md
+++ b/docs/test-functions/available.md
@@ -63,6 +63,7 @@ regardless of their typical applications.
 |            {ref}`Oakley & O'Hagan (2002) 1D <test-functions:oakley-1d>`             |        1        |          `Oakley1D()`           |
 |                   {ref}`OTL Circuit <test-functions:otl-circuit>`                   |     6 / 20      |         `OTLCircuit()`          |
 |                  {ref}`Piston Simulation <test-functions:piston>`                   |     7 / 20      |           `Piston()`            |
+|                     {ref}`Robot Arm <test-functions:robot-arm>`                     |        8        |          `RobotArm()`           |
 |             {ref}`Simple Portfolio Model <test-functions:portfolio-3d>`             |        3        |         `Portfolio3D()`         |
 |              {ref}`RS - Circular Bar <test-functions:rs-circular-bar>`              |        2        |        `RSCircularBar()`        |
 |                 {ref}`RS - Quadratic <test-functions:rs-quadratic>`                 |        2        |         `RSQuadratic()`         |
diff --git a/docs/test-functions/robot-arm.md b/docs/test-functions/robot-arm.md
new file mode 100644
index 0000000..f702051
--- /dev/null
+++ b/docs/test-functions/robot-arm.md
@@ -0,0 +1,119 @@
+---
+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:robot-arm)=
+# Robot Arm Function
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The robot arm function is an eight-dimensional, scalar-valued test function
+that calculates the distance between a fixed origin and the tip
+of a four-segment robot arm.
+
+The function is commonly used in metamodeling exercises; for example,
+see {cite}`An2001, Bouhlel2019`. Due to its complexity, the function is
+challenging to approximate using polynomials and has therefore been studied 
+extensively in the neural network literature {cite}`An2001`.
+
+## Test function instance
+
+To create a default instance of the robot arm test function:
+
+```{code-cell} ipython3
+my_testfun = uqtf.RobotArm()
+```
+
+Check if it has been correctly instantiated:
+
+```{code-cell} ipython3
+print(my_testfun)
+```
+
+## Description
+
+Consider a four-segment robot arm, with its shoulder fixed at the origin
+in the $(x, y)$-plane.
+The segments have lengths $L_1, L_2, L_3$ and $L_4$.
+The position of the end of the robot arm is given by:
+
+$$
+\begin{aligned}
+x_* (\boldsymbol{x}) & = \sum_{i = 1}^4 L_i \, \cos{\left( \sum_{j = 1}^i \theta_j \right)} \\
+y_* (\boldsymbol{x}) & = \sum_{i = 1}^4 L_i \, \sin{\left( \sum_{j = 1}^i \theta_j \right)},
+\end{aligned}
+$$
+
+where:
+
+- $\theta_1$ is the angle formed by the first segment and the horizontal axis, and
+- $\theta_2, \theta_3, \theta_4$ are the angles formed by consecutive segments
+  with the previous segments, i.e., the 2nd and 1st, 3rd and 2nd,
+  and 4th and 3rd segments, respectively.
+
+The robot arm function computes the Euclidean distance between the tip
+of the robot arm and the fixed origin, expressed as[^location]
+
+$$
+\mathcal{M}(\boldsymbol{x}) = \sqrt{x_*^2(\boldsymbol{x}) + y_*^2(\boldsymbol{x})},
+$$
+
+where $\boldsymbol{x} = \left( L_1, L_2, L_3, L_4, \theta_1, \theta_2, \theta_3, \theta_4 \right)$
+is the eight-dimensional vector of input variables further defined below.
+
+## Probabilistic input
+
+The probabilistic input model for the robot arm function consists of eight
+independent uniform random variables with the ranges shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
+```
+
+## 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 $1'000'000$ random points:
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+my_testfun.prob_input.reset_rng(42)
+xx_test = my_testfun.prob_input.get_sample(1000000)
+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);
+```
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^location]: see Section 6.2, pp. 600-601 in {cite}`An2001`.
\ No newline at end of file
diff --git a/src/uqtestfuns/test_functions/__init__.py b/src/uqtestfuns/test_functions/__init__.py
index 33588a2..54adf8e 100644
--- a/src/uqtestfuns/test_functions/__init__.py
+++ b/src/uqtestfuns/test_functions/__init__.py
@@ -29,6 +29,7 @@
 from .morris2006 import Morris2006
 from .piston import Piston
 from .portfolio_3d import Portfolio3D
+from .robot_arm import RobotArm
 from .rs_circular_bar import RSCircularBar
 from .rs_quadratic import RSQuadratic
 from .saltelli_linear import SaltelliLinear
@@ -90,6 +91,7 @@
     "Morris2006",
     "Piston",
     "Portfolio3D",
+    "RobotArm",
     "RSCircularBar",
     "RSQuadratic",
     "SaltelliLinear",
diff --git a/src/uqtestfuns/test_functions/robot_arm.py b/src/uqtestfuns/test_functions/robot_arm.py
new file mode 100644
index 0000000..c41ad9d
--- /dev/null
+++ b/src/uqtestfuns/test_functions/robot_arm.py
@@ -0,0 +1,130 @@
+"""
+Module with an implementation of the Robot Arm test function.
+
+The Robot Arm test function is an eight-dimensional scalar-valued test function
+typically used in metamodeling exercises (see, for example, [1] and [2]).
+
+References
+----------
+
+1. J. An and A. Owen, “Quasi-regression,” Journal of Complexity, vol. 17,
+   no. 4, pp. 588–607, 2001.
+   DOI: 10.1006/jcom.2001.0588
+2. M. A. Bouhlel, J. T. Hwang, N. Bartoli, R. Lafage, J. Morlier,
+   and J. R. R. A. Martins, “A Python surrogate modeling framework with
+   derivatives,” Advances in Engineering Software, vol. 135, p. 102662, 2019.
+   DOI: 10.1016/j.advengsoft.2019.03.005
+"""
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
+
+__all__ = ["RobotArm"]
+
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "An2001": {
+        "function_id": "RobotArm",
+        "description": (
+            "Input model for the Robot Arm function from An and Owen (2001)"
+        ),
+        "marginals": [
+            {
+                "name": "L1",
+                "distribution": "uniform",
+                "parameters": [0, 1],
+                "description": "Length of the 1st segment",
+            },
+            {
+                "name": "L2",
+                "distribution": "uniform",
+                "parameters": [0, 1],
+                "description": "Length of the 2nd segment",
+            },
+            {
+                "name": "L3",
+                "distribution": "uniform",
+                "parameters": [0, 1],
+                "description": "Length of the 3rd segment",
+            },
+            {
+                "name": "L4",
+                "distribution": "uniform",
+                "parameters": [0, 1],
+                "description": "Length of the 4th segment",
+            },
+            {
+                "name": "theta1",
+                "distribution": "uniform",
+                "parameters": [0, 2 * np.pi],
+                "description": "Angle between 1st segment and horizontal",
+            },
+            {
+                "name": "theta2",
+                "distribution": "uniform",
+                "parameters": [0, 2 * np.pi],
+                "description": "Angle between 2nd segment and 1st segment",
+            },
+            {
+                "name": "theta3",
+                "distribution": "uniform",
+                "parameters": [0, 2 * np.pi],
+                "description": "Angle between 3rd segment and 2nd segment",
+            },
+            {
+                "name": "theta4",
+                "distribution": "uniform",
+                "parameters": [0, 2 * np.pi],
+                "description": "Angle between 4th segment and 3rd segment",
+            },
+        ],
+        "copulas": None,
+    },
+}
+
+
+def evaluate(xx: np.ndarray) -> np.ndarray:
+    """Evaluate the Robot Arm test function on a set of input values.
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        8-Dimensional input values given by an N-by-8 array where
+        N is the number of input values.
+
+    Returns
+    -------
+    np.ndarray
+        The output of the test function evaluated on the input values.
+        The output is a 1-dimensional array of length N.
+    """
+    # Rearrange inputs
+    ll = xx[:, :4]  # Segment lengths
+    theta = xx[:, 4:]  # Angles
+
+    # Compute the position on the x-coord
+    xx_loc = np.zeros(len(xx))
+    yy_loc = np.zeros(len(xx))
+
+    # Compute the end locations
+    for i in range(4):
+        xx_loc += ll[:, i] * np.cos(np.sum(theta[:, : i + 1]))
+        yy_loc += ll[:, i] * np.sin(np.sum(theta[:, : i + 1]))
+
+    # Compute the distance between end location and origin
+    yy = np.sqrt(xx_loc**2 + yy_loc**2)
+
+    return yy
+
+
+class RobotArm(UQTestFunFixDimABC):
+    """A concrete implementation of the robot arm test function."""
+
+    _tags = ["metamodeling"]
+    _description = "Four-segment robot arm function from An and Owen (2001)"
+    _available_inputs = AVAILABLE_INPUTS
+    _available_parameters = None
+
+    evaluate = staticmethod(evaluate)  # type: ignore