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

Issue:
Update all organization docs to version 0.5.0
Bump version from 0.4.1 to 0.5.0. All relevant organizational documents have been updated to the new versions.

This PR partially resolves Issue #256; the `dev` branch is ready to be merged to `main`.
Prepare the release of version v0.5.0
- [x] Close (or defer) all issues related to v0.5.0
- [x] Update the CHANGELOG
- [x] Update the link in the CHANGELOG
- [x] Update the version in setup.cfg
- [x] Update the ZENODO link and badge in README.md (need candidate DOI)
- [x] Update the link in CFF file
   - [x] Zenodo
   - [x] Zenodo description
   - [x] Version
   - [x] Date released
- [x] Update the Zenodo archive citation version in README
    - [x] BibTeX key
    - [x] month
    - [x] year
    - [x] version
    - [x] doi
    - [x] url
- [x] Update the Zenodo archive citation version in the docs
    - [x] BibTeX key
    - [x] month
    - [x] year
    - [x] version
    - [x] doi
    - [x] url
- [x] Merge `dev` to `main` 
- [x] Create a release note
- [x] Create a tagged release
Rename `spatial_dimension` to `input_dimension` across codebase
Following Issue #345, if the new abstract property is introduced, then it would be more intuitive to also rename the property `spatial_dimension`[^spatial] to `input_dimension`.

[^spatial]: the main reason to adopt the name `spatial_dimension` in the first place (albeit rather unconventional to the community) was due to my involvement in maintaining the minterpy package. There the dimensionality of the polynomials is called `spatial_dimension`.
Issue on page /test-functions/sobol-g.html
The opening paragraphs of the page are redundant and not properly cross-referenced.


Discussion / hints:

Fix patch:
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 8b676a1..433be60 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -3,7 +3,7 @@ version: 2
 build:
   os: ubuntu-22.04
   tools:
-    python: "3.9"
+    python: "3.11"
   jobs:
     pre_build:
       - "jupyter-book config sphinx docs/"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c3568e..de1bd12 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+## [0.5.0] - 2024-11-18
+
+### Added
+
+- The 5-dimensional single-diode solar cell model from Constantine et al.
+  (2015); this is the first function of which the solution must be computed
+  numerically.
+- JOSS badge and instruction how to cite the paper and package in the README.
+- A commentary on sensitivity analysis within the UQ framework in the docs.
+- A commentary on metamodeling within the UQ framework in the docs.
+- A better overview of UQ framework in the docs.
+- A new parameter set for the Sobol'-G function from Sun et al. (2022).
+- The 6-dimensional undamped non-linear oscillator function for reliability
+  analysis exercises.
+- The M-dimension Sobol'-Levitan function from Sobol' and Levitan (1999) for
+  sensitivity analysis exercises.
+- The M-dimensional test function from Morris et al. (2006) for sensitivity
+  analysis exercises.
+- The modified Sobol'-G test function (i.e., Sobol'-G*) from Saltelli et al.
+  (2010) for sensitivity analysis exercises.
+- The two-dimensional test function from Cheng and Sandu (2010)
+  for metamodeling exercises.
+- The M-dimensional linear function from Saltelli et al. (2008) for sensitivity
+  analysis.
+- The two-dimensional non-polynomial test function for metamodeling from
+  Lim et al. (2002).
+- The two-dimensional polynomial test function for metamodeling from 
+  Lim et al. (2002).
+- The three-dimensional sensitivity test function from Moon (2010).
+- Two new abstract base classes are added, namely `UQTestFunFixDimABC` and
+  `UQTestFunVarDimABC` to deal with the construction of UQ test functions of
+  fixed and variable dimensions, respectively. Both abstract classes are derived
+  from `UQTestFunABC` such that the interfaces remain consistent. 
+- `function_id` and `input_id` are now property of `ProbInput`.
+- `output_dimension` is now property of `UQTestFunBareABC` and inherited to
+  all concrete classes of UQ test functions.
+- Printing a test function instance now shows whether the function is 
+  parameterized or not.
+- The information related to the parameterization of a function is now
+  shown in the output of `list_functions()`
+- New class `FunParams` to organize function parameters.
+- The six-dimensional and ten-dimensional Friedman functions from
+  Friedman et al. (1983) and Friedman (1991), respectively.
+- The three-dimensional simple portfolio model from Saltelli et al. (2004).
+- The 20-dimensional polynomial test function from Alemazkoor
+  and Meidani (2018).
+- The exponential distribution as a distribution of `UnivDist` instances.
+
+### Changed
+
+- "None" copula is now printed as "Independence"; this is a temporary solution
+  as there is no independence copula object yet.
+- Application tags are now displayed when an instance of test function is
+  printed on the terminal.
+- `list_functions()` is now printed in grid format and include information
+  regarding the output dimension and the parameterization. Furthermore,
+  filtering can be done based on the input dimension, output dimension,
+  tag, and parameterization.
+- The class `UnivDist` has been renamed to `Marginal`. The name more clearly
+  refers to one-dimensional marginal distributions (of a univariate random
+  variable), which form a `ProbInput`.
+- The property `spatial_dimension` of `ProbInput` and `UQTestFunBareABC` is
+  renamed to `input_dimension` for clarity (as opposed to `output_dimension`).
+- The property `name` of UQ test function instances has been renamed to
+  `function_id` that implies uniqueness, although it is not strictly enforced.
+- The parameter in the Gramacy 1D sine function is now removed. Noise can
+  be added on the fly if needed.
+- `evaluate()` abstract method is now must be implemented directly in the
+  concrete UQ test function; `eval_()` in the `UQTestFunABC` has been removed.
+
+### Fixed
+
+- Minor fixes of issues (typos and grammatical mistakes) related to the
+  documentation with additional overall improvements.
+
 ## [0.4.1] - 2023-10-27
 
 ### Added
@@ -185,6 +260,7 @@ First public release of UQTestFuns.
 - Mirror GitHub action to the [CASUS organization](https://github.com/casus)
 
 [Unreleased]: https://github.com/damar-wicaksono/uqtestfuns/compare/main...dev
+[0.5.0]: https://github.com/damar-wicaksono/uqtestfuns/compare/v0.4.1...v0.5.0
 [0.4.1]: https://github.com/damar-wicaksono/uqtestfuns/compare/v0.4.0...v0.4.1
 [0.4.0]: https://github.com/damar-wicaksono/uqtestfuns/compare/v0.3.0...v0.4.0
 [0.3.0]: https://github.com/damar-wicaksono/uqtestfuns/compare/v0.2.0...v0.3.0
diff --git a/CITATION.cff b/CITATION.cff
index 2cb299f..0e8e9e2 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -12,10 +12,14 @@ authors:
     family-names: Wicaksono
     affiliation: CASUS/HZDR
     orcid: 'https://orcid.org/0000-0001-8587-7730'
+  - given-names: Michael
+    family-names: Hecht
+    affiliation: CASUS/HZDR
+    orcid: 'https://orcid.org/0000-0001-9214-8253'
 identifiers:
   - type: doi
-    value: 10.5281/zenodo.10047512
-    description: The Zenodo URL of version 0.4.1 of the package
+    value: 10.5281/zenodo.14180639
+    description: The Zenodo URL of version 0.5.0 of the package
 repository-code: 'https://github.com/damar-wicaksono/uqtestfuns'
 url: 'https://uqtestfuns.readthedocs.io/en/latest/'
 repository-artifact: 'https://pypi.org/project/uqtestfuns/'
@@ -38,5 +42,5 @@ keywords:
   - reliability-analysis
   - surrogate-modeling
 license: MIT
-version: 0.4.1
-date-released: '2023-10-27'
+version: 0.5.0
+date-released: '2024-11-18'
diff --git a/README.md b/README.md
index 0f477f7..103c354 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@
 # UQTestFuns
-[![DOI](http://img.shields.io/badge/DOI-10.5281/zenodo.10047512-blue.svg?style=flat-square)](https://doi.org/10.5281/zenodo.10047512)
+[![JOSS](https://img.shields.io/badge/JOSS-10.21105/joss.05671-brightgreen?style=flat-square)](https://doi.org/10.21105/joss.05671)
+[![DOI](http://img.shields.io/badge/DOI-10.5281/zenodo.14180639-blue.svg?style=flat-square)](https://doi.org/10.5281/zenodo.14180639)
 [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square)](https://github.com/psf/black)
 [![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg?style=flat-square)](https://www.python.org/downloads/release/python-370/)
 [![License](https://img.shields.io/github/license/damar-wicaksono/uqtestfuns?style=flat-square)](https://choosealicense.com/licenses/mit/)
@@ -33,17 +34,23 @@ To list the available functions:
 ```python-repl
 >>> import uqtestfuns as uqtf
 >>> uqtf.list_functions()
- No.            Constructor            Dimension                Application                Description
------  -----------------------------  -----------  --------------------------------------  ----------------------------------------------------------------------------
-  1              Ackley()                  M             optimization, metamodeling        Optimization test function from Ackley (1987)
-  2           Alemazkoor2D()               2                    metamodeling               Two-dimensional high-degree polynomial from Alemazkoor & Meidani (2018)
-  3             Borehole()                 8             metamodeling, sensitivity         Borehole function from Harper and Gupta (1983)
-  4           Bratley1992a()               M              integration, sensitivity         Integration test function #1 from Bratley et al. (1992)
-  5           Bratley1992b()               M              integration, sensitivity         Integration test function #2 from Bratley et al. (1992)
-  6           Bratley1992c()               M              integration, sensitivity         Integration test function #3 from Bratley et al. (1992)
-  7           Bratley1992d()               M              integration, sensitivity         Integration test function #4 from Bratley et al. (1992)
-  8         CantileverBeam2D()             2                    reliability                Cantilever beam reliability problem from Rajashekhar and Ellington (1993)
-  9         CircularPipeCrack()            2                    reliability                Circular pipe under bending moment from Verma et al. (2015)
++-------+-------------------------------+-----------+------------+----------+---------------+--------------------------------+
+|  No.  |          Constructor          |  # Input  |  # Output  |  Param.  |  Application  | Description                    |
++=======+===============================+===========+============+==========+===============+================================+
+|   1   |           Ackley()            |     M     |     1      |   True   | optimization, | Optimization test function     |
+|       |                               |           |            |          | metamodeling  | from Ackley (1987)             |
++-------+-------------------------------+-----------+------------+----------+---------------+--------------------------------+
+|   2   |        Alemazkoor20D()        |    20     |     1      |  False   | metamodeling  | High-dimensional low-degree    |
+|       |                               |           |            |          |               | polynomial from Alemazkoor &   |
+|       |                               |           |            |          |               | Meidani (2018)                 |
++-------+-------------------------------+-----------+------------+----------+---------------+--------------------------------+
+|   3   |        Alemazkoor2D()         |     2     |     1      |  False   | metamodeling  | Low-dimensional high-degree    |
+|       |                               |           |            |          |               | polynomial from Alemazkoor &   |
+|       |                               |           |            |          |               | Meidani (2018)                 |
++-------+-------------------------------+-----------+------------+----------+---------------+--------------------------------+
+|   4   |          Borehole()           |     8     |     1      |  False   | metamodeling, | Borehole function from Harper  |
+|       |                               |           |            |          |  sensitivity  | and Gupta (1983)               |
++-------+-------------------------------+-----------+------------+----------+---------------+--------------------------------+
 ...
 ```
 
@@ -53,33 +60,37 @@ and sensitivity analysis purposes; to create an instance of this test function:
 ```python-repl
 >>> my_testfun = uqtf.Borehole()
 >>> print(my_testfun)
-Name              : Borehole
-Spatial dimension : 8
-Description       : Borehole function from Harper and Gupta (1983)
+Function ID      : Borehole
+Input Dimension  : 8 (fixed)
+Output Dimension : 1
+Parameterized    : False
+Description      : Borehole function from Harper and Gupta (1983)
+Applications     : metamodeling, sensitivity
 ```
 
 The probabilistic input specification of this test function is built-in:
 
 ```python-repl
 >>> print(my_testfun.prob_input)
-Name         : Borehole-Harper-1983
-Spatial Dim. : 8
-Description  : Probabilistic input model of the Borehole model from Harper and Gupta (1983).
-Marginals    :
-
-  No.   Name    Distribution        Parameters                          Description                  
-                                                                                                     
+Function ID     : Borehole
+Input ID        : Harper1983
+Input Dimension : 8
+Description     : Probabilistic input model of the Borehole model from
+                  Harper and Gupta (1983)
+Marginals       :
+
+ No.    Name    Distribution        Parameters                          Description
 -----  ------  --------------  ---------------------  -----------------------------------------------
-    1    rw        normal      [0.1       0.0161812]            radius of the borehole [m]
-    2    r       lognormal        [7.71   1.0056]                 radius of influence [m]
-    3    Tu       uniform        [ 63070. 115600.]      transmissivity of upper aquifer [m^2/year]
-    4    Hu       uniform          [ 990. 1100.]         potentiometric head of upper aquifer [m]
-    5    Tl       uniform          [ 63.1 116. ]        transmissivity of lower aquifer [m^2/year]
-    6    Hl       uniform           [700. 820.]          potentiometric head of lower aquifer [m]    
-    7    L        uniform          [1120. 1680.]                length of the borehole [m]                                       
-    8    Kw       uniform         [ 9985. 12045.]     hydraulic conductivity of the borehole [m/year]
-
-    Copulas      : None
+  1      rw        normal      [0.1       0.0161812]            radius of the borehole [m]
+  2      r       lognormal        [7.71   1.0056]                 radius of influence [m]
+  3      Tu       uniform        [ 63070. 115600.]      transmissivity of upper aquifer [m^2/year]
+  4      Hu       uniform          [ 990. 1100.]         potentiometric head of upper aquifer [m]
+  5      Tl       uniform          [ 63.1 116. ]        transmissivity of lower aquifer [m^2/year]
+  6      Hl       uniform           [700. 820.]          potentiometric head of lower aquifer [m]
+  7      L        uniform          [1120. 1680.]                length of the borehole [m]
+  8      Kw       uniform         [ 9985. 12045.]     hydraulic conductivity of the borehole [m/year]
+
+Copulas         : Independence
 ```
 
 A sample of input values can be generated from the input model:
@@ -145,6 +156,49 @@ and the documentation (including new reference results) are welcomed!
 Please consider the [Contribution Guidelines](CONTRIBUTING.MD) first,
 before making a pull request. 
 
+## Citing UQTestFuns
+
+If you use this package in your research or projects, please consider citing
+both the associated paper and the Zenodo archive (for the specific version
+used).
+
+### Citing the paper (JOSS)
+
+The citation of the paper associated with this package is:
+
+```bibtex
+@article{Wicaksono2023,
+  author    = {Wicaksono, Damar and Hecht, Michael},
+  title     = {{UQTestFuns}: A {Python3} library of uncertainty quantification ({UQ}) test functions},
+  journal   = {Journal of Open Source Software},
+  year      = {2023},
+  volume    = {8},
+  number    = {90},
+  doi       = {10.21105/joss.05671},
+}
+```
+
+### Citing a specific version (Zenodo)
+
+To ensure reproducibility, cite the exact version of the package you used.
+Each release is archived on Zenodo with a unique DOI; find and use the DOI
+for the version you used at [Zenodo].
+
+The citation for the current public version is:
+
+```bibtex
+@software{UQTestFuns_0_5_0,
+  author       = {Wicaksono, Damar and Hecht, Michael},
+  title        = {{UQTestFuns: A Python3 Library of Uncertainty Quantification (UQ) Test Functions}},
+  month        = nov,
+  year         = 2024,
+  publisher    = {Zenodo},
+  version      = {v0.5.0},
+  doi          = {10.5281/zenodo.14180639},
+  url          = {https://doi.org/10.5281/zenodo.14180639}
+}
+```
+
 ## Credits and contributors
 
 <!--Credits and contributors-->
@@ -166,3 +220,5 @@ led by Michael Hecht ([HZDR/CASUS](https://www.casus.science/)) at CASUS.
 
 <!--License-->
 UQTestFuns is released under the [MIT License](LICENSE).
+
+[Zenodo]: https://zenodo.org/search?q=parent.id%3A7701903&f=allversions%3Atrue&l=list&p=1&s=10&sort=version
diff --git a/docs/_toc.yml b/docs/_toc.yml
index a928c6c..14c14aa 100644
--- a/docs/_toc.yml
+++ b/docs/_toc.yml
@@ -18,8 +18,11 @@ parts:
           - file: getting-started/tutorial-custom-functions
             title: Custom Test Functions
       - file: getting-started/about-uq-test-functions
+      - file: getting-started/citing
   - caption: Test Functions
     chapters:
+      - file: fundamentals/overview
+        title: Overview
       - file: fundamentals/metamodeling
         title: Metamodeling
       - file: fundamentals/sensitivity
@@ -37,6 +40,8 @@ parts:
             title: Ackley
           - file: test-functions/alemazkoor-2d
             title: Alemazkoor & Meidani (2018) 2D
+          - file: test-functions/alemazkoor-20d
+            title: Alemazkoor & Meidani (2018) 20D
           - file: test-functions/borehole
             title: Borehole
           - file: test-functions/bratley1992a
@@ -49,6 +54,8 @@ parts:
             title: Bratley et al. (1992) D
           - file: test-functions/cantilever-beam-2d
             title: Cantilever Beam (2D)
+          - file: test-functions/cheng2d
+            title: Cheng and Sandu (2010) 2D
           - file: test-functions/circular-pipe-crack
             title: Circular Pipe Crack
           - file: test-functions/convex-fail-domain
@@ -77,6 +84,10 @@ parts:
             title: (5th) Franke
           - file: test-functions/franke-6
             title: (6th) Franke
+          - file: test-functions/friedman-6d
+            title: Friedman (6D)
+          - file: test-functions/friedman-10d
+            title: Friedman (10D)
           - file: test-functions/gayton-hat
             title: Gayton Hat
           - file: test-functions/gramacy-1d-sine
@@ -85,6 +96,10 @@ parts:
             title: Hyper-sphere Bound
           - file: test-functions/ishigami
             title: Ishigami
+          - file: test-functions/lim-non-poly
+            title: Lim et al. (2002) Non-Polynomial
+          - file: test-functions/lim-poly
+            title: Lim et al. (2002) Polynomial
           - file: test-functions/mclain-s1
             title: McLain S1
           - file: test-functions/mclain-s2
@@ -95,22 +110,38 @@ parts:
             title: McLain S4
           - file: test-functions/mclain-s5
             title: McLain S5
+          - file: test-functions/moon3d
+            title: Moon (2010) 3D
+          - file: test-functions/morris2006
+            title: Morris et al. (2006)
           - file: test-functions/oakley-1d
             title: Oakley & O'Hagan (2002) 1D
           - file: test-functions/otl-circuit
             title: OTL Circuit
           - file: test-functions/piston
             title: Piston Simulation
+          - file: test-functions/portfolio-3d
+            title: Simple Portfolio Model
           - file: test-functions/rs-circular-bar
             title: RS - Circular Bar
           - file: test-functions/rs-quadratic
             title: RS - Quadratic
+          - file: test-functions/saltelli-linear
+            title: Saltelli Linear
           - file: test-functions/sobol-g
             title: Sobol'-G
+          - file: test-functions/sobol-g-star
+            title: Sobol'-G*
+          - file: test-functions/sobol-levitan
+            title: Sobol'-Levitan
+          - file: test-functions/solar-cell
+            title: Solar Cell
           - file: test-functions/speed-reducer-shaft
             title: Speed Reducer Shaft
           - file: test-functions/sulfur
             title: Sulfur
+          - file: test-functions/undamped-oscillator
+            title: Undamped Oscillator
           - file: test-functions/webster-2d
             title: Webster et al. (1996) 2D
           - file: test-functions/welch1992
@@ -128,25 +159,27 @@ parts:
       - file: prob-input/probabilistic-input
         title: Probabilistic Input Model
       - file: prob-input/available
-        title: Univariate Distributions
+        title: One-Dimensional Marginal Distributions
         sections:
-          - file: prob-input/univariate-distributions/beta
+          - file: prob-input/marginal-distributions/beta
             title: Beta
-          - file: prob-input/univariate-distributions/gumbel
+          - file: prob-input/marginal-distributions/exponential
+            title: Exponential
+          - file: prob-input/marginal-distributions/gumbel
             title: Gumbel (max.)
-          - file: prob-input/univariate-distributions/logitnormal
+          - file: prob-input/marginal-distributions/logitnormal
             title: Logit-Normal
-          - file: prob-input/univariate-distributions/lognormal
+          - file: prob-input/marginal-distributions/lognormal
             title: Log-Normal
-          - file: prob-input/univariate-distributions/normal
+          - file: prob-input/marginal-distributions/normal
             title: Normal (Gaussian)
-          - file: prob-input/univariate-distributions/triangular
+          - file: prob-input/marginal-distributions/triangular
             title: Triangular
-          - file: prob-input/univariate-distributions/trunc-gumbel
+          - file: prob-input/marginal-distributions/trunc-gumbel
             title: Truncated Gumbel (max.)
-          - file: prob-input/univariate-distributions/trunc-normal
+          - file: prob-input/marginal-distributions/trunc-normal
             title: Truncated Normal (Gaussian)
-          - file: prob-input/univariate-distributions/uniform
+          - file: prob-input/marginal-distributions/uniform
             title: Uniform
   - caption: API Reference
     chapters:
@@ -160,10 +193,12 @@ parts:
         title: UQTestFun
       - file: api/probinput
         title: ProbInput
-      - file: api/univdist
-        title: UnivDist
+      - file: api/marginal
+        title: Marginal
       - file: api/input-spec
         title: ProbInputSpec
+      - file: api/funparams
+        title: FunParams
   - caption: Development
     chapters:
       - file: development/overview
@@ -176,8 +211,8 @@ parts:
         title: Test Function Docs
       - file: development/making-a-pull-request
         title: Making a Pull Request
-      - file: development/adding-univariate-distribution
-        title: Univariate Distribution
+      - file: development/adding-marginal-distribution
+        title: Adding a Marginal Distribution
       - file: development/about
         title: About UQTestFuns
       - file: development/code-of-conduct
diff --git a/docs/api/funparams.rst b/docs/api/funparams.rst
new file mode 100644
index 0000000..79b5340
--- /dev/null
+++ b/docs/api/funparams.rst
@@ -0,0 +1,9 @@
+.. _api_reference_function_parameters:
+
+Function Parameters
+===================
+
+.. automodule:: uqtestfuns.core.parameters
+
+.. autoclass:: uqtestfuns.core.parameters.FunParams
+   :members:
diff --git a/docs/api/input-spec.rst b/docs/api/input-spec.rst
deleted file mode 100644
index bca5e92..0000000
--- a/docs/api/input-spec.rst
+++ /dev/null
@@ -1,27 +0,0 @@
-.. _api_reference_input_spec:
-
-Input Specification
-===================
-
-.. automodule:: uqtestfuns.core.prob_input.input_spec
-
-.. _api_reference_input_spec_univdist:
-
-``UnivDistSpec``
-----------------
-
-.. autoclass:: uqtestfuns.core.prob_input.input_spec.UnivDistSpec
-
-.. _api_reference_input_spec_fixdim:
-
-``ProbInputSpecFixDim``
------------------------
-
-.. autoclass:: uqtestfuns.core.prob_input.input_spec.ProbInputSpecFixDim
-
-.. _api_reference_input_spec_vardim:
-
-``ProbInputSpecVarDim``
------------------------
-
-.. autoclass:: uqtestfuns.core.prob_input.input_spec.ProbInputSpecVarDim
diff --git a/docs/api/marginal.rst b/docs/api/marginal.rst
new file mode 100644
index 0000000..5d81b0e
--- /dev/null
+++ b/docs/api/marginal.rst
@@ -0,0 +1,9 @@
+.. _api_reference_marginal_distribution:
+
+One-Dimensional Marginal Distribution
+=====================================
+
+.. automodule:: uqtestfuns.core.prob_input.marginal
+
+.. autoclass:: uqtestfuns.core.prob_input.marginal.Marginal
+   :members:
diff --git a/docs/api/overview.md b/docs/api/overview.md
index 0e81ffe..ed7841a 100644
--- a/docs/api/overview.md
+++ b/docs/api/overview.md
@@ -8,9 +8,13 @@ To make sense of how the objects in UQTestFuns are organized,
 let's start from the top, the {ref}`built-in test functions <test-functions:available>`:
 
 - Each of the built-in UQ test functions is a concrete implementation of the
-  abstract base class {ref}`UQTestFunABC <api_reference_uqtestfun_abc>`.
-  The base class, in turns, is derived 
-  from {ref}`UQTestFunABC <api_reference_uqtestfun_bare_abc>`).
+  abstract base classes: {ref}`UQTestFunFixDimABC <api_reference_uqtestfun_fix_dim_abc>`
+  (for UQ test functions with fixed dimension) or
+  {ref}`UQTestFunVarDimABC <api_reference_uqtestfun_var_dim_abc>`
+  (for UQ test functions with variable dimension).
+- Both of those abstract classes are derived from {ref}`UQTestFunABC <api_reference_uqtestfun_abc>`.
+  This base class, in turn, is derived 
+  from {ref}`UQTestFunBareABC <api_reference_uqtestfun_bare_abc>`).
   Therefore, all the instances share the same underlying interfaces.
   In particular, all instances share, among other things, the ``evaluate()`` 
   method, the ``prob_input`` property, and the ``parameters`` property [^essence].
@@ -21,22 +25,17 @@ let's start from the top, the {ref}`built-in test functions <test-functions:avai
   represents the input of an uncertainty quantification (UQ) test function.
 - An instance of the ``ProbInput`` class consists mainly of the one-dimensional
   marginals and a copula specification (not yet supported). Each one-dimensional
-  marginal comes from a univariate random variable which in turn
-  is represented by the {ref}`UnivDist <api_reference_univariate_distribution>`
-  class. 
-- An instance of class ``UnivDist`` has a (parametric) probability distribution.
-  Although different instances may have different probability distributions,
-  they are all instances of the same class.
-- As lightweight containers to specify the specifications of a ``ProbInput`` and
-  a ``UnivDist``, {ref}`three custom <api_reference_input_spec>` ``NamedTuple``
-  are defined, namely {ref}`api_reference_input_spec_univdist`, 
-  {ref}`api_reference_input_spec_fixdim`, and {ref}`api_reference_input_spec_vardim`.
+  marginal comes is represented
+  by the {ref}`Marginal <api_reference_marginal_distribution>` class. 
+- An instance of the ``Marginal`` class has a (parametric) probability
+  distribution. Although different instances may have different
+  probability distributions, they are all instances of the same class.
 
 ```{note}
 To facilitate the creation of a custom UQ test function
 in runtime or within a running Python session, UQTestFuns also includes
 the concrete {ref}`api_reference_uqtestfun` class.
-You can see the usage {ref}`here <getting-started:creating-a-custom>`.
+You can see the usage {ref}`here <getting-started:tutorial-custom-functions>`.
 ```
 
 Additionally, there is currently one top-level convenient function used to
diff --git a/docs/api/univdist.rst b/docs/api/univdist.rst
deleted file mode 100644
index 26d8233..0000000
--- a/docs/api/univdist.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-.. _api_reference_univariate_distribution:
-
-Univariate Distribution
-=======================
-
-.. automodule:: uqtestfuns.core.prob_input.univariate_distribution
-
-.. autoclass:: uqtestfuns.core.prob_input.univariate_distribution.UnivDist
-   :members:
diff --git a/docs/development/adding-univariate-distribution.md b/docs/development/adding-marginal-distribution.md
similarity index 95%
rename from docs/development/adding-univariate-distribution.md
rename to docs/development/adding-marginal-distribution.md
index 2583652..1123712 100644
--- a/docs/development/adding-univariate-distribution.md
+++ b/docs/development/adding-marginal-distribution.md
@@ -1,8 +1,9 @@
-(development:adding-univ-dist)=
-# Adding a New Univariate Distribution Type
+(development:adding-marginal-distribution)=
+# Adding a New Marginal Distribution Type
 
-UQTestFuns is delivered with several {ref}`univariate distributions <prob-input:available-univariate-distributions>`
-that are used by the currently available test functions.
+UQTestFuns is delivered with several one-dimensional
+{ref}`marginal distributions <prob-input:available-marginal-distributions>`
+(i.e., univariate distributions) that are used by the currently available test functions.
 In case a new univariate distribution type is required for a new test function,
 the distribution should be added first to the code base.
 
@@ -27,12 +28,12 @@ For example:
 
 ```python
 >>> import uqtestfuns as uqtf
->>> my_var = uqtf.UnivDist(distribution="uniform", parameters=[3, 5])
+>>> my_var = uqtf.Marginal(distribution="uniform", parameters=[3, 5])
 ```
 
 ## Step 0: Putting things in the right place
 
-Univariate random variables in UQTestFuns are represented as instances of the `UnivDist` class.
+Univariate random variables in UQTestFuns are represented as instances of the `Marginal` class.
 The supported distributions for the random variables are implemented in separate modules
 located in the `src/uqtestfuns/core/prob_input/univariate_distributions` directory.
 Here's how the directory look:
@@ -361,11 +362,11 @@ SUPPORTED_MARGINALS = {
 }
 ```
 
-This way you can create a new instance of `UnivDist` class by passing the chosen name as the `distribution` and the corresponding parameters:
+This way you can create a new instance of `Marginal` class by passing the chosen name as the `distribution` and the corresponding parameters:
 
 ```python
 >>> import uqtestfuns as uqtf
->>> my_var = uqtf.UnivDist(distribution="uniform", parameters=[3, 5])
+>>> my_var = uqtf.Marginal(distribution="uniform", parameters=[3, 5])
 ```
 
 Remember that the name `uniform` was chosen as the name of this distribution via the module-level variable `DISTRIBUTION_NAME`.
diff --git a/docs/development/overview.md b/docs/development/overview.md
index 8b0d35a..99b293e 100644
--- a/docs/development/overview.md
+++ b/docs/development/overview.md
@@ -45,7 +45,8 @@ be sure to check out the guides on:
 - {ref}`adding a new test function implementation <development:adding-test-function-implementation>`
 - {ref}`adding a new test function documentation <development:adding-test-function-docs>`
 - {ref}`making a pull request <development:making-a-pull-request>`
-- In case the univariate distribution types are not yet available: {ref}`adding a new univariate distribution <development:adding-univ-dist>`
+- In case the univariate distribution types are not yet available:
+  {ref}`adding a new univariate distribution <development:adding-marginal-distribution>`
 
 ## Updating the documentation
 
diff --git a/docs/fundamentals/integration.md b/docs/fundamentals/integration.md
index b92d43a..e2a9179 100644
--- a/docs/fundamentals/integration.md
+++ b/docs/fundamentals/integration.md
@@ -12,19 +12,20 @@ kernelspec:
   name: python3
 ---
 
+(fundamentals:integration)=
 # Test Functions for Numerical Integration
 
 The table below listed the available test functions typically used
 in the testing and comparison of numerical integration method.
 
-|                             Name                             | Spatial 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}`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
@@ -35,5 +36,5 @@ using the ``tag`` parameter:
 
 import uqtestfuns as uqtf
 
-uqtf.list_functions(tag="integration")
+uqtf.list_functions(tag="integration", tablefmt="html")
 ```
diff --git a/docs/fundamentals/metamodeling.md b/docs/fundamentals/metamodeling.md
index 89e4c8c..9580e7e 100644
--- a/docs/fundamentals/metamodeling.md
+++ b/docs/fundamentals/metamodeling.md
@@ -12,48 +12,84 @@ kernelspec:
   name: python3
 ---
 
+(fundamentals:metamodeling)=
 # Test Functions for Metamodeling
 
 The table below listed the available test functions typically used
 in the comparison of metamodeling approaches.
 
-|                                 Name                                 | Spatial Dimension |     Constructor      |
-|:--------------------------------------------------------------------:|:-----------------:|:--------------------:|
-|                {ref}`Ackley <test-functions:ackley>`                 |         M         |      `Ackley()`      |
-| {ref}`Alemazkoor & Meidani (2018) 2D <test-functions:alemazkoor-2d>` |         2         |   `Alemazkoor2D()`   |
-|              {ref}`Borehole <test-functions:borehole>`               |         8         |     `Borehole()`     |
-|         {ref}`Damped Cosine <test-functions:damped-cosine>`          |         1         |   `DampedCosine()`   |
-|     {ref}`Damped Oscillator <test-functions:damped-oscillator>`      |         7         | `DampedOscillator()` |
-|                 {ref}`Flood <test-functions:flood>`                  |         8         |      `Flood()`       |
-|      {ref}`Forrester et al. (2008) <test-functions:forrester>`       |         1         |  `Forrester2008()`   |
-|            {ref}`(1st) Franke <test-functions:franke-1>`             |         2         |     `Franke1()`      |
-|            {ref}`(2nd) Franke <test-functions:franke-2>`             |         2         |     `Franke2()`      |
-|            {ref}`(3rd) Franke <test-functions:franke-3>`             |         2         |     `Franke3()`      |
-|            {ref}`(4th) Franke <test-functions:franke-4>`             |         2         |     `Franke4()`      |
-|            {ref}`(5th) Franke <test-functions:franke-5>`             |         2         |     `Franke5()`      |
-|            {ref}`(6th) Franke <test-functions:franke-6>`             |         2         |     `Franke6()`      |
-|    {ref}`Gramacy (2007) 1D Sine <test-functions:gramacy-1d-sine>`    |         1         |  `Gramacy1DSine()`   |
-|             {ref}`McLain S1 <test-functions:mclain-s1>`              |         2         |     `McLainS1()`     |
-|             {ref}`McLain S2 <test-functions:mclain-s2>`              |         2         |     `McLainS2()`     |
-|             {ref}`McLain S3 <test-functions:mclain-s3>`              |         2         |     `McLainS3()`     |
-|             {ref}`McLain S4 <test-functions:mclain-s4>`              |         2         |     `McLainS4()`     |
-|             {ref}`McLain S5 <test-functions:mclain-s5>`              |         2         |     `McLainS5()`     |
-|     {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}`Webster et al. (1996) 2D <test-functions:webster-2d>`      |         2         |    `Webster2D()`     |
-|                {ref}`Sulfur <test-functions:sulfur>`                 |         9         |      `Sulfur()`      |
-|             {ref}`Welch1992 <test-functions:welch1992>`              |        20         |    `Welch1992()`     |
-|           {ref}`Wing Weight <test-functions:wing-weight>`            |        10         |    `WingWeight()`    |
+|                                  Name                                  | Input Dimension |      Constructor       |
+|:----------------------------------------------------------------------:|:---------------:|:----------------------:|
+|                 {ref}`Ackley <test-functions:ackley>`                  |        M        |       `Ackley()`       |
+|  {ref}`Alemazkoor & Meidani (2018) 2D <test-functions:alemazkoor-2d>`  |        2        |    `Alemazkoor2D()`    |
+| {ref}`Alemazkoor & Meidani (2018) 20D <test-functions:alemazkoor-20d>` |       20        |   `Alemazkoor20D()`    |
+|               {ref}`Borehole <test-functions:borehole>`                |        8        |      `Borehole()`      |
+|       {ref}`Cheng and Sandu (2010) 2D <test-functions:cheng2d>`        |        2        |       `Cheng2D`        |
+|          {ref}`Damped Cosine <test-functions:damped-cosine>`           |        1        |    `DampedCosine()`    |
+|      {ref}`Damped Oscillator <test-functions:damped-oscillator>`       |        7        |  `DampedOscillator()`  |
+|                  {ref}`Flood <test-functions:flood>`                   |        8        |       `Flood()`        |
+|       {ref}`Forrester et al. (2008) <test-functions:forrester>`        |        1        |   `Forrester2008()`    |
+|             {ref}`(1st) Franke <test-functions:franke-1>`              |        2        |      `Franke1()`       |
+|             {ref}`(2nd) Franke <test-functions:franke-2>`              |        2        |      `Franke2()`       |
+|             {ref}`(3rd) Franke <test-functions:franke-3>`              |        2        |      `Franke3()`       |
+|             {ref}`(4th) Franke <test-functions:franke-4>`              |        2        |      `Franke4()`       |
+|             {ref}`(5th) Franke <test-functions:franke-5>`              |        2        |      `Franke5()`       |
+|             {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}`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()`       |
+|              {ref}`McLain S1 <test-functions:mclain-s1>`               |        2        |      `McLainS1()`      |
+|              {ref}`McLain S2 <test-functions:mclain-s2>`               |        2        |      `McLainS2()`      |
+|              {ref}`McLain S3 <test-functions:mclain-s3>`               |        2        |      `McLainS3()`      |
+|              {ref}`McLain S4 <test-functions:mclain-s4>`               |        2        |      `McLainS4()`      |
+|              {ref}`McLain S5 <test-functions:mclain-s5>`               |        2        |      `McLainS5()`      |
+|      {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}`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()` |
+|      {ref}`Webster et al. (1996) 2D <test-functions:webster-2d>`       |        2        |     `Webster2D()`      |
+|         {ref}`Welch et al. (1992) <test-functions:welch1992>`          |       20        |     `Welch1992()`      |
+|            {ref}`Wing Weight <test-functions:wing-weight>`             |       10        |     `WingWeight()`     |
 
 In a Python terminal, you can list all the available functions relevant
-for metamodeling applications using ``list_functions()`` and filter the results
-using the ``tag`` parameter:
+for metamodeling applications using ``list_functions()``
+and filter the results using the ``tag`` parameter
+(shown below in the HTML format):
 
 ```{code-cell} ipython3
 :tags: ["output_scroll"]
 
 import uqtestfuns as uqtf
 
-uqtf.list_functions(tag="metamodeling")
+uqtf.list_functions(tag="metamodeling", tablefmt="html")
+```
+
+## About metamodeling
+
+In practice, the computational model $\mathcal{M}$ (see {ref}`fundamentals:overview`)
+is typically complex.
+Since an uncertainty quantification (UQ) analysis usually require numerous
+evaluations of $\mathcal{M}$ ($\sim 10^2$---$10^6$ or more!), the process
+may become computationally intractable if $\mathcal{M}$ is expensive to evaluate.
+
+To address this challenge, many UQ analyses employ a metamodel (surrogate model).
+Constructed on a limited number of model $\mathcal{M}$ evaluations,
+such a metamodel should be able to capture the most important aspects
+of the input/output mapping while being significantly cheaper to evaluate.
+This metamodel can then replace $\mathcal{M}$ in the analysis,
+providing a significant reduction in computational cost without
+sacrificing the accuracy of the analysis much.
+
+While not a goal of UQ analyse per se, metamodeling is nowadays an indispensable
+component of the UQ framework {cite}`Sudret2012, Sudret2017`.
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
 ```
diff --git a/docs/fundamentals/optimization.md b/docs/fundamentals/optimization.md
index 96b15e5..fb3a18a 100644
--- a/docs/fundamentals/optimization.md
+++ b/docs/fundamentals/optimization.md
@@ -12,15 +12,16 @@ kernelspec:
   name: python3
 ---
 
+(fundamentals:optimization)=
 # Test Functions for Optimization
 
 The table below listed the available test functions typically used
 in the comparison of global optimization methods.
 
-|                            Name                             | Spatial Dimension |     Constructor      |
-|:-----------------------------------------------------------:|:-----------------:|:--------------------:|
-|            {ref}`Ackley <test-functions:ackley>`            |         M         |      `Ackley()`      |
-| {ref}`Forrester et al. (2008) <test-functions:forrester>`   |         1         |  `Forrester2008()`   |
+|                            Name                             | Input Dimension |     Constructor      |
+|:-----------------------------------------------------------:|:---------------:|:--------------------:|
+|            {ref}`Ackley <test-functions:ackley>`            |        M        |      `Ackley()`      |
+| {ref}`Forrester et al. (2008) <test-functions:forrester>`   |        1        |  `Forrester2008()`   |
 
 In a Python terminal, you can list all the available functions relevant
 for optimization applications using ``list_functions()`` and filter the results
@@ -31,5 +32,5 @@ using the ``tag`` parameter:
 
 import uqtestfuns as uqtf
 
-uqtf.list_functions(tag="optimization")
+uqtf.list_functions(tag="optimization", tablefmt="html")
 ```
diff --git a/docs/fundamentals/overview.md b/docs/fundamentals/overview.md
new file mode 100644
index 0000000..b6fbc41
--- /dev/null
+++ b/docs/fundamentals/overview.md
@@ -0,0 +1,77 @@
+(fundamentals:overview)=
+# Uncertainty Quantification Framework
+
+Consider a computational model that is represented as an $M$-dimensional
+black-box function:
+
+$$
+\mathcal{M}: \boldsymbol{x} \in \mathcal{D}_{\boldsymbol{X}} \subseteq \mathbb{R}^M \mapsto \boldsymbol{y} = \mathcal{M}(\boldsymbol{x}) \subseteq \mathbb{R}^P,
+$$
+
+where $\mathcal{D}_{\boldsymbol{X}}$, $\boldsymbol{y}$, $P$ denote
+the input domain, the quantity of interest (QoI), and the dimensionality of
+the output space, respectively.
+
+In practice, the exact values of the input variables are often not known
+exactly and may be considered _uncertain_.
+The ensuing analysis involving uncertain input variables can be formalized
+in the uncertainty quantification (UQ) framework following {cite}`Sudret2007`
+as illustrated in {numref}`uq-framework`.
+
+```{figure} ./uq-framework.png
+:name: uq-framework
+
+Uncertainty quantification (UQ) framework, adapted from {cite}`Sudret2007`.
+```
+
+The framework starts from the center,
+with the computational model $\mathcal{M}$ taken as a black-box
+as defined earlier.
+Then it moves on to the probabilistic modeling of the (uncertain) input
+variables.
+Under the probabilistic modeling, the uncertain input variables are represented
+by a random vector equipped with a joint probability density function
+(PDF)
+
+$$
+f_{\boldsymbol{X}}: \boldsymbol{x} \in \mathcal{D}_{\boldsymbol{X}} \subseteq \mathbb{R}^M \mapsto \mathbb{R}.
+$$
+
+Subsequently, the uncertainties of the input variables are propagated through
+the computational model $\mathcal{M}$. As a result, the quantity of interest
+$\boldsymbol{y}$ now itself becomes a random vector[^random-vector]:
+
+$$
+\boldsymbol{Y} = \mathcal{M}(\boldsymbol{X}), \boldsymbol{X} \sim f_{\boldsymbol{X}}.
+$$
+
+This leads to various downstream analyses such as:
+
+- {ref}`reliability analysis <fundamentals:reliability>`:
+  Estimating the probability of small or rare failure events.
+- {ref}`sensitivity analysis <fundamentals:sensitivity>`:
+  Quantifying the contribution of the input uncertainties to output uncertainty.
+- {ref}`metamodeling <fundamentals:metamodeling>`:
+  Constructing a fast surrogate approximation for a typically expensive
+  computational model.
+
+In UQTestFuns, these analyses form the primary categories of 
+UQ test functions based on their applications in the literature[^classifications].
+Additionally, many global sensitivity analysis problems reduce to solving
+an {ref}`integration <fundamentals:integration>` problem, which explains the
+extra category.
+For completeness, UQTestFuns also includes test functions commonly
+used for benchmarking testing
+{ref}`optimization <fundamentals:optimization>` methods.
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^random-vector]: or in the case of a single output (i.e., $P = 1$): random
+variable.
+[^classifications]: These categories are not mutually exclusive; a given UQ
+test function may be applied in several contexts.
\ No newline at end of file
diff --git a/docs/fundamentals/reliability.md b/docs/fundamentals/reliability.md
index db77f09..31e4c35 100644
--- a/docs/fundamentals/reliability.md
+++ b/docs/fundamentals/reliability.md
@@ -12,23 +12,25 @@ kernelspec:
   name: python3
 ---
 
+(fundamentals:reliability)=
 # Test Functions for Reliability Analysis
 
 The table below listed the available test functions typically used
 in the comparison of reliability analysis methods.
 
-|                                        Name                                         | Spatial Dimension |           Constructor           |
-|:-----------------------------------------------------------------------------------:|:-----------------:|:-------------------------------:|
-|           {ref}`Cantilever Beam (2D) <test-functions:cantilever-beam-2d>`           |         2         |       `CantileverBeam2D `       |
-|           {ref}`Circular Pipe Crack <test-functions:circular-pipe-crack>`           |         2         |      `CircularPipeCrack()`      |
-|          {ref}`Convex Failure Domain <test-functions:convex-fail-domain>`           |         2         |      `ConvexFailDomain()`       |
-| {ref}`Damped Oscillator Reliability <test-functions:damped-oscillator-reliability>` |         8         | `DampedOscillatorReliability()` |
-|                   {ref}`Four-branch <test-functions:four-branch>`                   |         2         |         `FourBranch()`          |
-|                    {ref}`Gayton Hat <test-functions:gayton-hat>`                    |         2         |          `GaytonHat()`          |
-|               {ref}`Hyper-sphere Bound <test-functions:hyper-sphere>`               |         2         |         `HyperSphere()`         |
-|              {ref}`RS - Circular Bar <test-functions:rs-circular-bar>`              |         2         |        `RSCircularBar()`        |
-|                 {ref}`RS - Quadratic <test-functions:rs-quadratic>`                 |         2         |         `RSQuadratic()`         |
-|           {ref}`Speed Reducer Shaft <test-functions:speed-reducer-shaft>`           |         5         |      `SpeedReducerShaft()`      |
+|                                        Name                                         | Input Dimension |           Constructor           |
+|:-----------------------------------------------------------------------------------:|:---------------:|:-------------------------------:|
+|           {ref}`Cantilever Beam (2D) <test-functions:cantilever-beam-2d>`           |        2        |       `CantileverBeam2D `       |
+|           {ref}`Circular Pipe Crack <test-functions:circular-pipe-crack>`           |        2        |      `CircularPipeCrack()`      |
+|          {ref}`Convex Failure Domain <test-functions:convex-fail-domain>`           |        2        |      `ConvexFailDomain()`       |
+| {ref}`Damped Oscillator Reliability <test-functions:damped-oscillator-reliability>` |        8        | `DampedOscillatorReliability()` |
+|                   {ref}`Four-branch <test-functions:four-branch>`                   |        2        |         `FourBranch()`          |
+|                    {ref}`Gayton Hat <test-functions:gayton-hat>`                    |        2        |          `GaytonHat()`          |
+|               {ref}`Hyper-sphere Bound <test-functions:hyper-sphere>`               |        2        |         `HyperSphere()`         |
+|              {ref}`RS - Circular Bar <test-functions:rs-circular-bar>`              |        2        |        `RSCircularBar()`        |
+|                 {ref}`RS - Quadratic <test-functions:rs-quadratic>`                 |        2        |         `RSQuadratic()`         |
+|           {ref}`Speed Reducer Shaft <test-functions:speed-reducer-shaft>`           |        5        |      `SpeedReducerShaft()`      |
+|           {ref}`Undamped Oscillator <test-functions:undamped-oscillator>`           |        6        |     `UndampedOscillator()`      |
 
 In a Python terminal, you can list all the available functions relevant
 for metamodeling applications using ``list_functions()`` and filter the results
@@ -39,7 +41,7 @@ using the ``tag`` parameter:
 
 import uqtestfuns as uqtf
 
-uqtf.list_functions(tag="reliability")
+uqtf.list_functions(tag="reliability", tablefmt="html")
 ```
 
 ## About reliability analysis
@@ -83,7 +85,7 @@ the system is in safe state if the maximum temperature
 does not exceed the regulatory limit.
 ```
 
-_Reliability analysis_[^rare-event] concerns with estimating
+**Reliability analysis**[^rare-event] concerns with estimating
 the failure probability of a system with a given performance function $g$. 
 For a given joint probability density function (PDF) $f_{\boldsymbol{X}}$
 of the uncertain input variables $\boldsymbol{X} = \{ \boldsymbol{X}_p, \boldsymbol{X}_s \}$,
diff --git a/docs/fundamentals/sensitivity.md b/docs/fundamentals/sensitivity.md
index c19ee2e..26ee368 100644
--- a/docs/fundamentals/sensitivity.md
+++ b/docs/fundamentals/sensitivity.md
@@ -12,36 +12,87 @@ kernelspec:
   name: python3
 ---
 
+(fundamentals:sensitivity)=
 # Test Functions for Sensitivity Analysis
 
 The table below listed the available test functions typically used
 in the comparison of sensitivity analysis methods.
 
-|                                 Name                                  | Spatial Dimension |     Constructor      |
-|:---------------------------------------------------------------------:|:-----------------:|:--------------------:|
-|               {ref}`Borehole <test-functions:borehole>`               |         8         |     `Borehole()`     |
-|          {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}`Damped Oscillator <test-functions:damped-oscillator>`      |         7         | `DampedOscillator()` |
-|                  {ref}`Flood <test-functions:flood>`                  |         8         |      `Flood()`       |
-|               {ref}`Ishigami <test-functions:ishigami>`               |         3         |     `Ishigami()`     |
-|            {ref}`OTL Circuit <test-functions:otl-circuit>`            |      6 / 20       |    `OTLCircuit()`    |
-|           {ref}`Piston Simulation <test-functions:piston>`            |      7 / 20       |      `Piston()`      |
-|               {ref}`Sobol'-G <test-functions:sobol-g>`                |         M         |      `SobolG()`      |
-|                 {ref}`Sulfur <test-functions:sulfur>`                 |         9         |      `Sulfur()`      |
-|         {ref}`Welch et al. (1992) <test-functions:welch1992>`         |        20         |    `Welch1992()`     |
-|            {ref}`Wing Weight <test-functions:wing-weight>`            |        10         |    `WingWeight()`    |
+|                             Name                             | Input Dimension |     Constructor      |
+|:------------------------------------------------------------:|:---------------:|:--------------------:|
+|          {ref}`Borehole <test-functions:borehole>`           |        8        |     `Borehole()`     |
+| {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}`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}`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()`    |
+|       {ref}`OTL Circuit <test-functions:otl-circuit>`        |     6 / 20      |    `OTLCircuit()`    |
+|       {ref}`Piston Simulation <test-functions:piston>`       |     7 / 20      |      `Piston()`      |
+| {ref}`Simple Portfolio Model <test-functions:portfolio-3d>`  |        3        |   `Portfolio3D()`    |
+|    {ref}`SaltelliLinear <test-functions:saltelli-linear>`    |        M        |  `SaltelliLinear()`  |
+|           {ref}`Sobol'-G <test-functions:sobol-g>`           |        M        |      `SobolG()`      |
+|        {ref}`Sobol'-G* <test-functions:sobol-g-star>`        |        M        |    `SobolGStar()`    |
+|     {ref}`Sobol'-Levitan <test-functions:sobol-levitan>`     |        M        |   `SobolLevitan()`   |
+|     {ref}`Solar Cell Model <test-functions:solar-cell>`      |        5        |    `SolarCell()`     |
+|            {ref}`Sulfur <test-functions:sulfur>`             |        9        |      `Sulfur()`      |
+|    {ref}`Welch et al. (1992) <test-functions:welch1992>`     |       20        |    `Welch1992()`     |
+|       {ref}`Wing Weight <test-functions:wing-weight>`        |       10        |    `WingWeight()`    |
 
 In a Python terminal, you can list all the available functions relevant
-for metamodeling applications using ``list_functions()`` and filter the results
-using the ``tag`` parameter:
+for metamodeling applications using ``list_functions()``
+and filter the results  using the ``tag`` parameter
+(shown below in the HTML format):
 
 ```{code-cell} ipython3
 :tags: ["output_scroll"]
 
 import uqtestfuns as uqtf
 
-uqtf.list_functions(tag="sensitivity")
+uqtf.list_functions(tag="sensitivity", tablefmt="html")
 ```
+
+## About sensitivity analysis
+
+**Sensitivity analysis** is a class of model inference techniques
+whose overarching aim is to _understand the input-output relationship_
+of a complex (perhaps, even a black-box) computational model.
+Within the uncertainty quantification (UQ) framework
+(see {ref}`fundamentals:overview`), this aim is reframed as determining
+how the uncertainty of the model output(s) is affected
+by the uncertainty of the inputs.
+
+While understanding the input-output relationship is valuable on its own[^model-building],
+sensitivity analysis often focuses on more practical tasks, including:
+
+- **Identifying of input variables that primarily drives the output uncertainty**:
+  This knowledge enables _factor prioritization_, where efforts are concentrated
+  on reducing the uncertainty of the most influential inputs (if possible)
+  to significantly decrease the uncertainty of the outputs
+- **Identifying of non-influential input variables**:
+  This knowledge enables _factor fixing/screening_, where non-influential
+  inputs are fixed to arbitrary value without 
+  affecting significantly (or at all) the uncertainty of the outputs.
+  In essence, factor fixing reduces the dimensionality of the problem.
+
+Sensitivity analysis within the UQ framework are typically carried out in
+a black-box manner, relying solely on model evaluations at carefully
+selected input points.
+The goal is then to achieve the aforementioned tasks with as few model
+evaluations as possible.
+
+For detailed discussions on this topic, see {cite}`Saltelli2007, Iooss2015`.
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^model-building]: especially during model building and the ensuing verification
+and validation activities.
diff --git a/docs/fundamentals/uq-framework.png b/docs/fundamentals/uq-framework.png
new file mode 100644
index 0000000..ac33241
Binary files /dev/null and b/docs/fundamentals/uq-framework.png differ
diff --git a/docs/getting-started/citing.md b/docs/getting-started/citing.md
new file mode 100644
index 0000000..d758b6d
--- /dev/null
+++ b/docs/getting-started/citing.md
@@ -0,0 +1,40 @@
+# Citing UQTestFuns
+
+If you use this package in your research or projects, please consider citing
+both the associated paper and the Zenodo archive (for the specific version
+used).
+
+The citation of the paper associated with this package is:
+
+```bibtex
+@article{Wicaksono2023,
+  author    = {Wicaksono, Damar and Hecht, Michael},
+  title     = {{UQTestFuns}: A {Python3} library of uncertainty quantification ({UQ}) test functions},
+  journal   = {Journal of Open Source Software},
+  year      = {2023},
+  volume    = {8},
+  number    = {90},
+  doi       = {10.21105/joss.05671},
+}
+```
+
+To ensure reproducibility, cite the exact version of the package you used.
+Each release is archived on Zenodo with a unique DOI; find and use the DOI
+for the version you used at [Zenodo].
+
+The citation for the current public version is:
+
+```bibtex
+@software{UQTestFuns_0_5_0,
+  author       = {Wicaksono, Damar and Hecht, Michael},
+  title        = {{UQTestFuns: A Python3 Library of Uncertainty Quantification (UQ) Test Functions}},
+  month        = nov,
+  year         = 2024,
+  publisher    = {Zenodo},
+  version      = {v0.5.0},
+  doi          = {10.5281/zenodo.14180639},
+  url          = {https://doi.org/10.5281/zenodo.14180639}
+}
+```
+
+[Zenodo]: https://zenodo.org/search?q=parent.id%3A7701903&f=allversions%3Atrue&l=list&p=1&s=10&sort=version
\ No newline at end of file
diff --git a/docs/getting-started/tutorial-built-in-functions.md b/docs/getting-started/tutorial-built-in-functions.md
index b534acf..b179b20 100644
--- a/docs/getting-started/tutorial-built-in-functions.md
+++ b/docs/getting-started/tutorial-built-in-functions.md
@@ -46,7 +46,7 @@ uqtf.list_functions()
 ```
 
 This function produces a list of test functions,
-their respective constructor, spatial dimension, typical applications,
+their respective constructor, input dimension, typical applications,
 as well as a short description.
 
 ## A Callable instance
@@ -135,6 +135,8 @@ yy_sample = my_testfun(xx_sample)
 The histogram of the output values can be created as follows:
 
 ```{code-cell} ipython3
+:tags: [hide-input]
+
 import matplotlib.pyplot as plt
 
 plt.hist(yy_sample, bins="auto", color="#8da0cb")
@@ -162,7 +164,7 @@ regarding some good practices on using NumPy RNG.
 ## Transformation to the function domain
 
 Some UQ methods often produce sample points in a hypercube domain
-(for example, $[0, 1]^M$ or $[-1, 1]^M$ where $M$ is the number of spatial dimension)
+(for example, $[0, 1]^M$ or $[-1, 1]^M$ where $M$ is the number of input dimension)
 at which the function should be evaluated.
 This hypercube domain may differ from the test function's domain.
 Before the test function can be evaluated,
@@ -250,15 +252,17 @@ and stored in the `parameters` property:
 
 ```{code-cell} ipython3
 my_testfun = uqtf.Ishigami()
-my_testfun.parameters
+
+print(my_testfun.parameters)
 ```
 
-To assign different parameter values, override the property values of the instance.
+To assign different parameter values, override the property values
+of the instance by specifying the name in brackets just like a dictionary:
 For example:
 
 ```{code-cell} ipython3
-my_testfun.parameters = (7, 0.35)
-my_testfun.parameters
+my_testfun.parameters["a"] = 7.0
+my_testfun.parameters["b"] = 0.35
 ```
 
 Note that once set, the parameter values are kept constant
@@ -273,9 +277,11 @@ as illustrated in the figure below.
 :tags: [remove-input]
 
 xx_sample = my_testfun.prob_input.get_sample(10000)
-my_testfun.parameters = (7, 0.05)
+my_testfun.parameters["a"] = 7.0
+my_testfun.parameters["b"] = 0.05
 yy_param_1 = my_testfun(xx_sample)
-my_testfun.parameters = (7, 0.35)
+my_testfun.parameters["a"] = 7.0
+my_testfun.parameters["b"] = 0.35
 yy_param_2 = my_testfun(xx_sample)
 
 plt.hist(yy_param_2, bins="auto", color="#fc8d62", label="parameter 2")
@@ -290,12 +296,12 @@ plt.gcf().set_dpi(150);
 ## Test functions with variable dimension
 
 ```{margin}
-Spatial dimension must be a positive integer.
+input dimension must be a positive integer.
 ```
 
 Some test functions support a _variable dimension_, meaning that an instance
 of a test function can be constructed for any number (positive integer, please) 
-of spatial dimension.
+of input dimension.
 
 Consider, for instance, the {ref}`Sobol'-G <test-functions:sobol-g>` function
 {cite}`Saltelli1995`,
@@ -311,10 +317,10 @@ of input variables,
 and $\boldsymbol{a} = \{ a_1, \ldots, a_M \}$ are parameters of the function.
 
 To create a six-dimensional Sobol'-G function,
-use the parameter `spatial_dimension` to specify the desired dimensionality:
+use the parameter `input_dimension` to specify the desired dimensionality:
 
 ```{code-cell} ipython3
-my_testfun = uqtf.SobolG(spatial_dimension=6)
+my_testfun = uqtf.SobolG(input_dimension=6)
 ```
 
 Verify that the function is indeed a six-dimension one:
diff --git a/docs/getting-started/tutorial-custom-functions.md b/docs/getting-started/tutorial-custom-functions.md
index 45e8ab9..3cb8a9c 100644
--- a/docs/getting-started/tutorial-custom-functions.md
+++ b/docs/getting-started/tutorial-custom-functions.md
@@ -92,11 +92,13 @@ The typical values for the parameters are shown in the table below.
 The first component of a test function is the evaluation function itself.
 UQTestFuns requires for such a function to have at least the input array
 as its first parameter.
-When applicable, the parameters must be the second parameter of the function.
+When applicable, the parameters must appear after the input array.
+It's your choice whether to lump the parameters together or split them as
+exemplified below.
 The Branin evaluation function can be defined as a Python function as follows:
 
 ```{code-cell} ipython3
-def evaluate_branin(xx: np.ndarray, params: np.ndarray):
+def evaluate_branin(xx: np.ndarray, a: float, b: float, c: float, r: float, s: float, t: float):
     """Evaluate the Branin function on a set of input values.
     
     Parameters
@@ -104,9 +106,18 @@ def evaluate_branin(xx: np.ndarray, params: np.ndarray):
     xx : np.ndarray
         2-Dimensional input values given by an N-by-2 array where
         N is the number of input values.
-    params : np.ndarray
-        The parameters of the Branin function;
-        a 1-Dimensional array of length 5.
+    a : float
+        Parameter 'a' of the Branin function.
+    b : float
+        Parameter 'b' of the Branin function.
+    c : float
+        Parameter 'c' of the Branin function.
+    r : float
+        Parameter 'r' of the Branin function.
+    s : float
+        Parameter 's' of the Branin function.
+    t : float
+        Parameter 't' of the Branin function.
     
     Returns
     -------
@@ -115,9 +126,9 @@ def evaluate_branin(xx: np.ndarray, params: np.ndarray):
         The output is a 1-dimensional array of length N.    
     """
     yy = (
-        params[0] * (xx[:, 1] - params[1] * xx[:, 0]**2 + params[2] * xx[:, 0] - params[3])**2
-        + params[4] * (1 - params[5]) * np.cos(xx[:, 0]) 
-        + params[4]
+        a * (xx[:, 1] - b * xx[:, 0]**2 + c * xx[:, 0] - r)**2
+        + s * (1 - t) * np.cos(xx[:, 0]) 
+        + s
     )
     
     return yy
@@ -125,21 +136,21 @@ def evaluate_branin(xx: np.ndarray, params: np.ndarray):
 
 ## Input and parameters
 
-The second and third components of a test function are
-the (probabilistic) input specification and the parameters.
+The second a test function is the (probabilistic) input specification.
 The input specification of the Branin function consists of
 two independent uniform random variables with different bounds.
-In UQTestFuns, a probabilistic input model is represented by `ProbInput` class
-and an instance of it can be defined as follows:
+In UQTestFuns, a probabilistic input model is represented
+by the the {ref}`ProbInput <api_reference_probabilistic_input>`
+class  and an instance of it can be defined as follows:
 
 ```{code-cell} ipython3
 # Define a list of marginals
 marginals = [
-    uqtf.UnivDist(distribution="uniform", parameters=[-5, 10], name="x1"),
-    uqtf.UnivDist(distribution="uniform", parameters=[0, 15], name="x2"),
+    uqtf.Marginal(distribution="uniform", parameters=[-5, 10], name="x1"),
+    uqtf.Marginal(distribution="uniform", parameters=[0, 15], name="x2"),
 ]
 # Create a probabilistic input
-my_input = uqtf.ProbInput(marginals=marginals, name="Branin-Input")
+my_input = uqtf.ProbInput(marginals=marginals, function_id="Branin", input_id="custom")
 ```
 
 To verify if the instance has been created successfully,
@@ -149,18 +160,52 @@ print it out to the terminal:
 print(my_input)
 ```
 
-Finally, the parameters of the Branin function defined above can be defined
-as a NumPy array as follows:
+## Parameters
+
+The third and final component of a test function is the parameter set.
+A test function may or may not have a parameter set; in the case of the Branin
+function, the set contains six parameters $\{ a, b, c, r, s, t \}$.
+
+A parameter set in UQTestFuns is represented
+by the {ref}`FunParams <api_reference_function_parameters>` class.
+As defined above, the test function requires six separate parameters.
+An instance of `FunParams` can be created as follows:
 
 ```{code-cell} ipython3
-my_params = np.array([1.0, 5.1 / (2 * np.pi)**2, 5 / np.pi, 6, 10, 1 / (8 * np.pi)])
+my_params = uqtf.FunParams(
+    function_id="Branin",
+    parameter_id="custom",
+    description="Parameter set for the Branin function",
+)
 ```
 
+Each of the parameters can then be added to the set as follows:
+
+```{code-cell} ipython3
+my_params.add(keyword="a", value=1.0, type=float)
+my_params.add(keyword="b", value=5.1 / (2 * np.pi)**2, type=float)
+my_params.add(keyword="c", value=5 / np.pi, type=float)
+my_params.add(keyword="r", value=6.0, type=float)
+my_params.add(keyword="s", value=10.0, type=float)
+my_params.add(keyword="t", value=1 / (8 * np.pi), type=float)
+```
+
+Notice that the values to the `keyword` parameter are consistent with the
+names that appear in the function definition
+The value of the parameters can practically be of any Python data type.
+They only depend on how they are going to be consumed by the specified
+evaluatuon function.
+
 ```{note}
-The parameters can practically be of any Python data type.
-They only depend on how they are going to be consumed
-by the specified evaluation function.
-In other words, you have full control on how to define the parameters.
+The `type` parameter is optional; when specified the set value must be
+consistent.
+```
+
+To verify if the instance has been created successfully,
+print it out to the terminal:
+
+```{code-cell} ipython3
+print(my_params)
 ```
 
 ## Creating a test function
@@ -179,7 +224,7 @@ my_testfun = uqtf.UQTestFun(
   evaluate=evaluate_branin,
   prob_input=my_input,
   parameters=my_params,
-  name="Branin function",
+  function_id="Branin",
 )
 ```
 
@@ -257,6 +302,63 @@ testing optimization algorithms.
 Shown in the contour plot above are the locations of
 the three global optima of the function.
 
+You can change the value of the parameters on the fly 
+by accessing the parameter by its keyword. For example:
+
+```{code-cell} ipython3
+my_testfun.parameters["a"] = 3.5
+my_testfun.parameters["t"] = 1 / (4 * np.pi)
+```
+
+which will alter the landscape.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+import matplotlib.pyplot as plt
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+
+# --- Create a 2-D grid
+xx_1 = np.linspace(
+    my_input.marginals[0].lower, my_input.marginals[0].upper, 2000
+)
+xx_2 = np.linspace(
+    my_input.marginals[1].lower, my_input.marginals[1].upper, 2000
+)
+mesh_2d = np.meshgrid(xx_1, xx_2)
+xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
+yy_2d = my_testfun(xx_2d)
+
+# --- Create the plots
+fig = plt.figure(figsize=(11, 5))
+
+# Surface
+axs_1 = plt.subplot(121, projection='3d')
+axs_1.plot_surface(
+    mesh_2d[0],
+    mesh_2d[1],
+    yy_2d.reshape(2000, 2000).T,
+    cmap="plasma",
+    linewidth=0,
+    antialiased=False,
+    alpha=0.5
+)
+axs_1.set_xlabel("$x_1$", fontsize=14)
+axs_1.set_ylabel("$x_2$", fontsize=14)
+axs_1.set_zlabel("$\mathcal{M}(x_1, x_2)$", fontsize=14)
+
+# Contour
+axs_2 = plt.subplot(122)
+cf = axs_2.contourf(
+    mesh_2d[0], mesh_2d[1], yy_2d.reshape(2000, 2000).T, 20, cmap="plasma"
+)
+axs_2.set_xlabel("$x_1$", fontsize=14)
+axs_2.set_ylabel("$x_2$", fontsize=14)
+divider = make_axes_locatable(axs_2)
+cax = divider.append_axes('right', size='5%', pad=0.05)
+fig.colorbar(cf, cax=cax, orientation='vertical')
+axs_2.axis('scaled');
+```
+
 ## Concluding remarks
 
 The test function created above only persists in the current Python session.
diff --git a/docs/getting-started/tutorial-reliability.md b/docs/getting-started/tutorial-reliability.md
index 50d8091..e0d90c4 100644
--- a/docs/getting-started/tutorial-reliability.md
+++ b/docs/getting-started/tutorial-reliability.md
@@ -220,13 +220,13 @@ The input variables $w$ and $h$ are probabilistically defined according
 to the table below.
 
 ```{code-cell} ipython3
-cantilever.prob_input
+print(cantilever.prob_input)
 ```
 
 The default values of the parameters $E$ and $l$ are:
 
 ```{code-cell} ipython3
-cantilever.parameters
+print(cantilever.parameters)
 ```
 
 For reproducibility of this tutorial, set the seed number of the 
diff --git a/docs/getting-started/tutorial-sensitivity.md b/docs/getting-started/tutorial-sensitivity.md
index f062922..75f7dc4 100644
--- a/docs/getting-started/tutorial-sensitivity.md
+++ b/docs/getting-started/tutorial-sensitivity.md
@@ -260,7 +260,7 @@ def estimate_sobol_indices(my_func, prob_input, num_sample):
     yy = my_func(xx)
     var_yy = np.var(yy)
 
-    num_dim = prob_input.spatial_dimension
+    num_dim = prob_input.input_dimension
 
     # --- Compute first-order Sobol' indices
     first_order = np.zeros(num_dim)
@@ -316,13 +316,13 @@ The input variables of the function are probabilistically defined according
 to the table below.
 
 ```{code-cell} ipython3
-ishigami.prob_input
+print(ishigami.prob_input)
 ```
 
 Finally, the default values for the parameters $a$ and $b$ are:
 
 ```{code-cell} ipython3
-ishigami.parameters
+print(ishigami.parameters)
 ```
 
 For reproducibility of this tutorial, set the seed number for the pseudo-random
@@ -388,8 +388,8 @@ several Monte-Carlo sample sizes are used:
 
 ```{code-cell} ipython3
 sample_sizes = np.arange(0, 3500, 500)[1:]
-first_order_indices = np.zeros((len(sample_sizes), ishigami.spatial_dimension))
-total_effect_indices = np.zeros((len(sample_sizes), ishigami.spatial_dimension))
+first_order_indices = np.zeros((len(sample_sizes), ishigami.input_dimension))
+total_effect_indices = np.zeros((len(sample_sizes), ishigami.input_dimension))
 
 for i, sample_size in enumerate(sample_sizes):
   first_order_indices[i, :], total_effect_indices[i, :] = estimate_sobol_indices(
@@ -400,7 +400,7 @@ for i, sample_size in enumerate(sample_sizes):
 Compute the analytical values for the given parameters:
 
 ```{code-cell} ipython3
-first_order_ref, total_effect_ref = compute_sobol_indices(*ishigami.parameters)
+first_order_ref, total_effect_ref = compute_sobol_indices(**ishigami.parameters.as_dict())
 ```
 
 The estimated indices as a function of sample size is plotted below;
diff --git a/docs/glossary.md b/docs/glossary.md
index f0012f2..b3d1641 100644
--- a/docs/glossary.md
+++ b/docs/glossary.md
@@ -6,6 +6,9 @@ CDF
     Cumulative distribution function, denoted by $F_X(x; \circ)$ where $\circ$
     is a placeholder for the distribution parameter(s).
 
+DS
+    Directional sampling
+
 FORM
     First-order reliability method
 
diff --git a/docs/prob-input/available.md b/docs/prob-input/available.md
index 2a88f5b..df0ccd3 100644
--- a/docs/prob-input/available.md
+++ b/docs/prob-input/available.md
@@ -1,18 +1,20 @@
-(prob-input:available-univariate-distributions)=
-# Available Univariate Distributions
+(prob-input:available-marginal-distributions)=
+# Available One-Dimensional Marginal Distributions
 
-The table below lists all the available univariate distribution types used
-to construct ``UnivDist`` instances. ``UnivDist`` are used to represent the
-univariate marginals of a (possibly, multivariate) probabilistic input model.
+The table below lists all the available one-dimensional marginal distribution
+types used to construct ``Marginal`` instances.
+``Marginal`` instances are used to represent the one-dimensional marginals
+of a (possibly, multivariate) probabilistic input model.
 
-|                                         Name                                          |     Keyword      |                     Notation                      |             Support              | Number of parameters |
-|:-------------------------------------------------------------------------------------:|:----------------:|:-------------------------------------------------:|:--------------------------------:|:--------------------:|
-|                {ref}`Beta <prob-input:univariate-distributions:beta>`                 |     `"beta"`     |       $\mathrm{Beta}(\alpha, \beta, a, b)$        | $[a, b], \; a, b \in \mathbb{R}$ |          4           |
-|           {ref}`Gumbel (max.) <prob-input:univariate-distributions:gumbel>`           |    `"gumbel"`    |           $\mathrm{Gumbel}(\mu, \beta)$           |       $(-\infty, \infty)$        |          2           |
-|         {ref}`Logit-Normal <prob-input:univariate-distributions:logitnormal>`         | `"logitnormal"`  |    $\mathcal{N}_{\mathrm{logit}}(\mu, \sigma)$    |             $(0, 1)$             |          2           |
-|           {ref}`Log-Normal <prob-input:univariate-distributions:lognormal>`           |  `"lognormal"`   |    $\mathcal{N}_{\mathrm{log}} (\mu, \sigma)$     |          $(0, \infty)$           |          2           |
-|         {ref}`Normal (Gaussian) <prob-input:univariate-distributions:normal>`         |    `"normal"`    |            $\mathcal{N}(\mu, \sigma)$             |       $(-\infty, \infty)$        |          2           |
-|          {ref}`Triangular <prob-input:univariate-distributions:triangular>`           |  `"triangular"`  |             $\mathcal{T}_r(a, b, c)$              | $[a, b], \; a, b \in \mathbb{R}$ |          3           |
-|   {ref}`Truncated Gumbel (max.) <prob-input:univariate-distributions:trunc-gumbel>`   | `"trunc-gumbel"` | $\mathrm{Gumbel}_{\mathrm{Tr}}(\mu, \beta, a, b)$ | $[a, b], \; a, b \in \mathbb{R}$ |          4           |
-| {ref}`Truncated Normal (Gaussian) <prob-input:univariate-distributions:trunc-normal>` | `"trunc-normal"` |  $\mathcal{N}_{\mathrm{Tr}}(\mu, \sigma, a, b)$   | $[a, b], \; a, b \in \mathbb{R}$ |          4           |
-|             {ref}`Uniform <prob-input:univariate-distributions:uniform>`              |   `"uniform"`    |                $\mathcal{U}(a, b)$                | $[a, b], \; a, b \in \mathbb{R}$ |          2           |
+|                                        Name                                         | Keyword value for `distribution` |                     Notation                      |             Support              | Number of parameters |
+|:-----------------------------------------------------------------------------------:|:--------------------------------:|:-------------------------------------------------:|:--------------------------------:|:--------------------:|
+|                {ref}`Beta <prob-input:marginal-distributions:beta>`                 |             `"beta"`             |       $\mathrm{Beta}(\alpha, \beta, a, b)$        | $[a, b], \; a, b \in \mathbb{R}$ |          4           |
+|         {ref}`Exponential <prob-input:marginal-distributions:exponential>`          |         `"exponential"`          |              $\mathcal{E}(\lambda)$               |          $[0, \infty)$           |          1           |
+|           {ref}`Gumbel (max.) <prob-input:marginal-distributions:gumbel>`           |            `"gumbel"`            |           $\mathrm{Gumbel}(\mu, \beta)$           |       $(-\infty, \infty)$        |          2           |
+|         {ref}`Logit-Normal <prob-input:marginal-distributions:logitnormal>`         |         `"logitnormal"`          |    $\mathcal{N}_{\mathrm{logit}}(\mu, \sigma)$    |             $(0, 1)$             |          2           |
+|           {ref}`Log-Normal <prob-input:marginal-distributions:lognormal>`           |          `"lognormal"`           |    $\mathcal{N}_{\mathrm{log}} (\mu, \sigma)$     |          $(0, \infty)$           |          2           |
+|         {ref}`Normal (Gaussian) <prob-input:marginal-distributions:normal>`         |            `"normal"`            |            $\mathcal{N}(\mu, \sigma)$             |       $(-\infty, \infty)$        |          2           |
+|          {ref}`Triangular <prob-input:marginal-distributions:triangular>`           |          `"triangular"`          |             $\mathcal{T}_r(a, b, c)$              | $[a, b], \; a, b \in \mathbb{R}$ |          3           |
+|   {ref}`Truncated Gumbel (max.) <prob-input:marginal-distributions:trunc-gumbel>`   |         `"trunc-gumbel"`         | $\mathrm{Gumbel}_{\mathrm{Tr}}(\mu, \beta, a, b)$ | $[a, b], \; a, b \in \mathbb{R}$ |          4           |
+| {ref}`Truncated Normal (Gaussian) <prob-input:marginal-distributions:trunc-normal>` |         `"trunc-normal"`         |  $\mathcal{N}_{\mathrm{Tr}}(\mu, \sigma, a, b)$   | $[a, b], \; a, b \in \mathbb{R}$ |          4           |
+|             {ref}`Uniform <prob-input:marginal-distributions:uniform>`              |           `"uniform"`            |                $\mathcal{U}(a, b)$                | $[a, b], \; a, b \in \mathbb{R}$ |          2           |
diff --git a/docs/prob-input/univariate-distributions/beta.md b/docs/prob-input/marginal-distributions/beta.md
similarity index 98%
rename from docs/prob-input/univariate-distributions/beta.md
rename to docs/prob-input/marginal-distributions/beta.md
index 2cc915f..d3f5a19 100644
--- a/docs/prob-input/univariate-distributions/beta.md
+++ b/docs/prob-input/marginal-distributions/beta.md
@@ -12,7 +12,7 @@ kernelspec:
   name: python3
 ---
 
-(prob-input:univariate-distributions:beta)=
+(prob-input:marginal-distributions:beta)=
 # Beta Distribution
 
 The Beta distribution is a four-parameter continuous probability distribution.
@@ -83,7 +83,7 @@ colors = ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"]
 
 univ_dists = []
 for parameter in parameters:
-    univ_dists.append(uqtf.UnivDist(distribution="beta", parameters=parameter))
+    univ_dists.append(uqtf.Marginal(distribution="beta", parameters=parameter))
 
 fig, axs = plt.subplots(2, 2, figsize=(10,10))
 
diff --git a/docs/prob-input/marginal-distributions/exponential.md b/docs/prob-input/marginal-distributions/exponential.md
new file mode 100644
index 0000000..f0fa400
--- /dev/null
+++ b/docs/prob-input/marginal-distributions/exponential.md
@@ -0,0 +1,116 @@
+---
+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
+---
+
+```{code-cell} ipython3
+:tags: [remove-cell]
+
+import uqtestfuns as uqtf
+import matplotlib.pyplot as plt
+import numpy as np
+```
+
+(prob-input:marginal-distributions:exponential)=
+# Exponential Distribution
+
+The exponential distribution is a single-parameter continuous probability
+distribution.
+
+The table below summarizes some important aspects of the distribution.
+
+|                      |                                                             |
+|---------------------:|-------------------------------------------------------------|
+|         **Notation** | $X \sim \mathcal{E}(\lambda)$                               |
+|       **Parameters** | $\lambda \in \mathbb{R}_{>0}$ (rate parameter)              |
+|  **{term}`Support`** | $\mathcal{D}_X = [0.0, \infty)$                             |
+|      **{term}`PDF`** | $f_X (x; \lambda) = \lambda e^{-\lambda x}$                 |
+|      **{term}`CDF`** | $F_X (x; \lambda) = 1 - e^{-\lambda x}$                     |
+|     **{term}`ICDF`** | $F^{-1}_X (x; \lambda) = - \frac{1}{\lambda} \ln{(1 - x)}$  |
+
+The plots of probability density functions (PDFs),
+sample histogram (of $5'000$ points),
+cumulative distribution functions (CDFs),
+and inverse cumulative distribution functions (ICDFs) for different parameter
+values are shown below.
+
+```{code-cell} ipython3
+:tags: [remove-input]
+
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+
+parameters = [[2.0], [1.5], [1.0], [0.5]]
+colors = ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"]
+
+univ_dists = []
+for parameter in parameters:
+    univ_dists.append(uqtf.Marginal(distribution="exponential", parameters=parameter))
+
+fig, axs = plt.subplots(2, 2, figsize=(10,10))
+
+# --- PDF
+xx = np.linspace(0, 5, 1000)
+for i, univ_dist in enumerate(univ_dists):
+    axs[0, 0].plot(
+        xx,
+        univ_dist.pdf(xx),
+        color=colors[i],
+        label=f"$\\lambda = {univ_dist.parameters[0]}$",
+        linewidth=2,
+    )
+axs[0, 0].legend();
+axs[0, 0].grid();
+axs[0, 0].set_title("PDF");
+
+# --- Sample histogram
+sample_size = 5000
+np.random.seed(42)
+for col, univ_dist in zip(reversed(colors), reversed(univ_dists)):
+    axs[0, 1].hist(
+        univ_dist.get_sample(sample_size),
+        color=col,
+        bins="auto",
+        alpha=0.75
+    )
+axs[0, 1].grid();
+axs[0, 1].set_xlim([0, 5]);
+axs[0, 1].set_title("Sample histogram");
+
+# --- CDF
+xx = np.linspace(0, 5, 1000)
+for i, univ_dist in enumerate(univ_dists):
+    axs[1, 0].plot(
+        xx,
+        univ_dist.cdf(xx),
+        color=colors[i],
+        linewidth=2,
+    )
+axs[1, 0].grid();
+axs[1, 0].set_title("CDF");
+
+# --- Inverse CDF
+xx = np.linspace(0, 1, 1000)
+for i, univ_dist in enumerate(univ_dists):
+    axs[1, 1].plot(
+        xx,
+        univ_dist.icdf(xx),
+        color=colors[i],
+        linewidth=2
+    )
+axs[1, 1].grid();
+axs[1, 1].set_ylim([0, 6]);
+axs[1, 1].set_title("Inverse CDF");
+
+plt.gcf().set_dpi(150)
+```
diff --git a/docs/prob-input/univariate-distributions/gumbel.md b/docs/prob-input/marginal-distributions/gumbel.md
similarity index 97%
rename from docs/prob-input/univariate-distributions/gumbel.md
rename to docs/prob-input/marginal-distributions/gumbel.md
index c2b1678..8e3a911 100644
--- a/docs/prob-input/univariate-distributions/gumbel.md
+++ b/docs/prob-input/marginal-distributions/gumbel.md
@@ -20,7 +20,7 @@ import matplotlib.pyplot as plt
 import numpy as np
 ```
 
-(prob-input:univariate-distributions:gumbel)=
+(prob-input:marginal-distributions:gumbel)=
 # Gumbel (max.) Distribution
 
 The Gumbel (max.) distribution is a two-parameter continuous probability distribution.
@@ -54,7 +54,7 @@ colors = ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"]
 
 univ_dists = []
 for parameter in parameters:
-    univ_dists.append(uqtf.UnivDist(distribution="gumbel", parameters=parameter))
+    univ_dists.append(uqtf.Marginal(distribution="gumbel", parameters=parameter))
     
 fig, axs = plt.subplots(2, 2, figsize=(10,10))
 
diff --git a/docs/prob-input/univariate-distributions/logitnormal.md b/docs/prob-input/marginal-distributions/logitnormal.md
similarity index 96%
rename from docs/prob-input/univariate-distributions/logitnormal.md
rename to docs/prob-input/marginal-distributions/logitnormal.md
index e783f4d..da8ba60 100644
--- a/docs/prob-input/univariate-distributions/logitnormal.md
+++ b/docs/prob-input/marginal-distributions/logitnormal.md
@@ -20,12 +20,12 @@ import matplotlib.pyplot as plt
 import numpy as np
 ```
 
-(prob-input:univariate-distributions:logitnormal)=
+(prob-input:marginal-distributions:logitnormal)=
 # Logit-Normal Distribution
 
 The logit-normal distribution is a two-parameter continuous probability distribution.
 A logit-normal random variable is a variable whose _logit_ is
-a {ref}`normally distributed <prob-input:univariate-distributions:normal>` 
+a {ref}`normally distributed <prob-input:marginal-distributions:normal>` 
 random variable.
 
 ```{admonition} Logit and logistic function
@@ -85,7 +85,7 @@ colors = ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"]
 univ_dists = []
 for parameter in parameters:
     univ_dists.append(
-    uqtf.UnivDist(distribution="logitnormal", parameters=parameter)
+    uqtf.Marginal(distribution="logitnormal", parameters=parameter)
     )
 
 fig, axs = plt.subplots(2, 2, figsize=(10,10))
diff --git a/docs/prob-input/univariate-distributions/lognormal.md b/docs/prob-input/marginal-distributions/lognormal.md
similarity index 96%
rename from docs/prob-input/univariate-distributions/lognormal.md
rename to docs/prob-input/marginal-distributions/lognormal.md
index 55510a3..d9b77b2 100644
--- a/docs/prob-input/univariate-distributions/lognormal.md
+++ b/docs/prob-input/marginal-distributions/lognormal.md
@@ -12,12 +12,12 @@ kernelspec:
   name: python3
 ---
 
-(prob-input:univariate-distributions:lognormal)=
+(prob-input:marginal-distributions:lognormal)=
 # Log-Normal Distribution
 
 The log-normal distribution is a two-parameter continuous probability distribution.
 A log-normal random variable is a variable whose (natural) logarithm is 
-a {ref}`normally distributed <prob-input:univariate-distributions:normal>`
+a {ref}`normally distributed <prob-input:marginal-distributions:normal>`
 random variable.
 The table below summarizes some important aspects of the distribution.
 
@@ -60,7 +60,7 @@ colors = ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"]
 univ_dists = []
 for parameter in parameters:
     univ_dists.append(
-        uqtf.UnivDist(distribution="lognormal", parameters=parameter)
+        uqtf.Marginal(distribution="lognormal", parameters=parameter)
     )
 
 fig, axs = plt.subplots(2, 2, figsize=(10,10))
diff --git a/docs/prob-input/univariate-distributions/normal.md b/docs/prob-input/marginal-distributions/normal.md
similarity index 97%
rename from docs/prob-input/univariate-distributions/normal.md
rename to docs/prob-input/marginal-distributions/normal.md
index d3d8632..a35b973 100644
--- a/docs/prob-input/univariate-distributions/normal.md
+++ b/docs/prob-input/marginal-distributions/normal.md
@@ -20,7 +20,7 @@ import matplotlib.pyplot as plt
 import numpy as np
 ```
 
-(prob-input:univariate-distributions:normal)=
+(prob-input:marginal-distributions:normal)=
 # Normal (Gaussian) Distribution
 
 The normal (or Gaussian) distribution is a two-parameter continuous probability
@@ -66,7 +66,7 @@ colors = ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"]
 
 univ_dists = []
 for parameter in parameters:
-    univ_dists.append(uqtf.UnivDist(distribution="normal", parameters=parameter))
+    univ_dists.append(uqtf.Marginal(distribution="normal", parameters=parameter))
 
 fig, axs = plt.subplots(2, 2, figsize=(10,10))
 
diff --git a/docs/prob-input/univariate-distributions/triangular.md b/docs/prob-input/marginal-distributions/triangular.md
similarity index 97%
rename from docs/prob-input/univariate-distributions/triangular.md
rename to docs/prob-input/marginal-distributions/triangular.md
index 0ebc64f..70db72f 100644
--- a/docs/prob-input/univariate-distributions/triangular.md
+++ b/docs/prob-input/marginal-distributions/triangular.md
@@ -20,7 +20,7 @@ import matplotlib.pyplot as plt
 import numpy as np
 ```
 
-(prob-input:univariate-distributions:triangular)=
+(prob-input:marginal-distributions:triangular)=
 # Triangular Distribution
 
 The triangular distribution is a three-parameter continuous probability
@@ -57,7 +57,7 @@ colors = ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"]
 
 univ_dists = []
 for parameter in parameters:
-    univ_dists.append(uqtf.UnivDist(distribution="triangular", parameters=parameter))
+    univ_dists.append(uqtf.Marginal(distribution="triangular", parameters=parameter))
 
 fig, axs = plt.subplots(2, 2, figsize=(10,10))
 
diff --git a/docs/prob-input/univariate-distributions/trunc-gumbel.md b/docs/prob-input/marginal-distributions/trunc-gumbel.md
similarity index 97%
rename from docs/prob-input/univariate-distributions/trunc-gumbel.md
rename to docs/prob-input/marginal-distributions/trunc-gumbel.md
index 478560b..ed66c19 100644
--- a/docs/prob-input/univariate-distributions/trunc-gumbel.md
+++ b/docs/prob-input/marginal-distributions/trunc-gumbel.md
@@ -20,7 +20,7 @@ import matplotlib.pyplot as plt
 import numpy as np
 ```
 
-(prob-input:univariate-distributions:trunc-gumbel)=
+(prob-input:marginal-distributions:trunc-gumbel)=
 # Truncated Gumbel (max.) Distribution
 
 The truncated Gumbel (max.) distribution is a four-parameter continuous
@@ -42,7 +42,7 @@ The table below summarizes some important aspects of the distribution.
 In the table above, $f_{\mathrm{Gumbel}}$, $F_{\mathrm{Gumbel}}$,
 and $F^{-1}_{\mathrm{Gumbel}}$ are the probability density,
 the cumulative, and the inverse cumulative distribution functions
-of {ref}`the (untruncated) Gumbel (max.) distribution <prob-input:univariate-distributions:gumbel>`,
+of {ref}`the (untruncated) Gumbel (max.) distribution <prob-input:marginal-distributions:gumbel>`,
 respectively.
 
 The plots of probability density functions (PDFs),
@@ -63,7 +63,7 @@ colors = ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"]
 
 univ_dists = []
 for parameter in parameters:
-    univ_dists.append(uqtf.UnivDist(distribution="trunc-gumbel", parameters=parameter))
+    univ_dists.append(uqtf.Marginal(distribution="trunc-gumbel", parameters=parameter))
 
 fig, axs = plt.subplots(2, 2, figsize=(10,10))
 
diff --git a/docs/prob-input/univariate-distributions/trunc-normal.md b/docs/prob-input/marginal-distributions/trunc-normal.md
similarity index 97%
rename from docs/prob-input/univariate-distributions/trunc-normal.md
rename to docs/prob-input/marginal-distributions/trunc-normal.md
index 278901c..22bed77 100644
--- a/docs/prob-input/univariate-distributions/trunc-normal.md
+++ b/docs/prob-input/marginal-distributions/trunc-normal.md
@@ -20,7 +20,7 @@ import matplotlib.pyplot as plt
 import numpy as np
 ```
 
-(prob-input:univariate-distributions:trunc-normal)=
+(prob-input:marginal-distributions:trunc-normal)=
 # Truncated Normal (Gaussian) Distribution
 
 The normal (or Gaussian) distribution is a two-parameter continuous probability
@@ -62,7 +62,7 @@ colors = ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"]
 
 univ_dists = []
 for parameter in parameters:
-    univ_dists.append(uqtf.UnivDist(distribution="trunc-normal", parameters=parameter))
+    univ_dists.append(uqtf.Marginal(distribution="trunc-normal", parameters=parameter))
 
 fig, axs = plt.subplots(2, 2, figsize=(10,10))
 
diff --git a/docs/prob-input/univariate-distributions/uniform.md b/docs/prob-input/marginal-distributions/uniform.md
similarity index 96%
rename from docs/prob-input/univariate-distributions/uniform.md
rename to docs/prob-input/marginal-distributions/uniform.md
index c408201..09cc638 100644
--- a/docs/prob-input/univariate-distributions/uniform.md
+++ b/docs/prob-input/marginal-distributions/uniform.md
@@ -20,7 +20,7 @@ import matplotlib.pyplot as plt
 import numpy as np
 ```
 
-(prob-input:univariate-distributions:uniform)=
+(prob-input:marginal-distributions:uniform)=
 # Uniform Distribution
 
 The uniform distribution is a two-parameter continuous probability distribution.
@@ -55,7 +55,7 @@ colors = ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"]
 
 univ_dists = []
 for parameter in parameters:
-    univ_dists.append(uqtf.UnivDist(distribution="uniform", parameters=parameter))
+    univ_dists.append(uqtf.Marginal(distribution="uniform", parameters=parameter))
 
 fig, axs = plt.subplots(2, 2, figsize=(10,10))
 
diff --git a/docs/prob-input/overview.md b/docs/prob-input/overview.md
index e1ebddd..bf03b5b 100644
--- a/docs/prob-input/overview.md
+++ b/docs/prob-input/overview.md
@@ -28,7 +28,7 @@ to handle the representation of a wide range of distributions
 for practical applications.
 Density functions and dependency structures are only made available
 when a specific UQ test function requires them.
-The list of supported univariate distributions can be found {ref}`here <prob-input:available-univariate-distributions>`.
+The list of supported univariate distributions can be found {ref}`here <prob-input:available-marginal-distributions>`.
 
 This section of the documentation explains in more detail how to specify
 a probabilistic input in UQTestFuns.
@@ -37,7 +37,7 @@ each of which is represented as a univariate random variable with a prescribed
 distribution:
 
 - To learn more about how to create a univariate random variable,
-  check out the {ref}`Creating a Univariate Random Variable <prob-input:univariate-random-variable>`
+  check out the {ref}`Creating a One-Dimensional Marginal Distribution <prob-input:marginal-distribution>`
   page.
 - To learn more about how to model one or more input variables probabilistically,
   check out the {ref}`Creating a Probabilistic Input Model <prob-input:probabilistic-input-model>`
diff --git a/docs/prob-input/probabilistic-input.md b/docs/prob-input/probabilistic-input.md
index a692dec..740957f 100644
--- a/docs/prob-input/probabilistic-input.md
+++ b/docs/prob-input/probabilistic-input.md
@@ -54,34 +54,35 @@ The first step of creating the probabilistic input model is to define
 all one-dimensional marginals according to the specification above.
 One-dimensional marginals are the distributions
 of the component univariate random variables.
-In UQTestFuns, a univariate random variable is represented by ``UnivDist`` class
-(please refer to {ref}`prob-input:univariate-random-variable` for more detail).
+In UQTestFuns, the distribution of a univariate random variable is
+represented by ``Marginal`` class
+(please refer to {ref}`prob-input:marginal-distribution` for more detail).
 Because three marginals belong to a single probabilistic input model,
 we need to collect all the marginals inside a list (or a tuple) as follows:
 
 ```{code-cell} ipython3
 my_marginals = [
-  uqtf.UnivDist(
+  uqtf.Marginal(
       distribution="gumbel", parameters=[3, 4], name="X1", description="1st input"
   ),
-  uqtf.UnivDist(
+  uqtf.Marginal(
       distribution="normal", parameters=[1, 0.2], name="X2", description="2nd input"
   ),
-  uqtf.UnivDist(
+  uqtf.Marginal(
       distribution="beta", parameters=[5, 2, 0.25, 1.0], name="X3", description="3rd input"
   ),
 ]
 ```
 
 Note that in the snippet above,
-the parameters `name` and `description` of `UnivDist()` are optional.
+the parameters `name` and `description` of `Marginal()` are optional.
 
 ## A ``ProbInput`` instance
 
 A probabilistic input model in UQTestFuns is represented by the ``ProbInput`` class.
 To create an instance of the class, you need to pass the following arguments:
 
-- `marginals`: a list or tuple of one-dimensional marginals each represented by an instance of ``UnivDist``.
+- `marginals`: a list or tuple of one-dimensional marginals each represented by an instance of ``Marginal``.
 - `name`: the name of the probabilistic input model (optional)
 - `description`: a short text describing the input model (optional)
 
@@ -104,14 +105,14 @@ You can print the instance to the terminal to verify it:
 print(my_probinput)
 ```
 
-An instance of ``UnivDist`` exposes the following properties:
+An instance of ``Marginal`` exposes the following properties:
 
 |      Property       |                                             Description                                              |
 |:-------------------:|:----------------------------------------------------------------------------------------------------:|
 |       `name`        |                  the assigned name of the probabilistic input model (may be `None`)                  |
 |    `description`    |          the assigned description of the probabilistic input model (may be `None`)                   |
 |    `marginals`      |                       One-dimensional marginal distribution of the input model                       | 
-| `spatial_dimension` |                             the number of dimensions of the input model                              |
+| `input_dimension` |                             the number of dimensions of the input model                              |
 |      `copulas`      | specified copulas that model dependency structure between random variables (currently not supported) |
 
 and methods:
@@ -197,16 +198,16 @@ plt.gcf().set_dpi(150)
 
 Using the `transform_sample()` method we can transform a sample generated
 from one probabilistic input model to another 
-with the same number of spatial dimensions.
+with the same number of input dimensions.
 
 Let's suppose we define a three-dimensional probabilistic input model
 consisting of three independent standard uniform distributions.
 
 ```{code-cell} ipython3
 my_marginals_2 = [
-  uqtf.UnivDist(distribution="uniform", parameters=[0, 1], name="X1", description="1st input"),
-  uqtf.UnivDist(distribution="uniform", parameters=[0, 1], name="X2", description="2nd input"),
-  uqtf.UnivDist(distribution="uniform", parameters=[0, 1], name="X3", description="3rd input"),
+  uqtf.Marginal(distribution="uniform", parameters=[0, 1], name="X1", description="1st input"),
+  uqtf.Marginal(distribution="uniform", parameters=[0, 1], name="X2", description="2nd input"),
+  uqtf.Marginal(distribution="uniform", parameters=[0, 1], name="X3", description="3rd input"),
 ]
 my_probinput_2 = uqtf.ProbInput(
   marginals=my_marginals_2,
diff --git a/docs/prob-input/univariate-random-variable.md b/docs/prob-input/univariate-random-variable.md
index a4c780b..5a26661 100644
--- a/docs/prob-input/univariate-random-variable.md
+++ b/docs/prob-input/univariate-random-variable.md
@@ -12,15 +12,15 @@ kernelspec:
   name: python3
 ---
 
-(prob-input:univariate-random-variable)=
-# Creating a Univariate Random Variable
+(prob-input:marginal-distribution)=
+# Creating a One-Dimensional Marginal Distribution
 
 A probabilistic input to a UQ test function consists of input variables
 each of which is a (univariate) random variable.
 Therefore, the starting point of defining a (possibly multivariate)
 probabilistic input is defining the distribution
 for each of the constituent random variables.
-This page explains how such a random variable can be created in UQTestFuns.
+This page explains how such a marginal distribution can be created in UQTestFuns.
 
 ```{code-cell} ipython3
 import matplotlib.pyplot as plt
@@ -28,8 +28,8 @@ import numpy as np
 import uqtestfuns as uqtf
 ```
 
-Suppose we would like to define a univariate random variable $X$
-whose distribution is triangular:
+Suppose we would like to define one-dimensional triangular marginal distribution
+for random variable $X$:
 
 $$
 X \sim \mathcal{T}_r(a, b, c)
@@ -40,12 +40,12 @@ These parameters correspond to the lower bound, the upper bound,
 and the mid-point of the distribution. 
 For this particular example, we set these values to $3.0$, $5.0$, and $4.0$.
 
-## A ``UnivDist`` instance
+## A ``Marginal`` instance
 
-A univariate random variable is represented in UQTestFuns by the ``UnivDist`` class.
+A univariate random variable is represented in UQTestFuns by the ``Marginal`` class.
 To create an instance of the class, you need to pass the following arguments:
 
-- `distribution`: the chosen univariate distribution (one from this {ref}`list <prob-input:available-univariate-distributions>`)
+- `distribution`: the chosen univariate distribution (one from this {ref}`list <prob-input:available-marginal-distributions>`)
 - `parameters`: the parameters of the distribution (note that the number of required parameters differs from distribution to another)
 - `name`: the name of the random variable (optional)
 - `description`: a short text describing the random variable (optional) 
@@ -53,7 +53,7 @@ To create an instance of the class, you need to pass the following arguments:
 To create an instance, type:
 
 ```{code-cell} ipython3
-my_rand_var = uqtf.UnivDist(
+my_rand_var = uqtf.Marginal(
     distribution="triangular",
     parameters=[3.0, 5.0, 4.0],
     name="X",
@@ -66,22 +66,22 @@ The variable `my_rand_var` now stores an instance
 of a univariate random variable distributed as triangular
 with the specified parameters.
 
-An instance of ``UnivDist`` exposes the following properties:
+An instance of ``Marginal`` exposes the following properties:
 
-|    Property    |                                               Description                                                |
-|:--------------:|:--------------------------------------------------------------------------------------------------------:|
-|     `name`     |                         the assigned name of the random variable (may be `None`)                         |
-| `description`  |                     the assigned description of the random variable (may be `None`)                      |
-| `distribution` | the distribution (one of {ref}`available distributions <prob-input:available-univariate-distributions>`) |
-|  `parameters`  |                                    the parameters of the distribution                                    |
-|    `lower`     |                                   the lower bound of the distribution                                    |
-|    `upper`     |                                   the upper bound of the distribution                                    |
+|    Property    |                                              Description                                               |
+|:--------------:|:------------------------------------------------------------------------------------------------------:|
+|     `name`     |                        the assigned name of the random variable (may be `None`)                        |
+| `description`  |                    the assigned description of the random variable (may be `None`)                     |
+| `distribution` | the distribution (one of {ref}`available distributions <prob-input:available-marginal-distributions>`) |
+|  `parameters`  |                                   the parameters of the distribution                                   |
+|    `lower`     |                                  the lower bound of the distribution                                   |
+|    `upper`     |                                  the upper bound of the distribution                                   |
 
 and methods:
 
 |            Method             |                                     Description                                      |
 |:-----------------------------:|:------------------------------------------------------------------------------------:|
-|           `cdf(xx)`           |       compute the cumulative distribution function on a set of values `xx`           |
+|           `cdf(xx)`           |         compute the cumulative distribution function on a set of values `xx`         |
 |           `pdf(xx)`           |           compute the probability density function on a set of values `xx`           |
 |          `icdf(xx)`           |     compute the inverse cumulative distribution function on a set of values `xx`     |
 |   `get_sample(sample_size)`   |               get a sample of size `sample_size` from the distribution               |
@@ -195,7 +195,7 @@ Let's suppose, complementary to the random variable $X$ we define
 another random variable as follows:
 
 ```{code-cell} ipython3
-my_rand_var_2 = uqtf.UnivDist(distribution="normal", parameters=[0, 1], name="Y")
+my_rand_var_2 = uqtf.Marginal(distribution="normal", parameters=[0, 1], name="Y")
 ```
 
 In other words, the variable $Y$ is a standard normal random variable.
@@ -239,7 +239,7 @@ Its support is, strictly speaking, $\mathcal{D}_X = (-\infty, \infty)$.
 This is an example of distributions that are supported on the whole real line;
 it is neither bounded from below nor from above.
 
-The properties of a ``UnivDist`` instance include among other things,
+The properties of a ``Marginal`` instance include among other things,
 `lower` (lower bound) and `upper` (upper bound).
 For the triangular distribution defined above, the lower and upper bounds are indeed:
 
diff --git a/docs/references.bib b/docs/references.bib
index 39b1439..d6ad70e 100644
--- a/docs/references.bib
+++ b/docs/references.bib
@@ -758,4 +758,188 @@ @Article{Alemazkoor2018
   doi     = {10.1016/j.jcp.2018.05.025},
 }
 
+@Book{Saltelli2004,
+  author    = {Saltelli, Andrea and Tarantola, Stefano and Campolongo, Francesca and Ratto, Marco},
+  publisher = {Wiley},
+  title     = {Sensitivity analysis in practice: a guide to assessing scientific models},
+  year      = {2004},
+  address   = {Hoboken, NJ},
+  isbn      = {9780470870938},
+}
+
+@Article{Friedman1983,
+  author  = {Friedman, J. H. and Grosse, E. and Stuetzle, W.},
+  journal = {{SIAM} Journal on Scientific and Statistical Computing},
+  title   = {Multidimensional additive spline approximation},
+  year    = {1983},
+  number  = {2},
+  volume  = {4},
+}
+
+ 
+@Article{Friedman1991,
+  author  = {Friedman, J. H.},
+  journal = {The Annals of Statistics},
+  title   = {Multivariate adaptive regression splines},
+  year    = {1991},
+  number  = {1},
+  volume  = {19},
+  doi     = {10.1214/aos/1176347963},
+}
+
+ 
+@Article{Sun2022,
+  author  = {Sun, Xifu and Croke, Barry and Jakeman, Anthony and Roberts, Stephen},
+  journal = {Environmental Modelling \& Software},
+  title   = {Benchmarking {Active Subspace} methods of global sensitivity analysis against variance-based {Sobol’} and {Morris} methods with established test functions},
+  year    = {2022},
+  pages   = {105310},
+  volume  = {149},
+  doi     = {10.1016/j.envsoft.2022.105310},
+}
+
+ 
+@Article{Horiguchi2021,
+  author    = {Horiguchi, Akira and Pratola, Matthew T. and Santner, Thomas J.},
+  journal   = {Reliability Engineering \& System Safety},
+  title     = {Assessing variable activity for {Bayesian} regression trees},
+  year      = {2021},
+  issn      = {0951-8320},
+  month     = mar,
+  pages     = {107391},
+  volume    = {207},
+  doi       = {10.1016/j.ress.2020.107391},
+  publisher = {Elsevier BV},
+}
+
+@Article{Lim2002,
+  author  = {Lim, Yong B. and Sacks, Jerome and Studden, W. J. and Welch, William J.},
+  journal = {Canadian Journal of Statistics},
+  title   = {Design and analysis of computer experiments when the output is highly correlated over the input space},
+  year    = {2002},
+  number  = {1},
+  pages   = {109--126},
+  volume  = {30},
+  doi     = {10.2307/3315868},
+}
+
+@Book{Saltelli2008,
+  editor    = {Andrea Saltelli and Karen Chan and Evelyn Marian Scott},
+  publisher = {John Wiley & Sons},
+  title     = {Sensitivity analysis},
+  year      = {2008},
+  address   = {Hoboken, NJ},
+  isbn      = {9780470743829},
+  series    = {Wiley Series in Probability and Statistics},
+}
+
+ 
+@InProceedings{Cheng2010,
+  author    = {Cheng, Haiyan and Sandu, Adrian},
+  booktitle = {Proceedings of the 2010 Spring Simulation Multiconference},
+  title     = {Collocation least-squares polynomial chaos method},
+  year      = {2010},
+  pages     = {1--6},
+  publisher = {Society for Computer Simulation International},
+  series    = {SpringSim ’10},
+  doi       = {10.1145/1878537.1878621},
+}
+
+@Article{Morris2006,
+  author  = {Morris, Max D. and Moore, Leslie M. and McKay, Michael D.},
+  journal = {Journal of Statistical Planning and Inference},
+  title   = {Sampling plans based on balanced incomplete block designs for evaluating the importance of computer model inputs},
+  year    = {2006},
+  number  = {9},
+  pages   = {3203--3220},
+  volume  = {136},
+  doi     = {10.1016/j.jspi.2005.01.001},
+}
+
+@Article{Moon2012,
+  author  = {Moon, Hyejung and Dean, Angela M. and Santner, Thomas J.},
+  journal = {Technometrics},
+  title   = {Two-stage sensitivity-based group screening in computer experiments},
+  year    = {2012},
+  number  = {4},
+  pages   = {376--387},
+  volume  = {54},
+  doi     = {10.1080/00401706.2012.725994},
+}
+
+@Article{Bucher1990,
+  author  = {Bucher, C. G. and Bourgund, U.},
+  journal = {Structural Safety},
+  title   = {A fast and efficient response surface approach for structural reliability problems},
+  year    = {1990},
+  number  = {1},
+  pages   = {57--66},
+  volume  = {7},
+  doi     = {10.1016/0167-4730(90)90012-e},
+}
+
+@Article{Luethen2021,
+  author  = {Lüthen, Nora and Marelli, Stefano and Sudret, Bruno},
+  journal = {SIAM/ASA Journal on Uncertainty Quantification},
+  title   = {Sparse polynomial chaos expansions: literature survey and benchmark},
+  year    = {2021},
+  number  = {2},
+  pages   = {593--649},
+  volume  = {9},
+  doi     = {10.1137/20m1315774},
+}
+
+@Article{Gayton2003,
+  author  = {Gayton, N. and Bourinet, J. M. and Lemaire, M.},
+  journal = {Structural Safety},
+  title   = {{CQ2RS}: a new statistical approach to the response surface method for reliability analysis},
+  year    = {2003},
+  number  = {1},
+  pages   = {99--121},
+  volume  = {25},
+  doi     = {10.1016/s0167-4730(02)00045-0},
+}
+
+@PhdThesis{Sudret2007,
+  author   = {Bruno Sudret},
+  school   = {Université Blaise Pascal - Clermont, France},
+  title    = {Uncertainty propagation and sensitivity analysis in mechanical models \textemdash {Contributions} to structural reliability and stochastic spectral methods.},
+  year     = {2007},
+  address  = {Clermont-Ferrand, France},
+  type     = {habilitation thesis},
+  abstract = {This thesis is a contribution to the resolution of the reliability-based design optimization problem. This probabilistic design approach is aimed at considering the uncertainty attached to the system of interest in order to provide optimal and safe solutions. The safety level is quantified in the form of a probability of failure. Then, the optimization problem consists in ensuring that this failure probability remains less than a threshold specified by the stakeholders. The resolution of this problem requires a high number of calls to the limit-state design function underlying the reliability analysis. Hence it becomes cumbersome when the limit-state function involves an expensive-to-evaluate numerical model (e.g. a finite element model). In this context, this manuscript proposes a surrogate-based strategy where the limit-state function is progressively replaced by a Kriging meta-model. A special interest has been given to quantifying, reducing and eventually eliminating the error introduced by the use of this meta-model instead of the original model. The proposed methodology is applied to the design of geometrically imperfect shells prone to buckling.},
+}
+
+@InProceedings{Sudret2017,
+  author    = {Sudret, Bruno and Marelli, Stefano and Wiart, Joe},
+  booktitle = {2017 11th European Conference on Antennas and Propagation (EUCAP)},
+  title     = {Surrogate models for uncertainty quantification: An overview},
+  year      = {2017},
+  month     = mar,
+  pages     = {793--797},
+  publisher = {IEEE},
+  doi       = {10.23919/eucap.2017.7928679},
+}
+
+@Book{Saltelli2007,
+  author    = {Saltelli, Andrea and Ratto, Marco and Andres, Terry and Campolongo, Francesca and Cariboni, Jessica and Gatelli, Debora and Saisana, Michaela and Tarantola, Stefano},
+  publisher = {Wiley},
+  title     = {Global sensitivity analysis. The primer},
+  year      = {2007},
+  isbn      = {9780470725184},
+  doi       = {10.1002/9780470725184},
+}
+
+@Article{Constantine2015,
+  author  = {Constantine, Paul G. and Zaharatos, Brian and Campanelli, Mark},
+  journal = {Statistical Analysis and Data Mining: The ASA Data Science Journal},
+  title   = {Discovering an active subspace in a single‐diode solar cell model},
+  year    = {2015},
+  month   = {]},
+  number  = {5–6},
+  pages   = {264--273},
+  volume  = {8},
+  doi     = {10.1002/sam.11281},
+}
+
 @Comment{jabref-meta: databaseType:bibtex;}
diff --git a/joss/paper.md b/joss/paper.md
index ca1e251..309e004 100644
--- a/joss/paper.md
+++ b/joss/paper.md
@@ -32,7 +32,7 @@ in the field of applied uncertainty quantification (UQ).
 During the development phase of a novel method or algorithm,
 researchers and developers often rely on test functions
 taken from the literature for validation purposes.
-Afterward, they employ these test functions as a common ground
+Afterward, they employ these test functions as a fair means
 to compare the performance of the novel method
 against that of the state-of-the-art methods
 in terms of accuracy and efficiency measures.
@@ -84,7 +84,7 @@ UQ test functions are widely used in the community for several reasons:
   for various types of analyses.
 - Although test functions are taken as black boxes, _their features are known_; 
   this knowledge enables a thorough diagnosis of a UQ method.
-- Test functions provide _a common ground_ for comparing the performance
+- Test functions provide a _fair means_ for comparing the performance
   of various UQ methods in solving the same class of problems. 
 
 Several efforts have been made to provide relevant UQ test functions
@@ -223,7 +223,7 @@ with another set of parameters (either uncertain or deterministic)
 to define the so-called _performance function_ or _limit-state function_
 of a system denoted by $g$.
 The task for a reliability analysis method is to estimate the failure
-probability of the system defined as [@Sudret2012]:
+probability of the system defined, following @Sudret2012, as:
 \begin{equation}\label{eq:pf}
 P_f = \mathbb{P}[g(\mathbf{X}; \mathbf{p}) \leq 0] = \int_{\mathcal{D}_f = \{ \mathbf{x} | g(\mathbf{x}; \mathbf{p}) \leq 0 \}} f_{\mathbf{X}} (\mathbf{x}) \; d\mathbf{x},
 \end{equation}
diff --git a/setup.cfg b/setup.cfg
index fa1cc98..e498a64 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = uqtestfuns
-version = 0.4.1
+version = 0.5.0
 url = https://github.com/damar-wicaksono/uqtestfuns
 author = Damar Wicaksono
 author_email = damar.wicaksono@outlook.com
@@ -35,6 +35,7 @@ install_requires =
    scipy>=1.7.3
    tabulate>=0.8.10
    importlib-metadata>=1.0; python_version < "3.8"
+   typing_extensions; python_version > "3.7"
 
 [options.packages.find]
 where = src
@@ -51,10 +52,10 @@ dev =
     types-tabulate
     types-setuptools
 docs =
-    jupyter-book==0.13.2
-    sphinx==4.5.0
+    jupyter-book
+    sphinx
     matplotlib>=3.7.0
-    sphinx-proof>=0.1.3
+    sphinx-proof
 all =
     uqtestfuns[dev]
     uqtestfuns[docs]


Test patch:
diff --git a/docs/api/uqtestfun-abc.rst b/docs/api/uqtestfun-abc.rst
index 9bcd5f6..fce2a49 100644
--- a/docs/api/uqtestfun-abc.rst
+++ b/docs/api/uqtestfun-abc.rst
@@ -22,3 +22,21 @@ Abstract Base Classes
 
 .. autoclass:: uqtestfuns.core.uqtestfun_abc.UQTestFunABC
    :members:
+
+
+.. _api_reference_uqtestfun_fix_dim_abc:
+
+``UQTestFunFixDimABC`` Abstract Base Class
+------------------------------------
+
+.. autoclass:: uqtestfuns.core.uqtestfun_abc.UQTestFunVarDimABC
+   :members:
+
+
+.. _api_reference_uqtestfun_var_dim_abc:
+
+``UQTestFunVarDimABC`` Abstract Base Class
+------------------------------------
+
+.. autoclass:: uqtestfuns.core.uqtestfun_abc.UQTestFunFixDimABC
+   :members:
diff --git a/docs/development/adding-test-function-implementation.md b/docs/development/adding-test-function-implementation.md
index 6f443af..d273bf0 100644
--- a/docs/development/adding-test-function-implementation.md
+++ b/docs/development/adding-test-function-implementation.md
@@ -109,8 +109,8 @@ Here are the few things we usually use:
 ```python
 import numpy as np
 
-from ..core.uqtestfun_abc import UQTestFunABC
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
+from uqtestfuns.core.custom_typing import ProbInputSpecs, FunParamSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["Branin"]
 ```
@@ -118,17 +118,18 @@ __all__ = ["Branin"]
 Here are some explanations:
 
 - NumPy is usually a must, especially for implementing the evaluation function.
-- All built-in test functions are concrete implementations of the abstract base
-  class `UQTestFunABC`.
-- The specification for a probabilistic input model is stored in `UnivDistSpec`
-  (the one-dimensional marginal specification) and `ProbInputSpecFixDim`
-  (the probabilistic input model). These are lightweight _containers_
-  (meaning no custom methods) of all the information required to construct,
-  later on, `UnivDist` and `ProbInput` instances, respectively.
-
-```{note}
-There is also the corresponding `ProbInputSpecVarDim` to store the information
-required to construct probabilistic input model with variable dimension.
+- Because Branin test function is a test function with a fixed number of (input)
+  dimensions, the class will be derived from the abstract base class
+  {ref}`UQTestFunFixDimABC <api_reference_uqtestfun_fix_dim_abc>`.
+- To assist specifying the data for probabilistic input model the custom type
+  `ProbInputSpecs` can be used; these are supposed to help you specifying
+  all the required data via the typechecker.
+- Similarly, the custom type `FunParamSpecs` is used for specifying the
+  function parameters.
+
+```{notes}
+In case the function is of variable dimension, use the abstract base class
+{ref}`UQTestFunVarDimABC <api_reference_uqtestfun_var_dim_abc>` instead.
 ```
 
 ### Implementing a concrete evaluation function
@@ -138,37 +139,39 @@ For an implementation of a test function, create a top module-level function
 module):
 
 ```python
-def evaluate(xx: np.ndarray, parameters: Any) -> np.ndarray:
+def evaluate(xx: np.ndarray, a: float, b: float, c: float, r: float, s: float, t: float):
     """Evaluate the Branin function on a set of input values.
-
+    
     Parameters
     ----------
     xx : np.ndarray
         2-Dimensional input values given by an N-by-2 array where
         N is the number of input values.
-    parameters : Any
-        The parameters of the test function (six numbers)
-
+    a : float
+        Parameter 'a' of the Branin function.
+    b : float
+        Parameter 'b' of the Branin function.
+    c : float
+        Parameter 'c' of the Branin function.
+    r : float
+        Parameter 'r' of the Branin function.
+    s : float
+        Parameter 's' of the Branin function.
+    t : float
+        Parameter 't' of the Branin function.
+    
     Returns
     -------
     np.ndarray
         The output of the Branin function evaluated on the input values.
-        The output is a 1-dimensional array of length N.
+        The output is a 1-dimensional array of length N.    
     """
-    params = parameters
     yy = (
-        params[0]
-        * (
-            xx[:, 1]
-            - params[1] * xx[:, 0] ** 2
-            + params[2] * xx[:, 0]
-            - params[3]
-        )
-        ** 2
-        + params[4] * (1 - params[5]) * np.cos(xx[:, 0])
-        + params[4]
+        a * (xx[:, 1] - b * xx[:, 0]**2 + c * xx[:, 0] - r)**2
+        + s * (1 - t) * np.cos(xx[:, 0]) 
+        + s
     )
-
+    
     return yy
 ```
 
@@ -179,36 +182,36 @@ include the parameters.
 
 The specification of the probabilistic model is stored in a module-level
 dictionary. While you're free to name the dictionary anything you like,
-the convention is `AVAILABLE_INPUT_SPECS`.
+the convention is `AVAILABLE_INPUTS`.
 We define the variable as follows:
 
 ```python
-AVAILABLE_INPUT_SPECS = {
-    "Dixon1978": ProbInputSpecFixDim(
-        name="Branin-Dixon-2008",
-        description=(
+AVAILABLE_INPUT_SPECS: ProbInputSpecs = {
+    "Dixon1978": {
+        "name": "Branin",
+        "description": (
             "Search domain for the Branin function from Dixon and Szegö (1978)."
         ),
-        marginals=[
-          UnivDistSpec(
-            name="x1",
-            distribution="uniform",
-            parameters=[-5, 10],
-            description="None",
-          ),
-          UnivDistSpec(
-            name="x2",
-            distribution="uniform",
-            parameters=[0.0, 15.0],
-            description="None",
-          )
+        "marginals": [
+            {
+                "name": "x1",
+                "distribution": "uniform",
+                "parameters": [-5, 10],
+                "description": None,
+            },
+            {
+                "name": "x2",
+                "distribution": "uniform",
+                "parameters": [0.0, 15.0],
+                "description": None,
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 ```
 
-Each key-value pair of this dictionary contains a complete specification
+Each key-value pair of this dictionary contains the specification
 to create an input model as an instance of the `ProbInput` class.
 For this particular example, we only have one input specification available.
 If there were more, we would add them here in the dictionary each with a unique keyword.
@@ -217,9 +220,9 @@ If there were more, we would add them here in the dictionary each with a unique
 The keyword is, by convention, the citation key of the particular reference as listed in the BibTeX file. 
 ```
 
-Also notice that in the code above, we store the specifications of the marginals
-in a list of {ref}`api_reference_input_spec_univdist`.
-Each element of this list is used to create an instance of `UnivDist`.
+Also notice that in the code above, we store the specifications of
+the marginals in a list of marginal specifications.
+Each element of this list is used to create an instance of `Marginal`.
 
 With that, we have completed the input specification of the Branin function.
 
@@ -230,15 +233,52 @@ dictionary that stores the parameter sets.
 Conventionally, we name this variable `AVAILABLE_PARAMETERS`:
 
 ```python
-AVAILABLE_PARAMETERS = {
-    "Dixon1978": np.array(
-        [1.0, 5.1 / (2 * np.pi) ** 2, 5 / np.pi, 6, 10, 1 / 8 / np.pi]
-    )
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Dixon1978": {
+      "function_id": "Branin",
+      "description": "Parameter set for the Branin function from Dixon (1978)",
+      "declared_parameters": [
+          {
+              "keyword": "a",
+              "value": 1.0,
+              "type": float,
+          },
+          {
+              "keyword": "b",
+              "value": 5.1 / (2 * np.pi) ** 2,
+              "type": float,
+          },
+          {
+              "keyword": "c",
+              "value": 5 / np.pi,
+              "type": float,
+          },
+          {
+              "keyword": "r",
+              "value": 6.0,
+              "type": float,
+          },
+          {
+              "keyword": "s",
+              "value": 10.0,
+              "type": float,
+          },
+          {
+              "keyword": "t",
+              "value": 1 / 8 / np.pi,
+              "type": float,
+          },
+      ],
+    },
 }
 ```
 
-The value of the parameters can be of any type, as long as it is consistent
-with how the parameters are going to be consumed by the `evaluate()` function.
+This is a nested dictionary, where each top key-value pair contains one set of 
+parameters from the literature.
+
+The value of the parameters in the set can be of any type, as long as it is
+consistent  with how the parameters are going to be consumed
+by the `evaluate()` function.
 
 As before, if there are multiple parameter sets available in the literature,
 additional key-value pair should be added here.
@@ -249,32 +289,30 @@ Each built-in test function is an implementation of the abstract base class
 `UQTestFunABC`.
 A concrete implementation of this base class requires the following:
 
-- a static method named `eval_()`
+- a static method named `evaluate()`
 - several class-level properties, namely: `_tags`, `_description`, 
-  `_available_inputs`, `_available_parameters`, `_default_spatial_dimension`,
-  `_default_input`, and `_default_parameters`.
+  `_available_inputs`, `_available_parameters`, `_default_input_id`,
+  and `_default_parameters_id`.
 
 The full definition of the class for the Branin test function is shown below.
 
 ```python
-class Branin(UQTestFunFixDimABC):
+class Branin(UQTestFunABC):
     """A concrete implementation of the Branin test function."""
   
     _tags = ["optimization"]  # Application tags
     _description = "Branin function from Dixon and Szegö (1978)"  # Short description
-    _available_inputs = AVAILABLE_INPUT_SPECS    # As defined above 
-    _available_parameters = AVAILABLE_PARAMETERS # As defined above
-    _default_spatial_dimension = 2  # Spatial dimension of the function
-    _default_input = "Dixon1978"       # Optional, if only one input is available
-    _default_parameters = "Dixon1978"  # Optional, if only one set of parameters is available
+    _available_inputs = AVAILABLE_INPUTS          # As defined above 
+    _available_parameters = AVAILABLE_PARAMETERS  # As defined above
+    _default_input_id = "Dixon1978"       # Optional, if only one input is available
+    _default_parameters_id = "Dixon1978"  # Optional, if only one set of parameters is available
 
-    eval_ = staticmethod(evaluate)  # assuming `evaluate()` has been defined
+    evaluate = staticmethod(evaluate)  # assuming `evaluate()` has been defined
 ```
 
 There is no need to define an `__init__()` method.
 We will use the default `__init__()` from the base class.
 
-
 Notice the two last class properties: `_default_input` and `_default_parameters`.
 In case of only one input specification (resp. set of parameters) is available,
 these properties are optional.
@@ -307,11 +345,26 @@ built-in functions from a Python terminal.
 ```python
 >>> import uqtestfuns as uqtf
 >>> uqtf.list_functions()
- No.      Constructor       Spatial Dimension          Application          Description
------  ------------------  -------------------  --------------------------  ----------------------------------------------------------------------------
-  1         Ackley()                M           optimization, metamodeling  Ackley function from Ackley (1987)
-  2        Borehole()               8           metamodeling, sensitivity   Borehole function from Harper and Gupta (1983)
-  3         Branin()                2                  optimization         Branin function from Dixon and Szegö (1978)
++-------+-------------------------------+-----------+------------+----------+---------------+--------------------------------+
+|  No.  |          Constructor          |  # Input  |  # Output  |  Param.  |  Application  | Description                    |
++=======+===============================+===========+============+==========+===============+================================+
+|   1   |           Ackley()            |     M     |     1      |   True   | optimization, | Optimization test function     |
+|       |                               |           |            |          | metamodeling  | from Ackley (1987)             |
++-------+-------------------------------+-----------+------------+----------+---------------+--------------------------------+
+|   2   |        Alemazkoor20D()        |    20     |     1      |  False   | metamodeling  | High-dimensional low-degree    |
+|       |                               |           |            |          |               | polynomial from Alemazkoor &   |
+|       |                               |           |            |          |               | Meidani (2018)                 |
++-------+-------------------------------+-----------+------------+----------+---------------+--------------------------------+
+|   3   |        Alemazkoor2D()         |     2     |     1      |  False   | metamodeling  | Low-dimensional high-degree    |
+|       |                               |           |            |          |               | polynomial from Alemazkoor &   |
+|       |                               |           |            |          |               | Meidani (2018)                 |
++-------+-------------------------------+-----------+------------+----------+---------------+--------------------------------+
+|   4   |          Borehole()           |     8     |     1      |  False   | metamodeling, | Borehole function from Harper  |
+|       |                               |           |            |          |  sensitivity  | and Gupta (1983)               |
++-------+-------------------------------+-----------+------------+----------+---------------+--------------------------------+
+|   5   |            Branin()           |     2     |     1      |  False   | optimization  | Branin function from Dixon     |
+|       |                               |           |            |          |               | and Szegö (1978)               |
++-------+-------------------------------+-----------+------------+----------+---------------+--------------------------------+
 ...
 ```
 
diff --git a/docs/getting-started/about-uq-test-functions.md b/docs/getting-started/about-uq-test-functions.md
index 84c6aaa..38126e1 100644
--- a/docs/getting-started/about-uq-test-functions.md
+++ b/docs/getting-started/about-uq-test-functions.md
@@ -96,7 +96,7 @@ reasons:
   for various types of analyses.
 - Although test functions are taken as black boxes, _their features are known_;
   this enables a thorough diagnosis of a UQ method.
-- Test functions provide _a common ground_ for comparing the performance of
+- Test functions provide a _fair means_ for comparing the performance of
   various UQ methods in solving the same class of problems.
 
 Assuming that real models are expensive to evaluate,
diff --git a/docs/test-functions/ackley.md b/docs/test-functions/ackley.md
index be6d053..2fa9d96 100644
--- a/docs/test-functions/ackley.md
+++ b/docs/test-functions/ackley.md
@@ -36,12 +36,12 @@ As can be seen, the function features many local optima with a single global opt
 from mpl_toolkits.axes_grid1 import make_axes_locatable
 
 # --- Create 1D data from Ackley
-my_ackley_1d = uqtf.Ackley(spatial_dimension=1)
+my_ackley_1d = uqtf.Ackley(input_dimension=1)
 xx_1d = np.linspace(-32.768, 32.768, 1000)[:, np.newaxis]
 yy_1d = my_ackley_1d(xx_1d)
 
 # --- Create 2D data from Ackley
-my_ackley_2d = uqtf.Ackley(spatial_dimension=2)
+my_ackley_2d = uqtf.Ackley(input_dimension=2)
 mesh_2d = np.meshgrid(xx_1d, xx_1d)
 xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
 yy_2d = my_ackley_2d(xx_2d)
@@ -104,13 +104,13 @@ Check if it has been correctly instantiated:
 print(my_testfun)
 ```
   
-By default, the spatial dimension is set to $2$[^default_dimension].
-To create an instance with another value of spatial dimension,
-pass an integer to the parameter `spatial_dimension` (keyword only).
+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` (keyword only).
 For example, to create an instance of 10-dimensional Ackley function, type:
 
 ```{code-cell} ipython3
-my_testfun = uqtf.Ackley(spatial_dimension=10)
+my_testfun = uqtf.Ackley(input_dimension=10)
 ```
 
 In the subsequent section, this 10-dimensional Ackley function will be used
@@ -137,14 +137,22 @@ In UQTestFuns, this search domain can be represented as probabilistic input
 using the uniform distribution with marginals shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Parameters
 
 The Ackley function requires three additional parameters
 to complete the specification.
-The recommended (and the default) values are $a_1 = 20, a_2 = 0.2, a_3 = 2 \pi$.
+The default values are shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.parameters)
+```
 
 ## Reference results
 
@@ -181,4 +189,4 @@ $\mathcal{M}(\boldsymbol{x}^*) = 0$ at $x_m^* = 0,\, m = 1, \ldots, M$.
 ```
 
 [^default_dimension]: This default dimension applies to all variable dimension
-test functions. It will be used if the `spatial_dimension` argument is not given.
+test functions. It will be used if the `input_dimension` argument is not given.
diff --git a/docs/test-functions/alemazkoor-20d.md b/docs/test-functions/alemazkoor-20d.md
new file mode 100644
index 0000000..076fafa
--- /dev/null
+++ b/docs/test-functions/alemazkoor-20d.md
@@ -0,0 +1,96 @@
+---
+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:alemazkoor-20d)=
+# Twenty-dimensional Function from Alemazkoor and Meidani (2018)
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The 20-dimensional test function from {cite}`Alemazkoor2018`
+(or `Alemazkoor20D` for short) is a polynomial function that features 
+low-degree of interactions (i.e., $2$) between the input variables
+(i.e., high in dimension but of low-degree).
+It was used as a test function for
+a metamodeling exercise (i.e., sparse polynomial chaos expansion).
+
+## Test function instance
+
+To create a default instance of the `Alemazkoor20D` function[^location]:
+
+```{code-cell} ipython3
+my_testfun = uqtf.Alemazkoor20D()
+```
+
+Check if it has been correctly instantiated:
+
+```{code-cell} ipython3
+print(my_testfun)
+```
+
+## Description
+
+The `Alemazkoor20D` function is defined as follows:
+
+$$
+\mathcal{M}(\boldsymbol{x}) = \sum_{i = 1}^{19} x_{i} x_{i+1}
+$$
+where $\boldsymbol{x} = \{ x_1, \ldots, x_{20} \}$
+is the twenty-dimensional vector of input variables further defined below.
+
+## Probabilistic input
+
+Based on {cite}`Alemazkoor2018`, the probabilistic input model
+for the function consists of twenty independent random variables
+as shown in the table below.
+
+|   No.    |   Name   | Distribution | Parameters  | Description |
+|:--------:|:--------:|:------------:|:-----------:|:-----------:|
+|    1     |  $x_1$   |   uniform    | [-1.0, 1.0] |     N/A     |
+| $\vdots$ | $\vdots$ |   $\vdots$   |  $\vdots$   |  $\vdots$   |
+|    20    | $x_{20}$ |   uniform    | [-1.0, 1.0] |     N/A     |
+
+## 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]
+
+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);
+```
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^location]: see Section 4.1, Eq. (32) in {cite}`Alemazkoor2018`.
\ No newline at end of file
diff --git a/docs/test-functions/alemazkoor-2d.md b/docs/test-functions/alemazkoor-2d.md
index 99b1cfe..aaa3562 100644
--- a/docs/test-functions/alemazkoor-2d.md
+++ b/docs/test-functions/alemazkoor-2d.md
@@ -26,7 +26,8 @@ is a two-dimensional polynomial function.
 It was used as a test function for
 a metamodeling exercise (i.e., sparse polynomial chaos expansion).
 The function features a low-dimensional polynomial function (two-dimensional)
-with a high degree (a total degree of $20$).
+with a high degree (a total degree of $20$); in other words, the function
+is low in dimension but of high-degree.
 
 The surface and contour plots of the `Alemazkoor2D` function are shown below.
 
@@ -95,11 +96,12 @@ print(my_testfun)
 
 ## Description
 
-The McLain S1 function is defined as follows:
+The `Alemazkoor2D` function is defined as follows:
 
 $$
 \mathcal{M}(\boldsymbol{x}) = \sum_{i = 1}^{5} x_1^{2i} x_2^{2i}
 $$
+
 where $\boldsymbol{x} = \{ x_1, x_2 \}$
 is the two-dimensional vector of input variables further defined below.
 
@@ -109,7 +111,9 @@ Based on {cite}`Alemazkoor2018`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
@@ -140,3 +144,5 @@ plt.gcf().set_dpi(150);
 :style: unsrtalpha
 :filter: docname in docnames
 ```
+
+[^location]: see Section 4.2, Eq. (33) in {cite}`Alemazkoor2018`.
\ No newline at end of file
diff --git a/docs/test-functions/available.md b/docs/test-functions/available.md
index cc76550..ed4d56f 100644
--- a/docs/test-functions/available.md
+++ b/docs/test-functions/available.md
@@ -19,58 +19,73 @@ The table below lists all the test functions available in UQTestFuns
 from the uncertainty quantification (UQ) literature,
 regardless of their typical applications.
 
-|                                        Name                                         | Spatial Dimension |           Constructor           |
-|:-----------------------------------------------------------------------------------:|:-----------------:|:-------------------------------:|
-|                        {ref}`Ackley <test-functions:ackley>`                        |         M         |           `Ackley()`            |
-|        {ref}`Alemazkoor & Meidani (2018) 2D <test-functions:alemazkoor-2d>`         |         2         |        `Alemazkoor2D()`         |
-|                      {ref}`Borehole <test-functions:borehole>`                      |         8         |          `Borehole()`           |
-|            {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}`Cantilever Beam (2D) <test-functions:cantilever-beam-2d>`           |         2         |       `CantileverBeam2D `       |
-|           {ref}`Circular Pipe Crack <test-functions:circular-pipe-crack>`           |         2         |      `CircularPipeCrack()`      |
-|          {ref}`Convex Failure Domain <test-functions:convex-fail-domain>`           |         2         |      `ConvexFailDomain()`       |
-|                 {ref}`Damped Cosine <test-functions:damped-cosine>`                 |         1         |        `DampedCosine()`         |
-|             {ref}`Damped Oscillator <test-functions:damped-oscillator>`             |         7         |      `DampedOscillator()`       |
-| {ref}`Damped Oscillator Reliability <test-functions:damped-oscillator-reliability>` |         8         | `DampedOscillatorReliability()` |
-|                         {ref}`Flood <test-functions:flood>`                         |         8         |            `Flood()`            |
-|              {ref}`Forrester et al. (2008) <test-functions:forrester>`              |         1         |        `Forrester2008()`        |
-|                   {ref}`Four-branch <test-functions:four-branch>`                   |         2         |         `FourBranch()`          |
-|                    {ref}`(1st) Franke <test-functions:franke-1>`                    |         2         |           `Franke1()`           |
-|                    {ref}`(2nd) Franke <test-functions:franke-2>`                    |         2         |           `Franke2()`           |
-|                    {ref}`(3rd) Franke <test-functions:franke-3>`                    |         2         |           `Franke3()`           |
-|                    {ref}`(4th) Franke <test-functions:franke-4>`                    |         2         |           `Franke4()`           |
-|                    {ref}`(5th) Franke <test-functions:franke-5>`                    |         2         |           `Franke5()`           |
-|                    {ref}`(6th) Franke <test-functions:franke-6>`                    |         2         |           `Franke6()`           |
-|                    {ref}`Gayton Hat <test-functions:gayton-hat>`                    |         2         |          `GaytonHat()`          |
-|           {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()`           |
-|                     {ref}`McLain S1 <test-functions:mclain-s1>`                     |         2         |          `McLainS1()`           |
-|                     {ref}`McLain S2 <test-functions:mclain-s2>`                     |         2         |          `McLainS2()`           |
-|                     {ref}`McLain S3 <test-functions:mclain-s3>`                     |         2         |          `McLainS3()`           |
-|                     {ref}`McLain S4 <test-functions:mclain-s4>`                     |         2         |          `McLainS4()`           |
-|                     {ref}`McLain S5 <test-functions:mclain-s5>`                     |         2         |          `McLainS5()`           |
-|            {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}`RS - Circular Bar <test-functions:rs-circular-bar>`              |         2         |        `RSCircularBar()`        |
-|                 {ref}`RS - Quadratic <test-functions:rs-quadratic>`                 |         2         |         `RSQuadratic()`         |
-|                      {ref}`Sobol'-G <test-functions:sobol-g>`                       |         M         |           `SobolG()`            |
-|           {ref}`Speed Reducer Shaft <test-functions:speed-reducer-shaft>`           |         5         |      `SpeedReducerShaft()`      |
-|                        {ref}`Sulfur <test-functions:sulfur>`                        |         9         |           `Sulfur()`            |
-|             {ref}`Webster et al. (1996) 2D <test-functions:webster-2d>`             |         2         |          `Webster2D()`          |
-|                     {ref}`Welch1992 <test-functions:welch1992>`                     |        20         |          `Welch1992()`          |
-|                   {ref}`Wing Weight <test-functions:wing-weight>`                   |        10         |         `WingWeight()`          |
+|                                        Name                                         | Input Dimension |           Constructor           |
+|:-----------------------------------------------------------------------------------:|:---------------:|:-------------------------------:|
+|                        {ref}`Ackley <test-functions:ackley>`                        |        M        |           `Ackley()`            |
+|        {ref}`Alemazkoor & Meidani (2018) 2D <test-functions:alemazkoor-2d>`         |        2        |        `Alemazkoor2D()`         |
+|       {ref}`Alemazkoor & Meidani (2018) 20D <test-functions:alemazkoor-20d>`        |       20        |        `Alemazkoor20D()`        |
+|                      {ref}`Borehole <test-functions:borehole>`                      |        8        |          `Borehole()`           |
+|            {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}`Cantilever Beam (2D) <test-functions:cantilever-beam-2d>`           |        2        |       `CantileverBeam2D `       |
+|              {ref}`Cheng and Sandu (2010) 2D <test-functions:cheng2d>`              |        2        |           `Cheng2D `            |
+|           {ref}`Circular Pipe Crack <test-functions:circular-pipe-crack>`           |        2        |      `CircularPipeCrack()`      |
+|          {ref}`Convex Failure Domain <test-functions:convex-fail-domain>`           |        2        |      `ConvexFailDomain()`       |
+|                 {ref}`Damped Cosine <test-functions:damped-cosine>`                 |        1        |        `DampedCosine()`         |
+|             {ref}`Damped Oscillator <test-functions:damped-oscillator>`             |        7        |      `DampedOscillator()`       |
+| {ref}`Damped Oscillator Reliability <test-functions:damped-oscillator-reliability>` |        8        | `DampedOscillatorReliability()` |
+|                         {ref}`Flood <test-functions:flood>`                         |        8        |            `Flood()`            |
+|              {ref}`Forrester et al. (2008) <test-functions:forrester>`              |        1        |        `Forrester2008()`        |
+|                   {ref}`Four-branch <test-functions:four-branch>`                   |        2        |         `FourBranch()`          |
+|                    {ref}`(1st) Franke <test-functions:franke-1>`                    |        2        |           `Franke1()`           |
+|                    {ref}`(2nd) Franke <test-functions:franke-2>`                    |        2        |           `Franke2()`           |
+|                    {ref}`(3rd) Franke <test-functions:franke-3>`                    |        2        |           `Franke3()`           |
+|                    {ref}`(4th) Franke <test-functions:franke-4>`                    |        2        |           `Franke4()`           |
+|                    {ref}`(5th) Franke <test-functions:franke-5>`                    |        2        |           `Franke5()`           |
+|                    {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}`Gayton Hat <test-functions:gayton-hat>`                    |        2        |          `GaytonHat()`          |
+|           {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()`           |
+|        {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()`           |
+|                     {ref}`McLain S1 <test-functions:mclain-s1>`                     |        2        |          `McLainS1()`           |
+|                     {ref}`McLain S2 <test-functions:mclain-s2>`                     |        2        |          `McLainS2()`           |
+|                     {ref}`McLain S3 <test-functions:mclain-s3>`                     |        2        |          `McLainS3()`           |
+|                     {ref}`McLain S4 <test-functions:mclain-s4>`                     |        2        |          `McLainS4()`           |
+|                     {ref}`McLain S5 <test-functions:mclain-s5>`                     |        2        |          `McLainS5()`           |
+|                    {ref}`Moon (2010) 3D <test-functions:moon3d>`                    |        3        |           `Moon3D()`            |
+|               {ref}`Morris et al. (2006) <test-functions:morris2006>`               |        M        |         `Morris2006()`          |
+|            {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}`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()`         |
+|               {ref}`SaltelliLinear <test-functions:saltelli-linear>`                |        M        |       `SaltelliLinear()`        |
+|                      {ref}`Sobol'-G <test-functions:sobol-g>`                       |        M        |           `SobolG()`            |
+|                   {ref}`Sobol'-G* <test-functions:sobol-g-star>`                    |        M        |         `SobolGStar()`          |
+|                {ref}`Sobol'-Levitan <test-functions:sobol-levitan>`                 |        M        |        `SobolLevitan()`         |
+|                 {ref}`Solar Cell Model <test-functions:solar-cell>`                 |        5        |          `SolarCell()`          |
+|           {ref}`Speed Reducer Shaft <test-functions:speed-reducer-shaft>`           |        5        |      `SpeedReducerShaft()`      |
+|                        {ref}`Sulfur <test-functions:sulfur>`                        |        9        |           `Sulfur()`            |
+|           {ref}`Undamped Oscillator <test-functions:undamped-oscillator>`           |        6        |     `UndampedOscillator()`      |
+|             {ref}`Webster et al. (1996) 2D <test-functions:webster-2d>`             |        2        |          `Webster2D()`          |
+|                {ref}`Welch et al. (1992) <test-functions:welch1992>`                |       20        |          `Welch1992()`          |
+|                   {ref}`Wing Weight <test-functions:wing-weight>`                   |       10        |         `WingWeight()`          |
 
 In a Python terminal, you can list all the available functions
-along with the corresponding constructor using ``list_functions()``:
+along with the corresponding constructor using ``list_functions()``
+(shown below in the HTML format):
 
 ```{code-cell} ipython3
 :tags: ["output_scroll"]
 
 import uqtestfuns as uqtf
 
-uqtf.list_functions()
+uqtf.list_functions(tablefmt="html")
 ```
diff --git a/docs/test-functions/borehole.md b/docs/test-functions/borehole.md
index 4098f85..3de7cdb 100644
--- a/docs/test-functions/borehole.md
+++ b/docs/test-functions/borehole.md
@@ -72,7 +72,9 @@ The marginals of the original specification (from {cite}`Harper1983`) are shown
 below:
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ```{note}
@@ -85,7 +87,7 @@ For example, to create a Borehole test function using
 the input specification by {cite}`Morris1993`:
 
 ```python
-my_testfun = uqtf.Borehole(prob_input_selection="Morris1993")
+my_testfun = uqtf.Borehole(input_id="Morris1993")
 ```
 
 ## Reference results
diff --git a/docs/test-functions/bratley1992a.md b/docs/test-functions/bratley1992a.md
index e238d17..71a19aa 100644
--- a/docs/test-functions/bratley1992a.md
+++ b/docs/test-functions/bratley1992a.md
@@ -38,9 +38,10 @@ There are four other test functions used in Bratley et al. {cite}`Bratley1992`:
 - {ref}`Bratley et al. (1992) D <test-functions:bratley1992d>`:
   A sum of product
   
-The function was reintroduced in {cite}`Saltelli1995` with additional
+This function was reintroduced in {cite}`Saltelli1995` with additional
 parameters for global sensitivity analysis purposes.
-The "generalized" function became known as the {ref}`Sobol'-G <test-functions:sobol-g>`. 
+The "generalized" function became known as
+the {ref}`Sobol'-G <test-functions:sobol-g>`. 
 ```
 
 The plots for one-dimensional and two-dimensional `Bratley1992a` functions
@@ -52,12 +53,12 @@ are shown below.
 from mpl_toolkits.axes_grid1 import make_axes_locatable
 
 # --- Create 1D data
-my_bratley1992a_1d = uqtf.Bratley1992a(spatial_dimension=1)
+my_bratley1992a_1d = uqtf.Bratley1992a(input_dimension=1)
 xx_1d = np.linspace(0, 1, 1000)[:, np.newaxis]
 yy_1d = my_bratley1992a_1d(xx_1d)
 
 # --- Create 2D data
-my_bratley1992a_2d = uqtf.Bratley1992a(spatial_dimension=2)
+my_bratley1992a_2d = uqtf.Bratley1992a(input_dimension=2)
 mesh_2d = np.meshgrid(xx_1d, xx_1d)
 xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
 yy_2d = my_bratley1992a_2d(xx_2d)
@@ -120,14 +121,14 @@ Check if it has been correctly instantiated:
 print(my_testfun)
 ```
 
-By default, the spatial dimension is set to $2$[^default_dimension].
-To create an instance with another value of spatial dimension,
-pass an integer to the parameter `spatial_dimension` (keyword only).
+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` (keyword only).
 For example, to create an instance of 10-dimensional `Bratley1992a` function,
 type:
 
 ```{code-cell} ipython3
-my_testfun = uqtf.Bratley1992a(spatial_dimension=10)
+my_testfun = uqtf.Bratley1992a(input_dimension=10)
 ```
 
 ## Description
@@ -154,7 +155,6 @@ random variables as shown in the table below.
 |  $\vdots$ | $\vdots$ | $\vdots$      | $\vdots$   |  $\vdots$   |
 |  M        | $x_M$    | uniform       | [0.0 1.0]  |     N/A     |
 
-
 ## Reference results
 
 This section provides several reference results of typical UQ analyses involving
@@ -222,4 +222,4 @@ for a very large dimension.
 in {cite}`Bratley1992`.
 
 [^default_dimension]: This default dimension applies to all variable dimension
-test functions. It will be used if the `spatial_dimension` argument is not given.
+test functions. It will be used if the `input_dimension` argument is not given.
diff --git a/docs/test-functions/bratley1992b.md b/docs/test-functions/bratley1992b.md
index a4785ae..0ca39f3 100644
--- a/docs/test-functions/bratley1992b.md
+++ b/docs/test-functions/bratley1992b.md
@@ -37,10 +37,6 @@ There are four other test functions used in Bratley et al. {cite}`Bratley1992`:
   A product of the Chebyshev polynomial of the first kind
 - {ref}`Bratley et al. (1992) D <test-functions:bratley1992d>`:
   A sum of product
-  
-The function was reintroduced in {cite}`Saltelli1995` with additional
-parameters for global sensitivity analysis purposes.
-The "generalized" function became known as the {ref}`Sobol'-G <test-functions:sobol-g>`. 
 ```
 
 The plots for one-dimensional and two-dimensional `Bratley1992b` functions
@@ -52,12 +48,12 @@ are shown below.
 from mpl_toolkits.axes_grid1 import make_axes_locatable
 
 # --- Create 1D data
-my_bratley1992b_1d = uqtf.Bratley1992b(spatial_dimension=1)
+my_bratley1992b_1d = uqtf.Bratley1992b(input_dimension=1)
 xx_1d = np.linspace(0, 1, 1000)[:, np.newaxis]
 yy_1d = my_bratley1992b_1d(xx_1d)
 
 # --- Create 2D data
-my_bratley1992b_2d = uqtf.Bratley1992b(spatial_dimension=2)
+my_bratley1992b_2d = uqtf.Bratley1992b(input_dimension=2)
 mesh_2d = np.meshgrid(xx_1d, xx_1d)
 xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
 yy_2d = my_bratley1992b_2d(xx_2d)
@@ -120,14 +116,14 @@ Check if it has been correctly instantiated:
 print(my_testfun)
 ```
 
-By default, the spatial dimension is set to $2$[^default_dimension].
-To create an instance with another value of spatial dimension,
-pass an integer to the parameter `spatial_dimension` (keyword only).
+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` (keyword only).
 For example, to create an instance of 10-dimensional `Bratley1992b` function,
 type:
 
 ```{code-cell} ipython3
-my_testfun = uqtf.Bratley1992b(spatial_dimension=10)
+my_testfun = uqtf.Bratley1992b(input_dimension=10)
 ```
 
 ## Description
@@ -242,4 +238,4 @@ for a very large dimension.
 in {cite}`Bratley1992`.
 
 [^default_dimension]: This default dimension applies to all variable dimension
-test functions. It will be used if the `spatial_dimension` argument is not given.
+test functions. It will be used if the `input_dimension` argument is not given.
diff --git a/docs/test-functions/bratley1992c.md b/docs/test-functions/bratley1992c.md
index 5f14c36..9b99de5 100644
--- a/docs/test-functions/bratley1992c.md
+++ b/docs/test-functions/bratley1992c.md
@@ -37,10 +37,6 @@ There are four other test functions used in Bratley et al. {cite}`Bratley1992`:
   A product of the Chebyshev polynomial of the first kind (_this function_)
 - {ref}`Bratley et al. (1992) D <test-functions:bratley1992d>`:
   A sum of product
-  
-The function was reintroduced in {cite}`Saltelli1995` with additional
-parameters for global sensitivity analysis purposes.
-The "generalized" function became known as the {ref}`Sobol'-G <test-functions:sobol-g>`. 
 ```
 
 The plots for one-dimensional and two-dimensional `Bratley1992c` functions
@@ -52,12 +48,12 @@ are shown below.
 from mpl_toolkits.axes_grid1 import make_axes_locatable
 
 # --- Create 1D data
-my_bratley1992c_1d = uqtf.Bratley1992c(spatial_dimension=1)
+my_bratley1992c_1d = uqtf.Bratley1992c(input_dimension=1)
 xx_1d = np.linspace(0, 1, 1000)[:, np.newaxis]
 yy_1d = my_bratley1992c_1d(xx_1d)
 
 # --- Create 2D data
-my_bratley1992c_2d = uqtf.Bratley1992c(spatial_dimension=2)
+my_bratley1992c_2d = uqtf.Bratley1992c(input_dimension=2)
 mesh_2d = np.meshgrid(xx_1d, xx_1d)
 xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
 yy_2d = my_bratley1992c_2d(xx_2d)
@@ -120,14 +116,14 @@ Check if it has been correctly instantiated:
 print(my_testfun)
 ```
 
-By default, the spatial dimension is set to $2$[^default_dimension].
-To create an instance with another value of spatial dimension,
-pass an integer to the parameter `spatial_dimension` (keyword only).
+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` (keyword only).
 For example, to create an instance of 10-dimensional `Bratley1992c` function,
 type:
 
 ```{code-cell} ipython3
-my_testfun = uqtf.Bratley1992c(spatial_dimension=10)
+my_testfun = uqtf.Bratley1992c(input_dimension=10)
 ```
 
 ## Description
@@ -189,4 +185,4 @@ the above integral value over the domain is the same as the expected value.
 in {cite}`Bratley1992`.
 
 [^default_dimension]: This default dimension applies to all variable dimension
-test functions. It will be used if the `spatial_dimension` argument is not given.
+test functions. It will be used if the `input_dimension` argument is not given.
diff --git a/docs/test-functions/bratley1992d.md b/docs/test-functions/bratley1992d.md
index c714e79..7097540 100644
--- a/docs/test-functions/bratley1992d.md
+++ b/docs/test-functions/bratley1992d.md
@@ -26,7 +26,8 @@ is an $M$-dimensional scalar-valued function.
 The function was introduced in {cite}`Bratley1992` as a test function
 for multi-dimensional numerical integration using low discrepancy sequences.
 It was used in {cite}`Kucherenko2009` and {cite}`Saltelli2010` in the context
-of global sensitivity analysis.
+of global sensitivity analysis; there the function is more commonly known as
+the $K$-function.
 
 ```{note}
 There are four other test functions used in Bratley et al. {cite}`Bratley1992`:
@@ -40,9 +41,9 @@ There are four other test functions used in Bratley et al. {cite}`Bratley1992`:
 - {ref}`Bratley et al. (1992) D <test-functions:bratley1992d>`:
   A sum of product (_this function_)
   
-The function was reintroduced in {cite}`Saltelli1995` with additional
-parameters for global sensitivity analysis purposes.
-The "generalized" function became known as the {ref}`Sobol'-G <test-functions:sobol-g>`. 
+This function was reintroduced by {cite}`Kucherenko2009` as a test function
+for global sensitivity analysis and was later renamed the $K$-function in 
+{cite}`Saltelli2010`.
 ```
 
 The plots for one-dimensional and two-dimensional `Bratley1992d` functions
@@ -54,12 +55,12 @@ are shown below.
 from mpl_toolkits.axes_grid1 import make_axes_locatable
 
 # --- Create 1D data
-my_bratley1992d_1d = uqtf.Bratley1992d(spatial_dimension=1)
+my_bratley1992d_1d = uqtf.Bratley1992d(input_dimension=1)
 xx_1d = np.linspace(0, 1, 1000)[:, np.newaxis]
 yy_1d = my_bratley1992d_1d(xx_1d)
 
 # --- Create 2D data
-my_bratley1992d_2d = uqtf.Bratley1992d(spatial_dimension=2)
+my_bratley1992d_2d = uqtf.Bratley1992d(input_dimension=2)
 mesh_2d = np.meshgrid(xx_1d, xx_1d)
 xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
 yy_2d = my_bratley1992d_2d(xx_2d)
@@ -122,14 +123,14 @@ Check if it has been correctly instantiated:
 print(my_testfun)
 ```
 
-By default, the spatial dimension is set to $2$[^default_dimension].
-To create an instance with another value of spatial dimension,
-pass an integer to the parameter `spatial_dimension` (keyword only).
+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` (keyword only).
 For example, to create an instance of 10-dimensional `Bratley1992d` function,
 type:
 
 ```{code-cell} ipython3
-my_testfun = uqtf.Bratley1992d(spatial_dimension=10)
+my_testfun = uqtf.Bratley1992d(input_dimension=10)
 ```
 
 ## Description
@@ -232,4 +233,4 @@ where:
 in {cite}`Bratley1992`.
 
 [^default_dimension]: This default dimension applies to all variable dimension
-test functions. It will be used if the `spatial_dimension` argument is not given.
+test functions. It will be used if the `input_dimension` argument is not given.
diff --git a/docs/test-functions/cantilever-beam-2d.md b/docs/test-functions/cantilever-beam-2d.md
index 4821cce..20aa108 100644
--- a/docs/test-functions/cantilever-beam-2d.md
+++ b/docs/test-functions/cantilever-beam-2d.md
@@ -174,8 +174,8 @@ These inputs are probabilistically defined further below.
 
 The parameters of the test function $\boldsymbol{p} = \{ E, l \}$,
 namely the beam's modulus of elasticity ($E$)
-and the span of the beam ($l$) are set to
-$2.6 \times 10^{4} \; \mathrm{[MPa]}$ and $6.0 \; \mathrm{[m]}$, respectively.
+and the span of the beam ($l$).
+The values for these parameters can be found the subsequent section.
 
 The failure state and the failure probability are defined as
 $g(\boldsymbol{x}; \boldsymbol{p}) \leq 0$
@@ -189,7 +189,19 @@ the test function consists of two independent standard normal random variables
 (see the table below).
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
+```
+
+## Parameters
+
+The default values of the parameters are shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.parameters)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/cheng2d.md b/docs/test-functions/cheng2d.md
new file mode 100644
index 0000000..457eb26
--- /dev/null
+++ b/docs/test-functions/cheng2d.md
@@ -0,0 +1,142 @@
+---
+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:cheng2d)=
+# Two-dimensional Function from Cheng and Sandu (2010)
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The two-dimensional test function from Cheng and Sandu (2002) (or
+`Cheng2D` for short) is used in a metamodeling exercise via polynomial
+chaos expansion in {cite}`Cheng2010`.
+
+```{code-cell} ipython3
+:tags: [remove-input]
+
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+
+my_fun = uqtf.Cheng2D()
+
+# --- Create 2D data
+xx_1d = np.linspace(0.0, 1.0, 1000)[:, np.newaxis]
+mesh_2d = np.meshgrid(xx_1d, xx_1d)
+xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
+yy_2d = my_fun(xx_2d)
+
+# --- Create two-dimensional plots
+fig = plt.figure(figsize=(10, 5))
+
+# Surface
+axs_1 = plt.subplot(121, projection='3d')
+axs_1.plot_surface(
+    mesh_2d[0],
+    mesh_2d[1],
+    yy_2d.reshape(1000,1000).T,
+    linewidth=0,
+    cmap="plasma",
+    antialiased=False,
+    alpha=0.5
+)
+axs_1.set_xlabel("$x_1$", fontsize=14)
+axs_1.set_ylabel("$x_2$", fontsize=14)
+axs_1.set_zlabel("$\mathcal{M}(x_1, x_2)$", fontsize=14)
+axs_1.set_title("Surface plot of Cheng2D", fontsize=14)
+
+# Contour
+axs_2 = plt.subplot(122)
+cf = axs_2.contourf(
+    mesh_2d[0], mesh_2d[1], yy_2d.reshape(1000, 1000).T, cmap="plasma", levels=10,
+)
+axs_2.set_xlabel("$x_1$", fontsize=14)
+axs_2.set_ylabel("$x_2$", fontsize=14)
+axs_2.set_title("Contour plot of Cheng2D", fontsize=14)
+divider = make_axes_locatable(axs_2)
+cax = divider.append_axes('right', size='5%', pad=0.05)
+fig.colorbar(cf, cax=cax, orientation='vertical')
+axs_2.axis('scaled')
+
+fig.tight_layout(pad=4.0)
+plt.gcf().set_dpi(75);
+```
+
+
+## Test function instance
+
+To create a default instance of the test function:
+
+```{code-cell} ipython3
+my_testfun = uqtf.Cheng2D()
+```
+
+Check if it has been correctly instantiated:
+
+```{code-cell} ipython3
+print(my_testfun)
+```
+
+## Description
+
+The test function is defined as follows[^location]:
+
+$$
+\mathcal{M}(\boldsymbol{x}) = \cos{(x_1 + x_2)} \exp{(x_1 x_2)},
+$$
+where $\boldsymbol{x} = \{ x_1, x_2 \}$
+is the two-dimensional vector of input variables further defined below.
+
+## Probabilistic input
+
+The input consists of two uniformly distributed random variables as 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 $100'000$ random points:
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+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);
+```
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^location]: See Eq. (6), Section 4, p. 4 {cite}`Cheng2010`.
diff --git a/docs/test-functions/circular-pipe-crack.md b/docs/test-functions/circular-pipe-crack.md
index e223149..f051387 100644
--- a/docs/test-functions/circular-pipe-crack.md
+++ b/docs/test-functions/circular-pipe-crack.md
@@ -165,7 +165,9 @@ the test function consists of two independent standard normal random variables
 (see the table below).
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Parameters
diff --git a/docs/test-functions/convex-fail-domain.md b/docs/test-functions/convex-fail-domain.md
index dd55f7f..a9e01c9 100644
--- a/docs/test-functions/convex-fail-domain.md
+++ b/docs/test-functions/convex-fail-domain.md
@@ -162,7 +162,9 @@ the test function consists of two independent standard normal random variables
 (see the table below).
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/damped-cosine.md b/docs/test-functions/damped-cosine.md
index 88e14ea..8a4f2e6 100644
--- a/docs/test-functions/damped-cosine.md
+++ b/docs/test-functions/damped-cosine.md
@@ -74,7 +74,9 @@ In UQTestFuns, this domain can be represented as a probabilistic input model
 using the uniform distribution shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/damped-oscillator-reliability.md b/docs/test-functions/damped-oscillator-reliability.md
index 9aae1b1..e0bddf9 100644
--- a/docs/test-functions/damped-oscillator-reliability.md
+++ b/docs/test-functions/damped-oscillator-reliability.md
@@ -83,7 +83,9 @@ for the damped oscillator reliability consists of eight independent random
 variables with marginal distributions shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 In the literature, the force capacity of the secondary spring (i.e., $F_s$)
@@ -103,8 +105,14 @@ With the higher mean of $F_s$, the failure probability becomes smaller.
 
 ## Parameters
 
-The performance function contains a single parameter (peak factor).
-From {cite}`DerKiureghian1991` this parameter is set to $3$.
+The performance function contains a single parameter whose default value
+is shown below {cite}`DerKiureghian1991`.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.parameters)
+```
 
 ## Reference results
 
diff --git a/docs/test-functions/damped-oscillator.md b/docs/test-functions/damped-oscillator.md
index ae884d6..e716f08 100644
--- a/docs/test-functions/damped-oscillator.md
+++ b/docs/test-functions/damped-oscillator.md
@@ -82,7 +82,9 @@ seven independent random variables with marginal distributions
 shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/flood.md b/docs/test-functions/flood.md
index 13a793c..4f782ed 100644
--- a/docs/test-functions/flood.md
+++ b/docs/test-functions/flood.md
@@ -71,7 +71,9 @@ for the flood model consists of eight independent random variables
 with marginals shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/forrester.md b/docs/test-functions/forrester.md
index b657cb0..803bdc3 100644
--- a/docs/test-functions/forrester.md
+++ b/docs/test-functions/forrester.md
@@ -88,7 +88,9 @@ In UQTestFuns, this search domain can be represented as probabilistic input
 using the uniform distribution with a marginal shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/four-branch.md b/docs/test-functions/four-branch.md
index cd7d389..497a1f9 100644
--- a/docs/test-functions/four-branch.md
+++ b/docs/test-functions/four-branch.md
@@ -174,7 +174,9 @@ the test function consists of two independent standard normal random variables
 (see the table below).
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Parameters
@@ -187,6 +189,14 @@ in the literature is given in the table below.
 |    $3.5 \sqrt{2}$    |        `Katsuki1994`        |   {cite}`Katsuki1994`   |
 | $\frac{6}{\sqrt{2}}$ | `Schueremans2005` (default) | {cite}`Schueremans2005` |
 
+The default value 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
diff --git a/docs/test-functions/franke-1.md b/docs/test-functions/franke-1.md
index 7158472..757a89d 100644
--- a/docs/test-functions/franke-1.md
+++ b/docs/test-functions/franke-1.md
@@ -134,7 +134,9 @@ Based on {cite}`Franke1979`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/franke-2.md b/docs/test-functions/franke-2.md
index 55e340a..99284de 100644
--- a/docs/test-functions/franke-2.md
+++ b/docs/test-functions/franke-2.md
@@ -137,7 +137,9 @@ Based on {cite}`Franke1979`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/franke-3.md b/docs/test-functions/franke-3.md
index f98187a..1276654 100644
--- a/docs/test-functions/franke-3.md
+++ b/docs/test-functions/franke-3.md
@@ -125,7 +125,9 @@ Based on {cite}`Franke1979`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/franke-4.md b/docs/test-functions/franke-4.md
index 195b37d..2ee8374 100644
--- a/docs/test-functions/franke-4.md
+++ b/docs/test-functions/franke-4.md
@@ -136,7 +136,9 @@ Based on {cite}`Franke1979`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/franke-5.md b/docs/test-functions/franke-5.md
index d4138a6..e90b6d5 100644
--- a/docs/test-functions/franke-5.md
+++ b/docs/test-functions/franke-5.md
@@ -136,7 +136,9 @@ Based on {cite}`Franke1979`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/franke-6.md b/docs/test-functions/franke-6.md
index b2bad03..17f9359 100644
--- a/docs/test-functions/franke-6.md
+++ b/docs/test-functions/franke-6.md
@@ -137,7 +137,9 @@ Based on {cite}`Franke1979`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/friedman-10d.md b/docs/test-functions/friedman-10d.md
new file mode 100644
index 0000000..01155b7
--- /dev/null
+++ b/docs/test-functions/friedman-10d.md
@@ -0,0 +1,100 @@
+---
+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:friedman-10d)=
+# Ten-dimensional (10D) Friedman Function
+
+The 10D Friedman function (or `Friedman6D` function for short) is
+a ten-dimensional (including five dummy variables) scalar-valued function.
+The function features a combination of non-linearity and variable interaction.
+
+It was originally used in {cite}`Friedman1991` as a test function for testing
+a regression spline method.
+
+```{note}
+The function was an extension of the six-dimensional version introduced
+in {cite}`Friedman1983` by adding four additional dummy variables
+(for a total of five);
+the function is also {ref}`available <test-functions:friedman-6d>`
+in UQTestFuns.
+```
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as pltx
+import uqtestfuns as uqtf
+```
+
+## Test function instance
+
+To create a default instance of the test function:
+
+```{code-cell} ipython3
+my_testfun = uqtf.Friedman10D()
+```
+
+Check if it has been correctly instantiated:
+
+```{code-cell} ipython3
+print(my_testfun)
+```
+
+## Description
+
+The test function is analytically defined as follows:
+
+$$
+\mathcal{M}(\boldsymbol{x}) = 10 \sin{(\pi x_1 x_2)} + 20 (x_3 - 0.5)^2 + 10 x_4 + 5 x_5 + 0 x_6 + 0 x_7 + 0 x_8 + 0 x_9 + 0 x_{10},
+$$
+where $x$ is defined below. Notice that the last five input variables are 
+inert.
+
+## Probabilistic input
+
+Based on {cite}`Friedman1991`, the probabilistic input model
+for the function consists of two independent random variables as 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 $100'000$ random points:
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+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);
+```
+
+## References
+
+```{bibliography}
+:filter: docname in docnames
+```
\ No newline at end of file
diff --git a/docs/test-functions/friedman-6d.md b/docs/test-functions/friedman-6d.md
new file mode 100644
index 0000000..75b24d5
--- /dev/null
+++ b/docs/test-functions/friedman-6d.md
@@ -0,0 +1,101 @@
+---
+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:friedman-6d)=
+# Six-dimensional (6D) Friedman Function
+
+The 6D Friedman function (or `Friedman6D` function for short) is
+a six-dimensional (including one dummy variable) scalar-valued function.
+The function features a combination of non-linearity and variable interaction.
+
+It was originally used in {cite}`Friedman1983` as a test function for testing
+a spline approximation method.
+In {cite}`Sun2022` and {cite}`Horiguchi2021` (albeit in a modified form)
+the function was employed as a test function in the context of
+sensitivity analysis.
+
+```{note}
+The function was later extended to ten dimension by incorporating four
+additional dummy variables (for a total of five) in {cite}`Friedman1991`;
+the function is also {ref}`available <test-functions:friedman-10d>`
+in UQTestFuns.
+```
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+## Test function instance
+
+To create a default instance of the test function:
+
+```{code-cell} ipython3
+my_testfun = uqtf.Friedman6D()
+```
+
+Check if it has been correctly instantiated:
+
+```{code-cell} ipython3
+print(my_testfun)
+```
+
+## Description
+
+The test function is analytically defined as follows:
+
+$$
+\mathcal{M}(\boldsymbol{x}) = 10 \sin{(\pi x_1 x_2)} + 20 (x_3 - 0.5)^2 + 10 x_4 + 5 x_5 + 0 x_6,
+$$
+where $x$ is defined below. Notice that the sixth input variable is inert.
+
+## Probabilistic input
+
+Based on {cite}`Friedman1983`, the probabilistic input model
+for the function consists of two independent random variables as 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 $100'000$ random points:
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+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);
+```
+
+## References
+
+```{bibliography}
+:filter: docname in docnames
+```
\ No newline at end of file
diff --git a/docs/test-functions/gayton-hat.md b/docs/test-functions/gayton-hat.md
index b03805f..f5d05d0 100644
--- a/docs/test-functions/gayton-hat.md
+++ b/docs/test-functions/gayton-hat.md
@@ -165,7 +165,9 @@ the test function consists of two independent standard normal random variables
 (see the table below).
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/gramacy-1d-sine.md b/docs/test-functions/gramacy-1d-sine.md
index e011307..adac7de 100644
--- a/docs/test-functions/gramacy-1d-sine.md
+++ b/docs/test-functions/gramacy-1d-sine.md
@@ -28,27 +28,18 @@ of sines and cosines, and another part is a linear function.
 The function was introduced in {cite}`Gramacy2007` in the context of 
 metamodeling with non-stationary Gaussian processes.
 
-In the original paper, the response of the function is disturbed by an 
-independent identically distributed (i.i.d) Gaussian noise.
-
 A plot of the function is shown below for $x \in [0, 20]$.
 
 ```{code-cell} ipython3
 :tags: [remove-input]
 
-my_testfun_noisy = uqtf.Gramacy1DSine()
-my_testfun_noiseless = uqtf.Gramacy1DSine(parameters_selection="noiseless")
-rng = np.random.default_rng(3423)
-my_testfun_noisy.parameters = rng
-
+my_testfun = uqtf.Gramacy1DSine()
 
 xx = np.linspace(0, 20, 100)[:, np.newaxis]
-yy_noisy = my_testfun_noisy(xx)
-yy_noiseless = my_testfun_noiseless(xx)
+yy = my_testfun(xx)
 
 # --- Create the plot
-plt.plot(xx, yy_noiseless, color="#8da0cb")
-plt.scatter(xx, yy_noisy, color="#8da0cb", marker="x", s=50)
+plt.plot(xx, yy, color="#8da0cb")
 plt.grid()
 plt.xlabel("$x$")
 plt.ylabel("$\mathcal{M}(x)$")
@@ -59,6 +50,12 @@ plt.gcf().set_dpi(150);
 Note that the function is discontinuous at $x = 9.6%$ which also pinpoints
 the change of regime.
 
+```{note}
+In the original paper, the response of the function is disturbed by an 
+independent identically distributed (i.i.d) Gaussian noise 
+$\varepsilon \sim \mathcal{N}(0, \sigma_n=0.1)$.
+```
+
 ## Test function instance
 
 To create a default instance of the test function:
@@ -80,58 +77,21 @@ The test function is analytically defined as follows[^location]:
 $$
 \mathcal{M}(x) = \begin{cases}
 \sin{(\frac{\pi x}{5})} + \frac{1}{5} \cos{(\frac{4 \pi x}{5})}, & x \leq 9.6 \\
-\frac{1}{10} x - 1, & x > 9.6
-\end{cases},
+\frac{1}{10} x - 1, & x > 9.6,
+\end{cases}
 $$
 where $x$ is defined below.
 
-The response of the function is disturbed by an i.i.d Gaussian noise,
-such that:
-
-$$
-y(x) = \mathcal{M}(x) + \varepsilon,
-$$
-where $\varepsilon \sim \mathcal{N}(0, \sigma_n = 0.1)$.
-
 ## Probabilistic input
 
 Based on {cite}`Gramacy2007`, the domain of the function is $[0, 20]$.
 This input can be modeled with a single uniform random variable shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
-```
-
-## Parameters
-
-The parameters of the test function is a NumPy random number generator with 
-which the Gaussian random noise is generated.
-Other available parameters are shown in the table below.
-
-| No. |                      Value                      |      Keyword      |       Source        |
-|:---:|:-----------------------------------------------:|:-----------------:|:-------------------:|
-|  1  | $\varepsilon \sim \mathcal{N}(0, \sigma_n=0.1)$ | `noisy` (default) | {cite}`Gramacy2007` |
-|  2  |                $\varepsilon = 0$                |    `noiseless`    |                     |
+:tags: [hide-input]
 
-Alternatively, to create an instance of the `Gramacy1DSine` function with
-different parameters, type
-
-```python
-my_testfun = uqtf.Gramacy1DSine(parameters_selection="noiseless")
-```
-
-````{note}
-To use a custom random number generator (perhaps with a fixed seed number),
-create a default test function and use the custom generator as the parameters
-after the instance has been
-created.
-For example:
-```python
-my_testfun = uqtf.Gramacy1DSine()
-my_rng = np.random.default_rng(322345)
-my_testfun.parameters = my_rng
+print(my_testfun.prob_input)
 ```
-````
 
 ## References
 
@@ -141,4 +101,4 @@ my_testfun.parameters = my_rng
 ```
 
 [^location]: see Section 4.2, p. 17, Eq. (16) in {cite}`Gramacy2007`;
-also the actual implementation as an R code not far below that.
\ No newline at end of file
+also the actual implementation as an R code not far below that.
diff --git a/docs/test-functions/hyper-sphere.md b/docs/test-functions/hyper-sphere.md
index 495afed..117c135 100644
--- a/docs/test-functions/hyper-sphere.md
+++ b/docs/test-functions/hyper-sphere.md
@@ -164,7 +164,9 @@ the test function consists of two independent standard normal random variables
 (see the table below).
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/ishigami.md b/docs/test-functions/ishigami.md
index c7102ec..61975f4 100644
--- a/docs/test-functions/ishigami.md
+++ b/docs/test-functions/ishigami.md
@@ -46,11 +46,11 @@ The Ishigami function, a highly non-linear and non-monotonic function,
 is given as follows:
 
 $$
-\mathcal{M}(\boldsymbol{x}) = \sin(x_1) + a \sin^2(x_2) + b \, x_3^4 \sin(x_1)
+\mathcal{M}(\boldsymbol{x}; \boldsymbol{p}) = \sin(x_1) + a \sin^2(x_2) + b \, x_3^4 \sin(x_1)
 $$
 where $\boldsymbol{x} = \{ x_1, x_2, x_3 \}$ is the three-dimensional vector of
-input variables further defined below, and $a$ and $b$ are parameters
-of the function.
+input variables further defined below, and $\boldsymbol{p} = \{ a, b \}$
+are the parameters of the function.
 
 ## Probabilistic input
 
@@ -59,7 +59,9 @@ function consists of three independent uniform random variables with the ranges
 shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Parameters
@@ -68,25 +70,24 @@ The parameters of the Ishigami function are two real-valued numbers.
 Some of the available parameter values taken from the literature are shown in
 the table below.
 
-| No. |        Value         |         Keyword          |        Source        |
-|:---:|:--------------------:|:------------------------:|:--------------------:|
-|  1  | $a = 7$, $b = 0.1$   | `Ishigami1991` (default) | {cite}`Ishigami1991` |
-|  2  | $a = 7$, $b = 0.05$  |       `Sobol1999`        |  {cite}`Sobol1999`   |
+| No. |        Value        |         Keyword          |        Source        |
+|:---:|:-------------------:|:------------------------:|:--------------------:|
+|  1  | $a = 7$, $b = 0.1$  | `Ishigami1991` (default) | {cite}`Ishigami1991` |
+|  2  | $a = 7$, $b = 0.05$ |       `Sobol1999`        |  {cite}`Sobol1999`   |
 
-Alternatively, to create an instance of the Ishigami function with
-different parameter values, type:
+```{code-cell} ipython3
+:tags: [hide-input]
 
-```python
-my_testfun = uqtf.Ishigami(parameters_selection="Sobol1999")
+print(my_testfun.parameters)
 ```
 
 ````{note}
 To use another set of parameters, create a default test function
 and pass one of the available keywords
-(as indicated in the table above) to the `parameters_selection` parameter.
+(as indicated in the table above) to the `parameters_id` parameter.
 For example:
 ```python
-my_testfun = uqtf.Ishigami(parameters_selection="Sobol1999")
+my_testfun = uqtf.Ishigami(parameters_id="Sobol1999")
 ```
 ````
 
@@ -132,7 +133,7 @@ analytical values.
 :tags: [hide-input]
 
 # --- Compute the mean and variance estimate
-np.random.seed(42)
+my_testfun.prob_input.reset_rng(42)
 sample_sizes = np.array([1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7], dtype=int)
 mean_estimates = np.empty(len(sample_sizes))
 var_estimates = np.empty(len(sample_sizes))
@@ -159,7 +160,7 @@ ax_1.errorbar(
     label="Mean"
 )
 # Plot the analytical mean
-a = my_testfun.parameters[0]
+a = my_testfun.parameters["a"]
 mean_analytical = a / 2.0
 ax_1.plot(
     sample_sizes,
@@ -184,7 +185,7 @@ ax_2.errorbar(
     label="Variance",
 )
 # Plot the analytical variance
-b = my_testfun.parameters[1]
+b = my_testfun.parameters["b"]
 var_analytical = a**2 / 8 + b * np.pi**4 / 5 + b**2 * np.pi**8 / 18 + 0.5
 ax_2.plot(
     sample_sizes,
diff --git a/docs/test-functions/lim-non-poly.md b/docs/test-functions/lim-non-poly.md
new file mode 100644
index 0000000..16638c1
--- /dev/null
+++ b/docs/test-functions/lim-non-poly.md
@@ -0,0 +1,150 @@
+---
+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:lim-non-poly)=
+# Two-dimensional Non-Polynomial Function from Lim et al. (2002)
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The non-polynomial test function from Lim et al. (2002) (or `LimNonPoly` for
+short) is a two-dimensional scalar-valued function.
+The function was used in {cite}`Lim2002` in the context of establishing the
+connection between Gaussian process metamodel and polynomials.
+
+```{code-cell} ipython3
+:tags: [remove-input]
+
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+
+my_fun = uqtf.LimNonPoly()
+
+# --- Create 2D data
+xx_1d = np.linspace(0.0, 1.0, 1000)[:, np.newaxis]
+mesh_2d = np.meshgrid(xx_1d, xx_1d)
+xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
+yy_2d = my_fun(xx_2d)
+
+# --- Create two-dimensional plots
+fig = plt.figure(figsize=(10, 5))
+
+# Surface
+axs_1 = plt.subplot(121, projection='3d')
+axs_1.plot_surface(
+    mesh_2d[0],
+    mesh_2d[1],
+    yy_2d.reshape(1000,1000).T,
+    linewidth=0,
+    cmap="plasma",
+    antialiased=False,
+    alpha=0.5
+)
+axs_1.set_xlabel("$x_1$", fontsize=14)
+axs_1.set_ylabel("$x_2$", fontsize=14)
+axs_1.set_zlabel("$\mathcal{M}(x_1, x_2)$", fontsize=14)
+axs_1.set_title("Surface plot of LimPoly", fontsize=14)
+
+# Contour
+axs_2 = plt.subplot(122)
+cf = axs_2.contourf(
+    mesh_2d[0], mesh_2d[1], yy_2d.reshape(1000, 1000).T, cmap="plasma", levels=10,
+)
+axs_2.set_xlabel("$x_1$", fontsize=14)
+axs_2.set_ylabel("$x_2$", fontsize=14)
+axs_2.set_title("Contour plot of LimPoly", fontsize=14)
+divider = make_axes_locatable(axs_2)
+cax = divider.append_axes('right', size='5%', pad=0.05)
+fig.colorbar(cf, cax=cax, orientation='vertical')
+axs_2.axis('scaled')
+
+fig.tight_layout(pad=4.0)
+plt.gcf().set_dpi(75);
+```
+
+
+## Test function instance
+
+To create a default instance of the test function:
+
+```{code-cell} ipython3
+my_testfun = uqtf.LimNonPoly()
+```
+
+Check if it has been correctly instantiated:
+
+```{code-cell} ipython3
+print(my_testfun)
+```
+
+## Description
+
+The test function is defined as follows[^location]:
+
+$$
+\mathcal{M}(\boldsymbol{x}) = \frac{(30 + 5 x_1 \sin{(5 x_1)}) (4 + \exp{(-5x_2)}) - 100}{6}
+$$
+where $\boldsymbol{x} = \{ x_1, x_2 \}$
+is the two-dimensional vector of input variables further defined below.
+
+```{note}
+This function is a rescaled version of Eq. (6) in {cite}`Welch1992`.
+The function is also similar to its
+{ref}`polynomial counterpart <test-functions:lim-poly>`; in fact,
+the coefficients of the polynomial are chosen with that goal in mind.
+```
+
+## Probabilistic input
+
+The input consists of two uniformly distributed random variables as 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 $100'000$ random points:
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+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);
+```
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^location]: See Eq. (28), Section 7, p. 121 {cite}`Lim2002`.
\ No newline at end of file
diff --git a/docs/test-functions/lim-poly.md b/docs/test-functions/lim-poly.md
new file mode 100644
index 0000000..8329967
--- /dev/null
+++ b/docs/test-functions/lim-poly.md
@@ -0,0 +1,148 @@
+---
+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:lim-poly)=
+# Two-dimensional Polynomial Function from Lim et al. (2002)
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The polynomial test function from Lim et al. (2002) (or `LimPoly` for short)
+is a two-dimensional scalar-valued function.
+The function was used in {cite}`Lim2002` in the context of establishing the
+connection between Gaussian process metamodel and polynomials.
+
+```{code-cell} ipython3
+:tags: [remove-input]
+
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+
+my_fun = uqtf.LimPoly()
+
+# --- Create 2D data
+xx_1d = np.linspace(0.0, 1.0, 1000)[:, np.newaxis]
+mesh_2d = np.meshgrid(xx_1d, xx_1d)
+xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
+yy_2d = my_fun(xx_2d)
+
+# --- Create two-dimensional plots
+fig = plt.figure(figsize=(10, 5))
+
+# Surface
+axs_1 = plt.subplot(121, projection='3d')
+axs_1.plot_surface(
+    mesh_2d[0],
+    mesh_2d[1],
+    yy_2d.reshape(1000,1000).T,
+    linewidth=0,
+    cmap="plasma",
+    antialiased=False,
+    alpha=0.5
+)
+axs_1.set_xlabel("$x_1$", fontsize=14)
+axs_1.set_ylabel("$x_2$", fontsize=14)
+axs_1.set_zlabel("$\mathcal{M}(x_1, x_2)$", fontsize=14)
+axs_1.set_title("Surface plot of LimPoly", fontsize=14)
+
+# Contour
+axs_2 = plt.subplot(122)
+cf = axs_2.contourf(
+    mesh_2d[0], mesh_2d[1], yy_2d.reshape(1000, 1000).T, cmap="plasma", levels=10,
+)
+axs_2.set_xlabel("$x_1$", fontsize=14)
+axs_2.set_ylabel("$x_2$", fontsize=14)
+axs_2.set_title("Contour plot of LimPoly", fontsize=14)
+divider = make_axes_locatable(axs_2)
+cax = divider.append_axes('right', size='5%', pad=0.05)
+fig.colorbar(cf, cax=cax, orientation='vertical')
+axs_2.axis('scaled')
+
+fig.tight_layout(pad=4.0)
+plt.gcf().set_dpi(75);
+```
+
+## Test function instance
+
+To create a default instance of the test function:
+
+```{code-cell} ipython3
+my_testfun = uqtf.LimPoly()
+```
+
+Check if it has been correctly instantiated:
+
+```{code-cell} ipython3
+print(my_testfun)
+```
+
+## Description
+
+The test function is defined as follows[^location]:
+
+$$
+\mathcal{M}(\boldsymbol{x}) = 9 + \frac{5}{2} x_1 - \frac{35}{2} x_2 + \frac{5}{2} x_1 x_2 + 19 x_2^2 - \frac{15}{2} x_1^3 - \frac{5}{2} x_1 x_2^2 - \frac{11}{2} x_2^4 + x_1^3 x_2^2,
+$$
+where $\boldsymbol{x} = \{ x_1, x_2 \}$
+is the two-dimensional vector of input variables further defined below.
+
+```{note}
+The coefficients of the test function are chosen such that its global features
+are similar to its
+{ref}`non-polynomial counterpart <test-functions:lim-non-poly>`.
+```
+
+## Probabilistic input
+
+The input consists of two uniformly distributed random variables as 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 $100'000$ random points:
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+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);
+```
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^location]: See Eq. (27), Section 7, p. 119 {cite}`Lim2002`.
\ No newline at end of file
diff --git a/docs/test-functions/mclain-s1.md b/docs/test-functions/mclain-s1.md
index bd44958..df40bc9 100644
--- a/docs/test-functions/mclain-s1.md
+++ b/docs/test-functions/mclain-s1.md
@@ -128,7 +128,9 @@ Based on {cite}`McLain1974`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/mclain-s2.md b/docs/test-functions/mclain-s2.md
index 3b24c24..708b363 100644
--- a/docs/test-functions/mclain-s2.md
+++ b/docs/test-functions/mclain-s2.md
@@ -128,7 +128,9 @@ Based on {cite}`McLain1974`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/mclain-s3.md b/docs/test-functions/mclain-s3.md
index 1196fc8..fcfd0e9 100644
--- a/docs/test-functions/mclain-s3.md
+++ b/docs/test-functions/mclain-s3.md
@@ -131,7 +131,9 @@ Based on {cite}`McLain1974`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/mclain-s4.md b/docs/test-functions/mclain-s4.md
index 731e9d0..6566452 100644
--- a/docs/test-functions/mclain-s4.md
+++ b/docs/test-functions/mclain-s4.md
@@ -119,7 +119,9 @@ Based on {cite}`McLain1974`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/mclain-s5.md b/docs/test-functions/mclain-s5.md
index 8618fa5..a7302d7 100644
--- a/docs/test-functions/mclain-s5.md
+++ b/docs/test-functions/mclain-s5.md
@@ -130,7 +130,9 @@ Based on {cite}`McLain1974`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/moon3d.md b/docs/test-functions/moon3d.md
new file mode 100644
index 0000000..c8ceb03
--- /dev/null
+++ b/docs/test-functions/moon3d.md
@@ -0,0 +1,258 @@
+---
+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:moon3d)=
+# Moon (2010) Three-Dimensional Function
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The three-dimensional function from {cite}`Moon2010` (or `Moon3D` for short) 
+is a scalar-valued test function used in {cite}`Moon2010` to illustrate
+the analytical derivation of Sobol' sensitivity indices.
+
+## Test function instance
+
+To create an instance of the test function:
+
+```{code-cell} ipython3
+my_testfun = uqtf.Moon3D()
+```
+
+Check if it has been correctly instantiated:
+
+```{code-cell} ipython3
+print(my_testfun)
+```
+
+## Description
+
+The function `Moon3D` is a three-dimensional function given by the following
+formula[^location]:
+
+$$
+\mathcal{M}(\boldsymbol{x}) = x_1 + x_2 + 3 \, x_1 x_3
+$$
+where $\boldsymbol{x} = \{ x_1, x_2, x_3 \}$ is the three-dimensional vector of
+input variables further defined below.
+
+## Probabilistic input
+
+Based on {cite}`Moon2010`, the probabilistic input model for the function
+consists of three independent uniform random variables with the ranges
+shown in the table 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 $100'000$ random points:
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+np.random.seed(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);
+```
+
+### Moment estimations
+
+The mean and variance of the test function can be computed analytically,  
+and the results are:
+  
+- $\mathbb{E}[Y] = \frac{7}{4}$
+- $\mathbb{V}[Y] = \frac{41}{48}$
+
+Shown below is the convergence of a direct Monte-Carlo estimation of
+the output mean and variance with increasing sample sizes compared with the
+analytical values.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+# --- Compute the mean and variance estimate
+np.random.seed(42)
+sample_sizes = np.array([1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7], dtype=int)
+mean_estimates = np.empty(len(sample_sizes))
+var_estimates = np.empty(len(sample_sizes))
+
+for i, sample_size in enumerate(sample_sizes):
+    xx_test = my_testfun.prob_input.get_sample(sample_size)
+    yy_test = my_testfun(xx_test)
+    mean_estimates[i] = np.mean(yy_test)
+    var_estimates[i] = np.var(yy_test)
+
+# --- Compute the error associated with the estimates
+mean_estimates_errors = np.sqrt(var_estimates) / np.sqrt(np.array(sample_sizes))
+var_estimates_errors = var_estimates * np.sqrt(2 / (np.array(sample_sizes) - 1))
+
+fig, ax_1 = plt.subplots(figsize=(6,4))
+
+# --- Mean plot
+ax_1.errorbar(
+    sample_sizes,
+    mean_estimates,
+    yerr=mean_estimates_errors,
+    marker="o",
+    color="#66c2a5",
+    label="Mean"
+)
+# Plot the analytical mean
+mean_analytical = 7/4
+ax_1.plot(
+    sample_sizes,
+    np.repeat(mean_analytical, len(sample_sizes)),
+    linestyle="--",
+    color="#66c2a5",
+    label="Analytical mean",
+)
+ax_1.set_xlim([9, 2e7])
+ax_1.set_xlabel("Sample size")
+ax_1.set_ylabel("Output mean estimate")
+ax_1.set_xscale("log");
+ax_2 = ax_1.twinx()
+
+# --- Variance plot
+ax_2.errorbar(
+    sample_sizes+1,
+    var_estimates,
+    yerr=var_estimates_errors,
+    marker="o",
+    color="#fc8d62",
+    label="Variance",
+)
+# Plot the analytical variance
+var_analytical = 41/48
+ax_2.plot(
+    sample_sizes,
+    np.repeat(var_analytical, len(sample_sizes)),
+    linestyle="--",
+    color="#fc8d62",
+    label="Analytical variance",
+)
+ax_2.set_ylabel("Output variance estimate")
+
+# Add the two plots together to have a common legend
+ln_1, labels_1 = ax_1.get_legend_handles_labels()
+ln_2, labels_2 = ax_2.get_legend_handles_labels()
+ax_2.legend(ln_1 + ln_2, labels_1 + labels_2, loc=0)
+
+plt.grid()
+fig.set_dpi(150)
+```
+
+The tabulated results for each sample size is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+from tabulate import tabulate
+
+# --- Compile data row-wise
+outputs = [
+    [
+        np.nan,
+        mean_analytical,
+        0.0,
+        var_analytical,
+        0.0,
+        "Analytical",
+    ]
+]
+
+for (
+    sample_size,
+    mean_estimate,
+    mean_estimate_error,
+    var_estimate,
+    var_estimate_error,
+) in zip(
+    sample_sizes,
+    mean_estimates,
+    mean_estimates_errors,
+    var_estimates,
+    var_estimates_errors,
+):
+    outputs += [
+        [
+            sample_size,
+            mean_estimate,
+            mean_estimate_error,
+            var_estimate,
+            var_estimate_error,
+            "Monte-Carlo",
+        ],
+    ]
+
+header_names = [
+    "Sample size",
+    "Mean",
+    "Mean error",
+    "Variance",
+    "Variance error",
+    "Remark",
+]
+
+tabulate(
+    outputs,
+    headers=header_names,
+    floatfmt=(".1e", ".4e", ".4e", ".4e", ".4e", "s"),
+    tablefmt="html",
+    stralign="center",
+    numalign="center",
+)
+```
+
+### Sensitivity indices
+
+The main-effect and total-effect Sobol' indices of the test function can be   
+derived analytically.
+
+```{table} Main-effect and Total-effect sensitivity indices of the function
+:name: moon3d-sensitivity-indices
+
+| Input   |     Main-effect ($S_i$)    |   Total-effect ($ST_i$)    |
+|:-------:|:--------------------------:|:--------------------------:|
+|  $x_1$  | $\frac{25}{41}$            | $\frac{28}{41}$            |
+|  $x_2$  | $\frac{4}{41}$             | $\frac{4}{41}$             |
+|  $x_3$  | $\frac{9}{41}$             | $\frac{12}{41}$            |
+```
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^location]: see Eq. (5.46) in Section 5.1.4, pp. 546-547 in {cite}`Moon2010`.
diff --git a/docs/test-functions/morris2006.md b/docs/test-functions/morris2006.md
new file mode 100644
index 0000000..17d3a15
--- /dev/null
+++ b/docs/test-functions/morris2006.md
@@ -0,0 +1,248 @@
+---
+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:morris2006)=
+# Test Function from Morris et al. (2006)
+
+The test function from Morris et al. (2006) {cite}`Morris2006`
+(or  `Morris2006` for short) is an $M$-dimensional scalar-valued function used
+in the context of sensitivity analysis
+{cite}`Morris2006, Horiguchi2021, Sun2022`.
+
+The function features a parameter that controls the number of important input
+variables; the remaining variables, if any, are inert. Furthermore, the Sobol'
+main-effect and total-effect sensitivity indices are the same for each input
+variable.
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The plots for one-dimensional and two-dimensional `Morris2006` function
+can be seen below.
+
+```{code-cell} ipython3
+:tags: [remove-input]
+
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+
+# --- Create 1D data
+my_fun_1d = uqtf.Morris2006(input_dimension=1)
+xx_1d = np.linspace(0, 1, 1000)[:, np.newaxis]
+yy_1d = my_fun_1d(xx_1d)
+
+# --- Create 2D data
+my_fun_2d = uqtf.Morris2006(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 Morris2006")
+
+# 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 Morris2006", 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 Morris2006", 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 function, type:
+
+```{code-cell} ipython3
+my_testfun = uqtf.Morris2006()
+```
+
+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 function in 30 dimensions,
+type:
+
+```{code-cell} ipython3
+my_testfun = uqtf.Morris2006(input_dimension=30)
+```
+
+In the subsequent section, the function will be illustrated
+using 30 dimensions as it originally appeared in {cite}`Morris2006`.
+
+## Description
+
+The `Morris2006` function is defined as follows[^location]:
+
+$$
+\mathcal{M}(\boldsymbol{x}; p) = \alpha(p) \sum_{i = 1}^p x_p + \beta(p) \sum_{i = 1}^{p - 1} x_i \left( \sum_{j = i + 1}^p x_j \right),
+$$
+where
+
+$$
+\alpha(p) = \sqrt{12} - 6 \sqrt{0.1 (p - 1)}
+$$
+
+and
+
+$$
+\beta(p) = \frac{12}{\sqrt{10 (p - 1)}}.
+$$
+
+where $\boldsymbol{x} = \{ x_1, \ldots, x_M \}$ is the $M$-dimensional vector
+of input variables further defined below,
+and $p$ is the parameter of the function.
+
+```{important}
+The original formula for $\beta$ in {cite}`Morris2006` contains an error.
+The formula given, $12 \sqrt{0.1} \sqrt{p - 1}$, fails to meet the specified
+condition for the function, where the products of the main-effect
+and total-effect indices with the variance should yield values $1.0$ and $1.1$,
+respectively.
+```
+
+## Probabilistic input
+
+The probabilistic input model for the `Morris2006` function consists of $M$
+independent uniform random variables in $[0.0, 1.0]^M$. 
+
+For the selected input dimension, the input model is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input, "output_scroll"]
+
+print(my_testfun.prob_input)
+```
+
+## Parameters
+
+The parameter $p$ of the test function controls the number of important input
+variables; if this number is larger than the actual number of input dimensions,
+then all input variables are deemed important.
+
+The default parameter is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.parameters)
+```
+
+````{note}
+You can replace the default value of the parameter by assigning a new value
+to it as follows:
+
+```python
+my_testfun.parameters["p"] = 5
+```
+````
+
+## 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]
+
+np.random.seed(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);
+```
+
+### Sensitivity indices
+
+The values of $p$, $\alpha$ ,and $\beta$ in the above equation are chosen
+such that the following conditions are satisfied:
+
+$$
+\begin{aligned}
+S_i \times \mathbb{V}[Y]  & = 1.0, & i & = 1, \ldots, p, \\
+S_i                       & = 0.0, & i &  = p + 1, \ldots, M,
+\end{aligned}
+$$
+
+and
+
+$$
+\begin{aligned}
+ST_i \times \mathbb{V}[Y] & = 1.1 & i & = 1, \ldots, p, \\
+ST_i & = 0.0, & i & = p + 1, \ldots, M,
+\end{aligned}
+$$
+
+where $S_i$ and $ST_i$ are the main-effect and total-effect indices for $i$-th
+input variable; and $\mathbb{V}[Y]$ is the output variance.
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^location]: see Section 4, p. 3213 in {cite}`Morris2006`.
+
+[^default_dimension]: This default dimension applies to all variable dimension
+test functions. It will be used if the `input_dimension` argument is not given.
diff --git a/docs/test-functions/oakley-1d.md b/docs/test-functions/oakley-1d.md
index b24f3ea..750e9b8 100644
--- a/docs/test-functions/oakley-1d.md
+++ b/docs/test-functions/oakley-1d.md
@@ -74,7 +74,9 @@ Oakley-O'Hagan function consists of a normal random variable
 with the parameters shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/otl-circuit.md b/docs/test-functions/otl-circuit.md
index bdd10a8..d54d2b7 100644
--- a/docs/test-functions/otl-circuit.md
+++ b/docs/test-functions/otl-circuit.md
@@ -71,7 +71,9 @@ contains six input variables given as independent uniform random variables
 with specified ranges shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ````{note}
@@ -82,10 +84,10 @@ these input variables, being inert, do not affect the output of the function.
 
 To create an instance of the OTL circuit test function with the probabilistic
 input specified in {cite}`Moon2010`, pass the corresponding keyword
-(`"Moon2010"`) to the parameter (`prob_input_selection`):
+(`"Moon2010"`) to the parameter `input_id`:
 
 ```python
-my_testfun = uqtf.OTLCircuit(prob_input_selection="Moon2010")
+my_testfun = uqtf.OTLCircuit(input_id="Moon2010")
 ```
 ````
 
diff --git a/docs/test-functions/piston.md b/docs/test-functions/piston.md
index d1f16a5..8d209e9 100644
--- a/docs/test-functions/piston.md
+++ b/docs/test-functions/piston.md
@@ -73,7 +73,9 @@ contains seven input variables given as independent uniform random variables
 with specified ranges shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ````{note}
@@ -85,10 +87,10 @@ these input variables, being inert, do not affect the output of the function.
 To create an instance of the piston simulation test function
 with the probabilistic input specified in {cite}`Moon2010`,
 pass the corresponding keyword (`"Moon2010"`)
-to the parameter (`prob_input_selection`):
+to the parameter `input_id`):
 
 ```python
-my_testfun = uqtf.Piston(prob_input_selection="Moon2010")
+my_testfun = uqtf.Piston(input_id="Moon2010")
 ```
 ````
 
diff --git a/docs/test-functions/portfolio-3d.md b/docs/test-functions/portfolio-3d.md
new file mode 100644
index 0000000..1616419
--- /dev/null
+++ b/docs/test-functions/portfolio-3d.md
@@ -0,0 +1,247 @@
+---
+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:portfolio-3d)=
+# Simple Portfolio Model
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The simple portfolio model (or `Portfolio3D` for short ) is a three-dimensional
+scalar-valued test function.
+The function was introduced in {cite}`Saltelli2004` as an example for
+illustrating some elementary sensitivity measures.
+
+## Test function instance
+
+To create a default instance of the simple portfolio model:
+
+```{code-cell} ipython3
+my_testfun = uqtf.Portfolio3D()
+```
+
+Check if it has been correctly instantiated:
+
+```{code-cell} ipython3
+print(my_testfun)
+```
+
+## Description
+
+The simple portfolio model computes the estimated risk (_return_) in &#8364;
+of an investment portfolio consisting of three hedged portfolios via 
+a linear relationship[^location]:
+
+$$
+\mathcal{M}(\boldsymbol{x}) = C_s P_s + C_t P_t + C_j P_j,
+$$
+
+where $\boldsymbol{x} = \{ P_s, P_t, P_j \}$ is the vector of hedged portfolios
+values in &#8364; and $\boldsymbol{p} = \{ C_s, C_t, C_j \}$ is the vector of
+quantities per hedged portfolio defined further below.
+$\boldsymbol{x}$ is assumed to be random variables further defined below.
+
+## Probabilistic input
+
+Based on {cite}`Saltelli2004`, the probabilistic input model for the simple
+portfolio model consists of three independent normal random variables with the
+distribution parameters shown in the table below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
+```
+
+## Parameters
+
+The parameters of the simple portfolio model are the quantities of each 
+of the hedged portfolios. The available values taken from {cite}`Saltelli2004`
+are shown in the table below
+
+```{table} Parameters of the simple portfolio model
+:name: portfolio-3d-parameters
+
+| No. |                 Value                  |         Keyword            |             Source              |
+|:---:|:--------------------------------------:|:--------------------------:|:-------------------------------:|
+|  1  | $C_s = 100$, $C_t = 500$, $C_j = 1000$ | `Saltelli2004-1` (default) | Table 1.1 {cite}`Saltelli2004`  |
+|  2  | $C_s = 300$, $C_t = 300$, $C_j = 300$  |      `Saltelli2004-2`      | Table 1.1 {cite}`Saltelli2004`  |
+|  3  | $C_s = 500$, $C_t = 400$, $C_j = 100$  |      `Saltelli2004-3`      | Table 1.1 {cite}`Saltelli2004`  |
+```
+
+The default value is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.parameters)
+```
+
+````{note}
+To create an instance of the simple portfolio model with a different set of
+parameter values from the selection above, type:
+
+```python
+my_testfun = uqtf.Portfolio3D(parameters_id="Saltelli2004-2")
+```
+````
+
+## Reference results
+
+This section provides several reference results of typical UQ analyses
+involving the test function.
+
+### Sample histogram
+
+Shown below are the histograms of the output based on $100'000$ random points
+for the simple portfolio model with three different sets of parameters.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+np.random.seed(42)
+xx_test = my_testfun.prob_input.get_sample(100000)
+yy_test_1 = my_testfun(xx_test)
+my_testfun_2 = uqtf.Portfolio3D(parameters_id="Saltelli2004-2")
+yy_test_2 = my_testfun_2(xx_test)
+my_testfun_3 = uqtf.Portfolio3D(parameters_id="Saltelli2004-3")
+yy_test_3 = my_testfun_3(xx_test)
+
+plt.hist(yy_test_3, bins="auto", color="#fc8d62", label="Saltelli2004-3")
+plt.hist(yy_test_1, bins="auto", color="#8da0cb", label="Saltelli2004-1")
+plt.hist(yy_test_2, bins="auto", color="#66c2a5", label="Saltelli2004-2")
+plt.grid()
+plt.ylabel("Counts [-]")
+plt.xlabel("$\mathcal{M}(\mathbf{X})$")
+plt.legend();
+
+plt.gcf().set_dpi(150);
+```
+
+### Moments
+
+Based on the model structure as well as the assumption on the uncertain
+input variables, the output of the simple portfolio model is normally
+distributed. The moments are available analytically.
+
+The mean reads:
+
+$$
+\bar{Y} = C_s \bar{p}_s + C_t \bar{p}_t + C_j \bar{p}_j,
+$$
+
+where $\bar{p}_s$,  $\bar{p}_t$, and $\bar{p}_j$ are the means of
+the three hedged portfolios, respectively.
+
+The standard deviation reads:
+
+$$
+\sigma_Y = \left( C^2_s \sigma^2_s + C^2_t \sigma^2_t + C^2_j \sigma^2_j \right)^{0.5},
+$$
+
+where $\sigma_s$,  $\sigma_t$, and $\sigma_j$ are the standard deviations of
+the three hedged portfolios, respectively.
+
+The analytical values of the mean and standard deviation of the model output
+for the three sets of parameters are shown in the table below.
+
+```{table} Analytical mean and standard deviation of the simple portfolio model
+:name: portfolio-3d-mean-std
+
+| No. |    Parameter     | $\bar{Y}$ |         $\sigma_Y$         |
+|:---:|:----------------:|:---------:|:--------------------------:|
+|  1  | `Saltelli2004-1` |   $0.0$   | $1.4696938457 \times 10^3$ |
+|  2  | `Saltelli2004-2` |   $0.0$   | $1.3747727085 \times 10^3$ |
+|  3  | `Saltelli2004-3` |   $0.0$   | $2.1563858653 \times 10^3$ |
+```
+
+### Sensitivity analysis
+
+In {cite}`Saltelli2004`, the simple portfolio model was used as an illustrating
+example of several model sensitivity measures. Due to the simple nature of
+the model (linear, monotonic), simple local or hybrid local-global sensitivity
+measures are deemed sufficient to characterize the model sensitivity.
+
+Local sensitivity measure based derivative is given by:
+
+$$
+S^d_x = \frac{\partial Y}{\partial P_x} = C_x; \;\; x = s, t, j.
+$$
+
+This measure is dimensioned, but it happens that the dimension cancels out
+because the input $P_x$ and the output $Y$ have the same unit.
+The table below provides the analytical values of the local sensitivity measure
+for the three set of parameters.
+
+```{table} Analytical sensitivity measure value $S^d_x$ of the simple portfolio model
+:name: portfolio-3d-local-measure
+
+|    Parameter     |  $P_s$  | $P_t$ |  $P_j$  |
+|:----------------:|:-------:|:-----:|:-------:|
+| `saltelli2004-1` |  $100$  | $500$ | $1'000$ |
+| `saltelli2004-2` |  $300$  | $300$ |  $300$  |
+| `saltelli2004-3` |  $500$  | $400$ |  $100$  |
+```
+
+To include the uncertainty of the input variables in the sensitivity measure,
+the local measure defined above is normalized by the standard deviation of both
+the input and the output. The hybrid local-global measure now reads:
+
+$$
+S^{\sigma}_x = \frac{\sigma_x}{\sigma_Y} \frac{\partial Y}{\partial P_x} = \frac{\sigma_x}{\sigma_Y} C_x; \;\; x = s, t, j.
+$$
+
+The table below provides the analytical values of the sensitivity measure
+for the three different sets of parameters.
+
+```{table} Analytical sensitivity measure value $S^{\sigma}_x$ of the simple portfolio model
+:name: portfolio-3d-hybrid-measure
+
+|    Parameter     |  $P_s$  |  $P_t$  |  $P_j$  |
+|:----------------:|:-------:|:-------:|:-------:|
+| `saltelli2004-1` | $0.272$ | $0.680$ | $0.680$ |
+| `saltelli2004-2` | $0.873$ | $0.436$ | $0.218$ |
+| `saltelli2004-3` | $0.928$ | $0.371$ | $0.046$ |
+```
+
+The measure $S^{\sigma}_x$ has the following relation:
+
+$$
+1.0 = (S^{\sigma}_s)^2 + (S^{\sigma}_t)^2 + (S^{\sigma}_j)^2.
+$$
+
+The relation implies that the main-effect Sobol' sensitivity indices are
+the squared of the hybrid local-global measures.
+
+```{table} Main-effect Sobol' sensitivity indices of the simple portfolio model
+:name: portfolio-3d-sobol-indices
+
+|    Parameter     |   $P_s$   |  $P_t$   |   $P_j$   |
+|:----------------:|:---------:|:--------:|:---------:|
+| `saltelli2004-1` |  $0.074$  | $0.4624$ | $0.4624$  |
+| `saltelli2004-2` |  $0.762$  | $0.1900$ | $0.0475$  |
+| `saltelli2004-3` |  $0.861$  | $0.1376$ | $0.0021$  |
+```
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^location]: see Section 1.1, pp. 1-2 in {cite}`Saltelli2004`.
\ No newline at end of file
diff --git a/docs/test-functions/rs-circular-bar.md b/docs/test-functions/rs-circular-bar.md
index 516b59d..a3b2251 100644
--- a/docs/test-functions/rs-circular-bar.md
+++ b/docs/test-functions/rs-circular-bar.md
@@ -166,13 +166,20 @@ the test function consists of two independent standard normal random variables
 (see the table below).
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Parameter
 
-The parameter of the function is $d$ which from {cite}`Verma2015` is set to
-$25\;\mathrm{[mm]}$.
+The parameter of the function is $d$ as shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.parameters)
+```
 
 ## Reference results
 
diff --git a/docs/test-functions/rs-quadratic.md b/docs/test-functions/rs-quadratic.md
index 0647f22..27610f8 100644
--- a/docs/test-functions/rs-quadratic.md
+++ b/docs/test-functions/rs-quadratic.md
@@ -162,7 +162,9 @@ the test function consists of two independent standard normal random variables
 (see the table below).
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/saltelli-linear.md b/docs/test-functions/saltelli-linear.md
new file mode 100644
index 0000000..662c6f4
--- /dev/null
+++ b/docs/test-functions/saltelli-linear.md
@@ -0,0 +1,455 @@
+---
+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:saltelli-linear)=
+# Saltelli Linear Function
+
+The Saltelli Linear function is an $M$-dimensional scalar-valued function.
+It was introduced in {cite}`Saltelli2008` for illustrating sensitivity
+analysis methods.
+It is later used in {cite}`Sun2022` for benchmarking various sensitivity
+analysis methods.
+
+Due to its simple form, the moments and Sobol' sensitivity indices may be
+computed analytically.
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The plots for one-dimensional and two-dimensional Sobol'-G function can be seen
+below.
+
+```{code-cell} ipython3
+:tags: [remove-input]
+
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+
+# --- Create 1D data from SaltelliLinear
+my_fun_1d = uqtf.SaltelliLinear(input_dimension=1)
+lb = my_fun_1d.prob_input.marginals[0].lower
+ub = my_fun_1d.prob_input.marginals[0].upper
+xx_1d = np.linspace(lb, ub, 1000)[:, np.newaxis]
+yy_1d = my_fun_1d(xx_1d)
+
+# --- Create 2D data from SaltelliLinear
+my_fun_2d = uqtf.SaltelliLinear(input_dimension=2)
+lb_1 = my_fun_2d.prob_input.marginals[0].lower
+ub_1 = my_fun_2d.prob_input.marginals[0].upper
+lb_2 = my_fun_2d.prob_input.marginals[1].lower
+ub_2 = my_fun_2d.prob_input.marginals[1].upper
+xx_1 = np.linspace(lb_1, ub_1, 1000)[:, np.newaxis]
+xx_2 = np.linspace(lb_2, ub_2, 1000)[:, np.newaxis]
+mesh_2d = np.meshgrid(xx_1, xx_2)
+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 Saltelli Linear")
+
+# 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 Saltelli Linear", 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 Saltelli Linear", 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')
+
+fig.tight_layout(pad=3.0)
+plt.gcf().set_dpi(150);
+```
+
+## Test function instance
+
+To create a default instance of the test function, type:
+
+```{code-cell} ipython3
+my_testfun = uqtf.SaltelliLinear()
+```
+
+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 Saltelli Linear function
+in six dimensions, type:
+
+```{code-cell} ipython3
+my_testfun = uqtf.SaltelliLinear(input_dimension=6)
+```
+
+In the subsequent section, the function will be illustrated
+using six dimensions.
+
+## Description
+
+The Saltelli Linear function is defined as follows:
+
+$$
+\mathcal{M}(\boldsymbol{x}) = \sum_{i = 1}^M x_i
+$$
+where $\boldsymbol{x} = \{ x_1, \ldots, x_M \}$ is the $M$-dimensional vector
+of input variables further defined below.
+
+## Probabilistic input
+
+Based on {cite}`Saltelli2008` the probabilistic input model for the function
+consists of $M$ independent uniform random variables with the following ranges:
+
+$$
+X_i \sim \mathcal{U}[x_{o, i} - \sigma_{o, i}, x_{o, i} + \sigma_{o, i}], \; i = 1, \ldots, M,
+$$
+where $x_{o, i} = 3^{i - 1}$ and $\sigma_{o, i} = 0.5 * x_{o, i}$.
+Notice that the higher the variable index, the larger its uncertainty both
+in absolute sense (i.e., the standard deviation is larger)
+and in relative sense (i.e., the coefficient of variation is larger).
+
+For the six-variable model, the ranges are 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 $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);
+```
+
+### Moments estimation
+
+The mean and variance of the Sobol'-G function can be computed analytically.
+
+The mean is given as follows:
+
+$$
+\mathbb{E}[Y] = \sum_{i = 1}^M \mathbb{E}[X_i] = \sum_{i = 1}^M x_{o, i},
+$$
+where $x_{o, i} = 3^{i - 1}, i = 1, \ldots, M$.
+
+The variance is given as follows:
+
+$$
+\mathbb{V}[Y] = \sum_{i = 1}^M \mathbb{V}[X_i] = \frac{1}{12} \sum_{i = 1}^M x_{o, i}^2,
+$$
+
+The means and variances for the linear function up to dimension $10$ are shown
+in the table below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+from tabulate import tabulate
+
+input_dims = 10
+
+# --- Compile data row-wise
+
+outputs = []
+for input_dim in range(1, input_dims + 1):
+    xx_o = 3**(np.arange(1, input_dim + 1) - 1)
+    outputs.append(
+        [input_dim, np.sum(xx_o), np.sum(xx_o**2) / 12]
+    )
+
+header_names = [
+    "Input dimensions",
+    "Mean",
+    "Variance",
+]
+
+tabulate(
+    outputs,
+    numalign="center",
+    stralign="center",
+    tablefmt="html",
+    floatfmt=("d", ".4e", ".4e"),
+    headers=header_names
+)
+```
+
+Shown below is the convergence of a direct Monte-Carlo estimation of
+the output mean and variance with increasing sample sizes compared with the
+analytical values.
+The error bars correspond to twice the standard deviation
+of the estimates obtained from $50$ replications.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+# --- Compute the mean and variance estimate
+my_testfun.prob_input.reset_rng(42)
+sample_sizes = np.array([1e1, 1e2, 1e3, 1e4, 1e5, 1e6], dtype=int)
+mean_estimates = np.empty((len(sample_sizes), 50))
+var_estimates = np.empty((len(sample_sizes), 50))
+
+for i, sample_size in enumerate(sample_sizes):
+    for j in range(50):
+        xx_test = my_testfun.prob_input.get_sample(sample_size)
+        yy_test = my_testfun(xx_test)
+        mean_estimates[i, j] = np.mean(yy_test)
+        var_estimates[i, j] = np.var(yy_test)
+
+mean_estimates_errors = np.std(mean_estimates, axis=1)
+var_estimates_errors = np.std(var_estimates, axis=1)
+
+# --- Compute analytical mean and variance
+xx_o = 3**(np.arange(1, my_testfun.input_dimension + 1) - 1)
+mean_analytical = np.sum(xx_o)
+var_analytical = np.sum(xx_o**2) / 12.0
+
+# --- Plot the mean and variance estimates
+fig, ax_1 = plt.subplots(figsize=(6,4))
+
+ext_sample_sizes = np.insert(sample_sizes, 0, 1)
+ext_sample_sizes = np.insert(ext_sample_sizes, -1, 5e6)
+
+# --- Mean plot
+ax_1.errorbar(
+    sample_sizes,
+    mean_estimates[:,0],
+    yerr=2.0*mean_estimates_errors,
+    marker="o",
+    color="#66c2a5",
+    label="Mean"
+)
+# Plot the analytical mean
+ax_1.plot(
+    ext_sample_sizes,
+    np.repeat(mean_analytical, len(ext_sample_sizes)),
+    linestyle="--",
+    color="#66c2a5",
+    label="Analytical mean",
+)
+ax_1.set_xlim([5, 5e6])
+ax_1.set_xlabel("Sample size")
+ax_1.set_ylabel("Output mean estimate")
+ax_1.set_xscale("log");
+ax_2 = ax_1.twinx()
+
+# --- Variance plot
+ax_2.errorbar(
+    sample_sizes+1,
+    var_estimates[:,0],
+    yerr=2.0*var_estimates_errors,
+    marker="o",
+    color="#fc8d62",
+    label="Variance",
+)
+# Plot the analytical variance
+ax_2.plot(
+    ext_sample_sizes,
+    np.repeat(var_analytical, len(ext_sample_sizes)),
+    linestyle="--",
+    color="#fc8d62",
+    label="Analytical variance",
+)
+ax_2.set_ylabel("Output variance estimate")
+
+# Add the two plots together to have a common legend
+ln_1, labels_1 = ax_1.get_legend_handles_labels()
+ln_2, labels_2 = ax_2.get_legend_handles_labels()
+ax_2.legend(ln_1 + ln_2, labels_1 + labels_2, loc=0)
+
+plt.grid()
+fig.set_dpi(150)
+```
+
+The tabulated results for each sample size is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+from tabulate import tabulate
+
+# --- Compile data row-wise
+outputs = [
+    [
+        np.nan,
+        mean_analytical,
+        0.0,
+        var_analytical,
+        0.0,
+        "Analytical",
+    ]
+]
+
+for (
+    sample_size,
+    mean_estimate,
+    mean_estimate_error,
+    var_estimate,
+    var_estimate_error,
+) in zip(
+    sample_sizes,
+    mean_estimates[:,0],
+    2.0*mean_estimates_errors,
+    var_estimates[:,0],
+    2.0*var_estimates_errors,
+):
+    outputs += [
+        [
+            sample_size,
+            mean_estimate,
+            mean_estimate_error,
+            var_estimate,
+            var_estimate_error,
+            "Monte-Carlo",
+        ],
+    ]
+
+header_names = [
+    "Sample size",
+    "Mean",
+    "Mean error",
+    "Variance",
+    "Variance error",
+    "Remark",
+]
+
+tabulate(
+    outputs,
+    numalign="center",
+    stralign="center",
+    tablefmt="html",
+    floatfmt=(".1e", ".4e", ".4e", ".4e", ".4e", "s"),
+    headers=header_names
+)
+```
+
+### Sensitivity indices
+
+The main-effect Sobol' sensitivity indices of the linear function are given
+by the following formula:
+
+$$
+S_i \equiv \frac{V_i}{\mathbb{V}[Y]} = \frac{\mathbb{V}[X_i]}{\mathbb{V}[Y]} = \frac{x_{o, i}^2}{\sum_{i = 1}^M x_{o, i}^2},\; i = 1, \ldots, M. 
+$$
+
+Since there is no interaction effect present in the model, the total-effect
+indices are equal to the main-effect indices.
+
+Some example values of the main-effect indices for the linear function 
+up to dimension $6$ is shown in the table below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+from tabulate import tabulate
+
+input_dims = 6
+
+# --- Compile data row-wise
+
+outputs = []
+for input_dim in range(input_dims):
+    output = [f"X{input_dim + 1}"]
+    for _ in range(input_dim):
+        output.append(0.0)
+    for i in range(input_dim, input_dims):
+        xx_o = 3**(np.arange(1, i + 2) - 1)
+        
+        output.append(
+            xx_o[input_dim]**2 / np.sum(xx_o**2)
+        )
+
+    outputs.append(output)
+
+header_names = [
+    "Si",
+    "m = 1",
+    "m = 2",
+    "m = 3",
+    "m = 4",
+    "m = 5",
+    "m = 6",
+]
+
+tabulate(
+    outputs,
+    numalign="center",
+    stralign="center",
+    tablefmt="html",
+    floatfmt=("s", ".4e", ".4e", ".4e", ".4e", ".4e", ".4e"),
+    headers=header_names
+)
+```
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^integral]: The expected value is the same as the integral over the domain
+because the input is uniform in a unit hypercube.
+
+[^default_dimension]: This default dimension applies to all variable dimension
+test functions. It will be used if the `input_dimension` argument is not given.
diff --git a/docs/test-functions/sobol-g-star.md b/docs/test-functions/sobol-g-star.md
new file mode 100644
index 0000000..b36116b
--- /dev/null
+++ b/docs/test-functions/sobol-g-star.md
@@ -0,0 +1,571 @@
+---
+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:sobol-g-star)=
+# Sobol'-G* Function
+
+The Sobol'-G* function (also known as the modified Sobol'-G function)
+is an $M$-dimensional scalar-valued function.
+It was used in {cite}`Saltelli2010, Sun2022` for testing sensitivity analysis
+methods.
+
+This function introduces shift and curvature parameters to the original 
+{ref}`Sobol'-G <test-functions:sobol-g>` test function
+{cite}`Saltelli1995`[^modified].
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The plots for one-dimensional and two-dimensional Sobol'-G* function
+are shown below.
+
+```{code-cell} ipython3
+:tags: [remove-input]
+
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+
+# --- Create 1D data from Sobol'-G*
+my_sobolgstar_1d = uqtf.SobolGStar(input_dimension=1)
+rng = np.random.default_rng(42)
+delta = rng.random(1)
+my_sobolgstar_1d.parameters["delta"] = delta
+xx_1d = np.linspace(0, 1, 1000)[:, np.newaxis]
+yy_1d = my_sobolgstar_1d(xx_1d)
+
+# --- Create 2D data from Sobol'-G*
+my_sobolgstar_2d = uqtf.SobolGStar(input_dimension=2)
+delta = rng.random(2)
+my_sobolgstar_2d.parameters["delta"] = delta
+mesh_2d = np.meshgrid(xx_1d, xx_1d)
+xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
+yy_2d = my_sobolgstar_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 Sobol'-G*")
+
+# 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 Sobol'-G*", 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 Sobol'-G*", 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 Sobol'-G* test function, type:
+
+```{code-cell} ipython3
+my_testfun = uqtf.SobolGStar()
+```
+
+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 Sobol'-G* function in ten dimensions,
+type:
+
+```{code-cell} ipython3
+my_testfun = uqtf.SobolGStar(input_dimension=10)
+```
+
+In the subsequent section, the function will be illustrated
+using ten dimensions as it originally appeared in {cite}`Saltelli2010`.
+
+## Description
+
+The Sobol'-G* function is defined as follows[^location]:
+
+$$
+\mathcal{M}(\boldsymbol{x}; \boldsymbol{a}, \boldsymbol{\delta}, \boldsymbol{\alpha}) = \prod_{m = 1}^M g^*_m(x_i; a_i, \delta_i, \alpha_i)
+$$
+
+where
+
+$$
+g^*_m(x_i; a_i, \delta_i, \alpha_i) = \frac{(1 + \alpha_i) \lvert 2 (x_i + \delta_i - \lfloor x_i + \delta_i \rfloor) - 1 \rvert^{\alpha_i} + a_i}{1 + a_i}
+$$
+
+where $\boldsymbol{x} = \{ x_1, \ldots, x_M \}$ is the $M$-dimensional vector
+of input variables further defined below,
+and $\boldsymbol{a}$, $\boldsymbol{\delta}$, and $\boldsymbol{\alpha}$ are the
+parameters of the function further defined below.
+
+## Probabilistic input
+
+The probabilistic input model for the Sobol'-G* function consists of $M$
+independent uniform random variables with the ranges shown in the table below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
+```
+
+## Parameters
+
+The parameters of the Sobol-G* function---namely the coefficients
+$\boldsymbol{a}$, the shift parameter $\boldsymbol{\delta}$, and the 
+curvature parameter $\boldsymbol{\alpha}$---determine the overall behavior
+of the function.
+It is worth noting that in the computation of moments and Sobol' sensitivity
+indices computation, the shift parameter $\boldsymbol{\delta}$ has no effect,
+as it cancels out. This property justifies why the value of
+$\boldsymbol{\delta}$ is typically generated randomly.
+
+The available parameters for the Sobol'-G* function are shown in the table
+below.
+
+```{table} Parameters of the Sobol'-G* function
+:name: sobol-g-star-parameters
+| No. |                                      $\boldsymbol{a}$                                       |       $\boldsymbol{\delta}$        | $\boldsymbol{\alpha}$ |             Keyword             |                   Source                    |          Remark          |
+|:---:|:-------------------------------------------------------------------------------------------:|:----------------------------------:|:---------------------:|:-------------------------------:|:-------------------------------------------:|:------------------------:|
+| 1.  |                           $a_1 = a_2 = 0$ <br> $a_3 = \ldots = 9$                           | $\delta_i \sim \mathcal{U}[0, 1]$  |   $\alpha_i = 1.0$    | `Saltelli2010-1` <br> (default) | {cite}`Saltelli2010` (Table 5, test case 1) | Low effective dimension  |
+| 2.  | $a_i = 0.1 (i - 1), 1 \leq i \leq 5$ <br> $a_6 = 0.8$ <br> $a_i = (i - 6), 7 \leq i \leq M$ | $\delta_i \sim \mathcal{U}[0, 1]$  |   $\alpha_i = 1.0$    |        `Saltelli2010-2`         | {cite}`Saltelli2010` (Table 5, test case 2) | High effective dimension |
+| 3.  |                           $a_1 = a_2 = 0$ <br> $a_3 = \ldots = 9$                           | $\delta_i \sim \mathcal{U}[0, 1]$  |   $\alpha_i = 0.5$    |        `Saltelli2010-3`         | {cite}`Saltelli2010` (Table 5, test case 3) |   Convex version of 1    |
+| 4.  | $a_i = 0.1 (i - 1), 1 \leq i \leq 5$ <br> $a_6 = 0.8$ <br> $a_i = (i - 6), 7 \leq i \leq M$ | $\delta_i \sim \mathcal{U}[0, 1]$  |   $\alpha_i = 0.5$    |        `Saltelli2010-4`         | {cite}`Saltelli2010` (Table 5, test case 4) |   Convex version of 2    |
+| 5.  |                           $a_1 = a_2 = 0$ <br> $a_3 = \ldots = 9$                           | $\delta_i \sim \mathcal{U}[0, 1]$  |   $\alpha_i = 2.0$    |        `Saltelli2010-5`         | {cite}`Saltelli2010` (Table 5, test case 5) |   Concave version of 1   |
+| 6.  | $a_i = 0.1 (i - 1), 1 \leq i \leq 5$ <br> $a_6 = 0.8$ <br> $a_i = (i - 6), 7 \leq i \leq M$ | $\delta_i \sim \mathcal{U}[0, 1]$  |   $\alpha_i = 2.0$    |        `Saltelli2010-6`         | {cite}`Saltelli2010` (Table 5, test case 6) |   Concave version of 2   |
+```
+
+The default parameter is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.parameters)
+```
+
+````{note}
+To create an instance of the Sobol'-G* function with a different set
+of built-in parameters, pass the corresponding keyword to the parameter
+`parameters_id`.
+For example, to use the parameters of test case 2 from {cite}`Saltelli2010`,
+type:
+
+```python
+my_testfun = uqtf.SobolGStar(parameters_id="Saltelli2010-2")
+```
+````
+
+```{note}
+The parameter $\boldsymbol{\delta}$ is randomly generated
+from a uniform distribution in $[0, 1]^M$ following {cite}`Saltelli2010` when
+an instance of the function is created;
+creating a new instance generates a new set of $\boldsymbol{\delta}$. 
+This parameter cancels out when relevant uncertainty quantification quantities
+of interest are computed (e.g., variance, sensitivity indices).
+
+To have control over the value of $\boldsymbol{\delta}$, you can set the value
+after an instance is created by assigning a set of new values to
+`my_fun.parameters["delta"]`.
+```
+
+## 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]
+
+np.random.seed(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);
+```
+
+### Definite integration
+
+The integral value of the function over the whole domain $[0, 1]^M$
+is analytical:
+
+$$
+\int_{[0, 1]^M} \mathcal{M}(\boldsymbol{x}) \; d\boldsymbol{x} = 1.0.
+$$
+
+### Moments estimation
+
+The mean and variance of the Sobol'-G function can be computed analytically. 
+
+The mean[^integral] is given as follows:
+
+$$
+\mathbb{E}[Y] = 1.0,
+$$
+
+while the variance is given as follows:
+
+$$
+\mathbb{V}[Y] = \prod_{i = 1}^M (1 + V_i) - 1,
+$$
+where
+
+$$
+V_i \equiv \mathbb{V}_{X_i} (\mathbb{E}_{\sim \boldsymbol{X}_i} (Y | X_i)) = \frac{\alpha_i^2}{(1 + 2 \alpha_i) (1 + a_i)^2}.
+$$
+
+Notice that the value of the variance depend on the choice of the parameter values.
+
+Shown below is the convergence of a direct Monte-Carlo estimation of
+the output mean and variance with increasing sample sizes compared with the
+analytical values.
+The error bars correspond to twice the standard deviation
+of the estimates obtained from $50$ replications.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+# --- Compute the mean and variance estimate
+np.random.seed(42)
+sample_sizes = np.array([1e1, 1e2, 1e3, 1e4, 1e5], dtype=int)
+mean_estimates = np.empty((len(sample_sizes), 50))
+var_estimates = np.empty((len(sample_sizes), 50))
+
+for i, sample_size in enumerate(sample_sizes):
+    for j in range(50):
+        xx_test = my_testfun.prob_input.get_sample(sample_size)
+        yy_test = my_testfun(xx_test)
+        mean_estimates[i, j] = np.mean(yy_test)
+        var_estimates[i, j] = np.var(yy_test)
+
+mean_estimates_errors = np.std(mean_estimates, axis=1)
+var_estimates_errors = np.std(var_estimates, axis=1)
+
+# --- Compute analytical mean and variance
+mean_analytical = 1.0
+aa = my_testfun.parameters["aa"]
+alpha = my_testfun.parameters["alpha"]
+vv = alpha**2 / (1 + 2 * alpha) / (1 + aa)**2
+var_analytical = np.prod(1 + vv) - 1
+
+# --- Plot the mean and variance estimates
+fig, ax_1 = plt.subplots(figsize=(6,4))
+
+ext_sample_sizes = np.insert(sample_sizes, 0, 1)
+ext_sample_sizes = np.insert(ext_sample_sizes, -1, 5e6)
+
+# --- Mean plot
+ax_1.errorbar(
+    sample_sizes,
+    mean_estimates[:,0],
+    yerr=2.0*mean_estimates_errors,
+    marker="o",
+    color="#66c2a5",
+    label="Mean"
+)
+# Plot the analytical mean
+ax_1.plot(
+    ext_sample_sizes,
+    np.repeat(mean_analytical, len(ext_sample_sizes)),
+    linestyle="--",
+    color="#66c2a5",
+    label="Analytical mean",
+)
+ax_1.set_xlim([5, 5e5])
+ax_1.set_xlabel("Sample size")
+ax_1.set_ylabel("Output mean estimate")
+ax_1.set_xscale("log");
+ax_2 = ax_1.twinx()
+
+# --- Variance plot
+ax_2.errorbar(
+    sample_sizes+1,
+    var_estimates[:,0],
+    yerr=2.0*var_estimates_errors,
+    marker="o",
+    color="#fc8d62",
+    label="Variance",
+)
+# Plot the analytical variance
+ax_2.plot(
+    ext_sample_sizes,
+    np.repeat(var_analytical, len(ext_sample_sizes)),
+    linestyle="--",
+    color="#fc8d62",
+    label="Analytical variance",
+)
+ax_2.set_ylabel("Output variance estimate")
+
+# Add the two plots together to have a common legend
+ln_1, labels_1 = ax_1.get_legend_handles_labels()
+ln_2, labels_2 = ax_2.get_legend_handles_labels()
+ax_2.legend(ln_1 + ln_2, labels_1 + labels_2, loc=0)
+
+plt.grid()
+fig.set_dpi(150)
+```
+
+The tabulated results for each sample size is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+from tabulate import tabulate
+
+# --- Compile data row-wise
+outputs = [
+    [
+        np.nan,
+        mean_analytical,
+        0.0,
+        var_analytical,
+        0.0,
+        "Analytical",
+    ]
+]
+
+for (
+    sample_size,
+    mean_estimate,
+    mean_estimate_error,
+    var_estimate,
+    var_estimate_error,
+) in zip(
+    sample_sizes,
+    mean_estimates[:,0],
+    2.0*mean_estimates_errors,
+    var_estimates[:,0],
+    2.0*var_estimates_errors,
+):
+    outputs += [
+        [
+            sample_size,
+            mean_estimate,
+            mean_estimate_error,
+            var_estimate,
+            var_estimate_error,
+            "Monte-Carlo",
+        ],
+    ]
+
+header_names = [
+    "Sample size",
+    "Mean",
+    "Mean error",
+    "Variance",
+    "Variance error",
+    "Remark",
+]
+
+tabulate(
+    outputs,
+    numalign="center",
+    stralign="center",
+    tablefmt="html",
+    floatfmt=(".1e", ".4e", ".4e", ".4e", ".4e", "s"),
+    headers=header_names
+)
+```
+
+### Sensitivity indices
+
+The main-effect Sobol' sensitivity indices of the Sobol'-G* function are given
+by the following formula:
+
+$$
+S_i \equiv \frac{V_i}{\mathbb{V}[Y]}, \; i = 1, \ldots, M,
+$$
+
+where
+
+$$
+\mathbb{V}[Y] = \prod_{i = 1}^M (1 + V_i) - 1,
+$$
+
+and
+
+$$
+V_i = \frac{\alpha_i^2}{(1 + 2 \alpha_i) (1 + a_i)^2}.
+$$
+
+The total-effect Sobol' sensitivity indices, on the other hand, are given
+by the following formula:
+
+$$
+ST_i \equiv \frac{VT_i}{\mathbb{V}[Y]}, \; i = 1, \ldots, M,
+$$
+
+where
+
+$$
+VT_i = V_i \prod_{j = 1, j \neq i}^M (1 + V_j).
+$$
+
+Some example values of the Sobol' main- and total-effect sensitivity indices
+for $10$-dimensional Sobol'-G* test function are shown in the table below.
+
+::::{tab-set}
+
+:::{tab-item} Saltelli2010-1
+|  Input   |          $S_i$           |          $ST_i$           |
+|:--------:|:------------------------:|:-------------------------:|
+|  $X_1$   | $4.03677 \times 10^{-1}$ | $5.52758 \times 10^{-1}$  | 
+|  $X_2$   | $4.03677 \times 10^{-1}$ | $5.52758 \times 10^{-1}$  | 
+|  $X_3$   | $4.03677 \times 10^{-3}$ | $7.34562 \times 10^{-3}$  | 
+|  $X_4$   | $4.03677 \times 10^{-3}$ | $7.34562 \times 10^{-3}$  | 
+|  $X_5$   | $4.03677 \times 10^{-3}$ | $7.34562 \times 10^{-3}$  | 
+|  $X_6$   | $4.03677 \times 10^{-3}$ | $7.34562 \times 10^{-3}$  | 
+|  $X_7$   | $4.03677 \times 10^{-3}$ | $7.34562 \times 10^{-3}$  | 
+|  $X_8$   | $4.03677 \times 10^{-3}$ | $7.34562 \times 10^{-3}$  | 
+|  $X_9$   | $4.03677 \times 10^{-3}$ | $7.34562 \times 10^{-3}$  | 
+| $X_{10}$ | $4.03677 \times 10^{-3}$ | $7.34562 \times 10^{-3}$  |
+:::
+
+
+:::{tab-item} Saltelli2010-2
+|  Input   |          $S_i$           |          $ST_i$           |
+|:--------:|:------------------------:|:-------------------------:|
+|  $X_1$   | $1.20759 \times 10^{-1}$ | $3.40569 \times 10^{-1}$  | 
+|  $X_2$   | $9.98008 \times 10^{-2}$ | $2.94228 \times 10^{-1}$  | 
+|  $X_3$   | $8.38604 \times 10^{-2}$ | $2.56067 \times 10^{-1}$  | 
+|  $X_4$   | $7.14550 \times 10^{-2}$ | $2.24428 \times 10^{-1}$  | 
+|  $X_5$   | $6.16117 \times 10^{-2}$ | $1.98005 \times 10^{-1}$  | 
+|  $X_6$   | $3.72713 \times 10^{-2}$ | $1.27078 \times 10^{-1}$  | 
+|  $X_7$   | $3.01897 \times 10^{-2}$ | $1.04791 \times 10^{-1}$  | 
+|  $X_8$   | $1.34177 \times 10^{-2}$ | $4.86527 \times 10^{-2}$  | 
+|  $X_9$   | $7.54743 \times 10^{-3}$ | $2.78016 \times 10^{-2}$  | 
+| $X_{10}$ | $4.83036 \times 10^{-3}$ | $1.79247 \times 10^{-2}$  |
+:::
+
+:::{tab-item} Saltelli2010-3
+|  Input   |          $S_i$           |          $ST_i$           |
+|:--------:|:------------------------:|:-------------------------:|
+|  $X_1$   | $4.49096 \times 10^{-1}$ | $5.10308 \times 10^{-1}$  | 
+|  $X_2$   | $4.49096 \times 10^{-1}$ | $5.10308 \times 10^{-1}$  | 
+|  $X_3$   | $4.49096 \times 10^{-3}$ | $5.73380 \times 10^{-3}$  | 
+|  $X_4$   | $4.49096 \times 10^{-3}$ | $5.73380 \times 10^{-3}$  | 
+|  $X_5$   | $4.49096 \times 10^{-3}$ | $5.73380 \times 10^{-3}$  | 
+|  $X_6$   | $4.49096 \times 10^{-3}$ | $5.73380 \times 10^{-3}$  | 
+|  $X_7$   | $4.49096 \times 10^{-3}$ | $5.73380 \times 10^{-3}$  | 
+|  $X_8$   | $4.49096 \times 10^{-3}$ | $5.73380 \times 10^{-3}$  | 
+|  $X_9$   | $4.49096 \times 10^{-3}$ | $5.73380 \times 10^{-3}$  | 
+| $X_{10}$ | $4.49096 \times 10^{-3}$ | $5.73380 \times 10^{-3}$  |
+:::
+
+:::{tab-item} Saltelli2010-4
+|  Input   |           $S_i$           |          $ST_i$          |
+|:--------:|:-------------------------:|:------------------------:|
+|  $X_1$   | $1.79845 \times 10^{-1}$  | $2.70974 \times 10^{-1}$ | 
+|  $X_2$   | $1.48633 \times 10^{-1}$  | $2.28349 \times 10^{-1}$ | 
+|  $X_3$   | $1.24893 \times 10^{-1}$  | $1.94789 \times 10^{-1}$ | 
+|  $X_4$   | $1.06417 \times 10^{-1}$  | $1.67959 \times 10^{-1}$ | 
+|  $X_5$   | $9.17578 \times 10^{-2}$  | $1.46209 \times 10^{-1}$ | 
+|  $X_6$   | $5.55078 \times 10^{-2}$  | $9.05930 \times 10^{-2}$ | 
+|  $X_7$   | $4.49613 \times 10^{-2}$  | $7.39019 \times 10^{-2}$ | 
+|  $X_8$   | $1.99828 \times 10^{-2}$  | $3.34077 \times 10^{-2}$ | 
+|  $X_9$   | $1.12403 \times 10^{-2}$  | $1.89051 \times 10^{-2}$ | 
+| $X_{10}$ | $7.19381 \times 10^{-3}$  | $1.21331 \times 10^{-2}$ |
+:::
+
+:::{tab-item} Saltelli2010-5
+|  Input   |          $S_i$           |          $ST_i$          |
+|:--------:|:------------------------:|:------------------------:|
+|  $X_1$   | $3.26097 \times 10^{-1}$ | $6.25609 \times 10^{-1}$ | 
+|  $X_2$   | $3.26097 \times 10^{-1}$ | $6.25609 \times 10^{-1}$ | 
+|  $X_3$   | $3.26097 \times 10^{-3}$ | $1.11716 \times 10^{-2}$ | 
+|  $X_4$   | $3.26097 \times 10^{-3}$ | $1.11716 \times 10^{-2}$ | 
+|  $X_5$   | $3.26097 \times 10^{-3}$ | $1.11716 \times 10^{-2}$ | 
+|  $X_6$   | $3.26097 \times 10^{-3}$ | $1.11716 \times 10^{-2}$ | 
+|  $X_7$   | $3.26097 \times 10^{-3}$ | $1.11716 \times 10^{-2}$ | 
+|  $X_8$   | $3.26097 \times 10^{-3}$ | $1.11716 \times 10^{-2}$ | 
+|  $X_9$   | $3.26097 \times 10^{-3}$ | $1.11716 \times 10^{-2}$ | 
+| $X_{10}$ | $3.26097 \times 10^{-3}$ | $1.11716 \times 10^{-2}$ |
+:::
+
+:::{tab-item} Saltelli2010-6
+|  Input   |          $S_i$           |          $ST_i$          |
+|:--------:|:------------------------:|:------------------------:|
+|  $X_1$   | $4.98832 \times 10^{-2}$ | $4.72157 \times 10^{-1}$ | 
+|  $X_2$   | $4.12258 \times 10^{-2}$ | $4.22827 \times 10^{-1}$ | 
+|  $X_3$   | $3.46411 \times 10^{-2}$ | $3.79412 \times 10^{-1}$ | 
+|  $X_4$   | $2.95167 \times 10^{-2}$ | $3.41319 \times 10^{-1}$ | 
+|  $X_5$   | $2.54506 \times 10^{-2}$ | $3.07929 \times 10^{-1}$ | 
+|  $X_6$   | $1.53961 \times 10^{-2}$ | $2.10367 \times 10^{-1}$ | 
+|  $X_7$   | $1.24708 \times 10^{-2}$ | $1.77059 \times 10^{-1}$ | 
+|  $X_8$   | $5.54258 \times 10^{-3}$ | $8.67228 \times 10^{-2}$ | 
+|  $X_9$   | $3.11770 \times 10^{-3}$ | $5.05883 \times 10^{-2}$ | 
+| $X_{10}$ | $1.99533 \times 10^{-3}$ | $3.29412 \times 10^{-2}$ |
+:::
+
+::::
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^modified]: In particular, to avoid unfair advantage to a design of experiment
+where the mid-point of the domain is discontinuous in the original formula.
+
+[^location]: see Eqs. (27) and (28), p. 265 in {cite}`Saltelli2010`.
+
+[^integral]: The expected value is the same as the integral over the domain
+because the input is uniform in a unit hypercube.
+
+[^default_dimension]: This default dimension applies to all variable dimension
+test functions. It will be used if the `input_dimension` argument is not given.
diff --git a/docs/test-functions/sobol-g.md b/docs/test-functions/sobol-g.md
index ea384db..26eb500 100644
--- a/docs/test-functions/sobol-g.md
+++ b/docs/test-functions/sobol-g.md
@@ -16,22 +16,16 @@ kernelspec:
 # Sobol'-G Function
 
 The Sobol'-G function is an $M$-dimensional scalar-valued function.
-It was introduced in {cite}`Radovic1996` for testing numerical integration
-algorithms (e.g., quasi-Monte-Carlo; see for instance {cite}`Sobol1998`.
+It was introduced in {cite}`Bratley1992` for testing numerical integration
+algorithms (e.g., quasi-Monte-Carlo; see, for instance,
+{cite}`Radovic1996, Sobol1998`).
+
+The current form (and name) was from {cite}`Saltelli1995` and used in
+the context of global sensitivity analysis.
+There, the function was generalized by introducing a set of parameters
+that determines the importance of each input variable.
 Later on, it becomes a popular testing function for global sensitivity analysis
-methods; see, for instances, {cite}`Marrel2008, Marrel2009, Kucherenko2011`.
-
-
-The Sobol'-G function is an M-dimensional scalar-valued function.
-It was introduced in [1] for testing numerical integration algorithms
-(e.g., quasi-Monte-Carlo; see also for instance [2] and [3]).
-The current form (and name) was from [4] and used in the context of global
-sensitivity analysis. There, the function was generalized by introducing
-a set of parameters that determines the importance of each input variable.
-Later on, it becomes a popular testing function for global sensitivity analysis
-methods; see, for instances, [5], [6], and [7].
-
-
+methods; see, for instance, {cite}`Marrel2008, Marrel2009, Kucherenko2011, Sun2022`.
 
 ```{code-cell} ipython3
 import numpy as np
@@ -48,12 +42,12 @@ below.
 from mpl_toolkits.axes_grid1 import make_axes_locatable
 
 # --- Create 1D data from Sobol'-G
-my_sobolg_1d = uqtf.SobolG(spatial_dimension=1)
+my_sobolg_1d = uqtf.SobolG(input_dimension=1)
 xx_1d = np.linspace(0, 1, 1000)[:, np.newaxis]
 yy_1d = my_sobolg_1d(xx_1d)
 
 # --- Create 2D data from Sobol'-G
-my_sobolg_2d = uqtf.SobolG(spatial_dimension=2)
+my_sobolg_2d = uqtf.SobolG(input_dimension=2)
 mesh_2d = np.meshgrid(xx_1d, xx_1d)
 xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
 yy_2d = my_sobolg_2d(xx_2d)
@@ -116,14 +110,14 @@ Check if it has been correctly instantiated:
 print(my_testfun)
 ```
 
-By default, the spatial dimension is set to $2$[^default_dimension].
-To create an instance with another value of spatial dimension,
-pass an integer to the parameter `spatial_dimension` (keyword only).
+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 Sobol'-G function in six dimensions,
 type:
 
 ```{code-cell} ipython3
-my_testfun = uqtf.SobolG(spatial_dimension=6)
+my_testfun = uqtf.SobolG(input_dimension=6)
 ```
 
 In the subsequent section, the function will be illustrated
@@ -134,7 +128,7 @@ using six dimensions.
 The Sobol'-G function is defined as follows[^location]:
 
 $$
-\mathcal{M}(\boldsymbol{x}) = \prod_{m = 1}^M \frac{\lvert 4 x_m - 2 \rvert + a_m}{1 + a_m}
+\mathcal{M}(\boldsymbol{x}; \boldsymbol{a}) = \prod_{m = 1}^M \frac{\lvert 4 x_m - 2 \rvert + a_m}{1 + a_m}
 $$
 where $\boldsymbol{x} = \{ x_1, \ldots, x_M \}$ is the $M$-dimensional vector
 of input variables further defined below,
@@ -147,7 +141,9 @@ function consists of $M$ independent uniform random variables with the ranges
 shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Parameters
@@ -158,37 +154,51 @@ input variable.
 There are several sets of parameters used in the literature
 as shown in the table below.  
   
-| No. |                           Value                            |          Keyword           |                             Source                             |                                             Remark                                             |  
-|:---:|:----------------------------------------------------------:|:--------------------------:|:--------------------------------------------------------------:|:----------------------------------------------------------------------------------------------:|  
-|  1  |                  $a_1 = \ldots = a_M = 0$                  |      `Saltelli1995-1`      |  {cite}`Saltelli1995` (Example 1) (also {cite}`Bratley1992`)   |                           All input variables are equally important                            |  
-|  2  | $a_1 = a_2 = 0$<br> $a_3 = 3$<br> $a_3 = \ldots = a_M = 9$ |      `Saltelli1995-2`      |                {cite}`Saltelli1995` (Example 2)                | The first two are important, the next is moderately important, and the rest is non-influential |  
-|  3  |      $a_m = \frac{m - 1}{2.0}$<br> $1 \leq m \leq M$       | `Saltelli1995-3` (default) | {cite}`Saltelli1995` (Example 3) (also {cite}`Crestaux2007`  ) |              The most important input is the first one, the least is the last one              |
-|  4  |                $a_1 = \ldots = a_M = 0.01$                 |       `Sobol1998-1`        |                  {cite}`Sobol1998` (choice 1)                  |                   The supremum of the function grows exponentially at $2^M$                    |  
-|  5  |                 $a_1 = \ldots = a_M = 1.0$                 |       `Sobol1998-2`        |                  {cite}`Sobol1998` (choice 2)                  |                  The supremum of the function grows exponentially at  $1.5^M$                  |  
-|  6  |             $a_m = m$<br> $\, 1 \leq m \leq M$             |       `Sobol1998-3`        |                  {cite}`Sobol1998` (choice 3)                  |                The supremum of the function grows linearly at $1 + \frac{M}{2}$                |  
-|  7  |             $a_m = m^2$<br> $1 \leq m \leq M$              |       `Sobol1998-4`        |                  {cite}`Sobol1998` (choice 4)                  |                                The supremum is bounded at $1.0$                                |  
-|  8  |     $a_1 = a_2 = 0.0$<br> $a_3 = \ldots = a_M = 6.52$      |    `Kucherenko2011-2a`     |              {cite}`Kucherenko2011` (Problem 2A)               |                                     Originally, $M = 100$                                      |  
-|  9  |             $a_m = 6,52$<br> $1 \leq m \leq M$             |    `Kucherenko2011-3b`     |              {cite}`Kucherenko2011` (Problem 3B)               |                                                                                                |  
+```{table} Available parameters of the Sobol'-G function
+:name: sobol-g-parameters
+| No. |                             Value                             |          Keyword           |                             Source                             |                                             Remark                                             |  
+|:---:|:-------------------------------------------------------------:|:--------------------------:|:--------------------------------------------------------------:|:----------------------------------------------------------------------------------------------:|  
+|  1  |                   $a_1 = \ldots = a_M = 0$                    |      `Saltelli1995-1`      |  {cite}`Saltelli1995` (Example 1) (also {cite}`Bratley1992`)   |                           All input variables are equally important                            |  
+|  2  |  $a_1 = a_2 = 0$<br> $a_3 = 3$<br> $a_3 = \ldots = a_M = 9$   |      `Saltelli1995-2`      |                {cite}`Saltelli1995` (Example 2)                | The first two are important, the next is moderately important, and the rest is non-influential |  
+|  3  |        $a_m = \frac{m - 1}{2.0}$<br> $1 \leq m \leq M$        | `Saltelli1995-3` (default) | {cite}`Saltelli1995` (Example 3) (also {cite}`Crestaux2007`  ) |              The most important input is the first one, the least is the last one              |
+|  4  |                  $a_1 = \ldots = a_M = 0.01$                  |       `Sobol1998-1`        |                  {cite}`Sobol1998` (choice 1)                  |                   The supremum of the function grows exponentially at $2^M$                    |  
+|  5  |                  $a_1 = \ldots = a_M = 1.0$                   |       `Sobol1998-2`        |                  {cite}`Sobol1998` (choice 2)                  |                  The supremum of the function grows exponentially at  $1.5^M$                  |  
+|  6  |              $a_m = m$<br> $\, 1 \leq m \leq M$               |       `Sobol1998-3`        |                  {cite}`Sobol1998` (choice 3)                  |                The supremum of the function grows linearly at $1 + \frac{M}{2}$                |  
+|  7  |               $a_m = m^2$<br> $1 \leq m \leq M$               |       `Sobol1998-4`        |                  {cite}`Sobol1998` (choice 4)                  |                                The supremum is bounded at $1.0$                                |  
+|  8  |       $a_1 = a_2 = 0.0$<br> $a_3 = \ldots = a_M = 6.52$       |    `Kucherenko2011-2a`     |              {cite}`Kucherenko2011` (Problem 2A)               |                                     Originally, $M = 100$                                      |  
+|  9  |              $a_m = 6.52$<br> $1 \leq m \leq M$               |    `Kucherenko2011-3b`     |              {cite}`Kucherenko2011` (Problem 3B)               |                                                                                                |
+| 10  | $a_{1 - 4} = (0, 1, 4.5, 9)$ <br> $a_m = 99, 5 \leq m \leq M$ |         `Sun2022`          |                 {cite}`Sun2022` (Section 3.2)                  |                                                                                                |
+```
 
 ```{note}
 The parameter values used in {cite}`Marrel2008` and {cite}`Marrel2009`
 correspond to the parameter choice 3 in {cite}`Sobol1998`.
 ```
 
+The default parameter is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.parameters)
+```
+
 ````{note}
-To create an instance of the Sobol'-G function with different built-in parameter values, 
-pass the corresponding keyword to the parameter `parameters_selection`.
+To create an instance of the Sobol'-G function with different built-in
+parameter values, pass the corresponding keyword
+to the parameter `parameters_id`.
 For example, to use the parameters of problem 3B from {cite}`Kucherenko2011`,
 type:
 
 ```python
-my_testfun = uqtf.SobolG(parameters_selection="Kucherenko2011-3b")
+my_testfun = uqtf.SobolG(parameters_id="Kucherenko2011-3b")
 ```
 ````
+
 ## Reference results
 
-This section provides several reference results of typical UQ analyses involving
-the test function.
+This section provides several reference results of typical UQ analyses
+involving the test function.
 
 ### Sample histogram
 
@@ -225,7 +235,7 @@ and the results are:
 - $\mathbb{E}[Y] = 1.0$[^integral]
 - $\mathbb{V}[Y] = \prod_{m = 1}^{M} \frac{\frac{4}{3} + 2 a_m + a_m^2}{(1 + a_m)^2} - 1$
 
-Notice that the values of these two moments depend on the choice of the parameter values.
+Notice that the value of the variance depends on the choice of the parameter values.
 
 Shown below is the convergence of a direct Monte-Carlo estimation of
 the output mean and variance with increasing sample sizes compared with the
@@ -253,7 +263,7 @@ mean_estimates_errors = np.std(mean_estimates, axis=1)
 var_estimates_errors = np.std(var_estimates, axis=1)
 
 # --- Compute analytical mean and variance
-params = my_testfun.parameters
+params = my_testfun.parameters["aa"]
 mean_analytical = 1.0
 var_analytical = np.prod((4 / 3 + 2 * params + params**2) / (1 + params)**2) - 1
 
@@ -389,4 +399,4 @@ tabulate(
 because the input is uniform in a unit hypercube.
 
 [^default_dimension]: This default dimension applies to all variable dimension
-test functions. It will be used if the `spatial_dimension` argument is not given.
+test functions. It will be used if the `input_dimension` argument is not given.
diff --git a/docs/test-functions/sobol-levitan.md b/docs/test-functions/sobol-levitan.md
new file mode 100644
index 0000000..39d961c
--- /dev/null
+++ b/docs/test-functions/sobol-levitan.md
@@ -0,0 +1,484 @@
+---
+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:sobol-levitan)=
+# Sobol'-Levitan Function
+
+The Sobol'-Levitan function is an M-dimensional, scalar-valued function
+commonly used as a benchmark for sensitivity analysis.
+The function was introduced in {cite}`Sobol1999` (as a six- and 20-dimensional
+functions) and revisited in, for example, {cite}`Moon2012` (as a 20-dimensional
+function) and {cite}`Sun2022` (as a seven- and 15-dimensional functions).
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The plots for one-dimensional and two-dimensional Sobol'-Levitan function
+can be seen below.
+
+```{code-cell} ipython3
+:tags: [remove-input]
+
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+
+# --- Create 1D data
+my_fun_1d = uqtf.SobolLevitan(input_dimension=1)
+xx_1d = np.linspace(0, 1, 1000)[:, np.newaxis]
+yy_1d = my_fun_1d(xx_1d)
+
+# --- Create 2D data
+my_fun_2d = uqtf.SobolLevitan(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 Sobol'-Levitan")
+
+# 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 Sobol'-Levitan", 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 Sobol'-Levitan", 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 Sobol'-Levitan function, type:
+
+```{code-cell} ipython3
+my_testfun = uqtf.SobolLevitan()
+```
+
+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 Sobol'-Levitan function
+in six dimensions, type:
+
+```{code-cell} ipython3
+my_testfun = uqtf.SobolLevitan(input_dimension=6)
+```
+
+In the subsequent section, the function will be illustrated
+using six dimensions as it originally appeared in {cite}`Sobol1999`.
+
+## Description
+
+The Sobol'-Levitan function is defined as follows[^location]:
+
+$$
+\mathcal{M}(\boldsymbol{x}; \boldsymbol{b}, c_0) = \exp{\left[ \sum_{i = 1}^M b_i x_i \right]} - I_M(\boldsymbol{b}) + c_0,
+$$
+
+where
+
+$$
+I_M (\boldsymbol{b}) = \prod_{i = 1}^M \frac{e^{b_i} - 1}{b_i},
+$$
+
+where $\boldsymbol{x} = \{ x_1, \ldots, x_M \}$ is the $M$-dimensional vector
+of input variables further defined below,
+and $\boldsymbol{b}$ and $c_0$ are parameters of the function also further
+defined below.
+
+## Probabilistic input
+
+The probabilistic input model for the Sobol'-Levitan function consists of $M$
+independent uniform random variables in $[0.0, 1.0]^M$.
+
+For the selected input dimension, the input model is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
+```
+
+## Parameters
+
+The parameters of the Sobol'-Levitan function consists of the coefficients
+$\boldsymbol{b}$ and the constant term $c_0$.
+The coefficients determine the importance of each input variables.
+The constant term, while influences the mean value of the function, does not
+alter the global sensitivity analysis.
+
+The available parameters for the Sobol'-Levitan function are shown in the table
+below.
+
+```{table} Available parameters of the Sobol'-Levitan function
+:name: sobol-levitan-parameters
+| No. |                           $\boldsymbol{b}$                           | $c_0$ |           Keyword            |             Source              |           Remark           |
+|:---:|:--------------------------------------------------------------------:|:-----:|:----------------------------:|:-------------------------------:|:--------------------------:|
+| 1.  |             $b_1 = 1.5$ <br> $b_2 = \ldots = b_M = 0.9$              | $0.0$ | `Sobol1999-1` <br> (default) | {cite}`Sobol1999` (Example 6.1) | Originally, six dimensions |
+| 2.  |   $b_1 = \ldots = b_{10} = 0.6$ <br> $b_{11} = \ldots = b_M = 0.4$   | $0.0$ |        `Sobol1999-2`         | {cite}`Sobol1999` (Example 6.2) | Originally, 20 dimensions  |
+| 3.  | $\boldsymbol{b}_{1-20}$[^moon-b] <br> $b_i = 0.0, i = 21, \ldots, M$ | $0.0$ |         `Moon2012-1`         |   {cite}`Moon2012` (Table 7)    | Originally, 20 dimensions  |
+```
+
+The default parameter is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.parameters)
+```
+
+````{note}
+To create an instance of the Sobol'-Levitan function with different built-in
+parameter values, pass the corresponding keyword to the parameter `parameters_id`.
+For example, to use the parameters of Example 6.2 from {cite}`Sobol1999`,
+type:
+
+```python
+my_testfun = uqtf.SobolLevitan(parameters_id="Sobol1999-2")
+```
+````
+
+```{attention}
+If the value of parameter $b_i$ is zero then the value of $\frac{e^{b_i} - 1}{b_i}$
+that appears in the expression of $I_M$ above is singular but in the limit
+is reduced to $1.0$.
+```
+
+## 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]
+
+np.random.seed(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);
+```
+
+### Definite integration
+
+The integral value of the function over the whole domain $[0, 1]^M$
+is analytical:
+
+$$
+\int_{[0, 1]^M} \mathcal{M}(\boldsymbol{x}) \; d\boldsymbol{x} = c_0.
+$$
+
+### Moments
+
+The mean and variance of the Sobol'-Levitan function can be computed
+analytically. 
+
+The mean[^integral] is given as follows:
+
+$$
+\mathbb{E}[Y] = c_0,
+$$
+
+while the variance is given as follows:
+
+$$
+\mathbb{V}[Y] (\boldsymbol{b}) = H_M(\boldsymbol{b}) - I_Mˆ2(\boldsymbol{b})
+$$
+
+where $I_M$ is given in the section above and
+
+$$
+H_M (\boldsymbol{b}) = \prod_{i = 1}^M \frac{e^{2 b_i} - 1}{2 b_i}.
+$$
+
+Notice that the values of the mean and variance depend on the choice
+of the parameter values.
+
+Shown below is the convergence of a direct Monte-Carlo estimation of
+the output mean and variance with increasing sample sizes compared with the
+analytical values.
+The error bars correspond to twice the standard deviation
+of the estimates obtained from $50$ replications.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+# --- Compute the mean and variance estimate
+sample_sizes = np.array([1e1, 1e2, 1e3, 1e4, 1e5], dtype=int)
+mean_estimates = np.empty((len(sample_sizes), 50))
+var_estimates = np.empty((len(sample_sizes), 50))
+my_testfun.prob_input.reset_rng(42)
+
+for i, sample_size in enumerate(sample_sizes):
+    for j in range(50):
+        xx_test = my_testfun.prob_input.get_sample(sample_size)
+        yy_test = my_testfun(xx_test)
+        mean_estimates[i, j] = np.mean(yy_test)
+        var_estimates[i, j] = np.var(yy_test)
+
+mean_estimates_errors = np.std(mean_estimates, axis=1)
+var_estimates_errors = np.std(var_estimates, axis=1)
+
+# --- Compute analytical mean and variance
+mean_analytical = my_testfun.parameters["c0"]
+bb = my_testfun.parameters["bb"]
+h_m = np.prod((np.exp(2 * bb) - 1) / 2 / bb)
+i_m = np.prod((np.exp(bb) - 1) / bb)
+var_analytical = h_m - i_m**2
+
+# --- Plot the mean and variance estimates
+fig, ax_1 = plt.subplots(figsize=(6,4))
+
+ext_sample_sizes = np.insert(sample_sizes, 0, 1)
+ext_sample_sizes = np.insert(ext_sample_sizes, -1, 5e6)
+
+# --- Mean plot
+ax_1.errorbar(
+    sample_sizes,
+    mean_estimates[:,0],
+    yerr=2.0*mean_estimates_errors,
+    marker="o",
+    color="#66c2a5",
+    label="Mean"
+)
+# Plot the analytical mean
+ax_1.plot(
+    ext_sample_sizes,
+    np.repeat(mean_analytical, len(ext_sample_sizes)),
+    linestyle="--",
+    color="#66c2a5",
+    label="Analytical mean",
+)
+ax_1.set_xlim([5, 5e5])
+ax_1.set_xlabel("Sample size")
+ax_1.set_ylabel("Output mean estimate")
+ax_1.set_xscale("log");
+ax_2 = ax_1.twinx()
+
+# --- Variance plot
+ax_2.errorbar(
+    sample_sizes+1,
+    var_estimates[:,0],
+    yerr=2.0*var_estimates_errors,
+    marker="o",
+    color="#fc8d62",
+    label="Variance",
+)
+# Plot the analytical variance
+ax_2.plot(
+    ext_sample_sizes,
+    np.repeat(var_analytical, len(ext_sample_sizes)),
+    linestyle="--",
+    color="#fc8d62",
+    label="Analytical variance",
+)
+ax_2.set_ylabel("Output variance estimate")
+
+# Add the two plots together to have a common legend
+ln_1, labels_1 = ax_1.get_legend_handles_labels()
+ln_2, labels_2 = ax_2.get_legend_handles_labels()
+ax_2.legend(ln_1 + ln_2, labels_1 + labels_2, loc=0)
+
+plt.grid()
+fig.set_dpi(150)
+```
+
+### Sensitivity indices
+
+The main-effect Sobol' sensitivity indices of the Sobol'-Levitan function are
+given by the following formula:
+
+$$
+S_i = \frac{R_i - 1}{R_M - 1},\; i = 1, \ldots, M,
+$$
+
+where
+
+$$
+R_M = \frac{H_M}{I_M^2};\;\; R_i = \frac{H_i}{I_i^2},
+$$
+
+and 
+
+$$
+H_i = \frac{e^{2 b_i} - 1}{2 b_i};\;\; I_i = \frac{e^{b_i} - 1}{b_i}.
+$$
+
+
+The total-effect Sobol' sensitivity indices, on the other hand, are given
+by the following formula:
+
+$$
+ST_i = 1 - S_{\sim i}, \; i = 1, \ldots, M,
+$$
+
+where
+
+$$
+S_{\sim i} = \frac{\left( R_M / R_i \right) - 1}{R_M - 1}
+$$
+
+The formulas are general in the sense that assuming
+$\boldsymbol{x}_a = (x_1, \ldots, x_a)$ and
+$\boldsymbol{x}_b = (x_{a + 1}, \ldots, x_M)$
+such that $\boldsymbol{x} = (\boldsymbol{x}_a, \boldsymbol{x}_b)$,
+the sensitivity indices of the sets are:
+
+$$
+S_{a} = \frac{R_a - 1}{R_M - 1}
+$$
+
+$$
+S_{b} = \frac{(R_M / R_a) - 1}{R_M - 1}
+$$
+
+$$
+ST_{a} = 1 - S_{b}.
+$$
+
+Some example values of the Sobol' main- and total-effect sensitivity indices
+for the Sobol'-Levitan function with the three available parameter sets and
+the original dimension as appeared in the corresponding literature.
+
+::::{tab-set}
+
+:::{tab-item} Sobol1999-1
+|  Input   |           $S_i$           |          $ST_i$          |
+|:--------:|:-------------------------:|:------------------------:|
+|  $X_1$   | $2.86993e \times 10^{-1}$ | $3.96179 \times 10^{-1}$ | 
+|  $X_2$   | $1.05712e \times 10^{-1}$ | $1.61558 \times 10^{-1}$ | 
+|  $X_3$   | $1.05712e \times 10^{-1}$ | $1.61558 \times 10^{-1}$ | 
+|  $X_4$   | $1.05712e \times 10^{-1}$ | $1.61558 \times 10^{-1}$ | 
+|  $X_5$   | $1.05712e \times 10^{-1}$ | $1.61558 \times 10^{-1}$ | 
+|  $X_6$   | $1.05712e \times 10^{-1}$ | $1.61558 \times 10^{-1}$ | 
+:::
+
+:::{tab-item} Sobol1999-2
+|  Input   |           $S_i$           |          $ST_i$           |
+|:--------:|:-------------------------:|:-------------------------:|
+|  $X_1$   | $5.61551 \times 10^{-2}$  | $8.34869 \times 10^{-2}$  | 
+|  $X_2$   | $5.61551 \times 10^{-2}$  | $8.34869 \times 10^{-2}$  | 
+|  $X_3$   | $5.61551 \times 10^{-2}$  | $8.34869 \times 10^{-2}$  | 
+|  $X_4$   | $5.61551 \times 10^{-2}$  | $8.34869 \times 10^{-2}$  | 
+|  $X_5$   | $5.61551 \times 10^{-2}$  | $8.34869 \times 10^{-2}$  | 
+|  $X_6$   | $5.61551 \times 10^{-2}$  | $8.34869 \times 10^{-2}$  | 
+|  $X_7$   | $5.61551 \times 10^{-2}$  | $8.34869 \times 10^{-2}$  | 
+|  $X_8$   | $5.61551 \times 10^{-2}$  | $8.34869 \times 10^{-2}$  | 
+|  $X_9$   | $5.61551 \times 10^{-2}$  | $8.34869 \times 10^{-2}$  | 
+| $X_{10}$ | $5.61551 \times 10^{-2}$  | $8.34869 \times 10^{-2}$  |
+| $X_{11}$ | $2.50405 \times 10^{-2}$  | $3.78353 \times 10^{-2}$  |
+| $X_{12}$ | $2.50405 \times 10^{-2}$  | $3.78353 \times 10^{-2}$  |
+| $X_{13}$ | $2.50405 \times 10^{-2}$  | $3.78353 \times 10^{-2}$  |
+| $X_{14}$ | $2.50405 \times 10^{-2}$  | $3.78353 \times 10^{-2}$  |
+| $X_{15}$ | $2.50405 \times 10^{-2}$  | $3.78353 \times 10^{-2}$  |
+| $X_{16}$ | $2.50405 \times 10^{-2}$  | $3.78353 \times 10^{-2}$  |
+| $X_{17}$ | $2.50405 \times 10^{-2}$  | $3.78353 \times 10^{-2}$  |
+| $X_{18}$ | $2.50405 \times 10^{-2}$  | $3.78353 \times 10^{-2}$  |
+| $X_{19}$ | $2.50405 \times 10^{-2}$  | $3.78353 \times 10^{-2}$  |
+| $X_{20}$ | $2.50405 \times 10^{-2}$  | $3.78353 \times 10^{-2}$  |
+:::
+
+:::{tab-item} Moon2012-1
+|  Input   |           $S_i$           |          $ST_i$          |
+|:--------:|:-------------------------:|:------------------------:|
+|  $X_1$   | $5.49487 \times 10^{-2}$  | $2.80254 \times 10^{-1}$ | 
+|  $X_2$   | $5.23891 \times 10^{-2}$  | $2.70200 \times 10^{-1}$ | 
+|  $X_3$   | $4.98801 \times 10^{-2}$  | $2.60124 \times 10^{-1}$ | 
+|  $X_4$   | $4.74227 \times 10^{-2}$  | $2.50034 \times 10^{-1}$ | 
+|  $X_5$   | $4.50178 \times 10^{-2}$  | $2.39943 \times 10^{-1}$ | 
+|  $X_6$   | $4.26663 \times 10^{-2}$  | $2.29860 \times 10^{-1}$ | 
+|  $X_7$   | $4.03691 \times 10^{-2}$  | $2.19798 \times 10^{-1}$ | 
+|  $X_8$   | $3.81272 \times 10^{-2}$  | $2.09769 \times 10^{-1}$ | 
+|  $X_9$   | $2.60713 \times 10^{-3}$  | $1.72041 \times 10^{-2}$ | 
+| $X_{10}$ | $1.38278 \times 10^{-3}$  | $9.18792 \times 10^{-3}$ |
+| $X_{11}$ | $6.87641 \times 10^{-4}$  | $4.58707 \times 10^{-3}$ |
+| $X_{12}$ | $3.16411 \times 10^{-4}$  | $2.11515 \times 10^{-3}$ |
+| $X_{13}$ | $1.32275 \times 10^{-4}$  | $8.85162 \times 10^{-4}$ |
+| $X_{14}$ | $4.86979 \times 10^{-5}$  | $3.26033 \times 10^{-4}$ |
+| $X_{15}$ | $1.52609 \times 10^{-5}$  | $1.02191 \times 10^{-4}$ |
+| $X_{16}$ | $3.79169 \times 10^{-6}$  | $2.53919 \times 10^{-5}$ |
+| $X_{17}$ | $6.76395 \times 10^{-7}$  | $4.52971 \times 10^{-6}$ |
+| $X_{18}$ | $6.45092 \times 10^{-8}$  | $4.32009 \times 10^{-7}$ |
+| $X_{19}$ | $2.34038 \times 10^{-9}$  | $1.56732 \times 10^{-8}$ |
+| $X_{20}$ |         $0.00000$         |        $0.00000$         |
+:::
+
+::::
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^location]: see Section 4, pp. 55-56 {cite}`Sobol1999`.
+
+[^moon-b]: $\boldsymbol{b}_{1 - 5} = \left(2.0000, 1.9500, 1.9000, 1.8500, 1.8000 \right)$,
+$\boldsymbol{b}_{6 - 10} = \left( 1.7500, 1.7000, 1.6500, 0.4228, 0.3077 \right)$,
+$\boldsymbol{b}_{11 - 15} = \left( 0.2169, 0.1471, 0.0951, 0.0577, 0.0323 \right)$, and
+$\boldsymbol{b}_{16 - 20} = \left( 0.0161, 0.0068, 0.0021, 0.0004, 0.0000 \right)$.
+
+[^integral]: The expected value is the same as the integral over the domain
+because the input is uniform in a unit hypercube.
+
+[^default_dimension]: This default dimension applies to all variable dimension
+test functions. It will be used if the `input_dimension` argument is not given.
diff --git a/docs/test-functions/solar-cell.md b/docs/test-functions/solar-cell.md
new file mode 100644
index 0000000..98b912c
--- /dev/null
+++ b/docs/test-functions/solar-cell.md
@@ -0,0 +1,236 @@
+---
+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:solar-cell)=
+# Single-Diode Solar Cell Model
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The test function is a five-dimensional, scalar-valued function that models
+the maximum power of a single-diode solar cell. The function was used in
+{cite}`Constantine2015` to demonstrate the active subspace method for
+input dimension reduction and sensitivity analysis.
+
+For a given voltage, the corresponding current is defined implicitly.
+The plot below (left) displays 15 current-voltage curves from 15 different
+input variable values.
+The dots on the curves indicate the current and voltage values that yield
+the maximum cell power; the power curves are shown in the right plot.
+
+```{code-cell} ipython3
+:tags: [remove-input]
+
+my_fun = uqtf.SolarCell()
+my_fun.prob_input.reset_rng(237324)
+num_sample = 15
+xx = my_fun.prob_input.get_sample(num_sample)
+
+n_s = my_fun.parameters["n_s"]
+v_th = my_fun.parameters["v_th"]
+def compute_ii(vv, xx, ns, v_th):
+    ii = np.empty((len(vv), len(xx)))
+    for idx_1 in range(len(xx)):
+        x = xx[idx_1]
+        for idx_2 in range(len(vv)):
+            v = vv[idx_2]
+            ii[idx_2, idx_1] = uqtf.test_functions.solar_cell.compute_current(
+                v,
+                x,
+                n_s,
+                v_th,
+            ).squeeze()
+
+    return ii
+
+vv = np.linspace(0, 1.4, 1000)
+ii = compute_ii(vv, xx, n_s, v_th)
+
+pp_max, vv_max = uqtf.test_functions.solar_cell.compute_power_max(
+    xx,
+    n_s,
+    v_th,
+)
+ii_max = np.array(
+    [
+        uqtf.test_functions.solar_cell.compute_current(
+            vv_max[k],
+            xx[k],
+            n_s,
+            v_th,
+        ).squeeze()
+        for k in range(num_sample)
+    ]
+)
+
+# --- Create a series of plots
+fig = plt.figure(figsize=(10, 5))
+
+# Current-voltage curves
+axs_1 = plt.subplot(121)
+for i in range(len(xx)):
+    axs_1.plot(vv, ii[:, i], color="#8da0cb")
+axs_1.set_ylim([0.0, 0.3])
+axs_1.set_xlim([0.0, 1.5])
+axs_1.scatter(vv_max, ii_max, color="#8da0cb")
+axs_1.grid()
+axs_1.set_xlabel("Voltage (V)", fontsize=14)
+axs_1.set_ylabel("Current (A)", fontsize=14)
+axs_1.set_title("Current-voltage curves")
+
+# Power-voltage curves
+axs_2 = plt.subplot(122)
+for i in range(len(xx)):
+    axs_2.plot(vv, ii[:, i] * vv, color="#8da0cb")
+axs_2.scatter(vv_max, pp_max, color="#8da0cb")
+axs_2.set_ylim([0.0, 0.18])
+axs_2.set_xlim([0.0, 1.5])
+axs_2.grid()
+axs_2.set_xlabel("Voltage (V)", fontsize=14)
+axs_2.set_ylabel("Power (W)", fontsize=14)
+axs_2.set_title("Power-voltage curves")
+
+plt.gcf().tight_layout(pad=4.0)
+plt.gcf().set_dpi(150);
+```
+
+## Test function instance
+
+To create a default instance of the test function:
+
+```{code-cell} ipython3
+my_testfun = uqtf.SolarCell()
+```
+
+Check if it has been correctly instantiated:
+
+```{code-cell} ipython3
+print(my_testfun)
+```
+
+## Description
+
+The model predicts the maximum power of a single-diode solar cell defined
+in the following formula:
+
+$$
+\mathcal{M}(\boldsymbol{x}; \boldsymbol{p}) = \max_{I, V}  I(V; \boldsymbol{x}, \boldsymbol{p}) V,
+$$
+
+where the current ($I$) is defined implicitly as a function of the voltage ($V$),
+input variables $\boldsymbol{x}$ and parameters $\boldsymbol{p}$.
+The implicit relationshow is described by the following equation:
+
+$$
+I(V; \boldsymbol{x}, \boldsymbol{p}) = I_L - I_S \left( \exp{\left( \frac{V + I R_S}{n_S \, n \, V_{\text{th}}} \right) } - 1 \right) - \frac{V + I R_S}{R_P},
+$$
+
+where $I_L$, the photocurrent, is defined by the auxiliary equation:
+
+$$
+I(\boldsymbol{x}, \boldsymbol{p}) = I_{SC} + I_S \left( \exp{ \left( \frac{I_{SC} R_S}{n_S \, n \, V_{\text{th}}} \right) } - 1 \right) + \frac{I_{SC} R_S}{R_P}.
+$$
+
+Here, $\boldsymbol{x} = \left( I_{SC}, I_S, n, R_S, R_P \right)$ represents
+the five-dimensional the vector of input variables probabilistically defined below.
+The vector $\boldsymbol{p} = \left( n_s, V_{\text{th}} \right)$ contains fixed parameters,
+which are also further detailed below.
+
+## Probabilistic input
+
+The probabilistic input model for the test function is shown below.
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
+```
+
+## Parameters
+
+The default values of the parameters are 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 $1000$ random points:
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+xx_test = my_testfun.prob_input.get_sample(1000)
+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);
+```
+
+## Notes on numerical algorithms
+
+The maximum power of the solar cell model is computed numerically
+using the following methods:
+
+- [`root()`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.root.html)
+  from SciPy, with its default method and parameter values,
+  to solve the implicit current as a function of voltage.
+- [`minimize()`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html)
+  from SciPy, with its default method (i.e., `BFGS`) and parameter values,
+  to find the maximum power by optimizing over the voltage (the current is
+  is computed on-the-fly during the iteration).
+
+The default parameter values for these methods can be overridden by providing
+a dictionary with new parameter values. For example:
+
+To override the tolerance value for `root()`:
+
+```python
+fun.parameters.add("root", {"tol": 1e-12})  # 'root' as the parameter keyword
+```
+
+To override the tolerance value for `minimize()`:
+
+```python
+fun.parameters.add("minimize", {"tol": 1e-12})  # 'minimize' as the parameter keyword
+```
+
+```{note}
+In this example, `tol` is acceptable keyword-named argument for both `root()`
+and `minimize()`; indeed, the key-value pairs specified for the parameters
+`root` and `minimize` must be recognized by the respective methods.
+```
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^location]: See Eqs. (9-12), Section 3.1 in {cite}`Constantine2015`.
diff --git a/docs/test-functions/speed-reducer-shaft.md b/docs/test-functions/speed-reducer-shaft.md
index c98477f..c272004 100644
--- a/docs/test-functions/speed-reducer-shaft.md
+++ b/docs/test-functions/speed-reducer-shaft.md
@@ -64,7 +64,9 @@ for the speed reducer shaft reliability problem consists of five independent
 random variables with marginal distributions shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 Note that the variables $F$, $D$, and $L$ must be first converted to their
diff --git a/docs/test-functions/sulfur.md b/docs/test-functions/sulfur.md
index a0cc321..c42308e 100644
--- a/docs/test-functions/sulfur.md
+++ b/docs/test-functions/sulfur.md
@@ -77,7 +77,9 @@ specification, all parameters of the log-normal marginals are given in terms
 of geometric mean and geometric standard deviation.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ```{note}
@@ -143,7 +145,7 @@ $$
 
 Shown below is the convergence of a direct Monte-Carlo estimation of
 the output mean and variance with increasing sample sizes compared with the
-analytical values. The error bars corresponds to twice the standard deviation
+analytical values. The error bars correspond to twice the standard deviation
 of the estimates obtained from $25$ replications.
 
 ```{code-cell} ipython3
diff --git a/docs/test-functions/undamped-oscillator.md b/docs/test-functions/undamped-oscillator.md
new file mode 100644
index 0000000..eae3463
--- /dev/null
+++ b/docs/test-functions/undamped-oscillator.md
@@ -0,0 +1,179 @@
+---
+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:undamped-oscillator)=
+# Undamped Oscillator
+
+```{code-cell} ipython3
+import numpy as np
+import matplotlib.pyplot as plt
+import uqtestfuns as uqtf
+```
+
+The undamped oscillator function (`UndampedOscillator`) is a six-dimensional,
+scalar-valued test function that models a non-linear, undamped,
+single-degree-of-freedom, forced oscillating mechanical system.
+
+This function is frequently used as a test function for reliability analysis
+methods (see  {cite}`Bucher1990, Rajashekhar1993, Gayton2003, Schueremans2005, Echard2011, Echard2013`).
+Additionally, in {cite}`Luethen2021`, the function is employed
+as a test function for metamodeling exercises.
+
+## Test function instance
+
+To create a default instance of the test function:
+
+```{code-cell} ipython3
+my_testfun = uqtf.UndampedOscillator()
+```
+
+Check if it has been correctly instantiated:
+
+```{code-cell} ipython3
+print(my_testfun)
+```
+
+## Description
+
+The system under consideration is a single-degree-of-freedom mechanical system
+that undergoes undamped forced oscillation.
+The performance function is analytically defined as follows[^location]:
+
+$$
+g(\boldsymbol{x}) = 3 r - \lvert z_{\text{max}} \rvert,
+$$
+
+where $z_{\text{max}}$ is the maximum displacement response of the system
+given by
+
+$$
+z_{\text{max}} (\boldsymbol{x}) = \frac{2 F_1}{m \omega_0^2} \sin{\left( \frac{\omega_0 t_1}{2} \right)}
+$$
+
+and
+
+$$
+\omega_0 = \sqrt{\frac{c_1 + c_2}{m}}.
+$$
+
+
+$\boldsymbol{x} = \{ m, c_1, c_2, r, F_1, t_1 \}$ is the six-dimensional vector
+of input variables probabilistically defined further below.
+
+The failure state and the failure probability are defined as
+$g(\boldsymbol{x}; \boldsymbol{p}) \leq 0$
+and $\mathbb{P}[g(\boldsymbol{X}; \boldsymbol{p}) \leq 0]$, respectively.
+
+## Probabilistic input
+
+The available probabilistic input models are shown in the table below.
+The different specifications alter the failure probability of the system
+(as expected).
+
+```{table} Available probabilistic input of the undamped oscillator function
+:name: undamped-oscillator-inputs
+| No. |       Remark       |        Keyword         |            Source            |  
+|:---:|:------------------:|:----------------------:|:----------------------------:|  
+| 1.  | $\mu_{F_1} = 1.00$ | `Gayton2003` (default) | {cite}`Gayton2003` (Table 9) |  
+| 2.  | $\mu_{F_1} = 0.60$ |     `Echard2013-1`     | {cite}`Echard2013` (Table 4) |
+| 3.  | $\mu_{F_1} = 0.45$ |     `Echard2013-2`     | {cite}`Echard2013` (Table 4) |
+```
+
+The default input is 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 $10^6$ random points:
+
+```{code-cell} ipython3
+:tags: [hide-input]
+
+xx_test = my_testfun.prob_input.get_sample(1000000)
+yy_test = my_testfun(xx_test)
+idx_pos = yy_test > 0
+idx_neg = yy_test <= 0
+
+hist_pos = plt.hist(yy_test, bins="auto", color="#0571b0")
+plt.hist(yy_test[idx_neg], bins=hist_pos[1], color="#ca0020")
+plt.axvline(0, linewidth=1.0, color="#ca0020")
+
+plt.grid()
+plt.ylabel("Counts [-]")
+plt.xlabel("$g(\mathbf{X})$")
+plt.gcf().set_dpi(150);
+```
+
+### Failure probability ($P_f$)
+
+Some reference values for the failure probability $P_f$ from the literature
+are summarized in the tables below according to the chosen input specification.
+
+::::{tab-set}
+
+:::{tab-item} Gayton2003
+|                 Method                 |        $N$        |      $\hat{P}_f$       | $\mathrm{CoV}[\hat{P}_f]$ |              Source               |
+|:--------------------------------------:|:-----------------:|:----------------------:|:-------------------------:|:---------------------------------:|
+|               {term}`DS`               |      $1281$       |  $3.5 \times 10^{-2}$  |          &#8212;          | {cite}`Schueremans2005` (Table 5) |
+|        {term}`DS` + Polynomial         |       $62$        |  $3.4 \times 10^{-2}$  |          &#8212;          | {cite}`Schueremans2005` (Table 5) |
+|          {term}`DS` + Splines          |       $76$        |  $3.4 \times 10^{-2}$  |          &#8212;          | {cite}`Schueremans2005` (Table 5) |
+|    {term}`DS` + Neural network (NN)    |       $86$        |  $2.8 \times 10^{-2}$  |          &#8212;          | {cite}`Schueremans2005` (Table 5) |
+|        {term}`MCS` + {term}`IS`        |      $6144$       |  $2.7 \times 10^{-2}$  |          &#8212;          | {cite}`Schueremans2005` (Table 5) |
+| {term}`MCS` + {term}`IS` + Polynomials |       $109$       |  $2.5 \times 10^{-2}$  |          &#8212;          | {cite}`Schueremans2005` (Table 5) |
+|   {term}`MCS` + {term}`IS` + Splines   |       $67$        |  $2.7 \times 10^{-2}$  |          &#8212;          | {cite}`Schueremans2005` (Table 5) |
+|     {term}`MCS` + {term}`IS` + NN      |       $68$        |  $3.1 \times 10^{-2}$  |          &#8212;          | {cite}`Schueremans2005` (Table 5) |
+|              {term}`MCS`               | $7 \times 10^{4}$ | $2.834 \times 10^{-2}$ |          $2.2\%$          |   {cite}`Echard2011` (Table 6)    |
+|  Adaptive Kriging + {term}`MCS` + EFF  |       $58$        | $2.834 \times 10^{-2}$ |          &#8212;          |   {cite}`Echard2011` (Table 6)    |
+|   Adaptive Kriging + {term}`MCS` + U   |       $45$        | $2.851 \times 10^{-2}$ |          &#8212;          |   {cite}`Echard2011` (Table 6)    |
+:::
+
+:::{tab-item} Echard2013-1
+|            Method             |         $N$          |      $\hat{P}_f$      | $\mathrm{CoV}[\hat{P}_f]$ |            Source            |
+|:-----------------------------:|:--------------------:|:---------------------:|:-------------------------:|:----------------------------:|
+|          {term}`MCS`          | $1.8 \times 10^{8}$  | $9.09 \times 10^{-6}$ |         $2.47\%$          | {cite}`Echard2013` (Table 5) |
+|         {term}`FORM`          |         $29$         | $9.76 \times 10^{-6}$ |          &#8212;          | {cite}`Echard2013` (Table 5) |
+|          {term}`IS`           | $20 + \times 10^{4}$ | $9.13 \times 10^{-6}$ |         $2.29\%$          | {cite}`Echard2013` (Table 5) |
+| Adaptive Kriging + {term}`IS` |      $29 + 38$       | $9.13 \times 10^{-6}$ |         $2.29\%$          | {cite}`Echard2013` (Table 5) |
+:::
+
+:::{tab-item} Echard2013-2
+|            Method             |         $N$          |      $\hat{P}_f$      | $\mathrm{CoV}[\hat{P}_f]$ |            Source            |
+|:-----------------------------:|:--------------------:|:---------------------:|:-------------------------:|:----------------------------:|
+|          {term}`MCS`          |  $9 \times 10^{8}$   | $1.55 \times 10^{-8}$ |         $2.68\%$          | {cite}`Echard2013` (Table 5) |
+|         {term}`FORM`          |         $29$         | $1.56 \times 10^{-8}$ |          &#8212;          | {cite}`Echard2013` (Table 5) |
+|          {term}`IS`           | $29 + \times 10^{4}$ | $1.53 \times 10^{-8}$ |         $2.70\%$          | {cite}`Echard2013` (Table 5) |
+| Adaptive Kriging + {term}`IS` |      $29 + 38$       | $1.54 \times 10^{-8}$ |         $2.70\%$          | {cite}`Echard2013` (Table 5) |
+:::
+
+::::
+
+
+## References
+
+```{bibliography}
+:style: unsrtalpha
+:filter: docname in docnames
+```
+
+[^location]: see Section 6.4.1 in {cite}`Gayton2003`.
\ No newline at end of file
diff --git a/docs/test-functions/webster-2d.md b/docs/test-functions/webster-2d.md
index 0a574b8..d8c7320 100644
--- a/docs/test-functions/webster-2d.md
+++ b/docs/test-functions/webster-2d.md
@@ -118,7 +118,9 @@ Based on {cite}`Webster1996`, the probabilistic input model
 for the function consists of two independent random variables as shown below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference results
diff --git a/docs/test-functions/welch1992.md b/docs/test-functions/welch1992.md
index e4f37bc..4c50fdc 100644
--- a/docs/test-functions/welch1992.md
+++ b/docs/test-functions/welch1992.md
@@ -73,7 +73,9 @@ This input can be modeled using 20 independent uniform random variables
 shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference Results
diff --git a/docs/test-functions/wing-weight.md b/docs/test-functions/wing-weight.md
index cec6828..f4bef98 100644
--- a/docs/test-functions/wing-weight.md
+++ b/docs/test-functions/wing-weight.md
@@ -59,7 +59,9 @@ Weight function consists of eight independent uniform random variables with
 ranges shown in the table below.
 
 ```{code-cell} ipython3
-my_testfun.prob_input
+:tags: [hide-input]
+
+print(my_testfun.prob_input)
 ```
 
 ## Reference Results
diff --git a/src/uqtestfuns/__init__.py b/src/uqtestfuns/__init__.py
index 4961a00..b177438 100644
--- a/src/uqtestfuns/__init__.py
+++ b/src/uqtestfuns/__init__.py
@@ -1,12 +1,19 @@
 """
 This is the package init for UQTestFuns.
 """
+
 import sys
 
-from .core import UnivDist
+from .core import Marginal
 from .core import ProbInput
-from .core import UQTestFunBareABC, UQTestFunABC
+from .core import (
+    UQTestFunBareABC,
+    UQTestFunABC,
+    UQTestFunFixDimABC,
+    UQTestFunVarDimABC,
+)
 from .core import UQTestFun
+from .core import FunParams
 
 
 from . import test_functions
@@ -19,17 +26,19 @@
 
 if sys.version_info >= (3, 8):
     from importlib import metadata
-else:
+else:  # pragma: no cover
     import importlib_metadata as metadata
 
 __version__ = metadata.version("uqtestfuns")
 
 __all__ = [
-    "UnivDist",
+    "Marginal",
     "ProbInput",
-    "UQTestFunABC",
+    "FunParams",
     "UQTestFunBareABC",
     "UQTestFunABC",
+    "UQTestFunFixDimABC",
+    "UQTestFunVarDimABC",
     "UQTestFun",
     "test_functions",
     "UQMetaFunSpec",
diff --git a/src/uqtestfuns/core/__init__.py b/src/uqtestfuns/core/__init__.py
index 0e7f3e6..7b086fa 100644
--- a/src/uqtestfuns/core/__init__.py
+++ b/src/uqtestfuns/core/__init__.py
@@ -1,15 +1,25 @@
 """
 The core subpackage of uqtestfuns.
 """
-from .prob_input.univariate_distribution import UnivDist
+
+from .parameters import FunParams
+from .prob_input.marginal import Marginal
 from .prob_input.probabilistic_input import ProbInput
-from .uqtestfun_abc import UQTestFunBareABC, UQTestFunABC
+from .uqtestfun_abc import (
+    UQTestFunBareABC,
+    UQTestFunABC,
+    UQTestFunFixDimABC,
+    UQTestFunVarDimABC,
+)
 from .uqtestfun import UQTestFun
 
 __all__ = [
-    "UnivDist",
+    "Marginal",
     "ProbInput",
+    "FunParams",
     "UQTestFunBareABC",
     "UQTestFunABC",
+    "UQTestFunFixDimABC",
+    "UQTestFunVarDimABC",
     "UQTestFun",
 ]
diff --git a/src/uqtestfuns/core/custom_typing.py b/src/uqtestfuns/core/custom_typing.py
new file mode 100644
index 0000000..e25a220
--- /dev/null
+++ b/src/uqtestfuns/core/custom_typing.py
@@ -0,0 +1,74 @@
+"""
+Core module that contains custom types used in UQTestFuns.
+
+Custom types are used to assist type checker during the code development.
+"""
+
+from typing import Callable, List, Union, Optional, Any, Dict, Sequence
+from typing_extensions import TypedDict
+
+from uqtestfuns.core.prob_input.marginal import Marginal
+
+__all__ = [
+    "MarginalSpec",
+    "MarginalSpecs",
+    "ProbInputSpecs",
+    "ProbInputArgs",
+    "FunParamSpecs",
+    "FunParamsArgs",
+    "DeclaredParameters",
+]
+
+
+class MarginalSpec(TypedDict):
+    name: str
+    distribution: str
+    parameters: List[Union[float, int]]
+    description: Optional[str]
+
+
+MarginalSpecs = List[MarginalSpec]
+
+
+class ProbInputSpec(TypedDict):
+    function_id: str
+    description: str
+    marginals: Union[List[MarginalSpec], Callable]
+    copulas: Optional[Any]
+
+
+ProbInputSpecs = Dict[str, ProbInputSpec]
+
+
+class ProbInputArgs(TypedDict):
+    function_id: str
+    input_id: str
+    description: str
+    marginals: Sequence[Marginal]
+    copulas: Optional[Any]
+
+
+class DeclaredParameter(TypedDict):
+    keyword: str
+    value: Any
+    type: Optional[type]
+    description: Optional[str]
+
+
+DeclaredParameters = List[DeclaredParameter]
+
+
+class FunParamsSpec(TypedDict):
+    function_id: str
+    description: str
+    declared_parameters: DeclaredParameters
+
+
+FunParamSpecs = Dict[str, FunParamsSpec]
+
+
+class FunParamsArgs(TypedDict):
+    function_id: str
+    parameter_id: str
+    description: str
+    declared_parameters: DeclaredParameters
diff --git a/src/uqtestfuns/core/parameters.py b/src/uqtestfuns/core/parameters.py
new file mode 100644
index 0000000..369becf
--- /dev/null
+++ b/src/uqtestfuns/core/parameters.py
@@ -0,0 +1,343 @@
+"""
+This module contains the implementation of FunParams a class that stores
+UQ Test function parameters.
+"""
+
+import textwrap
+
+from tabulate import tabulate
+from typing import Any, List, Optional
+
+import warnings
+
+__all__ = ["FunParams"]
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import DeclaredParameters
+
+FIELD_NAMES = ["Keyword", "Value", "Type", "Description"]
+
+
+class FunParams:
+    """A class that stores UQ Test function parameters.
+
+    Parameters
+    ----------
+    function_id : str, optional.
+        The UQ test function ID (typically the name of the test function).
+        If not provided, an empty string is used.
+    parameter_id : str, optional
+        The ID of the parameter set.
+        If not provided, an empty string is used.
+    description : str, optional
+        Descriptive string of the parameter set.
+        If not provided, an empty string is used.
+    declared_parameters : List[dict], optional
+        List of dictionaries containing the UQ test function parameters with
+        the following keywords: 'keyword', 'value', 'type', 'description'.
+        'keyword' value is a string that appears in the evaluation function.
+        The value is None if not provided.
+    """
+
+    def __init__(
+        self,
+        function_id: str = "",
+        parameter_id: str = "",
+        description: str = "",
+        declared_parameters: Optional[DeclaredParameters] = None,
+    ):
+        self._declared_parameters: dict = {}
+        self._dict: dict = {}
+
+        self.function_id = function_id
+        self.parameter_id = parameter_id
+        self.description = description
+
+        if declared_parameters is None:
+            return
+
+        # Parse the declared parameters
+        for parameter in declared_parameters:
+            self.add(**parameter)
+
+    def __getitem__(self, key: str):
+        """Get the current value by name.
+
+        Parameters
+        ----------
+        key : str
+            The name of the parameter, i.e., its keyword. The parameter
+            must already be declared.
+        """
+        return self._dict[key]
+
+    def __setitem__(self, key: str, value: Any):
+        """Set the current value by name.
+
+        Parameters
+        ----------
+        key : str
+            The name of the parameter, i.e., its keyword. The parameter must
+            already be declared.
+        value : Any
+            The value of the parameter.
+
+        Notes
+        -----
+        - This method allows the parameter value to be changed after
+          creation.
+        """
+        if key not in self._dict:
+            raise KeyError(f"Parameter '{key}' is not declared")
+
+        self._assert_value_assign(key, value)
+
+        self._dict[key] = value
+
+    def __len__(self) -> int:
+        """Return the length of the parameter set."""
+        return len(self._declared_parameters)
+
+    def __bool__(self) -> bool:
+        """Return True if one or more parameters are declared."""
+        return self.__len__() > 0
+
+    def __eq__(self, other) -> bool:
+        """Check the equality in value between two instances.
+
+        Parameters
+        ----------
+        other
+            The right-hand side of the operation.
+
+        Notes
+        -----
+        - Only the equality in the constituent parameters is checked.
+        """
+        if not isinstance(other, FunParams):
+            return False
+
+        a = _nested_dicts_equal(self._dict, other._dict)
+        b = _nested_dicts_equal(
+            self._declared_parameters,
+            other._declared_parameters,
+        )
+
+        return a and b
+
+    def __str__(self):
+        if not self:
+            return str(None)
+
+        table = f"Function ID  : {self.function_id}\n"
+        table += f"Parameter ID : {self.parameter_id}\n"
+        desc = textwrap.wrap(self.description, width=57)
+        # Pad new lines
+        if len(desc) > 1:
+            desc[1:] = ["               " + line for line in desc[1:]]
+        desc_joined = "\n".join(desc)
+        table += f"Description  : {desc_joined}\n\n"
+
+        # Get the header names
+        header_names = [name.capitalize() for name in FIELD_NAMES]
+        header_names.insert(0, "No.")
+
+        # Get the values for each field as a list
+        list_values = _get_values_as_list(
+            self._dict,
+            self._declared_parameters,
+        )
+
+        table += tabulate(
+            list_values,
+            headers=header_names,
+            stralign="center",
+            disable_numparse=True,
+        )
+
+        return table
+
+    def as_dict(self):
+        """Return key-value pairs of the parameter set."""
+        return self._dict
+
+    def add(
+        self,
+        keyword: str,
+        value: Any,
+        type: Optional[type] = None,
+        description: Optional[str] = "",
+    ) -> None:
+        """Add a parameter to the parameter set.
+
+        Parameters
+        ----------
+        keyword : str
+            The name of the parameter as it appears in the evaluation function
+            signature; must be a valid Python identifier.
+        value : Any
+            The default value of the parameter.
+        type : Optional[type]
+            The type of the parameter. If given, the type will be used to
+            validate the provided value.
+        description : str, optional
+            The description of the parameter.
+        """
+        if keyword in self._declared_parameters:
+            raise KeyError(f"Parameter '{keyword}' is already defined")
+
+        if not keyword.isidentifier():
+            raise ValueError(
+                f"Parameter '{keyword}' is not a valid identifier"
+            )
+
+        self._declared_parameters[keyword] = {
+            "value": value,
+            "type": type,
+            "description": description,
+        }
+
+        # Verify the value here
+        self._assert_value_set(keyword)
+
+        # Update the underlying dict
+        self._dict[keyword] = value
+
+    def reset_value(self) -> None:
+        """Reset the value of the parameter set to the declared values."""
+        self._dict.clear()
+        for key, value in self._declared_parameters.items():
+            self._dict[key] = value["value"]
+
+    def _assert_value_set(self, keyword: str):
+        """Assert the parameter set when they're added the first time.
+
+        Parameters
+        ----------
+        keyword : str
+            The name of the parameter.
+
+        Notes
+        -----
+        - The type of the value must be consistent with the type (if given).
+        """
+        type_ = self._declared_parameters[keyword]["type"]
+        value = self._declared_parameters[keyword]["value"]
+        if type_ is None:
+            return
+
+        # When a parameter is first added enforce consistency
+        if not isinstance(value, type_):
+            raise TypeError(f"Expected {type_} but got {type(value)}")
+
+    def _assert_value_assign(self, keyword, value):
+        """Assert the parameter set when they're assigned after creation.
+
+        Parameters
+        ----------
+        keyword : str
+            The name of the parameter.
+        value : Any
+            The value of the parameter.
+        Notes
+        -----
+        - If the value and the type are not consistent, a warning (but no
+          exception is raised).
+        """
+        type_ = self._declared_parameters[keyword]["type"]
+        if type_ is None:
+            return
+
+        # When a parameter is assigned, be more forgiving
+        if not isinstance(value, type_):
+            warnings.warn(
+                message="Expected {type_} but got {type(value)}",
+                category=UserWarning,
+                stacklevel=2,
+            )
+
+
+def _nested_dicts_equal(d1, d2) -> bool:
+    """Compare the equality in value between two nested dictionaries.
+
+    Parameters
+    ----------
+    d1
+        The first dictionary.
+    d2
+        The second dictionary
+
+    Returns
+    -------
+    bool
+        True if the dictionaries are equal, False otherwise.
+    """
+    if not isinstance(d1, dict) or not isinstance(d2, dict):
+
+        # Deal specifically with NumPy array values
+        if isinstance(d1, np.ndarray) and isinstance(d2, np.ndarray):
+            return np.array_equal(d1, d2)
+        return d1 == d2  # Compare values directly if not dictionaries
+
+    if d1.keys() != d2.keys():
+        return False  # Different keys
+
+    # Recursively check each key
+    return all(_nested_dicts_equal(d1[key], d2[key]) for key in d1)
+
+
+def _get_values_as_list(
+    current_values: dict,
+    declared_parameters: dict,
+) -> List[List[str]]:
+    """
+
+    Parameters
+    ----------
+    current_values : dict
+        The dictionary of current values.
+    declared_parameters : dict
+        The dictionaries of declared parameters.
+
+    Return
+    ------
+    List[List[str]]
+        List of lists of the relevant data as strings.
+    """
+    list_values = []
+    for i, parameter in enumerate(declared_parameters):
+        # Value
+        value = current_values[parameter]
+        if isinstance(value, np.ndarray):
+            value = f"{value.shape} array"
+        elif isinstance(value, int):
+            value = f"{value:1.0e}"
+        elif isinstance(value, float):
+            value = f"{value:1.5e}"
+        else:
+            value = str(value)
+
+        # Type
+        type_ = declared_parameters[parameter]["type"]
+        if type_ is not None:
+            type_ = type_.__name__
+        else:
+            type_ = "-"
+
+        # Description
+        desc = declared_parameters[parameter]["description"]
+        if desc == "":
+            desc = "-"
+
+        entry = [
+            i + 1,
+            parameter,
+            value,
+            type_,
+            desc,
+        ]
+
+        list_values.append(entry)
+
+    return list_values
diff --git a/src/uqtestfuns/core/prob_input/input_spec.py b/src/uqtestfuns/core/prob_input/input_spec.py
deleted file mode 100644
index fce0baf..0000000
--- a/src/uqtestfuns/core/prob_input/input_spec.py
+++ /dev/null
@@ -1,66 +0,0 @@
-"""
-Module with definition of data structure to hold information on prob. input.
-
-A probabilistic input specification may be stored in a built-in Python
-data type that contains the only information required to construct
-a ``UnivDist`` (for one-dimensional marginals) and ``ProbInput``
-(for probabilistic input models) instances.
-The container type should be free of custom methods and derived from
-``NamedTuple`` class to provide typing information.
-"""
-from typing import Any, Callable, List, NamedTuple, Optional, Tuple, Union
-
-
-__all__ = ["UnivDistSpec", "ProbInputSpecFixDim", "ProbInputSpecVarDim"]
-
-
-class UnivDistSpec(NamedTuple):
-    """A univariate marginal distribution specification.
-
-    Parameters
-    ----------
-    name : str
-        The name of the univariate marginal.
-    distribution : str
-        The name of the distribution of the univariate marginal.
-    parameters : Union[List[Union[int, float]], Tuple[Union[int, float], ...]]
-        Parameter values of the distribution.
-    description : str
-        Short description of the univariate marginal.
-    """
-
-    name: Optional[str]
-    distribution: str
-    parameters: Union[List[Union[int, float]], Tuple[Union[int, float], ...]]
-    description: Optional[str]
-
-
-class ProbInputSpecFixDim(NamedTuple):
-    """All the information to construct a ProbInput w/ a fixed dimension.
-
-    Parameters
-    ----------
-    name : str
-        The name of the probabilistic input model.
-    description : str
-        A short description of the probabilistic input model.
-    marginals : Union[Callable, List[UnivDistSpec]]
-        A list of univariate marginal specifications or a callable
-        to construct the list of marginal specifications.
-    copulas : Optional[Any]
-        The copula specification of the probabilistic input model.
-    """
-
-    name: Optional[str]
-    description: Optional[str]
-    marginals: List[UnivDistSpec]
-    copulas: Optional[Any]
-
-
-class ProbInputSpecVarDim(NamedTuple):
-    """All the information to construct a ProbInput w/ variable dimension."""
-
-    name: Optional[str]
-    description: Optional[str]
-    marginals_generator: Callable[[int], List[UnivDistSpec]]
-    copulas: Optional[Any]
diff --git a/src/uqtestfuns/core/prob_input/univariate_distribution.py b/src/uqtestfuns/core/prob_input/marginal.py
similarity index 81%
rename from src/uqtestfuns/core/prob_input/univariate_distribution.py
rename to src/uqtestfuns/core/prob_input/marginal.py
index 9195b54..28ca890 100644
--- a/src/uqtestfuns/core/prob_input/univariate_distribution.py
+++ b/src/uqtestfuns/core/prob_input/marginal.py
@@ -1,9 +1,11 @@
 """
-Module with an implementation of the ``UnivDist`` class.
+Module with an implementation of the ``Marginal`` class.
 
-The ``UnivDist`` class represents a univariate random variable.
+The ``Marginal`` class represents a one-dimensional marginal random variable
+(i.e., univariate random variable).
 Each random variable has a (parametric) probability distribution.
 """
+
 from __future__ import annotations
 
 import numpy as np
@@ -21,23 +23,22 @@
     get_cdf_values,
     get_icdf_values,
 )
-from .input_spec import UnivDistSpec
 from ...global_settings import ARRAY_FLOAT
 
-__all__ = ["UnivDist"]
+__all__ = ["Marginal"]
 
 # Ordered field names for printing purpose
 FIELD_NAMES = ["name", "distribution", "parameters", "description"]
 
 
 @dataclass(frozen=True)
-class UnivDist:
-    """A class for univariate random variables.
+class Marginal:
+    """A class for one-dimensional marginal random variables.
 
     Parameters
     ----------
     distribution : str
-        The type of the probability distribution
+        The type of the probability distribution.
     parameters : ArrayLike
         The parameters of the chosen probability distribution
     name : str, optional
@@ -93,11 +94,11 @@ def __post_init__(self) -> None:
     def transform_sample(
         self,
         xx: Union[float, np.ndarray],
-        other: UnivDist,
+        other: Marginal,
     ) -> np.ndarray:
         """Transform a sample from a given distribution to another."""
-        if not isinstance(other, UnivDist):
-            raise TypeError("Other instance must be of UnivariateType!")
+        if not isinstance(other, Marginal):
+            raise TypeError("Other instance must be of Marginal type!")
 
         xx_trans = self.cdf(xx)
 
@@ -171,25 +172,3 @@ def icdf(self, xx: Union[float, np.ndarray]) -> ARRAY_FLOAT:
         return get_icdf_values(
             xx, self.distribution, self.parameters, self.lower, self.upper
         )
-
-    @classmethod
-    def from_spec(
-        cls, marginal_spec: UnivDistSpec, rng_seed: Optional[int] = None
-    ):
-        """Create an instance of UnivDist from a marginal specification.
-
-        Parameters
-        ----------
-        marginal_spec : UnivDistSpec
-            The specification for the univariate marginal.
-        rng_seed : int, optional
-            The seed used to initialize the pseudo-random number generator.
-            If not specified, the value is taken from the system entropy.
-        """
-        return cls(
-            distribution=marginal_spec.distribution,
-            parameters=marginal_spec.parameters,
-            name=marginal_spec.name,
-            description=marginal_spec.description,
-            rng_seed=rng_seed,
-        )
diff --git a/src/uqtestfuns/core/prob_input/probabilistic_input.py b/src/uqtestfuns/core/prob_input/probabilistic_input.py
index 9b47230..4becfd5 100644
--- a/src/uqtestfuns/core/prob_input/probabilistic_input.py
+++ b/src/uqtestfuns/core/prob_input/probabilistic_input.py
@@ -3,69 +3,94 @@
 
 The ``ProbInput`` class represents a probabilistic input model.
 Each probabilistic input has a set of one-dimensional marginals each of which
-is defined by an instance of the ``UnivDist`` class.
+is defined by an instance of the ``Marginal`` class.
 """
+
 from __future__ import annotations
 
 import numpy as np
+import textwrap
 
-from dataclasses import dataclass, field
 from numpy.random._generator import Generator
 from tabulate import tabulate
-from typing import Any, List, Optional, Union, Tuple
+from typing import Any, List, Optional, Sequence
 
-from .univariate_distribution import UnivDist, FIELD_NAMES
-from .input_spec import ProbInputSpecFixDim, ProbInputSpecVarDim
+from .marginal import Marginal, FIELD_NAMES
 
 __all__ = ["ProbInput"]
 
 
-@dataclass
 class ProbInput:
     """A class for multivariate input variables.
 
     Parameters
     ----------
-    marginals : Union[List[UnivDist], Tuple[UnivDist, ...]]
+    marginals : Union[List[Marginal], Tuple[Marginal, ...]]
         A list of one-dimensional marginals (univariate random variables).
     copulas : Any
         Copulas between univariate inputs that define dependence structure
         (currently not used).
-    name : str, optional
-        The name of the probabilistic input model.
-    description : str, optional
+    input_id: str, optional
+        The ID of the probabilistic input. If not specified, the value is None.
+    function_id: str, optional
+        The ID of the function associated with the input. If not specified,
+        the value is None.
+    description: str, optional
         The short description regarding the input model.
     rng_seed : int, optional.
         The seed used to initialize the pseudo-random number generator.
         If not specified, the value is taken from the system entropy.
-
-    Attributes
-    ----------
-    spatial_dimension : int
-        Number of constituents (random) input variables.
-    _rng : Generator
-        The default pseudo-random number generator of NumPy.
-        The generator is only created if or when needed (e.g., generating
-        a random sample from the distribution).
     """
 
-    spatial_dimension: int = field(init=False)
-    marginals: Union[List[UnivDist], Tuple[UnivDist, ...]]
-    copulas: Any = None
-    name: Optional[str] = None
-    description: Optional[str] = None
-    rng_seed: Optional[int] = field(default=None, repr=False)
-    _rng: Optional[Generator] = field(init=False, default=None, repr=False)
-
-    def __post_init__(self):
-        self.spatial_dimension = len(self.marginals)
-        # Protect marginals by making it immutable
-        self.marginals = tuple(self.marginals)
+    def __init__(
+        self,
+        marginals: Sequence[Marginal],
+        copulas: Any = None,
+        input_id: Optional[str] = None,
+        function_id: Optional[str] = None,
+        description: Optional[str] = None,
+        rng_seed: Optional[int] = None,
+    ):
+        # Read-only properties
+        self._marginals = marginals
+        self._copulas = copulas
+        # Attributes
+        self.input_id = input_id
+        self.function_id = function_id
+        self.description = description
+        # Other properties
+        self._rng_seed = rng_seed
+        self._rng: Optional[Generator] = None
+
+    @property
+    def input_dimension(self) -> int:
+        """Return the number of constituents (random) input variables."""
+        return len(self._marginals)
+
+    @property
+    def marginals(self) -> Sequence[Marginal]:
+        """Return the sequence of Marginals that define the input variables."""
+        return self._marginals
+
+    @property
+    def copulas(self) -> Any:
+        """Return the underlying Copulas of the probabilistic input."""
+        return self._copulas
+
+    @property
+    def rng_seed(self) -> Optional[int]:
+        """Return the seed for RNG."""
+        return self._rng_seed
+
+    @rng_seed.setter
+    def rng_seed(self, value: Optional[int]):
+        """Set/reset the seed for RNG."""
+        self.reset_rng(self._rng_seed)
 
     def transform_sample(self, xx: np.ndarray, other: ProbInput):
         """Transform a sample from the distribution to another."""
         # Make sure the dimensionality is consistent
-        if self.spatial_dimension != other.spatial_dimension:
+        if self.input_dimension != other.input_dimension:
             raise ValueError(
                 "The dimensionality of the two inputs are not consistent!"
             )
@@ -97,13 +122,13 @@ def get_sample(self, sample_size: int = 1) -> np.ndarray:
         np.ndarray
             The generated sample in an :math:`N`-by-:math:`M` array
             where :math:`N` and :math:`M` are the sample size
-            and the number of spatial dimensions, respectively.
+            and the number of input dimensions, respectively.
         """
         if self._rng is None:  # pragma: no cover
             # Create a pseudo-random number generator (lazy evaluation)
             self._rng = np.random.default_rng(self.rng_seed)
 
-        xx = self._rng.random((sample_size, self.spatial_dimension))
+        xx = self._rng.random((sample_size, self.input_dimension))
         if not self.copulas:
             # Transform the sample in [0, 1] to the domain of the distribution
             for idx_dim, marginal in enumerate(self.marginals):
@@ -113,19 +138,6 @@ def get_sample(self, sample_size: int = 1) -> np.ndarray:
 
         return xx
 
-    def reset_rng(self, rng_seed: Optional[int]) -> None:
-        """Reset the random number generator.
-
-        Parameters
-        ----------
-        rng_seed : int, optional.
-            The seed used to initialize the pseudo-random number generator.
-            If not specified, the value is taken from the system entropy.
-        """
-        rng = np.random.default_rng(rng_seed)
-        object.__setattr__(self, "_rng", rng)
-        object.__setattr__(self, "rng_seed", rng_seed)
-
     def pdf(self, xx: np.ndarray) -> np.ndarray:
         """Get the PDF value of the distribution on a set of values.
 
@@ -150,32 +162,44 @@ def pdf(self, xx: np.ndarray) -> np.ndarray:
 
         return yy
 
-    def __str__(self):
-        table = f"Name         : {self.name}\n"
-        table += f"Spatial Dim. : {self.spatial_dimension}\n"
-        table += f"Description  : {self.description}\n"
-        table += "Marginals    :\n\n"
-
-        # Get the header names
-        header_names = [name.capitalize() for name in FIELD_NAMES]
-        header_names.insert(0, "No.")
-
-        # Get the values for each field as a list
-        list_values = _get_values_as_list(self.marginals, FIELD_NAMES)
-
-        table += tabulate(list_values, headers=header_names, stralign="center")
-
-        table += f"\n\nCopulas      : {self.copulas}"
+    def reset_rng(self, rng_seed: Optional[int]) -> None:
+        """Reset the random number generator.
 
-        return table
+        Parameters
+        ----------
+        rng_seed : int, optional.
+            The seed used to initialize the pseudo-random number generator.
+            If not specified, the value is taken from the system entropy.
+        """
+        rng = np.random.default_rng(rng_seed)
+        self._rng = rng
+        self._rng_seed = rng_seed
 
-    def _repr_html_(self):
-        table = f"<p><b>Name</b>:&nbsp;{self.name}\n</p>"
-        table += (
-            f"<p><b>Spatial Dimension</b>:&nbsp;{self.spatial_dimension}\n</p>"
-        )
-        table += f"<p><b>Description</b>:&nbsp;{self.description}\n</p>"
-        table += "<p><b>Marginals:</b>\n</p>"
+    def __str__(self):
+        """Return human-readable string representation of the instance."""
+        if self.input_id is None or self.input_id == "":
+            input_id = "-"
+        else:
+            input_id = self.input_id
+        if self.function_id is None or self.function_id == "":
+            function_id = "-"
+        else:
+            function_id = self.function_id
+        table = f"Function ID     : {function_id}\n"
+        table += f"Input ID        : {input_id}\n"
+        table += f"Input Dimension : {self.input_dimension}\n"
+
+        # Parse the description column
+        if self.description is None or self.description == "":
+            description = "-"
+        else:
+            desc = textwrap.wrap(self.description, width=57)
+            # Pad new lines
+            if len(desc) > 1:
+                desc[1:] = ["                  " + line for line in desc[1:]]
+            description = "\n".join(desc)
+        table += f"Description     : {description}\n"
+        table += "Marginals       :\n\n"
 
         # Get the header names
         header_names = [name.capitalize() for name in FIELD_NAMES]
@@ -188,48 +212,35 @@ def _repr_html_(self):
             list_values,
             headers=header_names,
             stralign="center",
-            tablefmt="html",
+            disable_numparse=True,
         )
 
-        table += "\n"
-        table += f"<p><b>Copulas</b>:&nbsp;{self.copulas}</p>"
+        # Temporary solution for independence copula
+        copulas = "Independence" if self.copulas is None else self.copulas
+
+        table += f"\n\nCopulas         : {copulas}"
 
         return table
 
-    @classmethod
-    def from_spec(
-        cls,
-        prob_input_spec: Union[ProbInputSpecFixDim, ProbInputSpecVarDim],
-        *,
-        spatial_dimension: Optional[int] = None,
-        rng_seed: Optional[int] = None,
-    ):
-        """Create an instance from a ProbInputSpec instance."""
+    def __repr__(self):
+        """Return the unambiguous string representation of the instance."""
+        class_name = self.__class__.__name__
+        # Get the value of the constructor arguments
+        attrs = {
+            "marginals": self.marginals,
+            "copulas": self.copulas,
+            "input_id": self.input_id,
+            "function_id": self.function_id,
+            "description": self.description,
+            "rng_seed": self.rng_seed,
+        }
+        attrs_str = ", ".join(f"{k}={v!r}" for k, v in attrs.items())
 
-        if isinstance(prob_input_spec, ProbInputSpecVarDim):
-            if spatial_dimension is None:
-                raise ValueError("Spatial dimension must be specified!")
-            marginals_gen = prob_input_spec.marginals_generator
-            marginals_spec = marginals_gen(spatial_dimension)
-        else:
-            marginals_spec = prob_input_spec.marginals
-
-        # Create a list of UnivDist instances representing univariate marginals
-        marginals = [
-            UnivDist.from_spec(marginal) for marginal in marginals_spec
-        ]
-
-        return cls(
-            marginals=marginals,
-            copulas=prob_input_spec.copulas,
-            name=prob_input_spec.name,
-            description=prob_input_spec.description,
-            rng_seed=rng_seed,
-        )
+        return f"{class_name}({attrs_str})"
 
 
 def _get_values_as_list(
-    univ_inputs: Union[List[UnivDist], Tuple[UnivDist, ...]],
+    univ_inputs: Sequence[Marginal],
     field_names: List[str],
 ) -> list:
     """Get the values from each field from a list of UnivariateInput
@@ -252,7 +263,10 @@ def _get_values_as_list(
     for i, marginal in enumerate(univ_inputs):
         values = [i + 1]
         for field_name in field_names:
-            values.append(getattr(marginal, field_name))
+            attr_value = getattr(marginal, field_name)
+            if attr_value is None:
+                attr_value = "-"
+            values.append(attr_value)
         list_values.append(values)
 
     return list_values
diff --git a/src/uqtestfuns/core/prob_input/univariate_distributions/beta.py b/src/uqtestfuns/core/prob_input/univariate_distributions/beta.py
index 3922530..046de41 100644
--- a/src/uqtestfuns/core/prob_input/univariate_distributions/beta.py
+++ b/src/uqtestfuns/core/prob_input/univariate_distributions/beta.py
@@ -19,6 +19,7 @@
 - ``loc`` == ``a``
 - ``scale`` == ``(b-a)``
 """
+
 import numpy as np
 
 from scipy.stats import beta
diff --git a/src/uqtestfuns/core/prob_input/univariate_distributions/exponential.py b/src/uqtestfuns/core/prob_input/univariate_distributions/exponential.py
new file mode 100644
index 0000000..c094f43
--- /dev/null
+++ b/src/uqtestfuns/core/prob_input/univariate_distributions/exponential.py
@@ -0,0 +1,225 @@
+"""
+Module with routines involving the exponential probability distribution.
+
+The exponential distribution in UQTestFuns is parametrized by a single
+parameter: the rate parametere.
+
+The underlying implementation is based on the implementation from scipy.stats.
+In the SciPy convention, the rate parameter corresponds to the reciprocal
+of the ``scale`` parameter.
+"""
+
+import numpy as np
+from scipy.stats import expon
+
+from .utils import verify_param_nums, postprocess_icdf
+from ....global_settings import ARRAY_FLOAT
+
+DISTRIBUTION_NAME = "exponential"
+
+NUM_PARAMS = 1
+
+
+def verify_parameters(parameters: ARRAY_FLOAT) -> None:
+    """Verify the parameters of an exponential distribution.
+
+    Parameters
+    ----------
+    parameters : ARRAY_FLOAT
+        The parameter of an exponential distribution
+        (i.e., the rate parameter).
+
+    Returns
+    ------
+    None
+        The function exits without any return value when nothing is wrong.
+
+    Raises
+    ------
+    ValueError
+        If any of the parameter values are invalid
+        or the shapes are inconsistent.
+    """
+    # Verify overall shape
+    verify_param_nums(parameters.size, NUM_PARAMS, DISTRIBUTION_NAME)
+
+    if parameters[0] <= 0.0:
+        raise ValueError(
+            f"The corresponding rate parameter {parameters[0]}"
+            f"must be larger than 0.0!"
+        )
+
+
+def lower(parameters: ARRAY_FLOAT) -> float:
+    """Get the lower bound of an exponential distribution.
+
+    Parameters
+    ----------
+    parameters : ARRAY_FLOAT
+        The parameter of an exponential distribution.
+
+    Returns
+    -------
+    float
+        The lower bound of the exponential distribution.
+
+    Notes
+    -----
+    - The parameters are not used in determining the lower bound of
+      the distribution; it must, however, appear for interface consistency.
+      The lower bound of a lognormal distribution is finite; it is 0.0.
+    """
+    lower_bound = 0.0
+
+    return lower_bound
+
+
+def upper(parameters: ARRAY_FLOAT) -> float:
+    """Get the upper bound of an exponential distribution.
+
+    Parameters
+    ----------
+    parameters : ARRAY_FLOAT
+        The parameters of an exponential distribution
+
+    Returns
+    -------
+    float
+        The upper bound of the exponential distribution.
+
+    Notes
+    -----
+    - Strictly speaking, an exponential distribution is unbounded on the right.
+      However, for numerical reason an upper bound is set.
+    - The upper bound of the exponential distribution is chosen such that
+      tbe probability mass between the lower and upper bound is at least
+      1 - 1e-15.
+    """
+    # 36.7368005696771 is the quantile values with probability of 1-1e-16
+    # for the exponential distribution with rate parameter value 1.0
+    upper_bound = float(36.7368005696771 / parameters[0])
+
+    return upper_bound
+
+
+def pdf(
+    xx: ARRAY_FLOAT,
+    parameters: ARRAY_FLOAT,
+    lower_bound: float,
+    upper_bound: float,
+) -> ARRAY_FLOAT:
+    """Get the PDF values of an exponential distribution.
+
+    Parameters
+    ----------
+    xx : ARRAY_FLOAT
+        Sample values (realizations) of an exponential distribution.
+    parameters : ARRAY_FLOAT
+        Parameters of the exponential distribution.
+    lower_bound : float
+        Lower bound of the exponential distribution.
+    upper_bound : float
+        Upper bound of the exponential distribution.
+
+    Returns
+    -------
+    np.ndarray
+        PDF values of the exponential distribution on the sample values.
+
+    Notes
+    -----
+    - The PDF for sample with values outside the bounds are set to 0.0.
+    """
+    yy = np.zeros(xx.shape)
+    idx = np.logical_and(xx >= lower_bound, xx <= upper_bound)
+    rate = parameters[0]
+    scale = 1 / rate
+    yy[idx] = expon.pdf(xx[idx], scale=scale)
+
+    return yy
+
+
+def cdf(
+    xx: ARRAY_FLOAT,
+    parameters: ARRAY_FLOAT,
+    lower_bound: float,
+    upper_bound: float,
+) -> ARRAY_FLOAT:
+    """Get the CDF values of an exponential distribution.
+
+    Parameters
+    ----------
+    xx : ARRAY_FLOAT
+        Sample values (realizations) of an exponential distribution.
+    parameters : ARRAY_FLOAT
+        Parameters of the exponential distribution.
+    lower_bound : float
+        Lower bound of the exponential distribution.
+    upper_bound : float
+        Upper bound of the exponential distribution.
+
+    Returns
+    -------
+    ARRAY_FLOAT
+        CDF values of the exponential distribution on the sample values.
+
+    Notes
+    -----
+    - The CDF for sample with values smaller (resp. larger)
+      than the lower bound (resp. upper bound) are set to 0.0 (resp. 1.0).
+    """
+    yy = np.empty(xx.shape)
+    idx_lower = xx < lower_bound
+    idx_upper = xx > upper_bound
+    idx_rest = np.logical_and(
+        np.logical_not(idx_lower), np.logical_not(idx_upper)
+    )
+
+    yy[idx_lower] = 0.0
+    yy[idx_upper] = 1.0
+    rate = parameters[0]
+    scale = 1 / rate
+    yy[idx_rest] = expon.cdf(xx[idx_rest], scale=scale)
+
+    return yy
+
+
+def icdf(
+    xx: ARRAY_FLOAT,
+    parameters: ARRAY_FLOAT,
+    lower_bound: float,
+    upper_bound: float,
+) -> ARRAY_FLOAT:
+    """Get the inverse CDF values of an exponential distribution.
+
+    Parameters
+    ----------
+    xx : ARRAY_FLOAT
+        Sample values (realizations) in the [0, 1] domain.
+    parameters : ARRAY_FLOAT
+        Parameters of an exponential distribution.
+    lower_bound : float
+        Lower bound of the exponential distribution.
+    upper_bound : float
+        Upper bound of the exponential distribution.
+
+    Returns
+    -------
+    ARRAY_FLOAT
+        Transformed values in the domain of the normal distribution.
+
+    Notes
+    -----
+    - ICDF for sample values outside [0.0, 1.0] is set to NaN according to
+      the underlying SciPy implementation.
+    """
+
+    # Compute the ICDF
+    rate = parameters[0]
+    scale = 1 / rate
+    yy = expon.ppf(xx, scale=scale)
+
+    # Check if values are within the set bounds
+    yy = postprocess_icdf(yy, lower_bound, upper_bound)
+
+    return yy
diff --git a/src/uqtestfuns/core/prob_input/univariate_distributions/gumbel.py b/src/uqtestfuns/core/prob_input/univariate_distributions/gumbel.py
index dc9a054..d3d0149 100644
--- a/src/uqtestfuns/core/prob_input/univariate_distributions/gumbel.py
+++ b/src/uqtestfuns/core/prob_input/univariate_distributions/gumbel.py
@@ -10,6 +10,7 @@
 to the ``loc`` parameter, while the parameter ``beta``
 corresponds to the ``scale`` parameter.
 """
+
 import numpy as np
 
 from scipy.stats import gumbel_r
diff --git a/src/uqtestfuns/core/prob_input/univariate_distributions/logitnormal.py b/src/uqtestfuns/core/prob_input/univariate_distributions/logitnormal.py
index 4f49761..fb3fed8 100644
--- a/src/uqtestfuns/core/prob_input/univariate_distributions/logitnormal.py
+++ b/src/uqtestfuns/core/prob_input/univariate_distributions/logitnormal.py
@@ -13,6 +13,7 @@
 to the ``loc`` parameter, while the standard deviation (sigma) corresponds
 to the ``scale`` parameter.
 """
+
 import numpy as np
 from scipy.stats import norm
 from scipy.special import logit
diff --git a/src/uqtestfuns/core/prob_input/univariate_distributions/lognormal.py b/src/uqtestfuns/core/prob_input/univariate_distributions/lognormal.py
index d0b3ea8..1a6acf7 100644
--- a/src/uqtestfuns/core/prob_input/univariate_distributions/lognormal.py
+++ b/src/uqtestfuns/core/prob_input/univariate_distributions/lognormal.py
@@ -17,6 +17,7 @@
 - ``s`` = ``sigma``
 - ``scale`` = ``np.exp(mu)``
 """
+
 import numpy as np
 from scipy.stats import lognorm
 
diff --git a/src/uqtestfuns/core/prob_input/univariate_distributions/normal.py b/src/uqtestfuns/core/prob_input/univariate_distributions/normal.py
index 6ed7c06..ad2a162 100644
--- a/src/uqtestfuns/core/prob_input/univariate_distributions/normal.py
+++ b/src/uqtestfuns/core/prob_input/univariate_distributions/normal.py
@@ -8,6 +8,7 @@
 In the SciPy convention, the mean (mu) corresponds to the ``loc`` parameter,
 while the standard deviation (sigma) corresponds to the ``scale`` parameter.
 """
+
 import numpy as np
 from scipy.stats import norm
 
@@ -68,7 +69,7 @@ def lower(parameters: ARRAY_FLOAT) -> float:
       However, for numerical reason a lower bound is set.
     - The lower bound of the normal distribution is chosen such that
       the probability mass between the lower and upper bound is at least
-      1 - 1e.15.
+      1 - 1e-15.
     """
     # -8.222082216130435 is the quantile values with probability of 1e-16
     # for the standard Normal distribution (mu = 0.0, sigma = 1.0)
diff --git a/src/uqtestfuns/core/prob_input/univariate_distributions/triangular.py b/src/uqtestfuns/core/prob_input/univariate_distributions/triangular.py
index b3ddd49..d48369b 100644
--- a/src/uqtestfuns/core/prob_input/univariate_distributions/triangular.py
+++ b/src/uqtestfuns/core/prob_input/univariate_distributions/triangular.py
@@ -9,6 +9,7 @@
 The density value at the modal value is computed such that the are under
 the distribution is 1.0.
 """
+
 import numpy as np
 
 from .utils import verify_param_nums, postprocess_icdf
diff --git a/src/uqtestfuns/core/prob_input/univariate_distributions/trunc_gumbel.py b/src/uqtestfuns/core/prob_input/univariate_distributions/trunc_gumbel.py
index 71c4c2e..124e51b 100644
--- a/src/uqtestfuns/core/prob_input/univariate_distributions/trunc_gumbel.py
+++ b/src/uqtestfuns/core/prob_input/univariate_distributions/trunc_gumbel.py
@@ -8,6 +8,7 @@
 The underlying implementation is based on the re-implementation of regular
 Gumbel (max.) distribution in UQTestFuns (which in turn, based on scipy.stats).
 """
+
 import numpy as np
 
 from scipy.stats import gumbel_r
diff --git a/src/uqtestfuns/core/prob_input/univariate_distributions/trunc_normal.py b/src/uqtestfuns/core/prob_input/univariate_distributions/trunc_normal.py
index 3b6657e..cbe35fa 100644
--- a/src/uqtestfuns/core/prob_input/univariate_distributions/trunc_normal.py
+++ b/src/uqtestfuns/core/prob_input/univariate_distributions/trunc_normal.py
@@ -32,6 +32,7 @@
 where lb and ub are the desired lower and upper bounds of the truncated
 normal distribution, respectively.
 """
+
 import numpy as np
 
 from scipy.stats import truncnorm
diff --git a/src/uqtestfuns/core/prob_input/univariate_distributions/uniform.py b/src/uqtestfuns/core/prob_input/univariate_distributions/uniform.py
index 2f8cc68..60204da 100644
--- a/src/uqtestfuns/core/prob_input/univariate_distributions/uniform.py
+++ b/src/uqtestfuns/core/prob_input/univariate_distributions/uniform.py
@@ -4,6 +4,7 @@
 The uniform distribution in UQTestFuns is parametrized by two parameters:
 the lower and upper bounds.
 """
+
 import numpy as np
 
 from .utils import postprocess_icdf, verify_param_nums
diff --git a/src/uqtestfuns/core/prob_input/univariate_distributions/utils.py b/src/uqtestfuns/core/prob_input/univariate_distributions/utils.py
index d4ed80c..1df5219 100644
--- a/src/uqtestfuns/core/prob_input/univariate_distributions/utils.py
+++ b/src/uqtestfuns/core/prob_input/univariate_distributions/utils.py
@@ -1,6 +1,7 @@
 """
 Utility module for univariate distribution calculations.
 """
+
 import numpy as np
 
 from ....global_settings import ARRAY_FLOAT
diff --git a/src/uqtestfuns/core/prob_input/utils.py b/src/uqtestfuns/core/prob_input/utils.py
index 524ab53..9e5650a 100644
--- a/src/uqtestfuns/core/prob_input/utils.py
+++ b/src/uqtestfuns/core/prob_input/utils.py
@@ -1,12 +1,14 @@
 """
 Utility module for probabilistic input modeling.
 """
+
 from numpy.typing import ArrayLike
 from types import ModuleType
 from typing import Tuple
 
 from .univariate_distributions import (
     beta,
+    exponential,
     normal,
     gumbel,
     lognormal,
@@ -20,6 +22,7 @@
 
 SUPPORTED_MARGINALS = {
     beta.DISTRIBUTION_NAME: beta,
+    exponential.DISTRIBUTION_NAME: exponential,
     lognormal.DISTRIBUTION_NAME: lognormal,
     normal.DISTRIBUTION_NAME: normal,
     gumbel.DISTRIBUTION_NAME: gumbel,
diff --git a/src/uqtestfuns/core/uqtestfun.py b/src/uqtestfuns/core/uqtestfun.py
index 8f3c59b..4e09295 100644
--- a/src/uqtestfuns/core/uqtestfun.py
+++ b/src/uqtestfuns/core/uqtestfun.py
@@ -2,9 +2,11 @@
 This module contains the concrete implementation of a generic class to
 create a UQ test function in runtime or within a running Python session.
 """
-from typing import Any, Callable, Optional
+
+from typing import Callable, Optional
 
 from .uqtestfun_abc import UQTestFunBareABC
+from .parameters import FunParams
 from .prob_input.probabilistic_input import ProbInput
 
 __all__ = ["UQTestFun"]
@@ -23,22 +25,24 @@ class UQTestFun(UQTestFunBareABC):
     parameters : Any, optional
         The parameters set of the UQ test function.
         If not specified, `None` is used.
-    name : str, optional
-        The name of the UQ test function.
+    function_id : str, optional
+        The ID of the UQ test function.
     """
 
     def __init__(
         self,
         evaluate: Callable,
         prob_input: ProbInput,
-        parameters: Optional[Any] = None,
-        name: Optional[str] = None,
+        parameters: Optional[FunParams] = None,
+        function_id: Optional[str] = None,
     ):
+        if parameters is None:
+            parameters = FunParams()
+
         self._evaluate = evaluate
-        super().__init__(prob_input, parameters, name)
+        super().__init__(prob_input, parameters, function_id)
 
-    def evaluate(self, xx):
-        if self.parameters is not None:
-            return self._evaluate(xx, self.parameters)
+    def _eval(self, xx):
+        return self._evaluate(xx, **self.parameters.as_dict())
 
-        return self._evaluate(xx)
+    evaluate = None  # type: ignore
diff --git a/src/uqtestfuns/core/uqtestfun_abc.py b/src/uqtestfuns/core/uqtestfun_abc.py
index b22e9b7..25ec141 100644
--- a/src/uqtestfuns/core/uqtestfun_abc.py
+++ b/src/uqtestfuns/core/uqtestfun_abc.py
@@ -1,33 +1,48 @@
 """
 This module provides abstract base classes for defining a test function class.
 """
+
 import abc
+from abc import ABC
+
 import numpy as np
 
+from copy import deepcopy
 from inspect import signature
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import Callable, cast, List, Optional, Type
 
+from .prob_input.marginal import Marginal
 from .prob_input.probabilistic_input import ProbInput
-from .prob_input.input_spec import ProbInputSpecFixDim, ProbInputSpecVarDim
+from .parameters import FunParams
+from .custom_typing import (
+    ProbInputSpecs,
+    ProbInputArgs,
+    FunParamSpecs,
+    FunParamsArgs,
+)
 from .utils import create_canonical_uniform_input
 
-__all__ = ["UQTestFunBareABC", "UQTestFunABC"]
+__all__ = [
+    "UQTestFunBareABC",
+    "UQTestFunABC",
+    "UQTestFunFixDimABC",
+    "UQTestFunVarDimABC",
+]
 
 CLASS_HIDDEN_ATTRIBUTES = [
     "_tags",
     "_description",
     "_available_inputs",
     "_available_parameters",
-    "_default_spatial_dimension",
 ]
 
 DEFAULT_DIMENSION = 2
 
 
-class classproperty(property):
+class classproperty(property):  # type: ignore
     """Decorator w/ descriptor to get and set class-level attributes."""
 
-    def __get__(self, owner_self, owner_cls):
+    def __get__(self, owner_self, owner_cls):  # type: ignore
         return self.fget(owner_cls)  # type: ignore
 
     def __set__(self, owner_self, owner_cls):  # pragma: no cover
@@ -43,24 +58,25 @@ class UQTestFunBareABC(abc.ABC):
         The probabilistic input model of the UQ test function.
     parameters : Any, optional
         A set of parameters. By default, it is None.
-    name : str, optional
-        The name of the UQ test function. By default, it is None.
+    function_id : str, optional
+        The ID of the UQ test function. By default, it is None.
 
     Notes
     -----
     - A bare UQ test function only includes the evaluation function,
-      probabilistic input model, parameters, and a (optional) name.
+      probabilistic input model, parameters, and a (optional) ID.
     """
 
     def __init__(
         self,
         prob_input: ProbInput,
-        parameters: Optional[Any] = None,
-        name: Optional[str] = None,
+        parameters: FunParams,
+        function_id: Optional[str] = None,
     ):
         self.prob_input = prob_input
         self._parameters = parameters
-        self._name = name
+        self._function_id = function_id
+        self._output_dimension: Optional[int] = None
 
     @property
     def prob_input(self) -> ProbInput:
@@ -79,24 +95,43 @@ def prob_input(self, value: ProbInput):
             )
 
     @property
-    def parameters(self) -> Any:
+    def parameters(self) -> FunParams:
         """The parameters of the UQ test function."""
         return self._parameters
 
     @parameters.setter
-    def parameters(self, value: Any):
+    def parameters(self, value: FunParams):
         """The setter for the parameters of the test function."""
+        if not isinstance(value, FunParams):
+            raise TypeError(
+                f"Expected a 'FunParams' type! Got instead {type(value)}"
+            )
         self._parameters = value
 
     @property
-    def name(self) -> Optional[str]:
-        """The name of the UQ test function."""
-        return self._name
+    def function_id(self) -> Optional[str]:
+        """The ID of the UQ test function."""
+        return self._function_id
+
+    @property
+    def input_dimension(self) -> int:
+        """The input dimension of the UQ test function."""
+        return self.prob_input.input_dimension
 
     @property
-    def spatial_dimension(self) -> int:
-        """The spatial dimension of the UQ test function."""
-        return self.prob_input.spatial_dimension
+    def output_dimension(self) -> int:
+        if self._output_dimension is None:
+            xx = self.prob_input.get_sample()
+            yy = self(xx)
+            if yy.ndim == 1:
+                self._output_dimension = 1
+            elif yy.ndim == 2:
+                self._output_dimension = yy.shape[1]
+            else:
+                self._output_dimension = yy.shape[1:]
+            return self._output_dimension
+
+        return self._output_dimension
 
     def transform_sample(
         self,
@@ -129,12 +164,12 @@ def transform_sample(
             f"smaller than max. value ({max_value})!"
         )
         # Verify the sampled input
-        _verify_sample_shape(xx, self.spatial_dimension)
+        _verify_sample_shape(xx, self.input_dimension)
         _verify_sample_domain(xx, min_value=min_value, max_value=max_value)
 
         # Create an input in the canonical uniform domain
         uniform_input = create_canonical_uniform_input(
-            self.spatial_dimension, min_value, max_value
+            self.input_dimension, min_value, max_value
         )
 
         # Transform the sampled value to the function domain
@@ -144,8 +179,10 @@ def transform_sample(
 
     def __str__(self):
         out = (
-            f"Name              : {self.name}\n"
-            f"Spatial dimension : {self.spatial_dimension}"
+            f"Function ID      : {self.function_id}\n"
+            f"Input Dimension  : {self.input_dimension}\n"
+            f"Output Dimension : {self.output_dimension}\n"
+            f"Parameterized    : {bool(self.parameters)}"
         )
 
         return out
@@ -153,45 +190,50 @@ def __str__(self):
     def __call__(self, xx):
         """Evaluation of the test function by calling the instance."""
         # Verify the shape of the input
-        _verify_sample_shape(xx, self.spatial_dimension)
+        _verify_sample_shape(xx, self.input_dimension)
 
         # Verify the domain of the input
-        for dim_idx in range(self.spatial_dimension):
+        for dim_idx in range(self.input_dimension):
             lb = self.prob_input.marginals[dim_idx].lower
             ub = self.prob_input.marginals[dim_idx].upper
             _verify_sample_domain(xx[:, dim_idx], min_value=lb, max_value=ub)
 
-        return self.evaluate(xx)
+        return self._eval(xx)
 
+    @staticmethod
     @abc.abstractmethod
-    def evaluate(self, *args):
+    def evaluate(xx: np.ndarray, **kwargs) -> np.ndarray:
         """Abstract method for the implementation of the UQ test function."""
         pass
 
+    def _eval(self, xx) -> np.ndarray:
+        """Actual computation is delegated to evaluate()."""
+        return self.__class__.evaluate(xx, **self.parameters.as_dict())
 
-class UQTestFunABC(UQTestFunBareABC):
+
+class UQTestFunABC(UQTestFunBareABC, ABC):
     """An abstract class for (published) UQ test functions.
 
     Parameters
     ----------
-    spatial_dimension : int, optional
-        The spatial dimension of the UQ test function.
+    input_dimension : int, optional
+        The input dimension of the UQ test function.
         This is used only when the function supports variable dimension;
         otherwise, if specified, an exception is raised.
         In the case of functions with variable dimension, the default dimension
         is set to 2.
         This is a keyword only parameter.
-    prob_input_selection : str, optional
+    input_id : str, optional
         The selection of probabilistic input model; this is used when there are
         multiple input specifications in the literature.
         This is a keyword only parameter.
-    parameters_selection : str, optional
+    parameters_id : str, optional
         The selection of parameters set; this is used when there are multiple
         sets of parameters available in the literature.
         This is a keyword only parameter.
-    name : str, optional
-        The name of the UQ test function.
-        If not given, `None` is used as name.
+    function_id : str, optional
+        The ID of the UQ test function.
+        If not given, `None` is used as the function ID.
         This is a keyword only parameter.
 
     Notes
@@ -204,135 +246,12 @@ class UQTestFunABC(UQTestFunBareABC):
     KeyError
         If selection is not in the list of available inputs and parameters.
     TypeError
-        If spatial dimension is specified for a UQ test function with
+        If input dimension is specified for a UQ test function with
         a fixed dimension.
     """
 
-    _default_input: Optional[str] = None
-    _default_parameters: Optional[str] = None
-
-    def __init__(
-        self,
-        *,
-        spatial_dimension: Optional[int] = None,
-        prob_input_selection: Optional[str] = None,
-        parameters_selection: Optional[str] = None,
-        name: Optional[str] = None,
-    ):
-        # --- Create a probabilistic input model
-        # Select the probabilistic input model
-        available_inputs = self.available_inputs
-        if not prob_input_selection:
-            prob_input_selection = self.default_input
-        if prob_input_selection not in available_inputs:
-            print(prob_input_selection)
-            raise KeyError(
-                "Input selection is not in the available specifications."
-            )
-        prob_input_spec = available_inputs[prob_input_selection]
-
-        # Determine the dimensionality of the test function
-        if isinstance(prob_input_spec, ProbInputSpecFixDim):
-            if spatial_dimension:
-                raise TypeError("Fixed test dimension!")
-        if not spatial_dimension:
-            spatial_dimension = DEFAULT_DIMENSION
-
-        # Create a ProbInput instance
-        if isinstance(prob_input_spec, ProbInputSpecVarDim):
-            prob_input = ProbInput.from_spec(
-                prob_input_spec,
-                spatial_dimension=spatial_dimension,
-            )
-        else:
-            prob_input = ProbInput.from_spec(prob_input_spec)
-
-        # --- Select the parameters set, when applicable
-        available_parameters = self.available_parameters
-        if available_parameters is not None:
-            if not parameters_selection:
-                parameters_selection = self.default_parameters
-            if parameters_selection not in available_parameters:
-                raise KeyError(
-                    "Parameters selection is not in the available sets."
-                )
-            parameters = available_parameters[parameters_selection]
-
-            # If the parameters set is a function of spatial dimension
-            if isinstance(prob_input_spec, ProbInputSpecVarDim):
-                if isinstance(parameters, Callable):  # type: ignore
-                    func_signature = signature(parameters).parameters
-                    if "spatial_dimension" in func_signature:
-                        parameters = parameters(
-                            spatial_dimension=spatial_dimension
-                        )
-
-        else:
-            parameters = None
-
-        # --- Process the default name
-        if name is None:
-            name = self.__class__.__name__
-
-        # --- Initialize the parent class
-        super().__init__(
-            prob_input=prob_input, parameters=parameters, name=name
-        )
-
-    @classmethod
-    def __init_subclass__(cls):
-        """Verify if concrete class has all the required hidden attributes.
-
-        Raises
-        ------
-        NotImplementedError
-            If required attributes are not implemented in the concrete class.
-        ValueError
-            If default input and parameters selections are not specified
-            when there are multiple of them.
-        KeyError
-            If the selections for the default input and parameters set are
-            not available.
-        """
-        for class_hidden_attribute in CLASS_HIDDEN_ATTRIBUTES:
-            # Some class attributes must be specified
-            if not hasattr(cls, class_hidden_attribute):
-                raise NotImplementedError(
-                    f"Class {cls} lacks required {class_hidden_attribute!r} "
-                    f"class attribute."
-                )
-
-        # Parse default input selection
-        if cls.default_input:
-            if cls.default_input not in cls.available_inputs:
-                raise KeyError("Input selection is not available!")
-        else:
-            if len(cls.available_inputs) > 1:
-                raise ValueError(
-                    "There are multiple available input specifications, "
-                    "the default input selection must be specified!"
-                )
-            else:
-                # If only one is available, use it without being specified
-                cls._default_input = list(cls.available_inputs.keys())[0]
-
-        # Parse default parameters set selection
-        if cls.available_parameters:
-            if cls.default_parameters:
-                if cls.default_parameters not in cls.available_parameters:
-                    raise KeyError("Parameters selection is not available!")
-
-            else:
-                if len(cls.available_parameters) > 1:
-                    raise ValueError(
-                        "There are multiple available parameters sets, "
-                        "the default input selection must be specified!"
-                    )
-                else:
-                    # If only one is available, use it without being specified
-                    cls._default_parameters = list(
-                        cls.available_parameters.keys()
-                    )[0]
+    _default_input_id: Optional[str] = None
+    _default_parameters_id: Optional[str] = None
 
     @classproperty
     def tags(cls) -> List[str]:
@@ -340,64 +259,443 @@ def tags(cls) -> List[str]:
         return cls._tags  # type: ignore
 
     @classproperty
-    def available_inputs(
-        cls,
-    ) -> Union[Dict[str, ProbInputSpecFixDim], Dict[str, ProbInputSpecVarDim]]:
+    def available_inputs(cls) -> ProbInputSpecs:
         """All the keys to the available probabilistic input specifications."""
         return cls._available_inputs  # type: ignore
 
     @classproperty
-    def default_input(cls) -> Optional[str]:
+    def default_input_id(cls) -> Optional[str]:
         """The key to the default probabilistic input specification."""
-        return cls._default_input  # type: ignore
+        return cls._default_input_id  # type: ignore
 
     @classproperty
-    def available_parameters(cls) -> Optional[Dict[str, Any]]:
+    def available_parameters(cls) -> Optional[FunParamSpecs]:
         """All the keys to the available set of parameter values."""
         return cls._available_parameters  # type: ignore
 
     @classproperty
-    def default_parameters(cls) -> Optional[str]:
+    def default_parameters_id(cls) -> Optional[str]:
         """The key to the default set of parameters."""
-        return cls._default_parameters  # type: ignore
-
-    @classproperty
-    def default_spatial_dimension(cls) -> Optional[int]:
-        """To store the default dimension of a test function."""
-        return cls._default_spatial_dimension  # type: ignore
+        return cls._default_parameters_id  # type: ignore
 
     @classproperty
     def description(cls) -> Optional[str]:
         """Short description of the UQ test function."""
         return cls._description  # type: ignore
 
+    @property
+    @abc.abstractmethod
+    def variable_dimension(self):
+        pass
+
     def __str__(self):
+        if self.variable_dimension:
+            input_dimension = f"{self.input_dimension} (variable)"
+        else:
+            input_dimension = f"{self.input_dimension} (fixed)"
+        tags = ", ".join(self.tags)
         out = (
-            f"Name              : {self.name}\n"
-            f"Spatial dimension : {self.spatial_dimension}\n"
-            f"Description       : {self.description}"
+            f"Function ID      : {self.function_id}\n"
+            f"Input Dimension  : {input_dimension}\n"
+            f"Output Dimension : {self.output_dimension}\n"
+            f"Parameterized    : {bool(self.parameters)}\n"
+            f"Description      : {self.description}\n"
+            f"Applications     : {tags}"
         )
 
         return out
 
-    def evaluate(self, xx):
-        """Concrete implementation, actual function is delegated to eval_()."""
-        if self.parameters is None:
-            return self.__class__.eval_(xx)
+    def _verify_input_id(self, input_id: Optional[str]) -> str:
+        """Verify the 'input_id' argument.
+
+        Parameters
+        ----------
+        input_id : Optional[str]
+            The ID of the probabilistic input specification.
+
+        Returns
+        -------
+        str
+            The verified ID of the probabilistic input specification.
+        """
+        # --- Verify the input
+        if input_id is None:
+            input_id = self.default_input_id
+            input_id = cast(str, input_id)
+
+        available_inputs = self.available_inputs
+        if input_id not in available_inputs:
+            raise KeyError(
+                f"Input ID {input_id} is not in the available "
+                f"specifications {list(available_inputs.keys())}"
+            )
+
+        return input_id
+
+    def _verify_parameters_id(self, parameters_id: Optional[str]) -> str:
+        """Verify the 'parameters_id' argument.
+
+        Parameters
+        ----------
+        parameters_id : Optional[str]
+            The ID of the function parameters specification.
+
+        Returns
+        -------
+        str
+            The verified ID of the function parameters specification.
+        """
+        available_parameters = self.available_parameters
+
+        if self.available_parameters is None and parameters_id is not None:
+            raise ValueError("No parameters are available")
+
+        if self.available_parameters is None:
+            return ""
+
+        if parameters_id is None:
+            parameters_id = self.default_parameters_id
+            parameters_id = cast(str, parameters_id)
+
+        if parameters_id not in available_parameters:
+            raise KeyError(
+                f"Input ID {parameters_id} is not in the available "
+                f"specifications {list(available_parameters.keys())}"
+            )
+
+        return parameters_id
+
+
+class UQTestFunFixDimABC(UQTestFunABC, ABC):
+    """An abstract class for (published) fixed-dimension UQ test functions.
+
+    Parameters
+    ----------
+    input_id : str, optional
+        The selection of probabilistic input model; this is used when there are
+        multiple input specifications in the literature.
+        This is a keyword only parameter.
+    parameters_id : str, optional
+        The selection of parameters set; this is used when there are multiple
+        sets of parameters available in the literature.
+        This is a keyword only parameter.
+    function_id : str, optional
+        The ID of the UQ test function.
+        If not given, `None` is used as the function ID.
+        This is a keyword only parameter.
+
+    Notes
+    -----
+    - A published UQ test function includes a couple of additional metadata,
+      namely tags and description.
+
+    Raises
+    ------
+    KeyError
+        If selection is not in the list of available inputs and parameters.
+    TypeError
+        If input dimension is specified for a UQ test function with
+        a fixed dimension.
+    """
+
+    def __init__(
+        self,
+        *,
+        input_id: Optional[str] = None,
+        parameters_id: Optional[str] = None,
+        function_id: Optional[str] = None,
+    ):
+        # --- Create a probabilistic input model
+        input_id = self._verify_input_id(input_id)
+        prob_input = self._create_prob_input(input_id)
+
+        # --- Create a set of function parameters
+        parameters_id = self._verify_parameters_id(parameters_id)
+        fun_params = self._create_fun_params(parameters_id)
+
+        # --- Process the default ID
+        if function_id is None:
+            function_id = self.__class__.__name__
+
+        # --- Initialize the parent class
+        super().__init__(
+            prob_input=prob_input,
+            parameters=fun_params,
+            function_id=function_id,
+        )
+
+    def __init_subclass__(cls, **kwargs):
+        """Verify if concrete class has all the required hidden attributes."""
+        _init_subclass(cls)
+
+    @property
+    def variable_dimension(self) -> bool:
+        """Return ``False`` due to fixed dimension function."""
+        return False
+
+    def _create_prob_input(self, input_id: str) -> ProbInput:
+        """Create an instance of probabilistic input.
+
+        Parameters
+        ----------
+        input_id : str
+            The ID of the available probabilistic input models.
+
+        Returns
+        -------
+        ProbInput
+            The probabilistic input model based on the selected ID.
+        """
+        # Get the input (copy to avoid mutation)
+        raw_data = deepcopy(self.available_inputs[input_id])
+
+        # Process the marginals
+        marginals = []
+        for marginal in raw_data["marginals"]:
+            marginals.append(Marginal(**marginal))
+
+        # Recast the type to satisfy type checker
+        input_data = cast(ProbInputArgs, raw_data)
+        input_data["input_id"] = input_id
+        input_data["marginals"] = marginals
+
+        return ProbInput(**input_data)
+
+    def _create_fun_params(self, parameters_id: Optional[str]) -> FunParams:
+        """Create an instance of function parameters.
+
+        Parameters
+        ----------
+        parameters_id : str
+            The ID of the available function parameters.
+
+        Returns
+        -------
+        FunParams
+            The set of function parameters based on the selected ID.
+        """
+        if self.available_parameters is None:
+            return FunParams()
+
+        # Must be deep-copied due to modification
+        param_data = deepcopy(self.available_parameters[parameters_id])
+
+        # Prepare the dictionary as input
+        param_data = cast(FunParamsArgs, param_data)  # for type checker
+        param_data["parameter_id"] = parameters_id
+
+        return FunParams(**param_data)
+
+
+class UQTestFunVarDimABC(UQTestFunABC, ABC):
+    """An abstract class for (published) variable-dimension UQ test functions.
+
+    Parameters
+    ----------
+    input_dimension : int, optional
+        The input dimension of the UQ test function.
+        This is used only when the function supports variable dimension;
+        otherwise, if specified, an exception is raised.
+        In the case of functions with variable dimension, the default dimension
+        is set to 2.
+    input_id : str, optional
+        The selection of probabilistic input model; this is used when there are
+        multiple input specifications in the literature.
+        This is a keyword only parameter.
+    parameters_id : str, optional
+        The selection of parameters set; this is used when there are multiple
+        sets of parameters available in the literature.
+        This is a keyword only parameter.
+    function_id : str, optional
+        The ID of the UQ test function.
+        If not given, `None` is used as the function ID.
+        This is a keyword only parameter.
+
+    Notes
+    -----
+    - A published UQ test function includes a couple of additional metadata,
+      namely tags and description.
+
+    Raises
+    ------
+    KeyError
+        If selection is not in the list of available inputs and parameters.
+    TypeError
+        If input dimension is specified for a UQ test function with
+        a fixed dimension.
+    """
+
+    def __init__(
+        self,
+        input_dimension: int = DEFAULT_DIMENSION,
+        *,
+        input_id: Optional[str] = None,
+        parameters_id: Optional[str] = None,
+        function_id: Optional[str] = None,
+    ):
+        # --- Create a probabilistic input model
+        input_id = self._verify_input_id(input_id)
+        prob_input = self._create_prob_input(input_id, input_dimension)
+
+        # --- Select the parameters set, when applicable
+        parameters_id = self._verify_parameters_id(parameters_id)
+        fun_params = self._create_fun_params(parameters_id, input_dimension)
+
+        # --- Process the default ID
+        if function_id is None:
+            function_id = self.__class__.__name__
+
+        # --- Initialize the parent class
+        super().__init__(
+            prob_input=prob_input,
+            parameters=fun_params,
+            function_id=function_id,
+        )
+
+    def __init_subclass__(cls, **kwargs):
+        """Verify if concrete class has all the required hidden attributes."""
+        _init_subclass(cls)
+
+    @property
+    def variable_dimension(self) -> bool:
+        return True
+
+    def _create_prob_input(self, input_id: str, input_dim: int) -> ProbInput:
+        """Create an instance of probabilistic input model.
+
+        Parameters
+        ----------
+        input_id : str
+            The ID of the available probabilistic input models.
+        input_dim : int
+            The input dimension of the UQ test function.
+
+        Returns
+        -------
+        ProbInput
+            The probabilistic input model based on the selected ID.
+        """
+
+        # Get the input
+        raw_data = deepcopy(self.available_inputs[input_id])
+
+        # Process the marginals
+        marginal = raw_data["marginals"]
+        if callable(marginal):
+            # Marginal specification is given as a function
+            marginals = marginal(input_dim)
+            marginals = [Marginal(**marginal) for marginal in marginals]
         else:
-            return self.__class__.eval_(xx, self.parameters)
+            # Marginal specification will be spawned up to the # of dimension
+            marginals = []
+            for i in range(input_dim):
+                marginal = raw_data["marginals"][0].copy()
+                marginal["name"] = f"{marginal['name']}{i}"
+                marginals.append(Marginal(**marginal))
 
-    @staticmethod
-    @abc.abstractmethod
-    def eval_(*args):  # pragma: no cover
-        """Static method for the concrete function implementation.
+        # Recast the type to satisfy type checker
+        input_data = cast(ProbInputArgs, raw_data)
+        input_data["input_id"] = input_id
+        input_data["marginals"] = marginals
+
+        return ProbInput(**input_data)
+
+    def _create_fun_params(
+        self,
+        parameters_id: Optional[str],
+        input_dim: int,
+    ) -> FunParams:
+        """Create an instance of function parameters.
+
+        Parameters
+        ----------
+        parameters_id : str
+            The ID of the available function parameters.
+        input_dim : int
+            The input dimension of the UQ test function.
 
-        Notes
-        -----
-        - The function evaluation is implemented as a static method so the
-          function can be added without being bounded to the instance.
+        Returns
+        -------
+        FunParams
+            The set of function parameters based on the selected ID.
         """
-        pass
+
+        if self.available_parameters is None:
+            return FunParams()
+
+        # Must be deep-copied due to modification
+        param_data = deepcopy(self.available_parameters[parameters_id])
+
+        # Prepare the dictionary as input
+        param_data = cast(FunParamsArgs, param_data)  # for type checker
+        param_data["parameter_id"] = parameters_id
+
+        # Deal with variable dimension parameter
+        declared_parameters = param_data["declared_parameters"]
+        for declared_parameter in declared_parameters:
+            value = declared_parameter["value"]
+            if isinstance(value, Callable):  # type: ignore
+                func_signature = signature(value).parameters
+                if "input_dimension" in func_signature:
+                    parameter_value = value(input_dimension=input_dim)
+                    declared_parameter["value"] = parameter_value
+
+        return FunParams(**param_data)
+
+
+def _init_subclass(cls: Type[UQTestFunABC]):
+    """Verify if concrete class has all the required hidden attributes.
+
+    Raises
+    ------
+    NotImplementedError
+        If required attributes are not implemented in the concrete class.
+    ValueError
+        If default input and parameters selections are not specified
+        when there are multiple of them.
+    KeyError
+        If the selections for the default input and parameters set are
+        not available.
+    """
+    for class_hidden_attribute in CLASS_HIDDEN_ATTRIBUTES:
+        # Some class attributes must be specified
+        if not hasattr(cls, class_hidden_attribute):
+            raise NotImplementedError(
+                f"Class {cls} lacks required {class_hidden_attribute!r} "
+                f"class attribute."
+            )
+
+    # Parse default input selection
+    if cls.default_input_id:
+        if cls.default_input_id not in cls.available_inputs:
+            raise KeyError("Input selection is not available!")
+    else:
+        if len(cls.available_inputs) > 1:
+            raise ValueError(
+                "There are multiple available input specifications, "
+                "the default input selection must be specified!"
+            )
+        else:
+            # If only one is available, use it without being specified
+            cls._default_input_id = list(cls.available_inputs.keys())[0]
+
+    # Parse default parameters set selection
+    if cls.available_parameters:
+        if cls.default_parameters_id:
+            if cls.default_parameters_id not in cls.available_parameters:
+                raise KeyError("Parameters selection is not available!")
+
+        else:
+            if len(cls.available_parameters) > 1:
+                raise ValueError(
+                    "There are multiple available parameters sets, "
+                    "the default input selection must be specified!"
+                )
+            else:
+                # If only one is available, use it without being specified
+                cls._default_parameters_id = list(
+                    cls.available_parameters.keys()
+                )[0]
 
 
 def _verify_sample_shape(xx: np.ndarray, num_cols: int):
@@ -407,7 +705,7 @@ def _verify_sample_shape(xx: np.ndarray, num_cols: int):
     ----------
     xx : np.ndarray
         Array of sampled input values with a shape of N-by-M, where N is
-        the number of realizations and M is the spatial dimension.
+        the number of realizations and M is the input dimension.
     num_cols : int
         The expected number of columns in the input.
 
@@ -431,7 +729,7 @@ def _verify_sample_domain(xx: np.ndarray, min_value: float, max_value: float):
     ----------
     xx : np.ndarray
         Array of sampled input values with a shape of N-by-M, where N is
-        the number of realizations and M is the spatial dimension.
+        the number of realizations and M is the input dimension.
     min_value : float
         The minimum value of the domain.
     max_value : float
diff --git a/src/uqtestfuns/core/utils.py b/src/uqtestfuns/core/utils.py
index d05ee28..432bf9c 100644
--- a/src/uqtestfuns/core/utils.py
+++ b/src/uqtestfuns/core/utils.py
@@ -1,18 +1,19 @@
 """
 Utility module for all the UQ test functions.
 """
-from .prob_input.univariate_distribution import UnivDist
+
+from .prob_input.marginal import Marginal
 from .prob_input.probabilistic_input import ProbInput
 
 
 def create_canonical_uniform_input(
-    spatial_dimension: int, min_value: float, max_value: float
+    input_dimension: int, min_value: float, max_value: float
 ) -> ProbInput:
     """Create a MultivariateInput in a canonical domain of [-1, 1]^M.
 
     Parameters
     ----------
-    spatial_dimension : int
+    input_dimension : int
         The requested number of dimension.
     min_value : float, optional
         The minimum value of the domain.
@@ -28,9 +29,9 @@ def create_canonical_uniform_input(
 
     marginals = []
 
-    for i in range(spatial_dimension):
+    for i in range(input_dimension):
         marginals.append(
-            UnivDist(
+            Marginal(
                 name=f"X{i+1}",
                 distribution="uniform",
                 parameters=[min_value, max_value],
diff --git a/src/uqtestfuns/helpers.py b/src/uqtestfuns/helpers.py
index d045bf9..bd167e4 100644
--- a/src/uqtestfuns/helpers.py
+++ b/src/uqtestfuns/helpers.py
@@ -6,6 +6,7 @@
 - High-level functions typically contain many guardrails against invalidity
   of the input arguments.
 """
+
 from tabulate import tabulate as tbl  # 'tabulate' is used as a parameter name
 from .utils import get_available_classes, SUPPORTED_TAGS
 from . import test_functions
@@ -16,26 +17,65 @@
 __all__ = ["list_functions"]
 
 
+HEADER: dict = {
+    "input_dim": {
+        "header_name": "# Input",
+        "colalign": "center",
+        "maxcolwidth": None,
+    },
+    "output_dim": {
+        "header_name": "# Output",
+        "colalign": "center",
+        "maxcolwidth": None,
+    },
+    "parameterized": {
+        "header_name": "Param.",
+        "colalign": "center",
+        "maxcolwidth": None,
+    },
+    "tags": {
+        "header_name": "Application",
+        "colalign": "center",
+        "maxcolwidth": 20,
+    },
+    "description": {
+        "header_name": "Description",
+        "colalign": "left",
+        "maxcolwidth": 30,
+    },
+}
+
+
 def list_functions(
-    spatial_dimension: Optional[Union[str, int]] = None,
+    input_dimension: Optional[Union[str, int]] = None,
     tag: Optional[str] = None,
+    output_dimension: Optional[int] = None,
+    parameterized: Optional[bool] = None,
     tabulate: bool = True,
-) -> Optional[List[UQTestFunABC]]:
+    tablefmt: str = "grid",
+) -> Optional[Union[List[UQTestFunABC], str]]:
     """List of all the available functions.
 
     Parameters
     ----------
-    spatial_dimension : Optional[Union[str, int]]
-        Filter based on the number of spatial dimension.
+    input_dimension : Optional[Union[str, int]]
+        Filter based on the number of input dimension.
         For variable dimension (i.e., M-dimensional test functions),
         use the string "M".
     tag : Optional[str]
         Filter based on the application tag.
         Supported tags: "metamodeling", "sensitivity", "optimization",
         "reliability".
+    output_dimension : Optional[Union[str, int]]
+        Filter based on the number of output dimension.
+    parameterized : bool, optional
+        Filter based on whether the test function is parameterized.
     tabulate : bool, optional
         The flag whether to print a table on the console or a list
         of the available functions (each in fully-qualified class name).
+    tablefmt : str, optional
+        Format of the table output; use "html" to return a table in HTML
+        format nicely rendered in Jupyter notebook.
 
     Returns
     -------
@@ -46,176 +86,132 @@ def list_functions(
 
     Notes
     -----
-    - When both ``spatial_dimension`` and ``tag`` are specified the results are
+    - When both ``input_dimension`` and ``tag`` are specified the results are
       intersected. Entries that satisfy both are returned.
     """
 
     # --- Parse input arguments
-    _verify_input_args(spatial_dimension, tag, tabulate)
+    _verify_input_args(
+        input_dimension, tag, output_dimension, parameterized, tabulate
+    )
 
-    # --- Get all the available classes that implement the test functions
-    available_classes = get_available_classes(test_functions)
-    available_classes_dict = dict(available_classes)
+    # --- Parse the module-level data
+    data = _parse_modules_data(test_functions)
 
-    # --- Filter according to the requested spatial dimension
-    if spatial_dimension:
-        available_classes_from_dimension = _get_functions_from_dimension(
-            available_classes_dict, spatial_dimension
-        )
-    else:
-        available_classes_from_dimension = list(available_classes_dict.keys())
+    # --- Filter based on the input dimension
+    data = _filter_on_input_dim(data, input_dimension)
 
-    # --- Filter according to the requested tag
-    if tag:
-        available_classes_from_tag = _get_functions_from_tag(
-            available_classes_dict, tag.lower()
-        )
-    else:
-        available_classes_from_tag = list(available_classes_dict.keys())
+    # --- Filter based on the output dimension
+    data = _filter_on_output_dim(data, output_dimension)
 
-    # --- Combine the results of both filters to obtain the final list
-    available_class_names = set(available_classes_from_dimension).intersection(
-        set(available_classes_from_tag)
-    )
+    # --- Filter based on parameterization
+    data = _filter_on_parameterized(data, parameterized)
 
-    if not available_class_names:
-        return None
-
-    constructors = []
+    # --- Filter based on the tags
+    data = _filter_on_tag(data, tag)
 
     # --- When asked, immediately return all the fully-qualified class name
     if not tabulate:
-        for available_class_name in available_class_names:
-            constructor = available_classes_dict[available_class_name]
-            constructors.append(constructor)
-
-        return constructors
-
-    # --- Create a tabulated view of the list
-    if tag is None:
-        header_names = [
-            "No.",
-            "Constructor",
-            "Dimension",
-            "Application",
-            "Description",
-        ]
-    else:
-        header_names = [
-            "No.",
-            "Constructor",
-            "Dimension",
-            "Description",
+        constructors = [
+            data[k]["full_path"] for k in sorted(list(data.keys()))
         ]
+        return constructors
 
-    values = []
-    for idx, available_class_name in enumerate(
-        sorted(list(available_class_names))
-    ):
-        available_class = available_classes_dict[available_class_name]
-
-        if not available_class.default_spatial_dimension:
-            default_spatial_dimension = "M"
-        else:
-            default_spatial_dimension = (
-                available_class.default_spatial_dimension
-            )
+    # --- Get the arguments for tabulate
+    print_attribs, header_names, colalign, maxcolwidth = _get_table_formatting(
+        input_dimension,
+        tag,
+        output_dimension,
+        parameterized,
+    )
 
-        description = available_class.description
-
-        if tag is None:
-            tags = ", ".join(available_class.tags)
-            value = [
-                idx + 1,
-                f"{available_class_name}()",
-                f"{default_spatial_dimension}",
-                tags,
-                f"{description}",
-            ]
-        else:
-            value = [
-                idx + 1,
-                f"{available_class_name}()",
-                f"{default_spatial_dimension}",
-                f"{description}",
-            ]
+    values = _create_list_values(data, print_attribs)
 
-        values.append(value)
+    if len(values) == 0:
+        return None
 
-    if tag is None:
+    if tablefmt == "html":
+        colalign[-1] = "center"
         table = tbl(
             values,
             headers=header_names,
-            stralign="center",
-            colalign=("center", "center", "center", "center", "left"),
+            tablefmt=tablefmt,
+            colalign=colalign,
+            maxcolwidths=maxcolwidth,
         )
+        return table
     else:
         table = tbl(
             values,
             headers=header_names,
-            stralign="center",
-            colalign=("center", "center", "center", "left"),
+            tablefmt=tablefmt,
+            colalign=colalign,
+            maxcolwidths=maxcolwidth,
         )
-
-    print(table)
+        print(table)
 
     return None
 
 
 def _verify_input_args(
-    spatial_dimension: Optional[Union[str, int]] = None,
+    input_dimension: Optional[Union[str, int]] = None,
     tag: Optional[str] = None,
+    output_dimension: Optional[int] = None,
+    parameterized: Optional[bool] = None,
     tabulate: bool = True,
 ) -> None:
     """Verify the input arguments.
 
     Parameters
     ----------
-    spatial_dimension : Optional[Union[str, int]]
-        The number of spatial dimension to filter the list.
+    input_dimension : Optional[Union[str, int]]
+        The number of input dimension to filter the list of test functions.
         For variable dimension (i.e., M-dimensional test functions),
         use the string "M".
     tag : Optional[str]
-        The application tag to filter the list.
+        The application tag to filter the list of test functions.
         Supported tags: "metamodeling", "sensitivity", "optimization",
         "reliability".
+    output_dimension : int, optional
+        The number of output dimension to filter the list of test functions.
+    parameterized : bool, optional
+        The flag based on whether the test function is parameterized to filter
+        the list of test functions.
     tabulate : bool, optional
         The flag whether to print a table on the console or a list
         of the available functions (each in fully-qualified class name).
 
-    Returns
-    ------
-    None
-        The function exits without any return value when nothing is wrong.
-
     Raises
     ------
     ValueError
-        If ``spatial_dimension`` is not a positive integer or the string "M".
+        If ``input_dimension`` is not a positive integer or the string "M".
         If ``tag`` is not one of the supported tags.
+        If ``output_dimension`` is not a positive integer.
     TypeError
-        If ``spatial_dimension`` is not either an integer, string, or NoneType.
+        If ``input_dimension`` is not either an integer, string, or NoneType.
         If ``tag`` is not a string.
+        If ``output_dimension`` is not an integer, string, or NoneType.
+        If ``parameterized`` is not a bool.
         If ``tabulate`` is not a bool.
     """
-    # --- Parse 'spatial_dimension'
-    if not isinstance(spatial_dimension, (int, str, type(None))):
+    # --- Parse 'input_dimension'
+    if not isinstance(input_dimension, (int, str, type(None))):
         raise TypeError(
-            f"Invalid type for spatial dimension! "
+            f"Invalid type for input dimension! "
             f"Expected either an integer or a string. "
-            f"Got instead {type(spatial_dimension)}."
+            f"Got instead {type(input_dimension)}."
         )
-    if spatial_dimension is not None and isinstance(spatial_dimension, str):
-        if spatial_dimension.lower() != "m":
+    if input_dimension is not None and isinstance(input_dimension, str):
+        if input_dimension.lower() != "m":
             raise ValueError(
-                f"Invalid value ({spatial_dimension}) for spatial dimension! "
+                f"Invalid value ({input_dimension}) for input dimension! "
                 f"Either a positive integer or 'M' to indicate "
                 f"a variable-dimension test function."
             )
-    if spatial_dimension is not None and isinstance(spatial_dimension, int):
-        if spatial_dimension <= 0:
+    if input_dimension is not None and isinstance(input_dimension, int):
+        if input_dimension <= 0:
             raise ValueError(
-                f"Invalid value ({spatial_dimension}) for spatial dimension! "
+                f"Invalid value ({input_dimension}) for input dimension! "
                 f"Either a positive integer or 'M' to indicate "
                 f"a variable-dimension test function."
             )
@@ -228,6 +224,27 @@ def _verify_input_args(
             f"Tag {tag!r} is not supported. Use one of {SUPPORTED_TAGS}!"
         )
 
+    # --- Parse 'input_dimension'
+    if not isinstance(output_dimension, (int, type(None))):
+        raise TypeError(
+            f"Invalid type for output dimension! "
+            f"Expected either an integer or a string. "
+            f"Got instead {type(output_dimension)}."
+        )
+    if output_dimension is not None:
+        if output_dimension <= 0:
+            raise ValueError(
+                f"Invalid value ({output_dimension}) for output dimension! "
+                f"Must be a positive integer."
+            )
+
+    # --- Parse 'parameterized'
+    if not isinstance(parameterized, (bool, type(None))):
+        raise TypeError(
+            f"'parameterized' argument must be of bool type! "
+            f"Got {type(parameterized)}."
+        )
+
     # --- Parse 'tabulate'
     if not isinstance(tabulate, (bool, type(None))):
         raise TypeError(
@@ -235,44 +252,123 @@ def _verify_input_args(
         )
 
 
-def _get_functions_from_dimension(
-    available_classes: dict,
-    spatial_dimension: Union[int, str],
-) -> List[str]:
-    """Get the function keys that satisfy the spatial dimension filter."""
-    values = []
+def _filter_on_input_dim(data, input_dimension):
+    """Filter the dictionary of test functions data based on the input dim."""
+    if input_dimension is not None:
+        if isinstance(input_dimension, str):
+            input_dimension = input_dimension.upper()
+        data = {
+            k: v for k, v in data.items() if v["input_dim"] == input_dimension
+        }
 
-    # --- Make sure to check against a lower-case string
-    if isinstance(spatial_dimension, str):
-        spatial_dimension = spatial_dimension.lower()
+    return data
 
-    for (
-        available_class_name,
-        available_class_path,
-    ) in available_classes.items():
-        default_spatial_dimension = (
-            available_class_path.default_spatial_dimension
-        )
-        if not default_spatial_dimension:
-            default_spatial_dimension = "m"
 
-        if default_spatial_dimension == spatial_dimension:
-            values.append(available_class_name)
+def _filter_on_output_dim(data, output_dimension):
+    """Filter the dictionary of test functions data based on the output dim."""
+    if output_dimension is not None:
+        data = {
+            k: v
+            for k, v in data.items()
+            if v["output_dim"] == output_dimension
+        }
 
-    return values
+    return data
 
 
-def _get_functions_from_tag(available_classes: dict, tag: str) -> List[str]:
-    """Get the function keys that satisfy the tag filter."""
-    values = []
+def _filter_on_parameterized(data, parameterized):
+    """Filter the dictionary of test functions data based on the param."""
+    if parameterized is not None:
+        data = {
+            k: v
+            for k, v in data.items()
+            if v["parameterized"] is parameterized
+        }
+
+    return data
+
 
-    for (
-        available_class_name,
-        available_class_path,
-    ) in available_classes.items():
-        tags = available_class_path.tags
+def _filter_on_tag(data, tag):
+    """Filter the dictionary of test functions data based on the tag."""
+    if tag is not None:
+        data = {k: v for k, v in data.items() if tag in v["tags"]}
 
-        if tag in tags:
-            values.append(available_class_name)
+    return data
+
+
+def _create_list_values(data, print_attribs):
+    """Get the selected values from dictionary test functions data."""
+    values = []
+    for i, function_name in enumerate(sorted(list(data.keys()))):
+        values_tmp = [i + 1, data[function_name]["constructor"]]
+        for print_attrib in print_attribs:
+            values_tmp.append(data[function_name][print_attrib])
+        values.append(values_tmp)
 
     return values
+
+
+def _get_table_formatting(
+    input_dimension,
+    tag,
+    output_dimension,
+    parameterized,
+):
+    """Get the table formatting as arguments to 'tabulate()'."""
+    header_names: List[str] = ["No.", "Constructor"]
+    colalign: List[str] = ["center", "center"]
+    maxcolwidth: List[Optional[int]] = [None, None]
+    print_attribs: List[str] = []
+
+    # The printed attribute should appear in this order
+    if input_dimension is None:
+        print_attribs.append("input_dim")
+
+    if output_dimension is None:
+        print_attribs.append("output_dim")
+
+    if parameterized is None:
+        print_attribs.append("parameterized")
+
+    if tag is None:
+        print_attribs.append("tags")
+
+    # --- Always get description
+    print_attribs.append("description")
+
+    # --- Get the arguments for tabulate
+    for print_attrib in print_attribs:
+        header_names.append(HEADER[print_attrib]["header_name"])
+        colalign.append(HEADER[print_attrib]["colalign"])
+        maxcolwidth.append(HEADER[print_attrib]["maxcolwidth"])
+
+    return print_attribs, header_names, colalign, maxcolwidth
+
+
+def _parse_modules_data(package):
+    available_classes = get_available_classes(package)
+
+    data = {}
+
+    for available_class, class_path in available_classes:
+
+        # Create an instance of test function to parse its properties
+        instance: UQTestFunABC = class_path()
+
+        # Get the dimension
+        if instance.variable_dimension:
+            input_dimension = "M"
+        else:
+            input_dimension = str(instance.input_dimension)
+
+        data[available_class] = {
+            "constructor": available_class + "()",
+            "input_dim": input_dimension,
+            "output_dim": instance.output_dimension,
+            "parameterized": True if instance.parameters else False,
+            "tags": ", ".join(instance.tags),
+            "description": instance.description,
+            "full_path": class_path,
+        }
+
+    return data
diff --git a/src/uqtestfuns/meta/__init__.py b/src/uqtestfuns/meta/__init__.py
index 89314de..d58dbf1 100644
--- a/src/uqtestfuns/meta/__init__.py
+++ b/src/uqtestfuns/meta/__init__.py
@@ -1,6 +1,7 @@
 # """
 # The init for 'meta' subpackage of UQTestFuns.
 # """
+
 from .metaspec import UQMetaFunSpec
 from .uqmetatestfun import UQMetaTestFun
 
diff --git a/src/uqtestfuns/meta/basis_functions.py b/src/uqtestfuns/meta/basis_functions.py
index 6309339..2027b84 100644
--- a/src/uqtestfuns/meta/basis_functions.py
+++ b/src/uqtestfuns/meta/basis_functions.py
@@ -2,6 +2,7 @@
 This module contains a collection of the default basis functions
 for the meta.
 """
+
 import numpy as np
 
 
diff --git a/src/uqtestfuns/meta/metaspec.py b/src/uqtestfuns/meta/metaspec.py
index df2bcca..b92a726 100644
--- a/src/uqtestfuns/meta/metaspec.py
+++ b/src/uqtestfuns/meta/metaspec.py
@@ -2,19 +2,20 @@
 This module contains the classes that implement specification
 for a meta and the resulting test function.
 """
+
 import numpy as np
 import itertools
 from dataclasses import dataclass, field, InitVar
 from scipy.special import comb
 from typing import Dict, Callable, Tuple, Optional, Union, List
 
-from ..core import UnivDist
+from ..core import Marginal
 
 __all__ = ["UQMetaFunSpec", "UQTestFunSpec"]
 
 
 def _preprocess_effects(
-    effects_dict: Dict[int, Optional[int]], spatial_dimension: int
+    effects_dict: Dict[int, Optional[int]], input_dimension: int
 ) -> Dict[int, int]:
     """Preprocess the effects dictionary.
 
@@ -25,8 +26,8 @@ def _preprocess_effects(
       or it's zero, then exclude the key.
 
     >>> effects_dict = {1: None, 2: 3, 3: None, 4: 10}
-    >>> spatial_dimension = 3
-    >>> _preprocess_effects(effects_dict, spatial_dimension)
+    >>> input_dimension = 3
+    >>> _preprocess_effects(effects_dict, input_dimension)
     {1: 3, 2: 3, 3: 1}
 
     Parameters
@@ -34,7 +35,7 @@ def _preprocess_effects(
     effects_dict : Dict[int, Optional[int]]
         Specified effects with the corresponding length to take into account
         in a realization of test function.
-    spatial_dimension : int
+    input_dimension : int
         Number of dimensions of the test function.
 
     Returns
@@ -45,27 +46,27 @@ def _preprocess_effects(
     effects = dict()
 
     for key in list(effects_dict):
-        if key <= spatial_dimension and effects_dict[key] != 0:
+        if key <= input_dimension and effects_dict[key] != 0:
             if effects_dict[key] is None:
                 # NOTE: scipy.special.comb returns a flot
-                effects[key] = int(comb(spatial_dimension, key))
+                effects[key] = int(comb(input_dimension, key))
             else:
                 effects[key] = effects_dict[key]  # type: ignore
 
     return effects
 
 
-def _select_basis(spatial_dimension: int, num_basis: int) -> Tuple[int, ...]:
+def _select_basis(input_dimension: int, num_basis: int) -> Tuple[int, ...]:
     """Select basis of a certain length from a selection of basis.
 
-    For example, with spatial dimension of 3 and number of basis functions
+    For example, with input dimension of 3 and number of basis functions
     of 9, the output may be a tuple of integers: (7, 3, 8) which means
     the first, second, and third dimension has the 7th, 3rd,
     and 8th basis function, respectively.
 
     Parameters
     ----------
-    spatial_dimension : int
+    input_dimension : int
         Number of dimensions of the test function.
     num_basis : int
         Number of available basis functions to select from.
@@ -75,33 +76,33 @@ def _select_basis(spatial_dimension: int, num_basis: int) -> Tuple[int, ...]:
     Tuple[int, ...]
         Selected basis function in each dimension.
     """
-    selection = tuple(np.random.randint(0, num_basis, size=spatial_dimension))
+    selection = tuple(np.random.randint(0, num_basis, size=input_dimension))
 
     return selection
 
 
 def _create_effects_tuples(
-    spatial_dimension: int, effects_dict: Dict[int, int]
+    input_dimension: int, effects_dict: Dict[int, int]
 ) -> Dict[int, Tuple[Tuple[int, ...], ...]]:
     """Create a dictionary of effect matrices.
 
-    >>> spatial_dimension = 3
+    >>> input_dimension = 3
     >>> effects_dict = {1: None, 2: None, 3: None}
-    >>> _create_effects_tuples(spatial_dimension, effects_dict)
+    >>> _create_effects_tuples(input_dimension, effects_dict)
     {1: ((0,), (1,), (2,)), 2: ((0, 1), (0, 2), (1, 2)), 3: ((0, 1, 2),)}
     >>> effects_dict = {}
-    >>> _create_effects_tuples(spatial_dimension, effects_dict)
+    >>> _create_effects_tuples(input_dimension, effects_dict)
     {}
     >>> effects_dict = {1: 0, 2: None, 3: 0}
-    >>> _create_effects_tuples(spatial_dimension, effects_dict)
+    >>> _create_effects_tuples(input_dimension, effects_dict)
     {2: ((0, 1), (0, 2), (1, 2))}
     >>> effects_dict = {5: None}
-    >>> _create_effects_tuples(spatial_dimension, effects_dict)
+    >>> _create_effects_tuples(input_dimension, effects_dict)
     {}
 
     Parameters
     ----------
-    spatial_dimension : int
+    input_dimension : int
         Number of dimensions of the test function.
     effects_dict : Dict[int, int]
         Specified effects to take into account.
@@ -114,17 +115,17 @@ def _create_effects_tuples(
     Notes
     -----
     - If an effect has a value of 0 then no effect is taken into account.
-    - If an effect is larger than the spatial dimension of the test function
+    - If an effect is larger than the input dimension of the test function
       then no effect is taken into account.
     """
     effects_tuples = dict()
 
     for key in effects_dict:
         if (
-            effects_dict[key] != 0 and key <= spatial_dimension
+            effects_dict[key] != 0 and key <= input_dimension
         ):  # pragma: no cover
             effects_tuples[key] = tuple(
-                itertools.combinations(np.arange(spatial_dimension), key)
+                itertools.combinations(np.arange(input_dimension), key)
             )
 
     return effects_tuples
@@ -194,7 +195,7 @@ def _generate_effect_coeffs(
 
 
 def _select_marginals(
-    marginals: Union[List[Dict], Tuple[Dict, ...]], spatial_dimension: int
+    marginals: Union[List[Dict], Tuple[Dict, ...]], input_dimension: int
 ) -> Union[List[Dict], Tuple[Dict, ...]]:
     """Randomly select marginals of a given dimension from a set of inputs.
     # TODO: Update the description and the type hints
@@ -203,7 +204,7 @@ def _select_marginals(
     inputs : Union[List[Dict], Tuple[Dict, ...]]
         List of available marginals to construct a probabilistic input model
         for a test function realization.
-    spatial_dimension : int
+    input_dimension : int
         Number of dimensions of the test function.
 
     Returns
@@ -214,7 +215,7 @@ def _select_marginals(
     """
     selected_marginals = []
     indices = np.random.randint(
-        low=0, high=len(marginals), size=spatial_dimension
+        low=0, high=len(marginals), size=input_dimension
     )
 
     for num, idx in enumerate(indices):
@@ -223,7 +224,7 @@ def _select_marginals(
         if name is None:
             name = f"X{num + 1}"
         # Create a new instance (now with a name)
-        selected_marginal = UnivDist(
+        selected_marginal = Marginal(
             name=name,
             distribution=selected_marginal.distribution,
             parameters=selected_marginal.parameters,
@@ -244,7 +245,7 @@ class UQTestFunSpec:
 
     Parameters
     ----------
-    spatial_dimension : int
+    input_dimension : int
         Number of dimension of the test function.
     basis_functions : Dict[int, Callable]
         Collection of basis functions to choose from.
@@ -261,12 +262,12 @@ class UQTestFunSpec:
         the test function.
     """
 
-    spatial_dimension: int
+    input_dimension: int
     basis_functions: Dict[int, Callable]
     selected_basis: Tuple[int, ...]
     effects_tuples: Dict[int, Tuple[Tuple[int, ...], ...]]
     effects_coeffs: Dict[int, np.ndarray]
-    inputs: Union[List[UnivDist], Tuple[UnivDist, ...]]
+    inputs: Union[List[Marginal], Tuple[Marginal, ...]]
 
 
 @dataclass
@@ -275,7 +276,7 @@ class UQMetaFunSpec:
 
     Parameters
     ----------
-    spatial_dimension : int
+    input_dimension : int
         Number of dimension of the generated test functions.
     basis_functions : Dict[int, Callable]
         Collection of basis functions to choose from.
@@ -289,27 +290,24 @@ class UQMetaFunSpec:
         Function to generate the coefficient values for each effect term.
     """
 
-    spatial_dimension: int
+    input_dimension: int
     basis_functions: Dict[int, Callable]
     effects: Dict[int, int] = field(init=False)
     effects_dict: InitVar[Dict[int, Optional[int]]]
-    input_marginals: Union[List[UnivDist], Tuple[UnivDist, ...]]
+    input_marginals: Union[List[Marginal], Tuple[Marginal, ...]]
     coeffs_generator: Callable
     _effects_tuples: Optional[Dict[int, Tuple[Tuple[int, ...], ...]]] = field(
         init=False, repr=False
     )
 
     def __post_init__(self, effects_dict):
-        if self.spatial_dimension < 1:
+        if self.input_dimension < 1:
             raise ValueError(
-                f"Spatial dimension must be > 0! "
-                f"Got {self.spatial_dimension}"
+                f"input dimension must be > 0! Got {self.input_dimension}"
             )
         self._effects_tuples = None
         # Clean up the effects dictionary
-        self.effects = _preprocess_effects(
-            effects_dict, self.spatial_dimension
-        )
+        self.effects = _preprocess_effects(effects_dict, self.input_dimension)
 
     def get_sample(
         self, sample_size: int = 1
@@ -340,14 +338,14 @@ def get_sample(
         # Generate all possible requested interactions and cache them
         if self._effects_tuples is None:
             self._effects_tuples = _create_effects_tuples(
-                self.spatial_dimension, self.effects
+                self.input_dimension, self.effects
             )
 
         sample = []
         for _ in range(sample_size):
             # Randomly select basis
             num_basis = len(self.basis_functions)
-            selected_basis = _select_basis(self.spatial_dimension, num_basis)
+            selected_basis = _select_basis(self.input_dimension, num_basis)
 
             # Randomly select tuples of interaction terms
             effects_tuples = _select_effects(
@@ -361,12 +359,12 @@ def get_sample(
 
             # Randomly select input marginals and create an instance
             selected_input_marginals = _select_marginals(
-                self.input_marginals, self.spatial_dimension
+                self.input_marginals, self.input_dimension
             )
 
             # Create an instance of UQTestFunSpec
             uqtestfun_spec = UQTestFunSpec(
-                self.spatial_dimension,
+                self.input_dimension,
                 self.basis_functions,
                 selected_basis,
                 effects_tuples,
diff --git a/src/uqtestfuns/meta/uqmetatestfun.py b/src/uqtestfuns/meta/uqmetatestfun.py
index e22e2af..0e9003e 100644
--- a/src/uqtestfuns/meta/uqmetatestfun.py
+++ b/src/uqtestfuns/meta/uqmetatestfun.py
@@ -13,6 +13,7 @@
    doi:
    `10.1016/j.ress.2020.107189 <https://doi.org/10.1016/j.ress.2020.107189>`_.
 """
+
 import math
 import numpy as np
 
@@ -20,9 +21,10 @@
 from numpy.typing import ArrayLike
 from typing import Optional, Union, List
 
+from uqtestfuns.core.parameters import FunParams
 from .metaspec import UQMetaFunSpec, UQTestFunSpec
 from .basis_functions import BASIS_BY_ID
-from ..core import UQTestFun, ProbInput, UnivDist
+from ..core import UQTestFun, ProbInput, Marginal
 
 
 __all__ = ["UQMetaTestFun", "default_coeffs_gen"]
@@ -65,21 +67,19 @@ def default_coeffs_gen(sample_size: int) -> np.ndarray:
     return yy
 
 
-def _evaluate_test_function(
-    xx: np.ndarray, uqtestfun_spec: UQTestFunSpec
-) -> np.ndarray:
+def _evaluate_test_function(xx: np.ndarray, spec: UQTestFunSpec) -> np.ndarray:
     """Alternative way to evaluate metafunction realizations."""
 
-    basis_functions = uqtestfun_spec.basis_functions
-    selected_basis = uqtestfun_spec.selected_basis
+    basis_functions = spec.basis_functions
+    selected_basis = spec.selected_basis
 
     # Evaluate the selected basis functions on xx
-    basis_vals = np.zeros((xx.shape[0], uqtestfun_spec.spatial_dimension))
+    basis_vals = np.zeros((xx.shape[0], spec.input_dimension))
     for idx, basis_id in enumerate(selected_basis):
         basis_vals[:, idx] = basis_functions[basis_id](xx[:, idx])
 
-    effects_tuples = uqtestfun_spec.effects_tuples
-    effects_coeffs = uqtestfun_spec.effects_coeffs
+    effects_tuples = spec.effects_tuples
+    effects_coeffs = spec.effects_coeffs
 
     # Evaluate each effect term of the test function
     yy = np.zeros(xx.shape[0])
@@ -147,13 +147,22 @@ def get_sample(
             # Create an instance of inputs
             prob_input = ProbInput(testfun_specs.inputs)
             # Assign the realized spec as a parameter
-            parameters = testfun_specs
+            parameters = FunParams(
+                declared_parameters=[
+                    {
+                        "keyword": "spec",
+                        "value": testfun_specs,
+                        "type": UQTestFunSpec,
+                        "description": None,
+                    },
+                ],
+            )
 
             return UQTestFun(
                 evaluate=evaluate,
                 prob_input=prob_input,
                 parameters=parameters,
-                name=name,
+                function_id=name,
             )
 
         sample = []
@@ -162,14 +171,23 @@ def get_sample(
             assert isinstance(testfun_specs, list)
             prob_input = ProbInput(testfun_specs[i].inputs)
             # Assign the realized spec as a parameter
-            parameters = testfun_specs[i]
+            parameters = FunParams(
+                declared_parameters=[
+                    {
+                        "keyword": "spec",
+                        "value": testfun_specs[i],
+                        "type": UQTestFunSpec,
+                        "description": None,
+                    },
+                ],
+            )
 
             sample.append(
                 UQTestFun(
                     evaluate=evaluate,
                     prob_input=prob_input,
                     parameters=parameters,
-                    name=name,
+                    function_id=name,
                 )
             )
 
@@ -178,14 +196,14 @@ def get_sample(
     @classmethod
     def from_default(
         cls,
-        spatial_dimension: ArrayLike,
+        input_dimension: ArrayLike,
         input_id: Optional[int] = None,
     ):
         """Create a metafunction with parameters according to Becker (2019).
 
         Parameters
         ----------
-        spatial_dimension : Union[int, Sized]
+        input_dimension : Union[int, Sized]
             Number of dimensions of the test functions generated
             by the meta. If a set of values are given, a single value
             will be selected at random.
@@ -195,38 +213,38 @@ def from_default(
         UQMetaTestFun
             An instance of metafunction with the default parameters.
         """
-        # Select a single spatial dimension randomly
-        if not isinstance(spatial_dimension, int):
-            spatial_dimension = np.asarray(spatial_dimension)
-            spatial_dimension = int(np.random.choice(spatial_dimension))
+        # Select a single input dimension randomly
+        if not isinstance(input_dimension, int):
+            input_dimension = np.asarray(input_dimension)
+            input_dimension = int(np.random.choice(input_dimension))
 
         effects_dict = {
             1: None,  # Take all
-            2: math.floor(0.5 * spatial_dimension),
-            3: math.floor(0.2 * spatial_dimension),
+            2: math.floor(0.5 * input_dimension),
+            3: math.floor(0.2 * input_dimension),
         }
 
         if input_id is None:
             input_id = np.random.randint(0, 8)
 
         input_marginals = [
-            UnivDist(distribution="uniform", parameters=[0, 1]),
-            UnivDist(
+            Marginal(distribution="uniform", parameters=[0, 1]),
+            Marginal(
                 distribution="trunc-normal",
                 parameters=[0.5, 0.15, 0.0, 1.0],
             ),
-            UnivDist(distribution="beta", parameters=[8.0, 2.0, 0.0, 1.0]),
-            UnivDist(distribution="beta", parameters=[2.0, 8.0, 0.0, 1.0]),
-            UnivDist(distribution="beta", parameters=[2.0, 0.8, 0.0, 1.0]),
-            UnivDist(distribution="beta", parameters=[0.8, 2.0, 0.0, 1.0]),
-            UnivDist(distribution="logitnormal", parameters=[0.0, 3.16]),
+            Marginal(distribution="beta", parameters=[8.0, 2.0, 0.0, 1.0]),
+            Marginal(distribution="beta", parameters=[2.0, 8.0, 0.0, 1.0]),
+            Marginal(distribution="beta", parameters=[2.0, 0.8, 0.0, 1.0]),
+            Marginal(distribution="beta", parameters=[0.8, 2.0, 0.0, 1.0]),
+            Marginal(distribution="logitnormal", parameters=[0.0, 3.16]),
         ]
 
         if input_id < 7:
             input_marginals = [input_marginals[input_id]]
 
         metafun_spec = UQMetaFunSpec(
-            spatial_dimension=spatial_dimension,
+            input_dimension=input_dimension,
             basis_functions=BASIS_BY_ID,
             effects_dict=effects_dict,
             input_marginals=input_marginals,
diff --git a/src/uqtestfuns/test_functions/__init__.py b/src/uqtestfuns/test_functions/__init__.py
index b85c4e7..33588a2 100644
--- a/src/uqtestfuns/test_functions/__init__.py
+++ b/src/uqtestfuns/test_functions/__init__.py
@@ -1,10 +1,13 @@
 """
 The init for the 'test_functions' subpackage of UQTestFuns.
 """
+
 from .ackley import Ackley
+from .alemazkoor import Alemazkoor2D, Alemazkoor20D
 from .borehole import Borehole
 from .bratley1992 import Bratley1992a, Bratley1992b, Bratley1992c, Bratley1992d
 from .cantilever_beam_2d import CantileverBeam2D
+from .cheng2010 import Cheng2D
 from .circular_pipe_crack import CircularPipeCrack
 from .convex_fail_domain import ConvexFailDomain
 from .damped_cosine import DampedCosine
@@ -13,20 +16,29 @@
 from .forrester import Forrester2008
 from .four_branch import FourBranch
 from .franke import Franke1, Franke2, Franke3, Franke4, Franke5, Franke6
+from .friedman import Friedman6D, Friedman10D
 from .gayton_hat import GaytonHat
 from .gramacy2007 import Gramacy1DSine
 from .hyper_sphere import HyperSphere
 from .ishigami import Ishigami
-from .alemazkoor import Alemazkoor2D
+from .lim import LimPoly, LimNonPoly
 from .oakley2002 import Oakley1D
 from .otl_circuit import OTLCircuit
 from .mclain import McLainS1, McLainS2, McLainS3, McLainS4, McLainS5
+from .moon3d import Moon3D
+from .morris2006 import Morris2006
 from .piston import Piston
+from .portfolio_3d import Portfolio3D
 from .rs_circular_bar import RSCircularBar
 from .rs_quadratic import RSQuadratic
+from .saltelli_linear import SaltelliLinear
 from .sobol_g import SobolG
+from .sobol_g_star import SobolGStar
+from .sobol_levitan import SobolLevitan
+from .solar_cell import SolarCell
 from .speed_reducer_shaft import SpeedReducerShaft
 from .sulfur import Sulfur
+from .undamped_oscillator import UndampedOscillator
 from .webster import Webster2D
 from .welch1992 import Welch1992
 from .wing_weight import WingWeight
@@ -37,12 +49,14 @@
 __all__ = [
     "Ackley",
     "Alemazkoor2D",
+    "Alemazkoor20D",
     "Borehole",
     "Bratley1992a",
     "Bratley1992b",
     "Bratley1992c",
     "Bratley1992d",
     "CantileverBeam2D",
+    "Cheng2D",
     "CircularPipeCrack",
     "ConvexFailDomain",
     "DampedCosine",
@@ -57,10 +71,14 @@
     "Franke4",
     "Franke5",
     "Franke6",
+    "Friedman6D",
+    "Friedman10D",
     "GaytonHat",
     "Gramacy1DSine",
     "HyperSphere",
     "Ishigami",
+    "LimNonPoly",
+    "LimPoly",
     "Oakley1D",
     "OTLCircuit",
     "McLainS1",
@@ -68,12 +86,20 @@
     "McLainS3",
     "McLainS4",
     "McLainS5",
+    "Moon3D",
+    "Morris2006",
     "Piston",
+    "Portfolio3D",
     "RSCircularBar",
     "RSQuadratic",
+    "SaltelliLinear",
     "SobolG",
+    "SobolGStar",
+    "SobolLevitan",
+    "SolarCell",
     "SpeedReducerShaft",
     "Sulfur",
+    "UndampedOscillator",
     "Webster2D",
     "Welch1992",
     "WingWeight",
diff --git a/src/uqtestfuns/test_functions/ackley.py b/src/uqtestfuns/test_functions/ackley.py
index af0fa8d..4328565 100644
--- a/src/uqtestfuns/test_functions/ackley.py
+++ b/src/uqtestfuns/test_functions/ackley.py
@@ -19,58 +19,65 @@
    vol. 1, no. 1, pp. 1–23, 1993.
    DOI: 10.1162/evco.1993.1.1.1.
 """
-import numpy as np
 
-from typing import List, Tuple
+import numpy as np
 
-from ..core.uqtestfun_abc import UQTestFunABC
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecVarDim
+from uqtestfuns.core.uqtestfun_abc import UQTestFunVarDimABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs, FunParamSpecs
 
 __all__ = ["Ackley"]
 
 
-def _ackley_input(spatial_dimension: int) -> List[UnivDistSpec]:
-    """Create a list of marginals for the M-dimensional Ackley function.
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Ackley1987": {
+        "function_id": "Ackley",
+        "description": (
+            "Search domain for the Ackley function from Ackley (1987)"
+        ),
+        "marginals": [
+            {
+                "name": "X",
+                "distribution": "uniform",
+                "parameters": [-32.768, 32.768],
+                "description": None,
+            },
+        ],
+        "copulas": None,
+    }
+}
 
-    Parameters
-    ----------
-    spatial_dimension : int
-        The requested spatial dimension of the probabilistic input model.
 
-    Returns
-    -------
-    List[UnivDistSpec]
-        A list of marginals for the multivariate input following Ref. [1]
-    """
-    marginals = []
-    for i in range(spatial_dimension):
-        marginals.append(
-            UnivDistSpec(
-                name=f"X{i + 1}",
-                distribution="uniform",
-                parameters=[-32.768, 32.768],
-                description="None",
-            )
-        )
-
-    return marginals
-
-
-AVAILABLE_INPUT_SPECS = {
-    "Ackley1987": ProbInputSpecVarDim(
-        name="Ackley1987",
-        description=(
-            "Search domain for the Ackley function from Ackley (1987)."
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Ackley1987": {
+        "function_id": "Ackley",
+        "description": (
+            "Parameter set for the Ackley function from Ackley (1987)"
         ),
-        marginals_generator=_ackley_input,
-        copulas=None,
-    ),
+        "declared_parameters": [
+            {
+                "keyword": "a",
+                "value": 20.0,
+                "type": float,
+                "description": "Height of the ridges surrounding the minimum",
+            },
+            {
+                "keyword": "b",
+                "value": 0.2,
+                "type": float,
+                "description": "Decay rate of the Euclidean distance",
+            },
+            {
+                "keyword": "c",
+                "value": 2 * np.pi,
+                "type": float,
+                "description": "Scaling constant for the cosine term",
+            },
+        ],
+    },
 }
 
-AVAILABLE_PARAMETERS = {"Ackley1987": np.array([20, 0.2, 2 * np.pi])}
-
 
-def evaluate(xx: np.ndarray, parameters: Tuple[float, float, float]):
+def evaluate(xx: np.ndarray, a: float, b: float, c: float) -> np.ndarray:
     """Evaluate the Ackley function on a set of input values.
 
     Parameters
@@ -78,8 +85,12 @@ def evaluate(xx: np.ndarray, parameters: Tuple[float, float, float]):
     xx : np.ndarray
         M-Dimensional input values given by an N-by-M array where
         N is the number of input values.
-    parameters : Tuple[float, float, float]
-        A tuple of length 3 for the parameters of the Ackley function.
+    a : float
+        Height of the ridges surrounding the minimum.
+    b : float
+        Decay rate of the Euclidean distance.
+    c : float
+        Scaling constant for the cosine term.
 
     Returns
     -------
@@ -89,7 +100,6 @@ def evaluate(xx: np.ndarray, parameters: Tuple[float, float, float]):
     """
 
     m = xx.shape[1]
-    a, b, c = parameters
 
     # Compute the Ackley function
     term_1 = -1 * a * np.exp(-1 * b * np.sqrt(np.sum(xx**2, axis=1) / m))
@@ -100,13 +110,12 @@ def evaluate(xx: np.ndarray, parameters: Tuple[float, float, float]):
     return yy
 
 
-class Ackley(UQTestFunABC):
+class Ackley(UQTestFunVarDimABC):
     """A concrete implementation of the M-dimensional Ackley test function."""
 
     _tags = ["optimization", "metamodeling"]
     _description = "Optimization test function from Ackley (1987)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = AVAILABLE_PARAMETERS
-    _default_spatial_dimension = None  # Indicate that this is variable dim.
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/alemazkoor.py b/src/uqtestfuns/test_functions/alemazkoor.py
index 8a5ee87..1633235 100644
--- a/src/uqtestfuns/test_functions/alemazkoor.py
+++ b/src/uqtestfuns/test_functions/alemazkoor.py
@@ -16,53 +16,75 @@
    Physics, vol. 371, pp. 137-151, 2018.
    DOI: 10.1016/j.jcp.2018.05.025
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
+
+__all__ = ["Alemazkoor2D", "Alemazkoor20D"]
 
-__all__ = ["Alemazkoor2D"]
 
-AVAILABLE_INPUT_SPECS_2D = {
-    "Alemazkoor2018": ProbInputSpecFixDim(
-        name="2D-Alemazkoor2018",
-        description=(
+AVAILABLE_INPUTS_2D: ProbInputSpecs = {
+    "Alemazkoor2018": {
+        "function_id": "Alemazkoor2D",
+        "description": (
             "Input specification for the 2D test function "
             "from Alemazkoor & Meidani (2018)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="X1",
-                distribution="uniform",
-                parameters=[-1, 1],
-                description="None",
-            ),
-            UnivDistSpec(
-                name="X2",
-                distribution="uniform",
-                parameters=[-1, 1],
-                description="None",
-            ),
+        "marginals": [
+            {
+                "name": "X1",
+                "distribution": "uniform",
+                "parameters": [-1.0, 1.0],
+                "description": None,
+            },
+            {
+                "name": "X2",
+                "distribution": "uniform",
+                "parameters": [-1.0, 1.0],
+                "description": None,
+            },
+        ],
+        "copulas": None,
+    },
+}
+
+AVAILABLE_INPUTS_20D: ProbInputSpecs = {
+    "Alemazkoor2018": {
+        "function_id": "Alemazkoor20D",
+        "description": (
+            "Input specification for the 20D test function "
+            "from Alemazkoor & Meidani (2018)"
+        ),
+        "marginals": [
+            {
+                "name": f"X{i + 1}",
+                "distribution": "uniform",
+                "parameters": [-1, 1],
+                "description": None,
+            }
+            for i in range(20)
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
 
 def evaluate_2d(xx: np.ndarray) -> np.ndarray:
-    """The evaluation for the Alemazkoor & Meidani (2018) 2D function.
+    """Evaluate the 2D test function from Alemazkoor & Meidani (2018).
 
     Parameters
     ----------
     xx : np.ndarray
-        A two-dimensional input values given by an N-by-2 array
-        where N is the number of input values.
+        A two-dimensional input values given by an ``(N, 2)`` 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.
+        The output is a one-dimensional array of length ``N``.
     """
     yy = np.zeros(xx.shape[0])
 
@@ -72,16 +94,51 @@ def evaluate_2d(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class Alemazkoor2D(UQTestFunABC):
+class Alemazkoor2D(UQTestFunFixDimABC):
     """An implementation of the 2D function of Alemazkoor & Meidani (2018)."""
 
     _tags = ["metamodeling"]
     _description = (
-        "Two-dimensional high-degree polynomial from Alemazkoor "
+        "Low-dimensional high-degree polynomial from Alemazkoor "
+        "& Meidani (2018)"
+    )
+    _available_inputs = AVAILABLE_INPUTS_2D
+    _available_parameters = None
+    _default_input_dimension = 2
+
+    evaluate = staticmethod(evaluate_2d)  # type: ignore
+
+
+def evaluate_20d(xx: np.ndarray) -> np.ndarray:
+    """Evaluate the 20D test function from Alemazkoor & Meidani (2018).
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        A 20-dimensional input values given by an ``(N, 20)`` 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 one-dimensional array of length ``N``.
+    """
+    yy = np.sum(xx[:, :-1] * xx[:, 1:], axis=1)
+
+    return yy
+
+
+class Alemazkoor20D(UQTestFunFixDimABC):
+    """An implementation of the 20D function of Alemazkoor & Meidani (2018)."""
+
+    _tags = ["metamodeling"]
+    _description = (
+        "High-dimensional low-degree polynomial from Alemazkoor "
         "& Meidani (2018)"
     )
-    _available_inputs = AVAILABLE_INPUT_SPECS_2D
+    _available_inputs = AVAILABLE_INPUTS_20D
     _available_parameters = None
-    _default_spatial_dimension = 2
+    _default_input_dimension = 20
 
-    eval_ = staticmethod(evaluate_2d)
+    evaluate = staticmethod(evaluate_20d)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/borehole.py b/src/uqtestfuns/test_functions/borehole.py
index 965d166..5477f98 100644
--- a/src/uqtestfuns/test_functions/borehole.py
+++ b/src/uqtestfuns/test_functions/borehole.py
@@ -21,101 +21,103 @@
    prediction,” Technometrics, vol. 35, no. 3, pp. 243–255, 1993.
    DOI: 10.1080/00401706.1993.10485320
 """
+
 import numpy as np
 
-from ..core.uqtestfun_abc import UQTestFunABC
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
+from uqtestfuns.core.custom_typing import MarginalSpecs, ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["Borehole"]
 
+
 # From Ref. [1]
-INPUT_MARGINALS_HARPER1983 = [
-    UnivDistSpec(
-        name="rw",
-        distribution="normal",
-        parameters=[0.10, 0.0161812],
-        description="radius of the borehole [m]",
-    ),
-    UnivDistSpec(
-        name="r",
-        distribution="lognormal",
-        parameters=[7.71, 1.0056],
-        description="radius of influence [m]",
-    ),
-    UnivDistSpec(
-        name="Tu",
-        distribution="uniform",
-        parameters=[63070.0, 115600.0],
-        description="transmissivity of upper aquifer [m^2/year]",
-    ),
-    UnivDistSpec(
-        name="Hu",
-        distribution="uniform",
-        parameters=[990.0, 1100.0],
-        description="potentiometric head of upper aquifer [m]",
-    ),
-    UnivDistSpec(
-        name="Tl",
-        distribution="uniform",
-        parameters=[63.1, 116.0],
-        description="transmissivity of lower aquifer [m^2/year]",
-    ),
-    UnivDistSpec(
-        name="Hl",
-        distribution="uniform",
-        parameters=[700.0, 820.0],
-        description="potentiometric head of lower aquifer [m]",
-    ),
-    UnivDistSpec(
-        name="L",
-        distribution="uniform",
-        parameters=[1120.0, 1680.0],
-        description="length of the borehole [m]",
-    ),
-    UnivDistSpec(
-        name="Kw",
-        distribution="uniform",
-        parameters=[9985.0, 12045.0],
-        description="hydraulic conductivity of the borehole [m/year]",
-    ),
+MARGINALS_HARPER1983: MarginalSpecs = [
+    {
+        "name": "rw",
+        "distribution": "normal",
+        "parameters": [0.10, 0.0161812],
+        "description": "radius of the borehole [m]",
+    },
+    {
+        "name": "r",
+        "distribution": "lognormal",
+        "parameters": [7.71, 1.0056],
+        "description": "radius of influence [m]",
+    },
+    {
+        "name": "Tu",
+        "distribution": "uniform",
+        "parameters": [63070.0, 115600.0],
+        "description": "transmissivity of upper aquifer [m^2/year]",
+    },
+    {
+        "name": "Hu",
+        "distribution": "uniform",
+        "parameters": [990.0, 1100.0],
+        "description": "potentiometric head of upper aquifer [m]",
+    },
+    {
+        "name": "Tl",
+        "distribution": "uniform",
+        "parameters": [63.1, 116.0],
+        "description": "transmissivity of lower aquifer [m^2/year]",
+    },
+    {
+        "name": "Hl",
+        "distribution": "uniform",
+        "parameters": [700.0, 820.0],
+        "description": "potentiometric head of lower aquifer [m]",
+    },
+    {
+        "name": "L",
+        "distribution": "uniform",
+        "parameters": [1120.0, 1680.0],
+        "description": "length of the borehole [m]",
+    },
+    {
+        "name": "Kw",
+        "distribution": "uniform",
+        "parameters": [9985.0, 12045.0],
+        "description": "hydraulic conductivity of the borehole [m/year]",
+    },
 ]
 
 # From Ref. [2]
-INPUT_MARGINALS_MORRIS1993 = list(INPUT_MARGINALS_HARPER1983)
-INPUT_MARGINALS_MORRIS1993[0:2] = [
-    UnivDistSpec(
-        name="rw",
-        distribution="uniform",
-        parameters=[0.05, 0.15],
-        description="radius of the borehole [m]",
-    ),
-    UnivDistSpec(
-        name="r",
-        distribution="uniform",
-        parameters=[100, 50000],
-        description="radius of influence [m]",
-    ),
+MARGINALS_MORRIS1993 = list(MARGINALS_HARPER1983)
+MARGINALS_MORRIS1993[0:2] = [
+    {
+        "name": "rw",
+        "distribution": "uniform",
+        "parameters": [0.05, 0.15],
+        "description": "radius of the borehole [m]",
+    },
+    {
+        "name": "r",
+        "distribution": "uniform",
+        "parameters": [100, 50000],
+        "description": "radius of influence [m]",
+    },
 ]
 
-AVAILABLE_INPUT_SPECS = {
-    "Harper1983": ProbInputSpecFixDim(
-        name="Borehole-Harper-1983",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Harper1983": {
+        "function_id": "Borehole",
+        "description": (
             "Probabilistic input model of the Borehole model "
-            "from Harper and Gupta (1983)."
+            "from Harper and Gupta (1983)"
         ),
-        marginals=INPUT_MARGINALS_HARPER1983,
-        copulas=None,
-    ),
-    "Morris1993": ProbInputSpecFixDim(
-        name="Borehole-Morris-1993",
-        description=(
+        "marginals": MARGINALS_HARPER1983,
+        "copulas": None,
+    },
+    "Morris1993": {
+        "function_id": "Borehole",
+        "description": (
             "Probabilistic input model of the Borehole model "
-            "from Morris et al. (1993)."
+            "from Morris et al. (1993)"
         ),
-        marginals=INPUT_MARGINALS_MORRIS1993,
-        copulas=None,
-    ),
+        "marginals": MARGINALS_MORRIS1993,
+        "copulas": None,
+    },
 }
 
 DEFAULT_INPUT_SELECTION = "Harper1983"
@@ -152,14 +154,13 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class Borehole(UQTestFunABC):
+class Borehole(UQTestFunFixDimABC):
     """A concrete implementation of the Borehole function."""
 
     _tags = ["metamodeling", "sensitivity"]
     _description = "Borehole function from Harper and Gupta (1983)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_input = DEFAULT_INPUT_SELECTION
-    _default_spatial_dimension = 8
+    _default_input_id = DEFAULT_INPUT_SELECTION
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/bratley1992.py b/src/uqtestfuns/test_functions/bratley1992.py
index 3f2d2e2..52a1253 100644
--- a/src/uqtestfuns/test_functions/bratley1992.py
+++ b/src/uqtestfuns/test_functions/bratley1992.py
@@ -32,68 +32,47 @@
    Communications, vol. 181, no. 2, pp. 259–270, 2010,
    DOI:10.1016/j.cpc.2009.09.018
 """
+
 import numpy as np
 
 from scipy.special import eval_chebyt
-from typing import List
 
 from .sobol_g import evaluate as evaluate_sobol_g
 
-from ..core.uqtestfun_abc import UQTestFunABC
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecVarDim
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunVarDimABC
 
 __all__ = ["Bratley1992a", "Bratley1992b", "Bratley1992c", "Bratley1992d"]
 
 
-def _bratley_input(spatial_dimension: int) -> List[UnivDistSpec]:
-    """Create a list of marginals for the M-dimensional Bratley test functions.
-
-    Parameters
-    ----------
-    spatial_dimension : int
-        The requested spatial dimension of the probabilistic input model.
-
-    Returns
-    -------
-    List[UnivDistSpec]
-        A list of marginals for the multivariate input following Ref. [1]
-    """
-    marginals = []
-    for i in range(spatial_dimension):
-        marginals.append(
-            UnivDistSpec(
-                name=f"X{i + 1}",
-                distribution="uniform",
-                parameters=[0.0, 1.0],
-                description="None",
-            )
-        )
-
-    return marginals
-
-
-AVAILABLE_INPUT_SPECS = {
-    "Bratley1992": ProbInputSpecVarDim(
-        name="Bratley1992",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Bratley1992": {
+        "function_id": "Bratley1992",
+        "description": (
             "Integration domain of the functions from Bratley et al. (1992)"
         ),
-        marginals_generator=_bratley_input,
-        copulas=None,
-    ),
+        "marginals": [
+            {
+                "name": "X",
+                "distribution": "uniform",
+                "parameters": [0.0, 1.0],
+                "description": None,
+            },
+        ],
+        "copulas": None,
+    },
 }
 
 # Common metadata used in each class definition of Bratley test functions
 COMMON_METADATA = dict(
     _tags=["integration", "sensitivity"],
-    _available_inputs=AVAILABLE_INPUT_SPECS,
+    _available_inputs=AVAILABLE_INPUTS,
     _available_parameters=None,
-    _default_spatial_dimension=None,
     _description="from Bratley et al. (1992)",
 )
 
 
-def evaluate_bratley1992a(xx: np.ndarray):
+def evaluate_bratley1992a(xx: np.ndarray) -> np.ndarray:
     """Evaluate the test function on a set of input values.
 
     Parameters
@@ -115,7 +94,7 @@ def evaluate_bratley1992a(xx: np.ndarray):
     return yy
 
 
-class Bratley1992a(UQTestFunABC):
+class Bratley1992a(UQTestFunVarDimABC):
     """An implementation of the test function 1 from Bratley et al. (1992).
 
     The function (used as an integrand) is a product of an absolute function.
@@ -131,12 +110,11 @@ class Bratley1992a(UQTestFunABC):
     )
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = None
 
-    eval_ = staticmethod(evaluate_bratley1992a)
+    evaluate = staticmethod(evaluate_bratley1992a)  # type: ignore
 
 
-def evaluate_bratley1992b(xx: np.ndarray):
+def evaluate_bratley1992b(xx: np.ndarray) -> np.ndarray:
     """Evaluate the test function on a set of input values.
 
     Parameters
@@ -161,7 +139,7 @@ def evaluate_bratley1992b(xx: np.ndarray):
     return yy
 
 
-class Bratley1992b(UQTestFunABC):
+class Bratley1992b(UQTestFunVarDimABC):
     """An implementation of the test function 2 from Bratley et al. (1992).
 
     The function (used as an integrand) is a product of a trigonometric
@@ -174,12 +152,11 @@ class Bratley1992b(UQTestFunABC):
     )
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = None
 
-    eval_ = staticmethod(evaluate_bratley1992b)
+    evaluate = staticmethod(evaluate_bratley1992b)  # type: ignore
 
 
-def evaluate_bratley1992c(xx: np.ndarray):
+def evaluate_bratley1992c(xx: np.ndarray) -> np.ndarray:
     """Evaluate the test function #3 of Bratley et al. (1992).
 
     Parameters
@@ -206,8 +183,8 @@ def evaluate_bratley1992c(xx: np.ndarray):
     return yy
 
 
-class Bratley1992c(UQTestFunABC):
-    """An implementation of the test function 2 from Bratley et al. (1992).
+class Bratley1992c(UQTestFunVarDimABC):
+    """An implementation of the test function 3 from Bratley et al. (1992).
 
     The function (used as an integrand) is a product of a trigonometric
     function.
@@ -219,12 +196,11 @@ class Bratley1992c(UQTestFunABC):
     )
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = None
 
-    eval_ = staticmethod(evaluate_bratley1992c)
+    evaluate = staticmethod(evaluate_bratley1992c)  # type: ignore
 
 
-def evaluate_bratley1992d(xx: np.ndarray):
+def evaluate_bratley1992d(xx: np.ndarray) -> np.ndarray:
     """Evaluate the test function on a set of input values.
 
     Parameters
@@ -250,7 +226,7 @@ def evaluate_bratley1992d(xx: np.ndarray):
     return yy
 
 
-class Bratley1992d(UQTestFunABC):
+class Bratley1992d(UQTestFunVarDimABC):
     """An implementation of the test function 4 from Bratley et al. (1992).
 
     The function (used as an integrand) is a sum of products.
@@ -262,6 +238,5 @@ class Bratley1992d(UQTestFunABC):
     )
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = None
 
-    eval_ = staticmethod(evaluate_bratley1992d)
+    evaluate = staticmethod(evaluate_bratley1992d)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/cantilever_beam_2d.py b/src/uqtestfuns/test_functions/cantilever_beam_2d.py
index e4c67df..92eac35 100644
--- a/src/uqtestfuns/test_functions/cantilever_beam_2d.py
+++ b/src/uqtestfuns/test_functions/cantilever_beam_2d.py
@@ -25,49 +25,67 @@
    Structural Safety, vol. 73, pp. 42–53, 2018.
    DOI: 10.1016/j.strusafe.2018.02.005
 """
-import numpy as np
 
-from typing import Tuple
+import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs, FunParamSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["CantileverBeam2D"]
 
-AVAILABLE_INPUT_SPECS = {
-    "Rajashekhar1993": ProbInputSpecFixDim(
-        name="Cantilever2D-Rajashekhar1993",
-        description=(
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Rajashekhar1993": {
+        "function_id": "Cantilever2D",
+        "description": (
             "Input model for the cantilever beam problem "
             "from Rajashekhar and Ellingwood (1993)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="W",
-                distribution="normal",
-                parameters=[1000.0, 200.0],
-                description="Load per unit area [N/m^2]",
-            ),
-            UnivDistSpec(
-                name="H",
-                distribution="normal",
-                parameters=[250.0, 37.5],
-                description="Depth of the cross-section [mm]",
-            ),
+        "marginals": [
+            {
+                "name": "W",
+                "distribution": "normal",
+                "parameters": [1000.0, 200.0],
+                "description": "Load per unit area [N/m^2]",
+            },
+            {
+                "name": "H",
+                "distribution": "normal",
+                "parameters": [250.0, 37.5],
+                "description": "Depth of the cross-section [mm]",
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
-AVAILABLE_PARAMETERS = {
-    "Rajashekhar1993": (
-        2.6e4,  # Modulus of elasticity [MPa]
-        6.0e3,  # Span of the beam [mm]
-    )
+
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Rajashekhar1993": {
+        "function_id": "CantileverBeam2D",
+        "description": (
+            "Parameter set for the 2D cantilever beam problem from "
+            "Rajashekhar and Ellingwood (1993)"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "modulus",
+                "value": 2.6e4,
+                "type": float,
+                "description": "Modulus of elasticity 'E' [MPa]",
+            },
+            {
+                "keyword": "span",
+                "value": 6.0e3,
+                "type": float,
+                "description": "Span of the beam 'l' [mm]",
+            },
+        ],
+    },
 }
 
 
-def evaluate(xx: np.ndarray, parameters: Tuple[float, float]) -> np.ndarray:
+def evaluate(xx: np.ndarray, modulus: float, span: float) -> np.ndarray:
     """Evaluate the 2D cantilever beam function on a set of input values.
 
     Parameters
@@ -75,10 +93,10 @@ def evaluate(xx: np.ndarray, parameters: Tuple[float, float]) -> np.ndarray:
     xx : np.ndarray
         A two-dimensional input values given by an N-by-2 array
         where N is the number of input values.
-
-    parameters : Tuple[float, float]
-        The parameters of the test function, namely the modulus of elasticity
-        and the span of the beam.
+    modulus : float
+        The modulus of elasticity in [MPa].
+    span : float
+        The span of the beam in [mm].
 
     Returns
     -------
@@ -87,23 +105,19 @@ def evaluate(xx: np.ndarray, parameters: Tuple[float, float]) -> np.ndarray:
         system. If negative, the system is in failed state.
         The output is a one-dimensional array of length N.
     """
-    # Get parameters
-    mod_el = parameters[0]
-    beam_span = parameters[1]
-
-    mod_el *= 1e6  # from [MPa] to [Pa]
+    modulus *= 1e6  # from [MPa] to [Pa]
 
     # Get the input
     ww = xx[:, 0]  # Load per unit area [N/m^2] ([Pa])
     hh = xx[:, 1]  # Depth of the cross-section [mm]
 
     # Compute the performance function
-    yy = beam_span / 325 - 12 / 8 * beam_span**4 / mod_el * ww / hh**3
+    yy = span / 325 - 12 / 8 * span**4 / modulus * ww / hh**3
 
     return yy
 
 
-class CantileverBeam2D(UQTestFunABC):
+class CantileverBeam2D(UQTestFunFixDimABC):
     """Concrete implementation of the 2D cantilever beam reliability."""
 
     _tags = ["reliability"]
@@ -111,8 +125,7 @@ class CantileverBeam2D(UQTestFunABC):
         "Cantilever beam reliability problem "
         "from Rajashekhar and Ellington (1993)"
     )
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = AVAILABLE_PARAMETERS
-    _default_spatial_dimension = 2
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/cheng2010.py b/src/uqtestfuns/test_functions/cheng2010.py
new file mode 100644
index 0000000..936400e
--- /dev/null
+++ b/src/uqtestfuns/test_functions/cheng2010.py
@@ -0,0 +1,77 @@
+"""
+Module with an implementation of the 2D function from Cheng and Sandu (2010).
+
+The two-dimensional function was used in [1] as an example for metamodeling
+using polynomial chaos expansion.
+
+References
+----------
+1. H. Cheng and A. Sandu, “Collocation least-squares polynomial chaos method,”
+   in Proceedings of the 2010 Spring Simulation Multiconference, Orlando,
+   Florida: Society for Computer Simulation International, 2010, pp. 1–6.
+   DOI: 10.1145/1878537.1878621.
+"""
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
+
+__all__ = ["Cheng2D"]
+
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Cheng2010": {
+        "function_id": "Cheng2D",
+        "description": (
+            "Probabilistic input model for the 2D test function "
+            "from Cheng and Sandu (2010)"
+        ),
+        "marginals": [
+            {
+                "name": "X1",
+                "distribution": "uniform",
+                "parameters": [0.0, 1.0],
+                "description": None,
+            },
+            {
+                "name": "X2",
+                "distribution": "uniform",
+                "parameters": [0.0, 1.0],
+                "description": None,
+            },
+        ],
+        "copulas": None,
+    },
+}
+
+
+def evaluate(xx: np.ndarray) -> np.ndarray:
+    """Evaluate the 2D test function from Cheng and Sandu (2010).
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        A two-dimensional input values given by an N-by-2 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.
+    """
+    yy = np.cos(np.sum(xx, axis=1)) * np.exp(np.prod(xx, axis=1))
+
+    return yy
+
+
+class Cheng2D(UQTestFunFixDimABC):
+    """Concrete implementation of the function from Cheng and Sandu (2010)."""
+
+    _tags = ["metamodeling"]
+    _description = "Two-dimensional test function from Cheng and Sandu (2010)"
+    _available_inputs = AVAILABLE_INPUTS
+    _available_parameters = None
+
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/circular_pipe_crack.py b/src/uqtestfuns/test_functions/circular_pipe_crack.py
index b0cca25..5da5d06 100644
--- a/src/uqtestfuns/test_functions/circular_pipe_crack.py
+++ b/src/uqtestfuns/test_functions/circular_pipe_crack.py
@@ -20,53 +20,77 @@
    Structural Safety, vol. 73, pp. 42–53, 2018.
    DOI: 10.1016/j.strusafe.2018.02.005
 """
-import numpy as np
 
-from typing import Tuple
+import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs, FunParamSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 
 __all__ = ["CircularPipeCrack"]
 
 
-AVAILABLE_INPUT_SPECS = {
-    "Verma2015": ProbInputSpecFixDim(
-        name="CircularPipeCrack-Verma2015",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Verma2015": {
+        "function_id": "CircularPipeCrack",
+        "description": (
             "Input model for the circular pipe crack problem "
             "from Verma et al. (2015)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="sigma_f",
-                distribution="normal",
-                parameters=[301.079, 14.78],
-                description="flow stress [MNm]",
-            ),
-            UnivDistSpec(
-                name="theta",
-                distribution="normal",
-                parameters=[0.503, 0.049],
-                description="half crack angle [-]",
-            ),
+        "marginals": [
+            {
+                "name": "sigma_f",
+                "distribution": "normal",
+                "parameters": [301.079, 14.78],
+                "description": "flow stress [MNm]",
+            },
+            {
+                "name": "theta",
+                "distribution": "normal",
+                "parameters": [0.503, 0.049],
+                "description": "half crack angle [-]",
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
-AVAILABLE_PARAMETERS = {
-    "Verma2016": (
-        3.377e-1,  # Radius of the pipe [m]
-        3.377e-2,  # Thickness of the pipe [m]
-        3.0,  # Applied bending moment [Nm]
-    ),
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Verman2016": {
+        "function_id": "CircularPipeCrack",
+        "description": (
+            "Parameter set for the circular pipe crack reliability problem "
+            "from Verma et al. (2016)"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "pipe_radius",
+                "value": 3.377e-1,
+                "type": float,
+                "description": "Radius of the pipe [m]",
+            },
+            {
+                "keyword": "pipe_thickness",
+                "value": 3.377e-2,
+                "type": float,
+                "description": "Thickness of the pipe [m]",
+            },
+            {
+                "keyword": "bending_moment",
+                "value": 3.0,
+                "type": float,
+                "description": "Applied bending moment [Nm]",
+            },
+        ],
+    },
 }
 
 
 def evaluate(
-    xx: np.ndarray, parameters: Tuple[float, float, float]
+    xx: np.ndarray,
+    pipe_radius: float,
+    pipe_thickness: float,
+    bending_moment: float,
 ) -> np.ndarray:
     """Evaluate the circular pipe crack reliability on a set of input values.
 
@@ -75,10 +99,12 @@ def evaluate(
     xx : np.ndarray
         A two-dimensional input values given by N-by-2 arrays
         where N is the number of input values.
-    parameters : Tuple[float, float, float]
-        The parameters of the test function, namely (and in the following
-        order) the radius of the pipe, the thickness of the pipe,
-        and the applied bending moment.
+    pipe_radius : float
+        The radius of the pipe in [m].
+    pipe_thickness : float
+        The thickness of the pipe in [m].
+    bending_moment : float
+        The applied bending moment in [Nm].
 
     Returns
     -------
@@ -87,9 +113,6 @@ def evaluate(
         If negative, then the system is in failed state.
         The output is a one-dimensional array of length N.
     """
-    # Get parameters
-    pipe_radius, pipe_thickness, bending_moment = parameters
-
     # NOTE: Convert the flow stress from [MNm] to [Nm]
     yy = (
         4
@@ -103,15 +126,14 @@ def evaluate(
     return yy
 
 
-class CircularPipeCrack(UQTestFunABC):
+class CircularPipeCrack(UQTestFunFixDimABC):
     """A concrete implementation of the circular pipe crack problem."""
 
     _tags = ["reliability"]
     _description = (
         "Circular pipe under bending moment from Verma et al. (2015)"
     )
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = AVAILABLE_PARAMETERS
-    _default_spatial_dimension = 2
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/convex_fail_domain.py b/src/uqtestfuns/test_functions/convex_fail_domain.py
index 6756486..c7565c8 100644
--- a/src/uqtestfuns/test_functions/convex_fail_domain.py
+++ b/src/uqtestfuns/test_functions/convex_fail_domain.py
@@ -14,36 +14,38 @@
    sampling," Civil Engineering and Geosciences, TU Delft, Delft,
    The Netherlands, 2000.
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["ConvexFailDomain"]
 
-AVAILABLE_INPUT_SPECS = {
-    "Borri1997": ProbInputSpecFixDim(
-        name="ConvexFailDomain-Borri1997",
-        description=(
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Borri1997": {
+        "function_id": "ConvexFailDomain",
+        "description": (
             "Input model for the convex failure domain problem "
             "from Borri and Speranzini (1997)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="X1",
-                distribution="normal",
-                parameters=[0, 1],
-                description="None",
-            ),
-            UnivDistSpec(
-                name="X2",
-                distribution="normal",
-                parameters=[0, 1],
-                description="None",
-            ),
+        "marginals": [
+            {
+                "name": "X1",
+                "distribution": "normal",
+                "parameters": [0, 1],
+                "description": None,
+            },
+            {
+                "name": "X2",
+                "distribution": "normal",
+                "parameters": [0, 1],
+                "description": None,
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
 
@@ -74,15 +76,14 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class ConvexFailDomain(UQTestFunABC):
+class ConvexFailDomain(UQTestFunFixDimABC):
     """Concrete implementation of the Convex failure domain reliability."""
 
     _tags = ["reliability"]
     _description = (
         "Convex failure domain problem from Borri and Speranzini (1997)"
     )
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 2
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/damped_cosine.py b/src/uqtestfuns/test_functions/damped_cosine.py
index 54dfa21..f9f163f 100644
--- a/src/uqtestfuns/test_functions/damped_cosine.py
+++ b/src/uqtestfuns/test_functions/damped_cosine.py
@@ -10,30 +10,32 @@
    p. 769-784, 2002.
    DOI: 10.1093/biomet/89.4.769
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["DampedCosine"]
 
-AVAILABLE_INPUT_SPECS = {
-    "Santner2018": ProbInputSpecFixDim(
-        name="Santner2018",
-        description=(
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Santner2018": {
+        "function_id": "DampedCosine",
+        "description": (
             "Input model for the one-dimensional damped cosine "
             "from Santner et al. (2018)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="x",
-                distribution="uniform",
-                parameters=[0.0, 1.0],
-                description="None",
-            )
+        "marginals": [
+            {
+                "name": "x",
+                "distribution": "uniform",
+                "parameters": [0.0, 1.0],
+                "description": None,
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    }
 }
 
 
@@ -58,13 +60,13 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class DampedCosine(UQTestFunABC):
+class DampedCosine(UQTestFunFixDimABC):
     """An implementation of the 1D damped cosine from Santner et al. (2018)."""
 
     _tags = ["metamodeling"]
     _description = "One-dimensional damped cosine from Santner et al. (2018)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 1
+    _default_input_dimension = 1
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/damped_oscillator.py b/src/uqtestfuns/test_functions/damped_oscillator.py
index f1989cf..7c45668 100644
--- a/src/uqtestfuns/test_functions/damped_oscillator.py
+++ b/src/uqtestfuns/test_functions/damped_oscillator.py
@@ -38,159 +38,179 @@
    Université Blaise Pascal - Clermont II, Clermont-Ferrand, France, 2011.
    URL: https://sites.google.com/site/vincentdubourg/phd-thesis
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import (
+    ProbInputSpecs,
+    FunParamSpecs,
+    MarginalSpecs,
+)
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 from .utils import lognorm2norm_mean, lognorm2norm_std
 
 __all__ = ["DampedOscillator", "DampedOscillatorReliability"]
 
-INPUT_MARGINALS_DERKIUREGHIAN1991 = [  # From [2]
-    UnivDistSpec(
-        name="Mp",
-        distribution="lognormal",
-        parameters=[
+
+MARGINALS_DERKIUREGHIAN1991: MarginalSpecs = [  # From [2]
+    {
+        "name": "Mp",
+        "distribution": "lognormal",
+        "parameters": [
             lognorm2norm_mean(1.5, 0.1 * 1.5),
             lognorm2norm_std(1.5, 0.1 * 1.5),
         ],
-        description="Primary mass",
-    ),
-    UnivDistSpec(
-        name="Ms",
-        distribution="lognormal",
-        parameters=[
+        "description": "Primary mass",
+    },
+    {
+        "name": "Ms",
+        "distribution": "lognormal",
+        "parameters": [
             lognorm2norm_mean(0.01, 0.1 * 0.01),
             lognorm2norm_std(0.01, 0.1 * 0.01),
         ],
-        description="Secondary mass",
-    ),
-    UnivDistSpec(
-        name="Kp",
-        distribution="lognormal",
-        parameters=[
+        "description": "Secondary mass",
+    },
+    {
+        "name": "Kp",
+        "distribution": "lognormal",
+        "parameters": [
             lognorm2norm_mean(1.0, 0.2 * 1.0),
             lognorm2norm_std(1.0, 0.2 * 1.0),
         ],
-        description="Primary spring stiffness",
-    ),
-    UnivDistSpec(
-        name="Ks",
-        distribution="lognormal",
-        parameters=[
+        "description": "Primary spring stiffness",
+    },
+    {
+        "name": "Ks",
+        "distribution": "lognormal",
+        "parameters": [
             lognorm2norm_mean(0.01, 0.2 * 0.01),
             lognorm2norm_std(0.01, 0.2 * 0.01),
         ],
-        description="Secondary spring stiffness",
-    ),
-    UnivDistSpec(
-        name="Zeta_p",
-        distribution="lognormal",
-        parameters=[
+        "description": "Secondary spring stiffness",
+    },
+    {
+        "name": "Zeta_p",
+        "distribution": "lognormal",
+        "parameters": [
             lognorm2norm_mean(0.05, 0.4 * 0.05),
             lognorm2norm_std(0.05, 0.4 * 0.05),
         ],
-        description="Primary damping ratio",
-    ),
-    UnivDistSpec(
-        name="Zeta_s",
-        distribution="lognormal",
-        parameters=[
+        "description": "Primary damping ratio",
+    },
+    {
+        "name": "Zeta_s",
+        "distribution": "lognormal",
+        "parameters": [
             lognorm2norm_mean(0.02, 0.5 * 0.02),
             lognorm2norm_std(0.02, 0.5 * 0.02),
         ],
-        description="Secondary damping ratio",
-    ),
-    UnivDistSpec(
-        name="S0",
-        distribution="lognormal",
-        parameters=[
+        "description": "Secondary damping ratio",
+    },
+    {
+        "name": "S0",
+        "distribution": "lognormal",
+        "parameters": [
             lognorm2norm_mean(100.0, 0.1 * 100.0),
             lognorm2norm_std(100.0, 0.1 * 100.0),
         ],
-        description="White noise base acceleration",
-    ),
+        "description": "White noise base acceleration",
+    },
 ]
 
-AVAILABLE_INPUT_SPECS_BASE = {
-    "DerKiureghian1991": ProbInputSpecFixDim(
-        name="DampedOscillator-DerKiureghian1991",
-        description=(
+AVAILABLE_INPUTS_BASE: ProbInputSpecs = {
+    "DerKiureghian1991": {
+        "function_id": "DampedOscillator",
+        "description": (
             "Probabilistic input model for the Damped Oscillator model "
             "from Der Kiureghian and De Stefano (1991)."
         ),
-        marginals=INPUT_MARGINALS_DERKIUREGHIAN1991,
-        copulas=None,
-    ),
+        "marginals": MARGINALS_DERKIUREGHIAN1991,
+        "copulas": None,
+    },
 }
 
 
-AVAILABLE_INPUT_SPECS_RELIABILITY = {
-    "DerKiureghian1990a": ProbInputSpecFixDim(
-        name="DampedOscillatorReliability-DerKiureghian1990a",
-        description=(
+AVAILABLE_INPUTS_RELIABILITY: ProbInputSpecs = {
+    "DerKiureghian1990a": {
+        "function_id": "DampedOscillatorReliability",
+        "description": (
             "Input model #1 for the damped oscillator reliability "
             "from Der Kiureghian and De Stefano (1990)"
         ),
-        marginals=INPUT_MARGINALS_DERKIUREGHIAN1991
+        "marginals": MARGINALS_DERKIUREGHIAN1991
         + [
-            UnivDistSpec(
-                name="Fs",
-                distribution="lognormal",
-                parameters=[
+            {
+                "name": "Fs",
+                "distribution": "lognormal",
+                "parameters": [
                     lognorm2norm_mean(15.0, 0.1 * 15.0),
                     lognorm2norm_std(15.0, 0.1 * 15.0),
                 ],
-                description="Force capacity of the secondary spring",
-            ),
+                "description": "Force capacity of the secondary spring",
+            },
         ],
-        copulas=None,
-    ),
-    "DerKiureghian1990b": ProbInputSpecFixDim(
-        name="DampedOscillatorReliability-DerKiureghian1990b",
-        description=(
+        "copulas": None,
+    },
+    "DerKiureghian1990b": {
+        "function_id": "DampedOscillatorReliability",
+        "description": (
             "Input model #2 for the damped oscillator reliability "
             "from Der Kiureghian and De Stefano (1990)"
         ),
-        marginals=INPUT_MARGINALS_DERKIUREGHIAN1991
+        "marginals": MARGINALS_DERKIUREGHIAN1991
         + [
-            UnivDistSpec(
-                name="Fs",
-                distribution="lognormal",
-                parameters=[
+            {
+                "name": "Fs",
+                "distribution": "lognormal",
+                "parameters": [
                     lognorm2norm_mean(21.5, 0.1 * 21.5),
                     lognorm2norm_std(21.5, 0.1 * 21.5),
                 ],
-                description="Force capacity of the secondary spring",
-            ),
+                "description": "Force capacity of the secondary spring",
+            },
         ],
-        copulas=None,
-    ),
-    "DerKiureghian1990c": ProbInputSpecFixDim(
-        name="DampedOscillatorReliability-DerKiureghian1990c",
-        description=(
+        "copulas": None,
+    },
+    "DerKiureghian1990c": {
+        "function_id": "DampedOscillatorReliability",
+        "description": (
             "Input model #3 for the damped oscillator reliability "
             "from Der Kiureghian and De Stefano (1990)"
         ),
-        marginals=INPUT_MARGINALS_DERKIUREGHIAN1991
+        "marginals": MARGINALS_DERKIUREGHIAN1991
         + [
-            UnivDistSpec(
-                name="Fs",
-                distribution="lognormal",
-                parameters=[
+            {
+                "name": "Fs",
+                "distribution": "lognormal",
+                "parameters": [
                     lognorm2norm_mean(27.5, 0.1 * 27.5),
                     lognorm2norm_std(27.5, 0.1 * 27.5),
                 ],
-                description="Force capacity of the secondary spring",
-            ),
+                "description": "Force capacity of the secondary spring",
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
-# peak factor
-AVAILABLE_PARAMETERS_RELIABILITY = {
-    "DerKiureghian1990": 3,
+
+AVAILABLE_PARAMETERS_RELIABILITY: FunParamSpecs = {
+    "DerKiureghian1990": {
+        "function_id": "DampedOscillatorReliability",
+        "description": (
+            "Parameter set for the damped oscillator reliability problem "
+            "from Der Kiureghian and De Stefano (1990)"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "pf",
+                "value": 3.0,
+                "type": float,
+                "description": "Peak factor [-]",
+            },
+        ],
+    },
 }
 
 
@@ -249,21 +269,20 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return np.sqrt(xx_s)
 
 
-class DampedOscillator(UQTestFunABC):
+class DampedOscillator(UQTestFunFixDimABC):
     """A concrete implementation of the Damped oscillator test function."""
 
     _tags = ["metamodeling", "sensitivity"]
     _description = (
         "Damped oscillator model from Igusa and Der Kiureghian (1985)"
     )
-    _available_inputs = AVAILABLE_INPUT_SPECS_BASE
+    _available_inputs = AVAILABLE_INPUTS_BASE
     _available_parameters = None
-    _default_spatial_dimension = 8
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
 
 
-def evaluate_reliability(xx: np.ndarray, parameters: float):
+def evaluate_reliability(xx: np.ndarray, pf: float):
     """Evaluate the performance function of the system reliability.
 
     Parameters
@@ -271,7 +290,7 @@ def evaluate_reliability(xx: np.ndarray, parameters: float):
     xx : np.ndarray
         An 8-dimensional input values given by an N-by-7 array
         where N is the number of input values.
-    parameters : float
+    pf : float
         The peak factor of the system.
 
     Returns
@@ -285,24 +304,21 @@ def evaluate_reliability(xx: np.ndarray, parameters: float):
     kk_s = xx[:, 3]  # Secondary spring stiffness
     ff_s = xx[:, -1]  # Force capacity of the secondary spring
 
-    pf = parameters  # peak factor
-
     yy = ff_s - pf * kk_s * rms_disp
 
     return yy
 
 
-class DampedOscillatorReliability(UQTestFunABC):
+class DampedOscillatorReliability(UQTestFunFixDimABC):
     """A concrete implementation of the Damped oscillator reliability func."""
 
     _tags = ["reliability"]
     _description = (
         "Performance function from Der Kiureghian and De Stefano (1990)"
     )
-    _available_inputs = AVAILABLE_INPUT_SPECS_RELIABILITY
+    _available_inputs = AVAILABLE_INPUTS_RELIABILITY
     _available_parameters = AVAILABLE_PARAMETERS_RELIABILITY
-    _default_spatial_dimension = 8
-    _default_input = "DerKiureghian1990a"
-    _default_parameters = "DerKiureghian1990"
+    _default_input_id = "DerKiureghian1990a"
+    _default_parameters_id = "DerKiureghian1990"
 
-    eval_ = staticmethod(evaluate_reliability)
+    evaluate = staticmethod(evaluate_reliability)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/flood.py b/src/uqtestfuns/test_functions/flood.py
index 630e46c..6d43863 100644
--- a/src/uqtestfuns/test_functions/flood.py
+++ b/src/uqtestfuns/test_functions/flood.py
@@ -30,74 +30,76 @@
    pp. 2001–2038.
    DOI: 10.1007/978-3-319-12385-1_64
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import MarginalSpecs, ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["Flood"]
 
-INPUT_MARGINALS_IOOSS2015 = [  # From Ref. [1]
-    UnivDistSpec(
-        name="Q",
-        distribution="trunc-gumbel",
-        parameters=[1013.0, 558.0, 500.0, 3000.0],
-        description="Maximum annual flow rate [m^3/s]",
-    ),
-    UnivDistSpec(
-        name="Ks",
-        distribution="trunc-normal",
-        parameters=[30.0, 8.0, 15.0, np.inf],
-        description="Strickler coefficient [m^(1/3)/s]",
-    ),
-    UnivDistSpec(
-        name="Zv",
-        distribution="triangular",
-        parameters=[49.0, 51.0, 50.0],
-        description="River downstream level [m]",
-    ),
-    UnivDistSpec(
-        name="Zm",
-        distribution="triangular",
-        parameters=[54.0, 56.0, 55.0],
-        description="River upstream level [m]",
-    ),
-    UnivDistSpec(
-        name="Hd",
-        distribution="uniform",
-        parameters=[7.0, 9.0],
-        description="Dyke height [m]",
-    ),
-    UnivDistSpec(
-        name="Cb",
-        distribution="triangular",
-        parameters=[55.0, 56.0, 55.5],
-        description="Bank level [m]",
-    ),
-    UnivDistSpec(
-        name="L",
-        distribution="triangular",
-        parameters=[4990.0, 5010.0, 5000.0],
-        description="Length of the river stretch [m]",
-    ),
-    UnivDistSpec(
-        name="B",
-        distribution="triangular",
-        parameters=[295.0, 305.0, 300.0],
-        description="River width [m]",
-    ),
+
+MARGINALS_IOOSS2015: MarginalSpecs = [  # From Ref. [1]
+    {
+        "name": "Q",
+        "distribution": "trunc-gumbel",
+        "parameters": [1013.0, 558.0, 500.0, 3000.0],
+        "description": "Maximum annual flow rate [m^3/s]",
+    },
+    {
+        "name": "Ks",
+        "distribution": "trunc-normal",
+        "parameters": [30.0, 8.0, 15.0, np.inf],
+        "description": "Strickler coefficient [m^(1/3)/s]",
+    },
+    {
+        "name": "Zv",
+        "distribution": "triangular",
+        "parameters": [49.0, 51.0, 50.0],
+        "description": "River downstream level [m]",
+    },
+    {
+        "name": "Zm",
+        "distribution": "triangular",
+        "parameters": [54.0, 56.0, 55.0],
+        "description": "River upstream level [m]",
+    },
+    {
+        "name": "Hd",
+        "distribution": "uniform",
+        "parameters": [7.0, 9.0],
+        "description": "Dyke height [m]",
+    },
+    {
+        "name": "Cb",
+        "distribution": "triangular",
+        "parameters": [55.0, 56.0, 55.5],
+        "description": "Bank level [m]",
+    },
+    {
+        "name": "L",
+        "distribution": "triangular",
+        "parameters": [4990.0, 5010.0, 5000.0],
+        "description": "Length of the river stretch [m]",
+    },
+    {
+        "name": "B",
+        "distribution": "triangular",
+        "parameters": [295.0, 305.0, 300.0],
+        "description": "River width [m]",
+    },
 ]
 
-AVAILABLE_INPUT_SPECS = {
-    "Iooss2015": ProbInputSpecFixDim(
-        name="Flood-Iooss2015",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Iooss2015": {
+        "function_id": "Flood",
+        "description": (
             "Probabilistic input model for the Flood model "
-            "from Iooss and Lemaître (2015)."
+            "from Iooss and Lemaître (2015)"
         ),
-        marginals=INPUT_MARGINALS_IOOSS2015,
-        copulas=None,
-    ),
+        "marginals": MARGINALS_IOOSS2015,
+        "copulas": None,
+    },
 }
 
 
@@ -137,13 +139,12 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return ss
 
 
-class Flood(UQTestFunABC):
+class Flood(UQTestFunFixDimABC):
     """Concrete implementation of the Flood model test function."""
 
     _tags = ["metamodeling", "sensitivity"]
     _description = "Flood model from Iooss and Lemaître (2015)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 8
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/forrester.py b/src/uqtestfuns/test_functions/forrester.py
index 24fac50..3fd35ef 100644
--- a/src/uqtestfuns/test_functions/forrester.py
+++ b/src/uqtestfuns/test_functions/forrester.py
@@ -13,30 +13,32 @@
    experiments," Technometrics, vol. 34, no. 1, pp. 15-25, 1992.
    DOI: 10.2307/1269548
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["Forrester2008"]
 
-AVAILABLE_INPUT_SPECS = {
-    "Forrester2008": ProbInputSpecFixDim(
-        name="Forrester2008",
-        description=(
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Forrester2008": {
+        "function_id": "Forrester2008",
+        "description": (
             "Input specification for the 1D test function "
             "from Forrester et al. (2008)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="x",
-                distribution="uniform",
-                parameters=[0, 1],
-                description="None",
-            )
+        "marginals": [
+            {
+                "name": "x",
+                "distribution": "uniform",
+                "parameters": [0, 1],
+                "description": None,
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
 
@@ -61,13 +63,12 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class Forrester2008(UQTestFunABC):
+class Forrester2008(UQTestFunFixDimABC):
     """An implementation of the 1D function of Forrester et al. (2008)."""
 
     _tags = ["optimization", "metamodeling"]
     _description = "One-dimensional function from Forrester et al. (2008)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 1
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/four_branch.py b/src/uqtestfuns/test_functions/four_branch.py
index e239ede..843364e 100644
--- a/src/uqtestfuns/test_functions/four_branch.py
+++ b/src/uqtestfuns/test_functions/four_branch.py
@@ -31,46 +31,76 @@
    p. D4016002, 2017.
    DOI: 10.1061/AJRUA6.0000870.
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs, FunParamSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["FourBranch"]
 
 
-AVAILABLE_INPUT_SPECS = {
-    "Katsuki1994": ProbInputSpecFixDim(
-        name="FourBranch-Katsuki1994",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Katsuki1994": {
+        "function_id": "FourBranch",
+        "description": (
             "Input model for the four-branch function "
             "from Katsuki and Frangopol (1994)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="X1",
-                distribution="normal",
-                parameters=[0, 1],
-                description="None",
-            ),
-            UnivDistSpec(
-                name="X2",
-                distribution="normal",
-                parameters=[0, 1],
-                description="None",
-            ),
+        "marginals": [
+            {
+                "name": "X1",
+                "distribution": "normal",
+                "parameters": [0, 1],
+                "description": None,
+            },
+            {
+                "name": "X2",
+                "distribution": "normal",
+                "parameters": [0, 1],
+                "description": None,
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
-AVAILABLE_PARAMETERS = {
-    "Katsuki1994": 3.5 * np.sqrt(2),
-    "Schueremans2005": 6.0 / np.sqrt(2),
+
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Katsuki1994": {
+        "function_id": "FourBranch",
+        "description": (
+            "Parameter set for the Four-branch function from Katsuki and "
+            "Frangopol (1994)"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "p",
+                "value": 3.5 * np.sqrt(2),
+                "type": float,
+                "description": None,
+            },
+        ],
+    },
+    "Schueremans2005": {
+        "function_id": "FourBranch",
+        "description": (
+            "Parameter set for the Four-branch function from Schueremans "
+            "(2005)"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "p",
+                "value": 6.0 / np.sqrt(2),
+                "type": float,
+                "description": None,
+            },
+        ],
+    },
 }
 
 
-def evaluate(xx: np.ndarray, parameters: float) -> np.ndarray:
+def evaluate(xx: np.ndarray, p: float) -> np.ndarray:
     """Evaluate the four-branch function on a set of input values.
 
     Parameters
@@ -78,7 +108,7 @@ def evaluate(xx: np.ndarray, parameters: float) -> np.ndarray:
     xx : np.ndarray
         A two-Dimensional input values given by an N-by-2 array
         where N is the number of input values.
-    parameters : float
+    p : float
         The parameter of the test function; a single float.
 
     Returns
@@ -87,7 +117,6 @@ def evaluate(xx: np.ndarray, parameters: float) -> np.ndarray:
         The output of the test function evaluated on the input values.
         The output is a 1-dimensional array of length N.
     """
-    p = parameters
 
     # Compute the performance function components
     yy_1 = (
@@ -108,16 +137,15 @@ def evaluate(xx: np.ndarray, parameters: float) -> np.ndarray:
     return np.min(yy, axis=0)
 
 
-class FourBranch(UQTestFunABC):
+class FourBranch(UQTestFunFixDimABC):
     """A concrete implementation of the four-branch test function."""
 
     _tags = ["reliability"]
     _description = (
         "Series system reliability from Katsuki and Frangopol (1994)"
     )
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = AVAILABLE_PARAMETERS
-    _default_spatial_dimension = 2
-    _default_parameters = "Schueremans2005"
+    _default_parameters_id = "Schueremans2005"
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/franke.py b/src/uqtestfuns/test_functions/franke.py
index c88a2e8..036aca5 100644
--- a/src/uqtestfuns/test_functions/franke.py
+++ b/src/uqtestfuns/test_functions/franke.py
@@ -39,46 +39,46 @@
    Journal, vol. 17, no. 4, pp. 318-324, 1974.
    DOI: 10.1093/comjnl/17.4.318
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["Franke1", "Franke2", "Franke3", "Franke4", "Franke5", "Franke6"]
 
-INPUT_MARGINALS_FRANKE1979 = [  # From Ref. [1]
-    UnivDistSpec(
-        name="X1",
-        distribution="uniform",
-        parameters=[0.0, 1.0],
-        description="None",
-    ),
-    UnivDistSpec(
-        name="X2",
-        distribution="uniform",
-        parameters=[0.0, 1.0],
-        description="None",
-    ),
-]
-
-AVAILABLE_INPUT_SPECS = {
-    "Franke1979": ProbInputSpecFixDim(
-        name="Franke1979",
-        description=(
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Franke1979": {
+        "function_id": "Franke",
+        "description": (
             "Input specification for the test functions from Franke (1979)."
         ),
-        marginals=INPUT_MARGINALS_FRANKE1979,
-        copulas=None,
-    ),
+        "marginals": [
+            {
+                "name": "X1",
+                "distribution": "uniform",
+                "parameters": [0.0, 1.0],
+                "description": None,
+            },
+            {
+                "name": "X2",
+                "distribution": "uniform",
+                "parameters": [0.0, 1.0],
+                "description": None,
+            },
+        ],
+        "copulas": None,
+    },
 }
 
+
 DEFAULT_INPUT_SELECTION = "Franke1979"
 
 COMMON_METADATA = dict(
     _tags=["metamodeling"],
-    _available_inputs=AVAILABLE_INPUT_SPECS,
+    _available_inputs=AVAILABLE_INPUTS,
     _available_parameters=None,
-    _default_spatial_dimension=2,
     _description="from Franke (1979)",
 )
 
@@ -116,7 +116,7 @@ def evaluate_franke1(xx: np.ndarray):
     return yy
 
 
-class Franke1(UQTestFunABC):
+class Franke1(UQTestFunFixDimABC):
     """A concrete implementation of the (1st) Franke function.
 
     The function features two Gaussian peaks and a Gaussian dip.
@@ -126,9 +126,8 @@ class Franke1(UQTestFunABC):
     _description = f"(1st) Franke function {COMMON_METADATA['_description']}"
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = COMMON_METADATA["_default_spatial_dimension"]
 
-    eval_ = staticmethod(evaluate_franke1)
+    evaluate = staticmethod(evaluate_franke1)  # type: ignore
 
 
 def evaluate_franke2(xx: np.ndarray):
@@ -152,7 +151,7 @@ def evaluate_franke2(xx: np.ndarray):
     return yy
 
 
-class Franke2(UQTestFunABC):
+class Franke2(UQTestFunFixDimABC):
     """A concrete implementation of the (2nd) Franke function.
 
     The function features two plateaus joined by a steep hill.
@@ -162,9 +161,8 @@ class Franke2(UQTestFunABC):
     _description = f"(2nd) Franke function {COMMON_METADATA['_description']}"
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = COMMON_METADATA["_default_spatial_dimension"]
 
-    eval_ = staticmethod(evaluate_franke2)
+    evaluate = staticmethod(evaluate_franke2)  # type: ignore
 
 
 def evaluate_franke3(xx: np.ndarray):
@@ -191,7 +189,7 @@ def evaluate_franke3(xx: np.ndarray):
     return yy
 
 
-class Franke3(UQTestFunABC):
+class Franke3(UQTestFunFixDimABC):
     """A concrete implementation of the (3rd) Franke function.
 
     The function features a saddle shaped surface.
@@ -201,9 +199,8 @@ class Franke3(UQTestFunABC):
     _description = f"(3rd) Franke function {COMMON_METADATA['_description']}"
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = COMMON_METADATA["_default_spatial_dimension"]
 
-    eval_ = staticmethod(evaluate_franke3)
+    evaluate = staticmethod(evaluate_franke3)  # type: ignore
 
 
 def evaluate_franke4(xx: np.ndarray):
@@ -230,7 +227,7 @@ def evaluate_franke4(xx: np.ndarray):
     return yy
 
 
-class Franke4(UQTestFunABC):
+class Franke4(UQTestFunFixDimABC):
     """A concrete implementation of the (4th) Franke function.
 
     The function features a gentle Gaussian hill.
@@ -240,9 +237,8 @@ class Franke4(UQTestFunABC):
     _description = f"(4th) Franke function {COMMON_METADATA['_description']}"
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = COMMON_METADATA["_default_spatial_dimension"]
 
-    eval_ = staticmethod(evaluate_franke4)
+    evaluate = staticmethod(evaluate_franke4)  # type: ignore
 
 
 def evaluate_franke5(xx: np.ndarray):
@@ -269,7 +265,7 @@ def evaluate_franke5(xx: np.ndarray):
     return yy
 
 
-class Franke5(UQTestFunABC):
+class Franke5(UQTestFunFixDimABC):
     """A concrete implementation of the (5th) Franke function.
 
     The function features a steep Gaussian hill.
@@ -279,9 +275,8 @@ class Franke5(UQTestFunABC):
     _description = f"(5th) Franke function {COMMON_METADATA['_description']}"
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = COMMON_METADATA["_default_spatial_dimension"]
 
-    eval_ = staticmethod(evaluate_franke5)
+    evaluate = staticmethod(evaluate_franke5)  # type: ignore
 
 
 def evaluate_franke6(xx: np.ndarray):
@@ -309,7 +304,7 @@ def evaluate_franke6(xx: np.ndarray):
     return yy
 
 
-class Franke6(UQTestFunABC):
+class Franke6(UQTestFunFixDimABC):
     """A concrete implementation of the (6th) Franke function.
 
     The function features a part of a sphere.
@@ -319,6 +314,5 @@ class Franke6(UQTestFunABC):
     _description = f"(6th) Franke function {COMMON_METADATA['_description']}"
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = COMMON_METADATA["_default_spatial_dimension"]
 
-    eval_ = staticmethod(evaluate_franke6)
+    evaluate = staticmethod(evaluate_franke6)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/friedman.py b/src/uqtestfuns/test_functions/friedman.py
new file mode 100644
index 0000000..5ec5ddf
--- /dev/null
+++ b/src/uqtestfuns/test_functions/friedman.py
@@ -0,0 +1,120 @@
+"""
+Module with an implementation of the Friedman test functions.
+
+The Friedman function is a function introduced in [2] as a six-dimensional
+(including one dummy variable) test function for testing a spline approximation
+method. It was later modified to be a ten-dimensional (including five dummy
+variables) function in [2].
+In [3], the function was employed as a test function in the context of
+sensitivity analysis.
+
+References
+----------
+
+1. J. H. Friedman, E. Grosse, and W. Stuetzle, "Multidimensional additive
+   spline approximation," SIAM Journal on Scientific and Statistical Computing,
+   vol. 4, no. 2, pp. 291-301, 1983.
+2. J. H. Friedman, "Multivariate adaptive regression splines," The Annals of
+   Statistics, vol. 19, no. 1, pp. 1-67, 1991.
+   DOI: 10.1214/aos/1176347963
+3. X. Sun, B. Croke, A. Jakeman, S. Roberts, "Benchmarking Active Subspace
+   methods of global sensitivity analysis against variance-based Sobol’
+   and Morris methods with established test functions," Environmental
+   Modelling & Software, vol. 149, p. 105310, 2022.
+   DOI: 10.1016/j.envsoft.2022.105310
+"""
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
+
+__all__ = ["Friedman6D", "Friedman10D"]
+
+
+AVAILABLE_INPUTS_6D: ProbInputSpecs = {
+    "Friedman1983": {
+        "function_id": "Friedman6D",
+        "description": (
+            "Input specification for the 6D test function "
+            "from Friedman et al. (1983)"
+        ),
+        "marginals": [
+            {
+                "name": f"x_{i + 1}",
+                "distribution": "uniform",
+                "parameters": [0, 1],
+                "description": None,
+            }
+            for i in range(6)
+        ],
+        "copulas": None,
+    },
+}
+
+
+AVAILABLE_INPUTS_10D: ProbInputSpecs = {
+    "Friedman1983": {
+        "function_id": "Friedman10D",
+        "description": (
+            "Input specification for the 6D test function "
+            "from Friedman (1991)"
+        ),
+        "marginals": [
+            {
+                "name": f"x_{i + 1}",
+                "distribution": "uniform",
+                "parameters": [0, 1],
+                "description": None,
+            }
+            for i in range(10)
+        ],
+        "copulas": None,
+    },
+}
+
+
+def evaluate_friedman(xx: np.ndarray) -> np.ndarray:
+    """Evaluate the six-dimensional Friedman test function.
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        6-Dimensional input values given by an N-by-6 array
+        where N is the number of input values.
+
+    Returns
+    -------
+    np.ndarray
+        The output of the six-dimensional Friedman function evaluated
+        on the input values.
+        The output is a 1-dimensional array of length N.
+    """
+    yy_1 = 10 * np.sin(np.pi * xx[:, 0] * xx[:, 1])
+    yy_2 = 20 * (xx[:, 2] - 0.5) ** 2
+    yy_3 = 10 * xx[:, 3]
+    yy_4 = 5 * xx[:, 5]
+
+    return yy_1 + yy_2 + yy_3 + yy_4
+
+
+class Friedman6D(UQTestFunFixDimABC):
+    """A concrete implementation of the 6D Friedman et al. (1983) function."""
+
+    _tags = ["metamodeling", "sensitivity"]
+    _description = "Six-dimensional function from Friedman et al. (1983)"
+    _available_inputs = AVAILABLE_INPUTS_6D
+    _available_parameters = None
+
+    evaluate = staticmethod(evaluate_friedman)  # type: ignore
+
+
+class Friedman10D(UQTestFunFixDimABC):
+    """A concrete implementation of the 10D Friedman (1991) function."""
+
+    _tags = ["metamodeling"]
+    _description = "Ten-dimensional function from Friedman (1991)"
+    _available_inputs = AVAILABLE_INPUTS_10D
+    _available_parameters = None
+
+    evaluate = staticmethod(evaluate_friedman)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/gayton_hat.py b/src/uqtestfuns/test_functions/gayton_hat.py
index 4ede290..73d4b2a 100644
--- a/src/uqtestfuns/test_functions/gayton_hat.py
+++ b/src/uqtestfuns/test_functions/gayton_hat.py
@@ -13,37 +13,38 @@
    and System Safety, vol. 111, pp. 232-240, 2013.
    DOI: 10.1016/j.ress.2012.10.008
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["GaytonHat"]
 
 
-AVAILABLE_INPUT_SPECS = {
-    "Echard2013": ProbInputSpecFixDim(
-        name="Echard2013",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Echard2013": {
+        "function_id": "GaytonHat",
+        "description": (
             "Input model for the Gayton Hat function "
             "from Echard et al. (2013)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="U1",
-                distribution="normal",
-                parameters=[0, 1],
-                description="None",
-            ),
-            UnivDistSpec(
-                name="U2",
-                distribution="normal",
-                parameters=[0, 1],
-                description="None",
-            ),
+        "marginals": [
+            {
+                "name": "U1",
+                "distribution": "normal",
+                "parameters": [0, 1],
+                "description": None,
+            },
+            {
+                "name": "U2",
+                "distribution": "normal",
+                "parameters": [0, 1],
+                "description": None,
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
 
@@ -67,15 +68,15 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class GaytonHat(UQTestFunABC):
+class GaytonHat(UQTestFunFixDimABC):
     """A concrete implementation of the Gayton Hat test function."""
 
     _tags = ["reliability"]
     _description = (
         "Two-Dimensional Gayton Hat function from Echard et al. (2013)"
     )
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 2
+    _default_input_dimension = 2
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/gramacy2007.py b/src/uqtestfuns/test_functions/gramacy2007.py
index bac5995..402de60 100644
--- a/src/uqtestfuns/test_functions/gramacy2007.py
+++ b/src/uqtestfuns/test_functions/gramacy2007.py
@@ -17,46 +17,36 @@
    models,” Journal of Statistical Software, vol. 19, no. 9, 2007.
    DOI: 10.18637/jss.v019.i09.
 """
-import numpy as np
 
-from numpy.random._generator import Generator
-from typing import Optional
+import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["Gramacy1DSine"]
 
-AVAILABLE_INPUT_SPECS = {
-    "Gramacy2007": ProbInputSpecFixDim(
-        name="Gramacy2007",
-        description=(
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Gramacy2007": {
+        "function_id": "Gramacy1DSine",
+        "description": (
             "Input model for the one-dimensional function "
             "from Gramacy (2007)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="x",
-                distribution="uniform",
-                parameters=[0.0, 20.0],
-                description="None",
-            )
+        "marginals": [
+            {
+                "name": "x",
+                "distribution": "uniform",
+                "parameters": [0.0, 20.0],
+                "description": None,
+            },
         ],
-        copulas=None,
-    ),
-}
-
-AVAILABLE_PARAMETERS = {
-    "noisy": np.random.default_rng(),
-    "noiseless": None,
+        "copulas": None,
+    },
 }
 
-DEFAULT_PARAMETERS_SELECTION = "noisy"
-
 
-def evaluate_1dsine(
-    xx: np.ndarray, parameters: Optional[Generator] = None
-) -> np.ndarray:
+def evaluate_1dsine(xx: np.ndarray) -> np.ndarray:
     """Evaluate the 1D Gramacy (2007) Sine function on a set of input values.
 
     Parameters
@@ -83,22 +73,15 @@ def evaluate_1dsine(
     )
     yy[idx_2] = -1 + 0.1 * xx[idx_2, 0]
 
-    if parameters is not None:
-        yy_noise = parameters.normal(size=len(xx), scale=0.1)
-
-        return yy + yy_noise
-
     return yy
 
 
-class Gramacy1DSine(UQTestFunABC):
+class Gramacy1DSine(UQTestFunFixDimABC):
     """A concrete implementation of the 1D Gramacy (2007) Sine function."""
 
     _tags = ["metamodeling"]
     _description = "One-dimensional sine function from Gramacy (2007)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
-    _available_parameters = AVAILABLE_PARAMETERS
-    _default_spatial_dimension = 1
-    _default_parameters = "noisy"
+    _available_inputs = AVAILABLE_INPUTS
+    _available_parameters = None
 
-    eval_ = staticmethod(evaluate_1dsine)
+    evaluate = staticmethod(evaluate_1dsine)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/hyper_sphere.py b/src/uqtestfuns/test_functions/hyper_sphere.py
index 2b0973e..54d2e53 100644
--- a/src/uqtestfuns/test_functions/hyper_sphere.py
+++ b/src/uqtestfuns/test_functions/hyper_sphere.py
@@ -12,37 +12,38 @@
    Structural Safety, vol. 73, pp. 42–53, 2018.
    DOI: 10.1016/j.strusafe.2018.02.005.
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["HyperSphere"]
 
 
-AVAILABLE_INPUT_SPECS = {
-    "Li2018": ProbInputSpecFixDim(
-        name="Li2018",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Li2018": {
+        "function_id": "Li2018",
+        "description": (
             "Input model for the hyper-sphere reliability problem "
             "from Li et al. (2018)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="X1",
-                distribution="normal",
-                parameters=[0.5, 0.2],
-                description="None",
-            ),
-            UnivDistSpec(
-                name="X2",
-                distribution="normal",
-                parameters=[0.5, 0.2],
-                description="None",
-            ),
+        "marginals": [
+            {
+                "name": "X1",
+                "distribution": "normal",
+                "parameters": [0.5, 0.2],
+                "description": None,
+            },
+            {
+                "name": "X2",
+                "distribution": "normal",
+                "parameters": [0.5, 0.2],
+                "description": None,
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
 
@@ -66,15 +67,15 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class HyperSphere(UQTestFunABC):
+class HyperSphere(UQTestFunFixDimABC):
     """A concrete implementation of the hyper-sphere reliability problem."""
 
     _tags = ["reliability"]
     _description = (
         "Hyper-sphere bound reliability problem from Li et al. (2018)"
     )
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 2
+    _default_input_dimension = 2
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/ishigami.py b/src/uqtestfuns/test_functions/ishigami.py
index b514c05..84ab8ca 100644
--- a/src/uqtestfuns/test_functions/ishigami.py
+++ b/src/uqtestfuns/test_functions/ishigami.py
@@ -27,58 +27,101 @@
    vol. 94, no. 3, pp. 742–751, 2009.
    DOI:10.1016/j.ress.2008.07.008
 """
-import numpy as np
 
-from typing import Tuple
+import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import (
+    MarginalSpecs,
+    ProbInputSpecs,
+    FunParamSpecs,
+)
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["Ishigami"]
 
 
-INPUT_MARGINALS_ISHIGAMI1991 = [
-    UnivDistSpec(
-        name="X1",
-        distribution="uniform",
-        parameters=[-np.pi, np.pi],
-        description="None",
-    ),
-    UnivDistSpec(
-        name="X2",
-        distribution="uniform",
-        parameters=[-np.pi, np.pi],
-        description="None",
-    ),
-    UnivDistSpec(
-        name="X3",
-        distribution="uniform",
-        parameters=[-np.pi, np.pi],
-        description="None",
-    ),
+MARGINALS_ISHIGAMI1991: MarginalSpecs = [
+    {
+        "name": "X1",
+        "distribution": "uniform",
+        "parameters": [-np.pi, np.pi],
+        "description": None,
+    },
+    {
+        "name": "X2",
+        "distribution": "uniform",
+        "parameters": [-np.pi, np.pi],
+        "description": None,
+    },
+    {
+        "name": "X3",
+        "distribution": "uniform",
+        "parameters": [-np.pi, np.pi],
+        "description": None,
+    },
 ]
 
-AVAILABLE_INPUT_SPECS = {
-    "Ishigami1991": ProbInputSpecFixDim(
-        name="Ishigami1991",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Ishigami1991": {
+        "function_id": "Ishigami",
+        "description": (
             "Probabilistic input model for the Ishigami function "
             "from Ishigami and Homma (1991)."
         ),
-        marginals=INPUT_MARGINALS_ISHIGAMI1991,
-        copulas=None,
-    ),
+        "marginals": MARGINALS_ISHIGAMI1991,
+        "copulas": None,
+    },
 }
 
-AVAILABLE_PARAMETERS = {
-    "Ishigami1991": (7, 0.05),  # from [1]
-    "Sobol1999": (7, 0.1),  # from [2]
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Ishigami1991": {
+        "function_id": "Ishigami",
+        "description": (
+            "Parameter set for the Ishigami function from Ishigami and Homma "
+            "(1991)"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "a",
+                "value": 7.0,
+                "type": float,
+                "description": None,
+            },
+            {
+                "keyword": "b",
+                "value": 0.1,
+                "type": float,
+                "description": None,
+            },
+        ],
+    },
+    "Sobol1999": {
+        "function_id": "Ishigami",
+        "description": (
+            "Parameter set for the Ishigami function from Sobol' and Levitan "
+            "(1999)"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "a",
+                "value": 7.0,
+                "type": float,
+                "description": None,
+            },
+            {
+                "keyword": "b",
+                "value": 0.05,
+                "type": float,
+                "description": None,
+            },
+        ],
+    },
 }
 
 DEFAULT_PARAMETERS_SELECTION = "Ishigami1991"
 
 
-def evaluate(xx: np.ndarray, parameters: Tuple[float, float]):
+def evaluate(xx: np.ndarray, a: float, b: float) -> np.ndarray:
     """Evaluate the Ishigami function on a set of input values.
 
     Parameters
@@ -86,8 +129,10 @@ def evaluate(xx: np.ndarray, parameters: Tuple[float, float]):
     xx : np.ndarray
         3-Dimensional input values given by N-by-3 arrays where
         N is the number of input values.
-    parameters : Tuple[float, float]
-        Tuple of two values as the parameters of the function.
+    a : float
+        The first parameter of the Ishigami function.
+    b : float
+        The second parameter of the Ishigami function.
 
     Returns
     -------
@@ -97,22 +142,21 @@ def evaluate(xx: np.ndarray, parameters: Tuple[float, float]):
     """
     # Compute the Ishigami function
     term_1 = np.sin(xx[:, 0])
-    term_2 = parameters[0] * np.sin(xx[:, 1]) ** 2
-    term_3 = parameters[1] * xx[:, 2] ** 4 * np.sin(xx[:, 0])
+    term_2 = a * np.sin(xx[:, 1]) ** 2
+    term_3 = b * xx[:, 2] ** 4 * np.sin(xx[:, 0])
 
     yy = term_1 + term_2 + term_3
 
     return yy
 
 
-class Ishigami(UQTestFunABC):
+class Ishigami(UQTestFunFixDimABC):
     """An implementation of the Ishigami test function."""
 
     _tags = ["sensitivity"]
     _description = "Ishigami function from Ishigami and Homma (1991)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = AVAILABLE_PARAMETERS
-    _default_parameters = DEFAULT_PARAMETERS_SELECTION
-    _default_spatial_dimension = 3
+    _default_parameters_id = DEFAULT_PARAMETERS_SELECTION
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/lim.py b/src/uqtestfuns/test_functions/lim.py
new file mode 100644
index 0000000..4df883e
--- /dev/null
+++ b/src/uqtestfuns/test_functions/lim.py
@@ -0,0 +1,139 @@
+"""
+Module with implementations of the two functions from Lim et al. (2002).
+
+The test functions are two-dimensional scalar-valued functions used in [1]
+to illustrate the connection between Gaussian process metamodels and
+polynomials.
+
+References
+----------
+
+1. Y. B. Lim, J. Sacks, W. J. Studden, and W. J. Welch, “Design and analysis of
+   computer experiments when the output is highly correlated over the input
+   space,” Canadian Journal of Statistics, vol. 30, no. 1, pp. 109–126, 2002,
+   DOI: 10.2307/3315868
+"""
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import ProbInputSpecs, MarginalSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
+
+__all__ = ["LimPoly", "LimNonPoly"]
+
+
+MARGINALS_LIM2002: MarginalSpecs = [
+    {
+        "name": "x1",
+        "distribution": "uniform",
+        "parameters": [0.0, 1.0],
+        "description": None,
+    },
+    {
+        "name": "x2",
+        "distribution": "uniform",
+        "parameters": [0.0, 1.0],
+        "description": None,
+    },
+]
+
+
+def evaluate_poly(xx: np.ndarray) -> np.ndarray:
+    """Evaluate the polynomial test function from Lim et al (2002).
+
+    The function is a polynomial with maximum total degree of 5.
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        Two-Dimensional input values given by N-by-2 arrays 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.
+    """
+
+    trm_1 = 5.0 / 2.0 * xx[:, 0]
+    trm_2 = 35.0 / 2.0 * xx[:, 1]
+    trm_3 = 5.0 / 2.0 * xx[:, 0] * xx[:, 1]
+    trm_4 = 19 * xx[:, 1] ** 2
+    trm_5 = 15.0 / 2.0 * xx[:, 0] ** 3
+    trm_6 = 5.0 / 2.0 * xx[:, 0] * xx[:, 1] ** 2
+    trm_7 = 11.0 / 2.0 * xx[:, 1] ** 4
+    trm_8 = xx[:, 0] ** 3 * xx[:, 1] ** 2
+
+    yy = 9 + trm_1 - trm_2 + trm_3 + trm_4 - trm_5 - trm_6 - trm_7 + trm_8
+
+    return yy
+
+
+class LimPoly(UQTestFunFixDimABC):
+    """An implementation of the 2D polynomial from Lim et al. (2002)."""
+
+    _tags = ["metamodeling"]
+    _description = "Two-dimensional polynomial function from Lim et al. (2002)"
+    _available_inputs: ProbInputSpecs = {
+        "Lim2002": {
+            "function_id": "LimPoly",
+            "description": (
+                "Input specification for the polynomial function "
+                "from Lim et al. (2002)"
+            ),
+            "marginals": MARGINALS_LIM2002,
+            "copulas": None,
+        }
+    }
+    _available_parameters = None
+
+    evaluate = staticmethod(evaluate_poly)  # type: ignore
+
+
+def evaluate_non_poly(xx: np.ndarray) -> np.ndarray:
+    """Evaluate the non-polynomial test function from Lim et al (2002).
+
+    The function is a polynomial with maximum total degree of 5.
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        Two-Dimensional input values given by N-by-2 arrays 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.
+    """
+    trm_1 = 30 + 5 * xx[:, 0] * np.sin(5 * xx[:, 0])
+    trm_2 = 4 + np.exp(-5 * xx[:, 1])
+
+    yy = (trm_1 * trm_2 - 100) / 6.0
+
+    return yy
+
+
+class LimNonPoly(UQTestFunFixDimABC):
+    """An implementation of the 2D non-polynomial from Lim et al. (2002)."""
+
+    _tags = ["metamodeling"]
+    _description = (
+        "Two-dimensional non-polynomial function from Lim et al. (2002)"
+    )
+    _available_inputs: ProbInputSpecs = {
+        "Lim2002": {
+            "function_id": "LimNonPoly",
+            "description": (
+                "Input specification for the non-polynomial function "
+                "from Lim et al. (2002)"
+            ),
+            "marginals": MARGINALS_LIM2002,
+            "copulas": None,
+        }
+    }
+    _available_parameters = None
+
+    evaluate = staticmethod(evaluate_non_poly)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/mclain.py b/src/uqtestfuns/test_functions/mclain.py
index b99bdd4..4f03282 100644
--- a/src/uqtestfuns/test_functions/mclain.py
+++ b/src/uqtestfuns/test_functions/mclain.py
@@ -27,45 +27,44 @@
    Technical Report No. NPS53-79-003, 1979.
    URL: https://core.ac.uk/reader/36727660
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["McLainS1", "McLainS2", "McLainS3", "McLainS4", "McLainS5"]
 
-INPUT_MARGINALS_MCLAIN1974 = [  # From Ref. [1]
-    UnivDistSpec(
-        name="X1",
-        distribution="uniform",
-        parameters=[1.0, 10.0],
-        description="None",
-    ),
-    UnivDistSpec(
-        name="X2",
-        distribution="uniform",
-        parameters=[1.0, 10.0],
-        description="None",
-    ),
-]
-
-AVAILABLE_INPUT_SPECS = {
-    "McLain1974": ProbInputSpecFixDim(
-        name="McLain-1974",
-        description=(
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "McLain1974": {
+        "function_id": "McLain",
+        "description": (
             "Input specification for the McLain's test functions "
             "from McLain (1974)."
         ),
-        marginals=INPUT_MARGINALS_MCLAIN1974,
-        copulas=None,
-    ),
+        "marginals": [  # From Ref. [1]
+            {
+                "name": "X1",
+                "distribution": "uniform",
+                "parameters": [1.0, 10.0],
+                "description": None,
+            },
+            {
+                "name": "X2",
+                "distribution": "uniform",
+                "parameters": [1.0, 10.0],
+                "description": None,
+            },
+        ],
+        "copulas": None,
+    },
 }
 
 COMMON_METADATA = dict(
     _tags=["metamodeling"],
-    _available_inputs=AVAILABLE_INPUT_SPECS,
+    _available_inputs=AVAILABLE_INPUTS,
     _available_parameters=None,
-    _default_spatial_dimension=2,
     _description="from McLain (1974)",
 )
 
@@ -91,7 +90,7 @@ def evaluate_mclain_s1(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class McLainS1(UQTestFunABC):
+class McLainS1(UQTestFunFixDimABC):
     """A concrete implementation of the McLain S1 function.
 
     The function features a part of a sphere.
@@ -101,9 +100,8 @@ class McLainS1(UQTestFunABC):
     _description = f"McLain S1 function {COMMON_METADATA['_description']}"
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = COMMON_METADATA["_default_spatial_dimension"]
 
-    eval_ = staticmethod(evaluate_mclain_s1)
+    evaluate = staticmethod(evaluate_mclain_s1)  # type: ignore
 
 
 def evaluate_mclain_s2(xx: np.ndarray) -> np.ndarray:
@@ -126,7 +124,7 @@ def evaluate_mclain_s2(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class McLainS2(UQTestFunABC):
+class McLainS2(UQTestFunFixDimABC):
     """A concrete implementation of the McLain S2 function.
 
     The function features a steep hill rising from a plain.
@@ -136,9 +134,8 @@ class McLainS2(UQTestFunABC):
     _description = f"McLain S2 function {COMMON_METADATA['_description']}"
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = COMMON_METADATA["_default_spatial_dimension"]
 
-    eval_ = staticmethod(evaluate_mclain_s2)
+    evaluate = staticmethod(evaluate_mclain_s2)  # type: ignore
 
 
 def evaluate_mclain_s3(xx: np.ndarray) -> np.ndarray:
@@ -161,7 +158,7 @@ def evaluate_mclain_s3(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class McLainS3(UQTestFunABC):
+class McLainS3(UQTestFunFixDimABC):
     """A concrete implementation of the McLain S3 function.
 
     The function features a less steep hill (compared to S2).
@@ -171,9 +168,8 @@ class McLainS3(UQTestFunABC):
     _description = f"McLain S3 function {COMMON_METADATA['_description']}"
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = COMMON_METADATA["_default_spatial_dimension"]
 
-    eval_ = staticmethod(evaluate_mclain_s3)
+    evaluate = staticmethod(evaluate_mclain_s3)  # type: ignore
 
 
 def evaluate_mclain_s4(xx: np.ndarray) -> np.ndarray:
@@ -199,7 +195,7 @@ def evaluate_mclain_s4(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class McLainS4(UQTestFunABC):
+class McLainS4(UQTestFunFixDimABC):
     """A concrete implementation of the McLain S4 function.
 
     The function features a long narrow hill.
@@ -209,9 +205,8 @@ class McLainS4(UQTestFunABC):
     _description = f"McLain S4 function {COMMON_METADATA['_description']}"
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = COMMON_METADATA["_default_spatial_dimension"]
 
-    eval_ = staticmethod(evaluate_mclain_s4)
+    evaluate = staticmethod(evaluate_mclain_s4)  # type: ignore
 
 
 def evaluate_mclain_s5(xx: np.ndarray) -> np.ndarray:
@@ -234,7 +229,7 @@ def evaluate_mclain_s5(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class McLainS5(UQTestFunABC):
+class McLainS5(UQTestFunFixDimABC):
     """A concrete implementation of the McLain S5 function.
 
     The function features two plateaus separated by a steep cliff.
@@ -244,6 +239,5 @@ class McLainS5(UQTestFunABC):
     _description = f"McLain S5 function {COMMON_METADATA['_description']}"
     _available_inputs = COMMON_METADATA["_available_inputs"]
     _available_parameters = COMMON_METADATA["_available_parameters"]
-    _default_spatial_dimension = COMMON_METADATA["_default_spatial_dimension"]
 
-    eval_ = staticmethod(evaluate_mclain_s5)
+    evaluate = staticmethod(evaluate_mclain_s5)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/moon3d.py b/src/uqtestfuns/test_functions/moon3d.py
new file mode 100644
index 0000000..0429a02
--- /dev/null
+++ b/src/uqtestfuns/test_functions/moon3d.py
@@ -0,0 +1,83 @@
+"""
+Module with an implementation of the 3D test function from Moon (2010).
+
+The Moon3D function is a three-dimensional scalar-valued function used in
+[1] to illustrate the analytical derivation of Sobol' sensitivity indices.
+
+References
+----------
+
+1. H. Moon, "Design and Analysis of Computer Experiments for Screening Input
+   Variables," Ph.D. dissertation, Ohio State University, Ohio, 2010.
+   URL: http://rave.ohiolink.edu/etdc/view?acc_num=osu1275422248
+"""
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
+
+__all__ = ["Moon3D"]
+
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Moon2010": {
+        "function_id": "Moon3D",
+        "description": (
+            "Probabilistic input model for the 3D test function "
+            "from Moon (2010)"
+        ),
+        "marginals": [
+            {
+                "name": "X1",
+                "distribution": "uniform",
+                "parameters": [0, 1],
+                "description": None,
+            },
+            {
+                "name": "X2",
+                "distribution": "uniform",
+                "parameters": [0, 1],
+                "description": None,
+            },
+            {
+                "name": "X3",
+                "distribution": "uniform",
+                "parameters": [0, 1],
+                "description": None,
+            },
+        ],
+        "copulas": None,
+    },
+}
+
+
+def evaluate(xx: np.ndarray) -> np.ndarray:
+    """The evaluation function for the Moon (2010) 3D function.
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        One-dimensional input values given by an N-by-3 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.
+    """
+    yy = xx[:, 0] + xx[:, 1] + 3 * xx[:, 0] * xx[:, 2]
+
+    return yy
+
+
+class Moon3D(UQTestFunFixDimABC):
+    """An implementation of the 3D function of Moon (2010)."""
+
+    _tags = ["sensitivity"]
+    _description = "Three-dimensional function from Moon (2010)"
+    _available_inputs = AVAILABLE_INPUTS
+    _available_parameters = None
+
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/morris2006.py b/src/uqtestfuns/test_functions/morris2006.py
new file mode 100644
index 0000000..6a37953
--- /dev/null
+++ b/src/uqtestfuns/test_functions/morris2006.py
@@ -0,0 +1,129 @@
+"""
+This module implements the Morris function from Morris et al. (2006).
+
+The Morris function is an M-dimensional, scalar-valued function commonly used
+as a test function for sensitivity analysis.
+It was first appeared in [1] and it has been revisited multiple times in the
+literature in similar contexts, e.g., [2], [3].
+
+The function features a parameter that dictates the number of important
+input variables. The remaining input variables are inert. Furthermore,
+all sensitivity indices of the important variables have
+the same Sobol' sensitivity indices.
+
+References
+----------
+
+1. M. D. Morris, L. M. Moore, and M. D. McKay, “Sampling plans based on
+   balanced incomplete block designs for evaluating the importance of
+   computer model inputs,” Journal of Statistical Planning and Inference,
+   vol. 136, no. 9, pp. 3203–3220, 2006.
+   DOI: 10.1016/j.jspi.2005.01.001
+2. X. Sun, B. Croke, A. Jakeman, S. Roberts, "Benchmarking Active Subspace
+   methods of global sensitivity analysis against variance-based Sobol’
+   and Morris methods with established test functions," Environmental Modelling
+   & Software, vol. 149, p. 105310, 2022.
+   DOI: 10.1016/j.envsoft.2022.105310
+3. A. Horiguchi, M. T. Pratola, and T. J. Santner, “Assessing variable activity
+   for Bayesian regression trees,” Reliability Engineering & System Safety,
+   vol. 207, p. 107391, 2021,
+   DOI: 10.1016/j.ress.2020.107391
+"""
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import FunParamSpecs, ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunVarDimABC
+
+__all__ = ["Morris2006"]
+
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Morris2006": {
+        "function_id": "Morris2006",
+        "description": (
+            "Probabilistic input model for the M-dimensional function "
+            "from Morris et al. (2006)"
+        ),
+        "marginals": [
+            {
+                "name": "X",
+                "distribution": "uniform",
+                "parameters": [0.0, 1.0],
+                "description": None,
+            },
+        ],
+        "copulas": None,
+    },
+}
+
+
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Morris2006": {
+        "function_id": "Morris2006",
+        "description": (
+            "Parameter set for the M-dimensional function from "
+            "Morris et al. (2006); the parameter controls the number of "
+            "important input variables"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "p",
+                "value": 10,
+                "type": int,
+                "description": "# of important inputs",
+            },
+        ],
+    }
+}
+
+
+def evaluate(xx: np.ndarray, p: int) -> np.ndarray:
+    """Evaluate the Morris2006 test function.
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        M-Dimensional input values given by an N-by-M array where
+        N is the number of input values.
+    p : int
+        The number of important input variables. If p > M then p is set
+        equal to 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.
+    """
+    input_dim = xx.shape[1]
+    if p > input_dim:
+        p = input_dim
+
+    alpha = np.sqrt(12) - 6 * np.sqrt(0.1 * (p - 1))
+
+    term_1 = alpha * np.sum(xx[:, :p], axis=1)
+    if p == 1:
+        return term_1
+
+    beta = 12 / np.sqrt(10 * (p - 1))
+    term_2 = np.zeros(len(xx))
+    for i in range(p - 1):
+        ip1 = i + 1
+        term_2[:] += xx[:, i] * np.sum(xx[:, ip1:p], axis=1)
+    term_2 *= beta
+
+    yy = term_1 + term_2
+
+    return yy
+
+
+class Morris2006(UQTestFunVarDimABC):
+    """An implementation of the M-dimensional Morris2006."""
+
+    _tags = ["sensitivity"]
+    _description = "Test function from Morris et al. (2006)"
+    _available_inputs = AVAILABLE_INPUTS
+    _available_parameters = AVAILABLE_PARAMETERS
+
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/oakley2002.py b/src/uqtestfuns/test_functions/oakley2002.py
index 94e4dc9..630e658 100644
--- a/src/uqtestfuns/test_functions/oakley2002.py
+++ b/src/uqtestfuns/test_functions/oakley2002.py
@@ -14,30 +14,32 @@
    p. 769-784, 2002.
    DOI: 10.1093/biomet/89.4.769
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["Oakley1D"]
 
-AVAILABLE_INPUT_SPECS = {
-    "Oakley2002": ProbInputSpecFixDim(
-        name="Oakley2002",
-        description=(
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Oakley2002": {
+        "function_id": "Oakley1D",
+        "description": (
             "Probabilistic input model for the one-dimensional function "
             "from Oakley and O'Hagan (2002)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="x",
-                distribution="normal",
-                parameters=[0.0, 4.0],
-                description="None",
-            )
+        "marginals": [
+            {
+                "name": "x",
+                "distribution": "normal",
+                "parameters": [0.0, 4.0],
+                "description": None,
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
 
@@ -62,13 +64,12 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class Oakley1D(UQTestFunABC):
+class Oakley1D(UQTestFunFixDimABC):
     """An implementation of the 1D function from Oakley & O'Hagan (2002)."""
 
     _tags = ["metamodeling"]
     _description = "One-dimensional function from Oakley and O'Hagan (2002)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 1
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/otl_circuit.py b/src/uqtestfuns/test_functions/otl_circuit.py
index adaa315..22b5dad 100644
--- a/src/uqtestfuns/test_functions/otl_circuit.py
+++ b/src/uqtestfuns/test_functions/otl_circuit.py
@@ -17,87 +17,89 @@
    vol. 19, pp. 327-338, 2007.
    DOI: 10.1080/08982110701580930
 2. H. Moon, "Design and Analysis of Computer Experiments for Screening Input
-   Variables," Ph. D. dissertation, Ohio State University, Ohio, 2010.
+   Variables," Ph.D. dissertation, Ohio State University, Ohio, 2010.
    URL: http://rave.ohiolink.edu/etdc/view?acc_num=osu1275422248
 """
+
 import numpy as np
 
-from copy import copy
+from copy import deepcopy
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import MarginalSpecs, ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["OTLCircuit"]
 
-INPUT_MARGINALS_BENARI2007 = [
-    UnivDistSpec(
-        name="Rb1",
-        distribution="uniform",
-        parameters=[50.0, 150.0],
-        description="Resistance b1 [kOhm]",
-    ),
-    UnivDistSpec(
-        name="Rb2",
-        distribution="uniform",
-        parameters=[25.0, 70.0],
-        description="Resistance b2 [kOhm]",
-    ),
-    UnivDistSpec(
-        name="Rf",
-        distribution="uniform",
-        parameters=[0.5, 3.0],
-        description="Resistance f [kOhm]",
-    ),
-    UnivDistSpec(
-        name="Rc1",
-        distribution="uniform",
-        parameters=[1.2, 2.5],
-        description="Resistance c1 [kOhm]",
-    ),
-    UnivDistSpec(
-        name="Rc2",
-        distribution="uniform",
-        parameters=[0.25, 1.20],
-        description="Resistance c2 [kOhm]",
-    ),
-    UnivDistSpec(
-        name="beta",
-        distribution="uniform",
-        parameters=[50.0, 300.0],
-        description="Current gain [A]",
-    ),
+
+MARGINALS_BENARI2007: MarginalSpecs = [
+    {
+        "name": "Rb1",
+        "distribution": "uniform",
+        "parameters": [50.0, 150.0],
+        "description": "Resistance b1 [kOhm]",
+    },
+    {
+        "name": "Rb2",
+        "distribution": "uniform",
+        "parameters": [25.0, 70.0],
+        "description": "Resistance b2 [kOhm]",
+    },
+    {
+        "name": "Rf",
+        "distribution": "uniform",
+        "parameters": [0.5, 3.0],
+        "description": "Resistance f [kOhm]",
+    },
+    {
+        "name": "Rc1",
+        "distribution": "uniform",
+        "parameters": [1.2, 2.5],
+        "description": "Resistance c1 [kOhm]",
+    },
+    {
+        "name": "Rc2",
+        "distribution": "uniform",
+        "parameters": [0.25, 1.20],
+        "description": "Resistance c2 [kOhm]",
+    },
+    {
+        "name": "beta",
+        "distribution": "uniform",
+        "parameters": [50.0, 300.0],
+        "description": "Current gain [A]",
+    },
 ]
 
-INPUT_MARGINALS_MOON2010 = [copy(_) for _ in INPUT_MARGINALS_BENARI2007]
+MARGINALS_MOON2010 = [deepcopy(_) for _ in MARGINALS_BENARI2007]
 for i in range(14):
-    INPUT_MARGINALS_MOON2010.append(
-        UnivDistSpec(
-            name=f"Inert {i+1}",
-            distribution="uniform",
-            parameters=[100.0, 200.0],
-            description="Inert input [-]",
-        )
+    MARGINALS_MOON2010.append(
+        {
+            "name": f"Inert {i+1}",
+            "distribution": "uniform",
+            "parameters": [100.0, 200.0],
+            "description": "Inert input [-]",
+        },
     )
 
-AVAILABLE_INPUT_SPECS = {
-    "BenAri2007": ProbInputSpecFixDim(
-        name="OTLCircuit-BenAri2007",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "BenAri2007": {
+        "function_id": "OTLCircuit",
+        "description": (
             "Probabilistic input model for the OTL Circuit function "
             "from Ben-Ari and Steinberg (2007)."
         ),
-        marginals=INPUT_MARGINALS_BENARI2007,
-        copulas=None,
-    ),
-    "Moon2010": ProbInputSpecFixDim(
-        name="OTLCircuit-Moon2010",
-        description=(
+        "marginals": MARGINALS_BENARI2007,
+        "copulas": None,
+    },
+    "Moon2010": {
+        "function_id": "OTLCircuit",
+        "description": (
             "Probabilistic input model for the OTL Circuit function "
             "from Moon (2010)."
         ),
-        marginals=INPUT_MARGINALS_MOON2010,
-        copulas=None,
-    ),
+        "marginals": MARGINALS_MOON2010,
+        "copulas": None,
+    },
 }
 
 DEFAULT_INPUT_SELECTION = "BenAri2007"
@@ -146,17 +148,17 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return vm
 
 
-class OTLCircuit(UQTestFunABC):
+class OTLCircuit(UQTestFunFixDimABC):
     """A concrete implementation of the OTL circuit test function."""
 
     _tags = ["metamodeling", "sensitivity"]
-    _default_spatial_dimension = 6
+    _default_input_dimension = 6
     _description = (
         "Output transformerless (OTL) circuit model "
         "from Ben-Ari and Steinberg (2007)"
     )
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_input = DEFAULT_INPUT_SELECTION
+    _default_input_id = DEFAULT_INPUT_SELECTION
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/piston.py b/src/uqtestfuns/test_functions/piston.py
index a6e3466..2e893c3 100644
--- a/src/uqtestfuns/test_functions/piston.py
+++ b/src/uqtestfuns/test_functions/piston.py
@@ -20,92 +20,94 @@
    Variables," Ph. D. dissertation, Ohio State University, Ohio, 2010.
    URL: http://rave.ohiolink.edu/etdc/view?acc_num=osu1275422248
 """
+
 import numpy as np
 
-from copy import copy
+from copy import deepcopy
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import MarginalSpecs, ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["Piston"]
 
+
 # Marginals specification from [1]
-INPUT_MARGINALS_BENARI2007 = [
-    UnivDistSpec(
-        name="M",
-        distribution="uniform",
-        parameters=[30.0, 60.0],
-        description="Piston weight [kg]",
-    ),
-    UnivDistSpec(
-        name="S",
-        distribution="uniform",
-        parameters=[0.005, 0.020],
-        description="Piston surface area [m^2]",
-    ),
-    UnivDistSpec(
-        name="V0",
-        distribution="uniform",
-        parameters=[0.002, 0.010],
-        description="Initial gas volume [m^3]",
-    ),
-    UnivDistSpec(
-        name="k",
-        distribution="uniform",
-        parameters=[1000.0, 5000.0],
-        description="Spring coefficient [N/m]",
-    ),
-    UnivDistSpec(
-        name="P0",
-        distribution="uniform",
-        parameters=[90000.0, 110000.0],
-        description="Atmospheric pressure [N/m^2]",
-    ),
-    UnivDistSpec(
-        name="Ta",
-        distribution="uniform",
-        parameters=[290.0, 296.0],
-        description="Ambient temperature [K]",
-    ),
-    UnivDistSpec(
-        name="T0",
-        distribution="uniform",
-        parameters=[340.0, 360.0],
-        description="Filling gas temperature [K]",
-    ),
+MARGINALS_BENARI2007: MarginalSpecs = [
+    {
+        "name": "M",
+        "distribution": "uniform",
+        "parameters": [30.0, 60.0],
+        "description": "Piston weight [kg]",
+    },
+    {
+        "name": "S",
+        "distribution": "uniform",
+        "parameters": [0.005, 0.020],
+        "description": "Piston surface area [m^2]",
+    },
+    {
+        "name": "V0",
+        "distribution": "uniform",
+        "parameters": [0.002, 0.010],
+        "description": "Initial gas volume [m^3]",
+    },
+    {
+        "name": "k",
+        "distribution": "uniform",
+        "parameters": [1000.0, 5000.0],
+        "description": "Spring coefficient [N/m]",
+    },
+    {
+        "name": "P0",
+        "distribution": "uniform",
+        "parameters": [90000.0, 110000.0],
+        "description": "Atmospheric pressure [N/m^2]",
+    },
+    {
+        "name": "Ta",
+        "distribution": "uniform",
+        "parameters": [290.0, 296.0],
+        "description": "Ambient temperature [K]",
+    },
+    {
+        "name": "T0",
+        "distribution": "uniform",
+        "parameters": [340.0, 360.0],
+        "description": "Filling gas temperature [K]",
+    },
 ]
 
 # Marginals specification from [2]
-INPUT_MARGINALS_MOON2010 = [copy(_) for _ in INPUT_MARGINALS_BENARI2007]
+MARGINALS_MOON2010 = [deepcopy(_) for _ in MARGINALS_BENARI2007]
 for i in range(13):
-    INPUT_MARGINALS_MOON2010.append(
-        UnivDistSpec(
-            name=f"Inert {i+1}",
-            distribution="uniform",
-            parameters=[100.0, 200.0],
-            description="Inert input [-]",
-        )
+    MARGINALS_MOON2010.append(
+        {
+            "name": f"Inert {i+1}",
+            "distribution": "uniform",
+            "parameters": [100.0, 200.0],
+            "description": "Inert input [-]",
+        }
     )
 
-AVAILABLE_INPUT_SPECS = {
-    "BenAri2007": ProbInputSpecFixDim(
-        name="Piston-BenAri2007",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "BenAri2007": {
+        "function_id": "Piston",
+        "description": (
             "Probabilistic input model for the Piston simulation model "
             "from Ben-Ari and Steinberg (2007)."
         ),
-        marginals=INPUT_MARGINALS_BENARI2007,
-        copulas=None,
-    ),
-    "Moon2010": ProbInputSpecFixDim(
-        name="Piston-Moon2010",
-        description=(
+        "marginals": MARGINALS_BENARI2007,
+        "copulas": None,
+    },
+    "Moon2010": {
+        "function_id": "Piston",
+        "description": (
             "Probabilistic input model for the Piston simulation model "
             "from Moon (2010)."
         ),
-        marginals=INPUT_MARGINALS_MOON2010,
-        copulas=None,
-    ),
+        "marginals": MARGINALS_MOON2010,
+        "copulas": None,
+    },
 }
 
 DEFAULT_INPUT_SELECTION = "BenAri2007"
@@ -159,14 +161,13 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return cc
 
 
-class Piston(UQTestFunABC):
+class Piston(UQTestFunFixDimABC):
     """A concrete implementation of the Piston simulation test function."""
 
     _tags = ["metamodeling", "sensitivity"]
     _description = "Piston simulation model from Ben-Ari and Steinberg (2007)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 7
-    _default_input = DEFAULT_INPUT_SELECTION
+    _default_input_id = DEFAULT_INPUT_SELECTION
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/portfolio_3d.py b/src/uqtestfuns/test_functions/portfolio_3d.py
new file mode 100644
index 0000000..4d3e01c
--- /dev/null
+++ b/src/uqtestfuns/test_functions/portfolio_3d.py
@@ -0,0 +1,191 @@
+"""
+Module with an implementation of the simple 3D portfolio model.
+
+The simple portfolio model is a three-dimensional scalar-valued function.
+The function was first introduced in [1] as an example for illustrating
+different sensitivity measures (i.e., local and hybrid-local).
+
+References
+----------
+
+1. A. Saltelli, S. Tarantola, F. Campolongo, M. Rattom, "Sensitivity analysis
+   in practice: a guide to assessing scientific models," Hoboken, NJ: Wiley,
+   2004.
+"""
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import (
+    MarginalSpecs,
+    ProbInputSpecs,
+    FunParamSpecs,
+)
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
+
+
+__all__ = ["Portfolio3D"]
+
+
+MARGINALS_SALTELLI2004: MarginalSpecs = [
+    {
+        "name": "Ps",
+        "distribution": "normal",
+        "parameters": [0.0, 4.0],
+        "description": "Hedged portfolio 's' [\N{euro sign}]",
+    },
+    {
+        "name": "Pt",
+        "distribution": "normal",
+        "parameters": [0.0, 2.0],
+        "description": "Hedged portfolio 't' [\N{euro sign}]",
+    },
+    {
+        "name": "Pj",
+        "distribution": "normal",
+        "parameters": [0.0, 1.0],
+        "description": "Hedged portfolio 'j' [\N{euro sign}]",
+    },
+]
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Saltelli2004": {
+        "function_id": "Portfolio3D",
+        "description": (
+            "Probabilistic input model for the simple portfolio model "
+            "from Saltelli et al. (2004)."
+        ),
+        "marginals": MARGINALS_SALTELLI2004,
+        "copulas": None,
+    },
+}
+
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Saltelli2004-1": {
+        "function_id": "Portfolio3D",
+        "description": (
+            "Parameter set for the simple 3D portfolio model from "
+            "Saltelli et al. (2004); the least volatile hedged portfolio is "
+            "the largest quantity"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "cs",
+                "value": 100.0,
+                "type": float,
+                "description": "Quantities of the portfolio 's'",
+            },
+            {
+                "keyword": "ct",
+                "value": 500.0,
+                "type": float,
+                "description": "Quantities of the portfolio 't'",
+            },
+            {
+                "keyword": "cj",
+                "value": 1000.0,
+                "type": float,
+                "description": "Quantities of the portfolio 'j'",
+            },
+        ],
+    },
+    "Saltelli2004-2": {
+        "function_id": "Portfolio3D",
+        "description": (
+            "Parameter set for the simple 3D portfolio model from "
+            "Saltelli et al. (2004); the same quantities are hold for each "
+            "hedged portfolio"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "cs",
+                "value": 300.0,
+                "type": float,
+                "description": "Quantities of the portfolio 's'",
+            },
+            {
+                "keyword": "ct",
+                "value": 300.0,
+                "type": float,
+                "description": "Quantities of the portfolio 't'",
+            },
+            {
+                "keyword": "cj",
+                "value": 300.0,
+                "type": float,
+                "description": "Quantities of the portfolio 'j'",
+            },
+        ],
+    },
+    "Saltelli2004-3": {
+        "function_id": "Portfolio3D",
+        "description": (
+            "Parameter set for the simple 3D portfolio model from "
+            "Saltelli et al. (2004); the most volatile hedged portfolio is"
+            "the largest quantity"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "cs",
+                "value": 500.0,
+                "type": float,
+                "description": "Quantities of the portfolio 's'",
+            },
+            {
+                "keyword": "ct",
+                "value": 400.0,
+                "type": float,
+                "description": "Quantities of the portfolio 't'",
+            },
+            {
+                "keyword": "cj",
+                "value": 100.0,
+                "type": float,
+                "description": "Quantities of the portfolio 'j'",
+            },
+        ],
+    },
+}
+
+DEFAULT_PARAMETERS_SELECTION = "Saltelli2004-1"
+
+
+def evaluate(xx: np.ndarray, cs: float, ct: float, cj: float) -> np.ndarray:
+    """Evaluate the simple portfolio model on a set of input values.
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        Three-Dimensional input values given by an ``(N, 3)`` array where
+        ``N`` is the number of input values.
+    cs : float
+        The quantities of the portfolio 's' (the most volatile).
+    ct : float
+        The quantities of the portfolio 't' (average volatility).
+    cj : float
+        The quantities of the portfolio 'j' (the least volatile).
+
+    Returns
+    -------
+    np.ndarray
+        The output of the simple portfolio model on the given input values.
+        The output is a one-dimensional array of length ``N``.
+    """
+    # Read the parameters
+    p = np.array([cs, ct, cj])
+
+    # Compute the simple portfolio model
+    yy = np.sum(p * xx, axis=1)
+
+    return yy
+
+
+class Portfolio3D(UQTestFunFixDimABC):
+    """An implementation of the simple portfolio model test function."""
+
+    _tags = ["sensitivity"]
+    _description = "Simple portfolio model from Saltelli et al. (2004)"
+    _available_inputs = AVAILABLE_INPUTS
+    _available_parameters = AVAILABLE_PARAMETERS
+    _default_parameters_id = DEFAULT_PARAMETERS_SELECTION
+
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/rs_circular_bar.py b/src/uqtestfuns/test_functions/rs_circular_bar.py
index 43051d7..6e48528 100644
--- a/src/uqtestfuns/test_functions/rs_circular_bar.py
+++ b/src/uqtestfuns/test_functions/rs_circular_bar.py
@@ -11,43 +11,60 @@
    in Reliability Engineering. London: Springer London, 2016, pp. 257–292.
    DOI: 10.1007/978-1-4471-6269-8_8
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs, FunParamSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
+
 
 __all__ = ["RSCircularBar"]
 
-AVAILABLE_INPUT_SPECS = {
-    "Verma2016": ProbInputSpecFixDim(
-        name="RSCircularBar-Verma2016",
-        description=(
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Verma2016": {
+        "function_id": "RSCircularBar",
+        "description": (
             "Input model for the circular bar RS from Verma et al. (2016)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="Y",
-                distribution="normal",
-                parameters=[250.0, 25.0],
-                description="Material mean yield strength [MPa]",
-            ),
-            UnivDistSpec(
-                name="F",
-                distribution="normal",
-                parameters=[70.0, 7.0],
-                description="Force mean value [kN]",
-            ),
+        "marginals": [
+            {
+                "name": "Y",
+                "distribution": "normal",
+                "parameters": [250.0, 25.0],
+                "description": "Material mean yield strength [MPa]",
+            },
+            {
+                "name": "F",
+                "distribution": "normal",
+                "parameters": [70.0, 7.0],
+                "description": "Force mean value [kN]",
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
-AVAILABLE_PARAMETERS = {
-    "Verma2016": 25,  # bar diameter in [mm]
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Verma2016": {
+        "function_id": "RSCircularBar",
+        "description": (
+            "Parameter set for the RS circular bar reliability problem "
+            "from Verma et al. (2016)"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "bar_diameter",
+                "value": 25.0,
+                "type": float,
+                "description": "Diameter of the circular bar [mm]",
+            },
+        ],
+    },
 }
 
 
-def evaluate(xx: np.ndarray, parameter: float) -> np.ndarray:
+def evaluate(xx: np.ndarray, bar_diameter: float) -> np.ndarray:
     """Evaluate the circular bar RS problem on a set of input values.
 
     Parameters
@@ -55,8 +72,8 @@ def evaluate(xx: np.ndarray, parameter: float) -> np.ndarray:
     xx : np.ndarray
         A two-dimensional input values given by an N-by-2 array
         where N is the number of input values.
-    parameter : float
-        The parameter of the function (i.e., the diameter of the bar in [mm]).
+    bar_diameter : float
+        The diameter of the bar in [mm].
 
     Returns
     -------
@@ -68,7 +85,7 @@ def evaluate(xx: np.ndarray, parameter: float) -> np.ndarray:
     # Convert to SI units
     yy_ = xx[:, 0] * 1e6  # From [MPa] to [Pa]
     ff = xx[:, 1] * 1e3  # From [kN] to [N]
-    diam = parameter * 1e-3  # From [mm] to [m]
+    diam = bar_diameter * 1e-3  # From [mm] to [m]
 
     # Compute the performance function
     yy = yy_ - ff / (np.pi * diam**2 / 4)
@@ -76,13 +93,13 @@ def evaluate(xx: np.ndarray, parameter: float) -> np.ndarray:
     return yy
 
 
-class RSCircularBar(UQTestFunABC):
+class RSCircularBar(UQTestFunFixDimABC):
     """Concrete implementation of the circular bar RS reliability problem."""
 
     _tags = ["reliability"]
     _description = "RS problem as a circular bar from Verma et al. (2016)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = AVAILABLE_PARAMETERS
-    _default_spatial_dimension = 2
+    _default_input_dimension = 2
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/rs_quadratic.py b/src/uqtestfuns/test_functions/rs_quadratic.py
index 5316bd5..c7493a2 100644
--- a/src/uqtestfuns/test_functions/rs_quadratic.py
+++ b/src/uqtestfuns/test_functions/rs_quadratic.py
@@ -11,33 +11,35 @@
    sampling," Civil Engineering and Geosciences, TU Delft, Delft,
    The Netherlands, 2000.
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["RSQuadratic"]
 
-AVAILABLE_INPUT_SPECS = {
-    "Waarts2000": ProbInputSpecFixDim(
-        name="RSQuadratic-Waarts2000",
-        description="Input model for the quadratic RS from Waarts (2000)",
-        marginals=[
-            UnivDistSpec(
-                name="X1",
-                distribution="normal",
-                parameters=[11.0, 1.0],
-                description="None",
-            ),
-            UnivDistSpec(
-                name="X2",
-                distribution="normal",
-                parameters=[1.5, 0.5],
-                description="None",
-            ),
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Waarts2000": {
+        "function_id": "RSQuadratic",
+        "description": "Input model for the quadratic RS from Waarts (2000)",
+        "marginals": [
+            {
+                "name": "X1",
+                "distribution": "normal",
+                "parameters": [11.0, 1.0],
+                "description": None,
+            },
+            {
+                "name": "X2",
+                "distribution": "normal",
+                "parameters": [1.5, 0.5],
+                "description": None,
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
 
@@ -64,13 +66,13 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class RSQuadratic(UQTestFunABC):
+class RSQuadratic(UQTestFunFixDimABC):
     """Concrete implementation of the quadratic RS reliability problem."""
 
     _tags = ["reliability"]
     _description = "RS problem w/ one quadratic term from Waarts (2000)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 2
+    _default_input_dimension = 2
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/saltelli_linear.py b/src/uqtestfuns/test_functions/saltelli_linear.py
new file mode 100644
index 0000000..1cd1ce2
--- /dev/null
+++ b/src/uqtestfuns/test_functions/saltelli_linear.py
@@ -0,0 +1,111 @@
+"""
+Module with an implementation of the linear fun. from Saltelli et al. (2008).
+
+The linear function from Saltelli et al. (2008) is an M-dimensional
+scalar-valued function.
+It was introduced in [1] to illustrate sensitivity analysis methods and used
+in [2] for benchmarking purposes.
+
+Due to its simple structure, its moments and Sobol' sensitivity indices can
+be computed analytically.
+
+References
+----------
+1. A. Saltelli, K. Chan, and E. Scott, 2008. Sensitivity Analysis: Wiley Series
+   in Probability and Statistics. Wiley, New York.
+2. X. Sun, B. Croke, A. Jakeman, S. Roberts, "Benchmarking Active Subspace
+   methods of global sensitivity analysis against variance-based Sobol’
+   and Morris methods with established test functions," Environmental Modelling
+   & Software, vol. 149, p. 105310, 2022.
+   DOI: 10.1016/j.envsoft.2022.105310
+"""
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import (
+    MarginalSpec,
+    MarginalSpecs,
+    ProbInputSpecs,
+)
+from uqtestfuns.core.uqtestfun_abc import UQTestFunVarDimABC
+
+__all__ = ["SaltelliLinear"]
+
+
+def _create_marginals(input_dimension: int) -> MarginalSpecs:
+    r"""Create a list of marginal specifications of the given dimension.
+
+    .. math::
+
+      X_i \sim \mathcal{U}[x_{o, i} - \sigma_{o, i}, x_{o, i} + \sigma_{o, i}]
+
+    where :math:`x_{o, i} = 3^{i - 1}` and :math:`\sigma_{o, i} = 0.5 x_{o, i}`
+    for :math:`i = 1, 2, \ldots`.
+
+    Parameters
+    ----------
+    input_dimension : int
+        The number of marginal specifications in the list.
+
+    Returns
+    -------
+    MarginalSpecs
+        The list of marginal specifications.
+    """
+    marginals = []
+    for i in range(input_dimension):
+        mid = 3**i
+        delta = 0.5 * mid
+        marginal: MarginalSpec = {
+            "name": f"X{i + 1}",
+            "distribution": "uniform",
+            "parameters": [mid - delta, mid + delta],
+            "description": None,
+        }
+        marginals.append(marginal)
+
+    return marginals
+
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Saltelli2000": {
+        "function_id": "SaltelliLinear",
+        "description": (
+            "Probabilistic input model for the linear function "
+            "from Saltelli et al. (2000)"
+        ),
+        "marginals": _create_marginals,
+        "copulas": None,
+    },
+}
+
+
+def evaluate(xx: np.ndarray):
+    """Evaluate the linear 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.
+
+    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.sum(xx, axis=1)
+
+    return yy
+
+
+class SaltelliLinear(UQTestFunVarDimABC):
+    """An implementation of the M-dimensional Saltelli linear function."""
+
+    _tags = ["sensitivity"]
+    _description = "Linear function from Saltelli et al. (2000)"
+    _available_inputs = AVAILABLE_INPUTS
+    _available_parameters = None
+
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/sobol_g.py b/src/uqtestfuns/test_functions/sobol_g.py
index 7331a67..b8eba3b 100644
--- a/src/uqtestfuns/test_functions/sobol_g.py
+++ b/src/uqtestfuns/test_functions/sobol_g.py
@@ -8,7 +8,7 @@
 sensitivity analysis. There, the function was generalized by introducing
 a set of parameters that determines the importance of each input variable.
 Later on, it becomes a popular testing function for global sensitivity analysis
-methods; see, for instances, [5], [6], and [7].
+methods; see, for instances, [5], [6], [7], and [9].
 
 There are several sets of parameters used in the literature.
 
@@ -54,61 +54,44 @@
    of Model Output, 2007.
    Accessed: Jan. 25, 2023
    URL: http://samo2007.chem.elte.hu/lectures/Crestaux.pdf
+9. X. Sun, B. Croke, A. Jakeman, S. Roberts, "Benchmarking Active Subspace
+   methods of global sensitivity analysis against variance-based Sobol’
+   and Morris methods with established test functions," Environmental Modelling
+   & Software, vol. 149, p. 105310, 2022.
+   DOI: 10.1016/j.envsoft.2022.105310
 """
-import numpy as np
 
-from typing import List
+import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecVarDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs, FunParamSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunVarDimABC
 
 __all__ = ["SobolG"]
 
 
-def _create_sobol_input(spatial_dimension: int) -> List[UnivDistSpec]:
-    """Construct an input instance for a given dimension according to [1].
-
-    Parameters
-    ----------
-    spatial_dimension : int
-        The number of marginals to be created.
-
-    Returns
-    -------
-    List[UnivDistSpec]
-        A list of M marginals as UnivariateInput instances to construct
-        the MultivariateInput.
-    """
-    marginals = []
-    for i in range(spatial_dimension):
-        marginals.append(
-            UnivDistSpec(
-                name=f"X{i + 1}",
-                distribution="uniform",
-                parameters=[0.0, 1.0],
-                description="None",
-            )
-        )
-
-    return marginals
-
-
-AVAILABLE_INPUT_SPECS = {
-    "Saltelli1995": ProbInputSpecVarDim(
-        name="Sobol-G-Saltelli1995",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Saltelli1995": {
+        "function_id": "SobolG",
+        "description": (
             "Probabilistic input model for the Sobol'-G function "
             "from Saltelli and Sobol' (1995)"
         ),
-        marginals_generator=_create_sobol_input,
-        copulas=None,
-    ),
+        "marginals": [
+            {
+                "name": "X",
+                "distribution": "uniform",
+                "parameters": [0.0, 1.0],
+                "description": None,
+            },
+        ],
+        "copulas": None,
+    },
 }
 
 DEFAULT_INPUT_SELECTION = "Saltelli1995"
 
 
-def _get_params_saltelli_1995_1(spatial_dimension: int) -> np.ndarray:
+def _get_params_saltelli_1995_1(input_dimension: int) -> np.ndarray:
     """Construct a parameter array for Sobol'-G according to example 1 in [4].
 
     Notes
@@ -117,12 +100,12 @@ def _get_params_saltelli_1995_1(spatial_dimension: int) -> np.ndarray:
       in [1] (without parameters).
     - With the selected parameters, all input variables are equally important.
     """
-    yy = np.zeros(spatial_dimension)
+    yy = np.zeros(input_dimension)
 
     return yy
 
 
-def _get_params_saltelli_1995_2(spatial_dimension: int) -> np.ndarray:
+def _get_params_saltelli_1995_2(input_dimension: int) -> np.ndarray:
     """Construct a parameter array for Sobol'-G according to example 2 in [4].
 
     Notes
@@ -132,21 +115,21 @@ def _get_params_saltelli_1995_2(spatial_dimension: int) -> np.ndarray:
     - Originally, the dimension is limited to 8-dimensions; if more dimensions
       are used then the remaining dimension is also non-influential.
     """
-    yy = np.zeros(spatial_dimension)
+    yy = np.zeros(input_dimension)
 
-    if spatial_dimension > 1:
+    if input_dimension > 1:
         yy[1] = 0
 
-    if spatial_dimension > 2:
+    if input_dimension > 2:
         yy[2] = 3
 
-    if spatial_dimension > 3:
+    if input_dimension > 3:
         yy[3:] = 9
 
     return yy
 
 
-def _get_params_saltelli_1995_3(spatial_dimension: int) -> np.ndarray:
+def _get_params_saltelli_1995_3(input_dimension: int) -> np.ndarray:
     """Construct a parameter array for Sobol'-G according to example 3 in [4].
 
     Notes
@@ -157,102 +140,269 @@ def _get_params_saltelli_1995_3(spatial_dimension: int) -> np.ndarray:
       are used then the remaining dimension is also non-influential.
     - The parameter set is also used in [8].
     """
-    yy = (np.arange(1, spatial_dimension + 1) - 1) / 2.0
+    yy = (np.arange(1, input_dimension + 1) - 1) / 2.0
 
     return yy
 
 
-def _get_params_sobol_1998_1(spatial_dimension: int) -> np.ndarray:
-    """Construct a parameter array for Sobol'-G according to choice 1 in [2].
+def _get_params_sobol_1998_1(input_dimension: int) -> np.ndarray:
+    """Construct a parameter array for Sobol'-G according to choice 1 in [3].
 
     Notes
     -----
     - Using this choice of parameters, the supremum of the Sobol-G function
-      grows exponentially as a function of dimension about 2^M [1].
+      grows exponentially as a function of dimension about 2^M.
     """
-    yy = 0.01 * np.ones(spatial_dimension)
+    yy = 0.01 * np.ones(input_dimension)
 
     return yy
 
 
-def _get_params_sobol_1998_2(spatial_dimension: int) -> np.ndarray:
-    """Construct a parameter array for Sobol'-G according to choice 2 in [2].
+def _get_params_sobol_1998_2(input_dimension: int) -> np.ndarray:
+    """Construct a parameter array for Sobol'-G according to choice 2 in [3].
 
     Notes
     -----
     - Using this choice of parameters, the supremum of the Sobol-G function
-      grows exponentially as a function of dimension about (1.5)^M [1];
+      grows exponentially as a function of dimension about (1.5)^M;
       it's a bit slower than choice 1.
     """
-    yy = np.ones(spatial_dimension)
+    yy = np.ones(input_dimension)
 
     return yy
 
 
-def _get_params_sobol_1998_3(spatial_dimension: int) -> np.ndarray:
-    """Construct a parameter array for Sobol'-G according to choice 3 in [2].
+def _get_params_sobol_1998_3(input_dimension: int) -> np.ndarray:
+    """Construct a parameter array for Sobol'-G according to choice 3 in [3].
 
     Notes
     -----
     - Using this choice of parameters, the supremum of the Sobol-G function
-      grows linearly as a function of dimension, i.e., 1 + (M/2) [1].
+      grows linearly as a function of dimension, i.e., 1 + (M/2).
     """
-    yy = np.arange(1, spatial_dimension + 1)
+    yy = np.arange(1, input_dimension + 1)
 
     return yy
 
 
-def _get_params_sobol_1998_4(spatial_dimension: int) -> np.ndarray:
-    """Construct a parameter array for Sobol-G according to choice 4 in [2].
+def _get_params_sobol_1998_4(input_dimension: int) -> np.ndarray:
+    """Construct a parameter array for Sobol-G according to choice 4 in [3].
 
     Notes
     -----
     - Using this choice of parameters, the supremum of the Sobol-G function
-      is bounded at 1.0 [1].
+      is bounded at 1.0.
     """
-    yy = np.arange(1, spatial_dimension + 1) ** 2
+    yy = np.arange(1, input_dimension + 1) ** 2
 
     return yy
 
 
-def _get_params_kucherenko_2011_2a(spatial_dimension: int) -> np.ndarray:
-    """Construct a param. array for Sobol'-G according to problem 2A in [5]."""
-    yy = np.zeros(spatial_dimension)
-    if spatial_dimension >= 2:
+def _get_params_kucherenko_2011_2a(input_dimension: int) -> np.ndarray:
+    """Construct a param. array for Sobol'-G according to problem 2A in [7]."""
+    yy = np.zeros(input_dimension)
+    if input_dimension >= 2:
         yy[2:] = 6.52
 
     return yy
 
 
-def _get_params_kucherenko_2011_3b(spatial_dimension: int) -> np.ndarray:
-    """Construct a parameter array for Sobol'-G according to problem 3B in [5].
+def _get_params_kucherenko_2011_3b(input_dimension: int) -> np.ndarray:
+    """Construct a parameter array for Sobol'-G according to problem 3B in [7].
 
     Notes
     -----
     - Using this choice of parameters, the supremum of the Sobol-G function
       grows exponentially as a function of dimension about (1.13)^M.
     """
-    yy = 6.52 * np.ones(spatial_dimension)
+    yy = 6.52 * np.ones(input_dimension)
 
     return yy
 
 
-AVAILABLE_PARAMETERS = {
-    "Saltelli1995-1": _get_params_saltelli_1995_1,
-    "Saltelli1995-2": _get_params_saltelli_1995_2,
-    "Saltelli1995-3": _get_params_saltelli_1995_3,
-    "Sobol1998-1": _get_params_sobol_1998_1,
-    "Sobol1998-2": _get_params_sobol_1998_2,
-    "Sobol1998-3": _get_params_sobol_1998_3,
-    "Sobol1998-4": _get_params_sobol_1998_4,
-    "Kucherenko2011-2a": _get_params_kucherenko_2011_2a,
-    "Kucherenko2011-3b": _get_params_kucherenko_2011_3b,
+def _get_params_sun_2022(input_dimension: int) -> np.ndarray:
+    """Construct a parameter array for Sobol'-G according to Sec. 3. 2. [9].
+
+    Notes
+    -----
+    - The length of the array in [9] is originally seven following
+      a 7-dimensional function. If input dimension is larger than 7,
+      then the remaining coefficients are extrapolated
+      from the last available value.
+    """
+    aa = np.array([0.0, 1.0, 4.5, 9.0, 99, 99, 99])
+
+    if input_dimension > len(aa):
+        aa_ext = 99 * np.ones(input_dimension - len(aa))
+        aa = np.append(aa, aa_ext)
+
+    return aa[:input_dimension]
+
+
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Saltelli1995-1": {
+        "function_id": "SobolG",
+        "description": (
+            "Parameter set for the Sobol-G function from Saltelli and Sobol' "
+            "(1995), example 1; all input variables are equally important"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_params_saltelli_1995_1,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+        ],
+    },
+    "Saltelli1995-2": {
+        "function_id": "SobolG",
+        "description": (
+            "Parameter set for the Sobol-G function from Saltelli and Sobol' "
+            "(1995), example 2; the first two input variables are the most "
+            "important and the rest are non-influential"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_params_saltelli_1995_2,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+        ],
+    },
+    "Saltelli1995-3": {
+        "function_id": "SobolG",
+        "description": (
+            "Parameter set for the Sobol-G function from Saltelli and Sobol' "
+            "(1995), example 3; the first input variable is the most "
+            "important and the importance of the remaining variables is "
+            "decreasing"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_params_saltelli_1995_3,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+        ],
+    },
+    "Sobol1998-1": {
+        "function_id": "SobolG",
+        "description": (
+            "Parameter set for the Sobol-G function from Sobol' (1998)"
+            "(1995), choice 1; the supremum of the function grows "
+            "exponentially as a function of dimension about 2^M"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_params_sobol_1998_1,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+        ],
+    },
+    "Sobol1998-2": {
+        "function_id": "SobolG",
+        "description": (
+            "Parameter set for the Sobol-G function from Sobol' (1998)"
+            "(1995), choice 2; the supremum of the function grows "
+            "exponentially as a function of dimension about (1.5)^M"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_params_sobol_1998_2,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+        ],
+    },
+    "Sobol1998-3": {
+        "function_id": "SobolG",
+        "description": (
+            "Parameter set for the Sobol-G function from Sobol' (1998)"
+            "(1995), choice 3; the supremum of the function grows "
+            "linearly as a function of dimension, i.e., 1 + M / 2"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_params_sobol_1998_3,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+        ],
+    },
+    "Sobol1998-4": {
+        "function_id": "SobolG",
+        "description": (
+            "Parameter set for the Sobol-G function from Sobol' (1998)"
+            "(1995), choice 4; the supremum of the function is bounded at 1.0"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_params_sobol_1998_4,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+        ],
+    },
+    "Kucherenko2011-2a": {
+        "function_id": "SobolG",
+        "description": (
+            "Parameter set for the Sobol-G function from Kucherenko et al. "
+            "(2011), problem 2A"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_params_kucherenko_2011_2a,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+        ],
+    },
+    "Kucherenko2011-3b": {
+        "function_id": "SobolG",
+        "description": (
+            "Parameter set for the Sobol-G function from Kucherenko et al. "
+            "(2011), problem 3B; the supremum of the function grows "
+            "exponentially as a function of dimension about (1.13)^M"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_params_kucherenko_2011_3b,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+        ],
+    },
+    "Sun2022": {
+        "function_id": "SobolG",
+        "description": (
+            "Parameter set for the Sobol-G function from Sun et al. "
+            "(2022), Section 3.2"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_params_sun_2022,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+        ],
+    },
 }
 
 DEFAULT_PARAMETERS_SELECTION = "Saltelli1995-3"
 
 
-def evaluate(xx: np.ndarray, parameters: np.ndarray):
+def evaluate(xx: np.ndarray, aa: np.ndarray):
     """Evaluate the Sobol-G function on a set of input values.
 
     Parameters
@@ -260,8 +410,9 @@ def evaluate(xx: np.ndarray, parameters: np.ndarray):
     xx : np.ndarray
         M-Dimensional input values given by an N-by-M array where
         N is the number of input values.
-    parameters : np.ndarray
-        The parameters (i.e., coefficients) of the Sobol'-G function.
+    aa : np.ndarray
+        The vector of parameters (i.e., coefficients) of the Sobol'-G function;
+        the length of the vector is the same as the number of input dimensions.
 
     Returns
     -------
@@ -269,20 +420,18 @@ def evaluate(xx: np.ndarray, parameters: np.ndarray):
         The output of the Sobol-G function evaluated on the input values.
         The output is a 1-dimensional array of length N.
     """
-    aa = parameters
     yy = np.prod(((np.abs(4 * xx - 2) + aa) / (1 + aa)), axis=1)
 
     return yy
 
 
-class SobolG(UQTestFunABC):
+class SobolG(UQTestFunVarDimABC):
     """An implementation of the M-dimensional Sobol'-G test function."""
 
     _tags = ["sensitivity", "integration"]
     _description = "Sobol'-G function from Saltelli and Sobol' (1995)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = AVAILABLE_PARAMETERS
-    _default_parameters = DEFAULT_PARAMETERS_SELECTION
-    _default_spatial_dimension = None
+    _default_parameters_id = DEFAULT_PARAMETERS_SELECTION
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/sobol_g_star.py b/src/uqtestfuns/test_functions/sobol_g_star.py
new file mode 100644
index 0000000..6f01c22
--- /dev/null
+++ b/src/uqtestfuns/test_functions/sobol_g_star.py
@@ -0,0 +1,371 @@
+"""
+Module with an implementation of the Modified Sobol-G test function.
+
+The modified Sobol'-G function, often written as Sobol'-G* function,
+is a scalar-valued function in M dimensions.
+It was first introduced in [1] to serve as a test function for sensitivity
+analysis methods.
+This function, which introduces shift and curvature parameters,
+is a modification of the original Sobol'-G test function [2].
+
+The function has found application in the study of sensitivity analysis, e.g.,
+[3].
+
+There are several sets of parameters used in the literature.
+
+References
+----------
+
+1. A. Saltelli, P. Annoni, I. Azzini, F. Campolongo, M. Ratto,
+   and S. Tarantola, “Variance based sensitivity analysis of model output.
+   Design and estimator for the total sensitivity index,”
+   Computer Physics Communications, vol. 181, no. 2, pp. 259–270, 2010.
+   DOI: 10.1016/j.cpc.2009.09.018
+2. A. Saltelli and I. M. Sobol’, “About the use of rank transformation in
+   sensitivity analysis of model output,” Reliability Engineering
+   & System Safety, vol. 50, no. 3, pp. 225–239, 1995.
+   DOI: 10.1016/0951-8320(95)00099-2.
+3. X. Sun, B. Croke, A. Jakeman, S. Roberts, "Benchmarking Active Subspace
+   methods of global sensitivity analysis against variance-based Sobol’
+   and Morris methods with established test functions," Environmental Modelling
+   & Software, vol. 149, p. 105310, 2022.
+   DOI: 10.1016/j.envsoft.2022.105310
+"""
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import ProbInputSpecs, FunParamSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunVarDimABC
+
+__all__ = ["SobolGStar"]
+
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Saltelli2010": {
+        "function_id": "SobolGStar",
+        "description": (
+            "Probabilistic input model for the Sobol'-G* function "
+            "from Saltelli et al. (2010)"
+        ),
+        "marginals": [
+            {
+                "name": "X",
+                "distribution": "uniform",
+                "parameters": [0.0, 1.0],
+                "description": None,
+            },
+        ],
+        "copulas": None,
+    },
+}
+
+
+def _get_aa_saltelli_2010_a(input_dimension: int) -> np.ndarray:
+    """Construct a coefficients array for the Sobol'-G* function from [1].
+
+    Notes
+    -----
+    - This value is used in test case 1, 3, and 5 (see Table 5 in [1]).
+    - In [1], an input dimension of 10 was used. If the input dimension is less
+      than 10, the parameters array is truncated; if the input dimension exceed
+      10, the parameters are is extrapolated.
+    - With the selected parameters, the function is of low effective dimension.
+    """
+    aa = 9 * np.ones(input_dimension)
+    aa[:2] = 0
+
+    return aa
+
+
+def _get_aa_saltelli_2010_b(input_dimension: int) -> np.ndarray:
+    """Construct a coefficients array for the Sobol'-G* function from [1].
+
+    Notes
+    -----
+    - This value is used in test case 2, 5, and 6 (see Table 5 in [1]).
+    - In [1], an input dimension of 10 was used. If the input dimension is less
+      than 10, the parameters array is truncated; if the input dimension exceed
+      10, the parameters are is extrapolated.
+    """
+    aa = np.array([0.0, 0.1, 0.2, 0.3, 0.4, 0.8, 1, 2, 3, 4])
+
+    if input_dimension > 10:
+        aa_ext = np.arange(5, input_dimension - 5)
+        aa = np.append(aa, aa_ext)
+
+    return aa[:input_dimension]
+
+
+def _get_alpha_saltelli_2010_a(input_dimension: int) -> np.ndarray:
+    """Construct an alpha array for the Sobol'-G* function from [1].
+
+    Notes
+    -----
+    - This value is used in test case 1 and 2 (see Table 5 in [1]).
+    - In [1], an input dimension of 10 was used. If the input dimension is less
+      than 10, the parameters array is truncated; if the input dimension exceed
+      10, the parameters are is extrapolated.
+    """
+    alpha = np.ones(input_dimension)
+
+    return alpha
+
+
+def _get_alpha_saltelli_2010_b(input_dimension: int) -> np.ndarray:
+    """Construct an alpha array for the Sobol'-G* function from [1].
+
+    Notes
+    -----
+    - This value is used in test case 3 and 4 (see Table 5 in [1]).
+    - In [1], an input dimension of 10 was used. If the input dimension is less
+      than 10, the parameters array is truncated; if the input dimension exceed
+      10, the parameters are is extrapolated.
+    """
+    alpha = 0.5 * np.ones(input_dimension)
+
+    return alpha
+
+
+def _get_alpha_saltelli_2010_c(input_dimension: int) -> np.ndarray:
+    """Construct an alpha array for the Sobol'-G* function from [1].
+
+    Notes
+    -----
+    - This value is used in test case 5 and 6 (see Table 5 in [1]).
+    - In [1], an input dimension of 10 was used. If the input dimension is less
+      than 10, the parameters array is truncated; if the input dimension exceed
+      10, the parameters are is extrapolated.
+    """
+    alpha = 2 * np.ones(input_dimension)
+
+    return alpha
+
+
+def _get_delta_saltelli_2010(input_dimension: int) -> np.ndarray:
+    """Construct a delta array for the Sobol'-G* function from [1].
+
+    The parameter delta (shift parameter) is randomly generated
+    from a uniform distribution in [0, 1].
+    """
+    rng = np.random.default_rng()
+    delta = rng.random(input_dimension)
+
+    return delta
+
+
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Saltelli2010-1": {
+        "function_id": "SobolGStar",
+        "description": (
+            "Parameter set for the Sobol-G* function from Saltelli et al."
+            "(2010), test case 1; low-effective dimension, easier than "
+            "test case 2"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_aa_saltelli_2010_a,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+            {
+                "keyword": "delta",
+                "value": _get_delta_saltelli_2010,
+                "type": np.ndarray,
+                "description": "Shift parameter",
+            },
+            {
+                "keyword": "alpha",
+                "value": _get_alpha_saltelli_2010_a,
+                "type": np.ndarray,
+                "description": "Curvature parameter",
+            },
+        ],
+    },
+    "Saltelli2010-2": {
+        "function_id": "SobolGStar",
+        "description": (
+            "Parameter set for the Sobol-G* function from Saltelli et al. "
+            "(2010), test case 2; high-effective dimension, more difficult "
+            "than test case 1"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_aa_saltelli_2010_b,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+            {
+                "keyword": "delta",
+                "value": _get_delta_saltelli_2010,
+                "type": np.ndarray,
+                "description": "Shift parameter",
+            },
+            {
+                "keyword": "alpha",
+                "value": _get_alpha_saltelli_2010_a,
+                "type": np.ndarray,
+                "description": "Curvature parameter",
+            },
+        ],
+    },
+    "Saltelli2010-3": {
+        "function_id": "SobolGStar",
+        "description": (
+            "Parameter set for the Sobol-G* function from Saltelli et al. "
+            "(2010), test case 3; concave version of test case 1"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_aa_saltelli_2010_a,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+            {
+                "keyword": "delta",
+                "value": _get_delta_saltelli_2010,
+                "type": np.ndarray,
+                "description": "Shift parameter",
+            },
+            {
+                "keyword": "alpha",
+                "value": _get_alpha_saltelli_2010_b,
+                "type": np.ndarray,
+                "description": "Curvature parameter",
+            },
+        ],
+    },
+    "Saltelli2010-4": {
+        "function_id": "SobolGStar",
+        "description": (
+            "Parameter set for the Sobol-G* function from Saltelli et al. "
+            "(2010), test case 4; concave version of test case 2"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_aa_saltelli_2010_b,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+            {
+                "keyword": "delta",
+                "value": _get_delta_saltelli_2010,
+                "type": np.ndarray,
+                "description": "Shift parameter",
+            },
+            {
+                "keyword": "alpha",
+                "value": _get_alpha_saltelli_2010_b,
+                "type": np.ndarray,
+                "description": "Curvature parameter",
+            },
+        ],
+    },
+    "Saltelli2010-5": {
+        "function_id": "SobolGStar",
+        "description": (
+            "Parameter set for the Sobol-G* function from Saltelli et al. "
+            "(2010), test case 5; convex version of test case 1"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_aa_saltelli_2010_a,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+            {
+                "keyword": "delta",
+                "value": _get_delta_saltelli_2010,
+                "type": np.ndarray,
+                "description": "Shift parameter",
+            },
+            {
+                "keyword": "alpha",
+                "value": _get_alpha_saltelli_2010_c,
+                "type": np.ndarray,
+                "description": "Curvature parameter",
+            },
+        ],
+    },
+    "Saltelli2010-6": {
+        "function_id": "SobolGStar",
+        "description": (
+            "Parameter set for the Sobol-G* function from Saltelli et al. "
+            "(2010), test case 6; convex version of test case 2"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "aa",
+                "value": _get_aa_saltelli_2010_b,
+                "type": np.ndarray,
+                "description": "Coefficients 'a'",
+            },
+            {
+                "keyword": "delta",
+                "value": _get_delta_saltelli_2010,
+                "type": np.ndarray,
+                "description": "Shift parameter",
+            },
+            {
+                "keyword": "alpha",
+                "value": _get_alpha_saltelli_2010_c,
+                "type": np.ndarray,
+                "description": "Curvature parameter",
+            },
+        ],
+    },
+}
+
+DEFAULT_PARAMETERS_SELECTION = "Saltelli2010-1"
+
+
+def evaluate(
+    xx: np.ndarray,
+    aa: np.ndarray,
+    delta: np.ndarray,
+    alpha: np.ndarray,
+) -> np.ndarray:
+    """Evaluate the modified Sobol-G 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 parameters (i.e., coefficients) of the modified Sobol'-G
+        function; the length of the vector is the same as the number
+        of input dimensions.
+    delta: np.ndarray
+        The vector of shift parameters of length M.
+    alpha: np.ndarray
+        The vector of curvature parameters of length 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.
+    """
+    term = np.abs(2 * (xx + delta - np.floor(xx + delta)) - 1) ** alpha
+    g_star = ((1 + alpha) * term + aa) / (1 + aa)
+
+    yy = np.prod(g_star, axis=1)
+
+    return yy
+
+
+class SobolGStar(UQTestFunVarDimABC):
+    """An implementation of the M-dimensional Sobol'-G* test function."""
+
+    _tags = ["sensitivity"]
+    _description = "Sobol'-G* function from Saltelli et al. (2010)"
+    _available_inputs = AVAILABLE_INPUTS
+    _available_parameters = AVAILABLE_PARAMETERS
+    _default_parameters_id = DEFAULT_PARAMETERS_SELECTION
+
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/sobol_levitan.py b/src/uqtestfuns/test_functions/sobol_levitan.py
new file mode 100644
index 0000000..46ec034
--- /dev/null
+++ b/src/uqtestfuns/test_functions/sobol_levitan.py
@@ -0,0 +1,252 @@
+"""
+This module implements the Sobol'-Levitan test function.
+
+The Sobol'-Levitan function is an M-dimensional, scalar-valued function
+commonly used as a benchmark for sensitivity analysis.
+The function was introduced in [1] (as a six- and 20-dimensional functions)
+and revisited in, for example, [2] (as a 20-dimensional function)
+and [3] (as a seven- and 15-dimensional functions).
+
+The Sobol' sensitivity indices of the test function can be derived analytically
+as functions of the parameter.
+
+References
+----------
+
+1. I. M. Sobol’ and Yu. L. Levitan, “On the use of variance reducing
+   multipliers in Monte Carlo computations of a global sensitivity index,”
+   Computer Physics Communications, vol. 117, no. 1–2, pp. 52–61, 1999.
+   DOI: 10.1016/S0010-4655(98)00156-8
+2. H. Moon, A. M. Dean, and T. J. Santner, “Two-Stage Sensitivity-Based Group
+   Screening in Computer Experiments,” Technometrics*, vol. 54, no. 4,
+   pp. 376–387, 2012.
+   DOI: 10.1080/00401706.2012.725994
+3. X. Sun, B. Croke, A. Jakeman, S. Roberts, "Benchmarking Active Subspace
+   methods of global sensitivity analysis against variance-based Sobol’
+   and Morris methods with established test functions," Environmental Modelling
+   & Software, vol. 149, p. 105310, 2022.
+   DOI: 10.1016/j.envsoft.2022.105310
+"""
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import FunParamSpecs, ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunVarDimABC
+
+__all__ = ["SobolLevitan"]
+
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Sobol1999": {
+        "function_id": "SobolLevitan",
+        "description": (
+            "Probabilistic input model for the Sobol'-Levitan function "
+            "from Sobol' and Levitan (1999)"
+        ),
+        "marginals": [
+            {
+                "name": "X",
+                "distribution": "uniform",
+                "parameters": [0.0, 1.0],
+                "description": None,
+            },
+        ],
+        "copulas": None,
+    },
+}
+
+
+def _get_bb_sobol_1999_1(input_dimension: int) -> np.ndarray:
+    """Construct the coefficients from Sobol' and Levitan (1999) 6D case.
+
+    Notes
+    -----
+    - In [1], an input dimension of 6 was used. If the input dimension is less
+      than 6, the parameters array is truncated; if the input dimension exceed
+      10, the parameters are is extrapolated.
+    """
+    bb = np.array([1.5, 0.9, 0.9, 0.9, 0.9, 0.9])
+
+    if input_dimension > 6:
+        bb_ext = 0.9 * np.ones(input_dimension - len(bb))
+        bb = np.append(bb, bb_ext)
+
+    return bb[:input_dimension]
+
+
+def _get_bb_sobol_1999_2(input_dimension: int) -> np.ndarray:
+    """Construct the coefficients from Sobol' and Levitan (1999) 20D case.
+
+    Notes
+    -----
+    - In [1], an input dimension of 20 was used. If the input dimension is less
+      than 20, the parameters array is truncated; if the input dimension exceed
+      20, the parameters are is extrapolated.
+    """
+    bb_1 = 0.6 * np.ones(10)
+    bb_2 = 0.4 * np.ones(10)
+    bb = np.append(bb_1, bb_2)
+
+    if input_dimension > 20:
+        bb_ext = 0.4 * np.ones(input_dimension - len(bb))
+        bb = np.append(bb, bb_ext)
+
+    return bb[:input_dimension]
+
+
+def _get_bb_moon_2012_1(input_dimension: int) -> np.ndarray:
+    """Construct the coefficients from Moon et al. (2012) base case.
+
+    Notes
+    -----
+    - In [1], an input dimension of 20 was used. If the input dimension is less
+      than 20, the parameters array is truncated; if the input dimension exceed
+      20, the parameters are is extrapolated.
+    """
+    bb = np.array(
+        [
+            2.0000,
+            1.9500,
+            1.9000,
+            1.8500,
+            1.8000,
+            1.7500,
+            1.7000,
+            1.6500,
+            0.4228,
+            0.3077,
+            0.2169,
+            0.1471,
+            0.0951,
+            0.0577,
+            0.0323,
+            0.0161,
+            0.0068,
+            0.0021,
+            0.0004,
+            0.0000,
+        ]
+    )
+
+    if input_dimension > 20:
+        bb_ext = np.zeros(input_dimension - len(bb))
+        bb = np.append(bb, bb_ext)
+
+    return bb[:input_dimension]
+
+
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Sobol1999-1": {
+        "function_id": "SobolLevitan",
+        "description": (
+            "Parameter set for the M-dimensional function from "
+            "Sobol' and Levitan (1999), 6D case"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "bb",
+                "value": _get_bb_sobol_1999_1,
+                "type": np.ndarray,
+                "description": "Coefficients 'b'",
+            },
+            {
+                "keyword": "c0",
+                "value": 0.0,
+                "type": float,
+                "description": "Constant term",
+            },
+        ],
+    },
+    "Sobol1999-2": {
+        "function_id": "SobolLevitan",
+        "description": (
+            "Parameter set for the M-dimensional function from "
+            "Sobol' and Levitan (1999), 20D case"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "bb",
+                "value": _get_bb_sobol_1999_2,
+                "type": np.ndarray,
+                "description": "Coefficients 'b'",
+            },
+            {
+                "keyword": "c0",
+                "value": 0.0,
+                "type": float,
+                "description": "Constant term",
+            },
+        ],
+    },
+    "Moon2012-1": {
+        "function_id": "SobolLevitan",
+        "description": (
+            "Parameter set for the M-dimensional function from "
+            "Moon et al. (2012), base case"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "bb",
+                "value": _get_bb_moon_2012_1,
+                "type": np.ndarray,
+                "description": "Coefficients 'b'",
+            },
+            {
+                "keyword": "c0",
+                "value": 0.0,
+                "type": float,
+                "description": "Constant term",
+            },
+        ],
+    },
+}
+
+
+DEFAULT_PARAMETERS_SELECTION = "Sobol1999-1"
+
+
+def evaluate(xx: np.ndarray, bb: np.ndarray, c0: float) -> np.ndarray:
+    """Evaluate the Sobol'-Levitan test function
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        M-Dimensional input values given by an N-by-M array where
+        N is the number of input values.
+    bb : np.ndarray
+        The coefficients of the function which control the importance of each
+        input variable.
+    c0 : float
+        The constant term of the function.
+
+    Returns
+    -------
+    np.ndarray
+        The output of the test function evaluated on the input values.
+        The output is a 1-dimensional array of length N.
+    """
+    input_dim = xx.shape[1]
+    ii = 1.0
+    for i in range(input_dim):
+        b = bb[i]
+        # Safeguard against zero-value coefficient
+        if b == 0:
+            ii *= 1.0  # the limit as b approaches 0
+        else:
+            ii *= (np.exp(b) - 1) / b
+
+    yy = np.exp(np.sum(bb * xx, axis=1)) - ii + c0
+
+    return yy
+
+
+class SobolLevitan(UQTestFunVarDimABC):
+    """An implementation of the M-dimensional Sobol'-Levitan function."""
+
+    _tags = ["sensitivity"]
+    _description = "Test function from Sobol' and Levitan (1999)"
+    _available_inputs = AVAILABLE_INPUTS
+    _available_parameters = AVAILABLE_PARAMETERS
+    _default_parameters_id = DEFAULT_PARAMETERS_SELECTION
+
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/solar_cell.py b/src/uqtestfuns/test_functions/solar_cell.py
new file mode 100644
index 0000000..b8bb403
--- /dev/null
+++ b/src/uqtestfuns/test_functions/solar_cell.py
@@ -0,0 +1,271 @@
+"""
+This module implements the solar cell model from Constantine et al. (2015).
+
+The test function is a five-dimensional, scalar-valued function that models
+the maximum power of a single-diode solar cell. The function was used in
+Constantine et al. (2015) to demonstrate the active subspace method for
+input dimension reduction and sensitivity analysis.
+
+References
+----------
+
+1. P. G. Constantine, B. Zaharatos, and M. Campanelli, “Discovering an active
+   subspace in a single‐diode solar cell model,” Statistical Analysis, vol. 8,
+   no. 5–6, pp. 264–273, 2015.
+   DOI: 10.1002/sam.11281
+"""
+
+import numpy as np
+from scipy.optimize import root, minimize
+from typing import Tuple
+
+from uqtestfuns.core.custom_typing import (
+    MarginalSpecs,
+    FunParamSpecs,
+    ProbInputSpecs,
+)
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
+
+__all__ = ["SolarCell"]
+
+
+MARGINALS_CONSTANTINE2015: MarginalSpecs = [
+    {
+        "name": "Isc",
+        "distribution": "uniform",
+        "parameters": [0.05989, 0.23958],
+        "description": "Short-circuit current [A]",
+    },
+    {
+        "name": "log_Is",
+        "distribution": "uniform",
+        "parameters": [np.log(2.2e-11), np.log(2.2e-7)],
+        "description": "(log) Diode reverse saturation current [A]",
+    },
+    {
+        "name": "n",
+        "distribution": "uniform",
+        "parameters": [1.0, 2.0],
+        "description": "Ideality factor [-]",
+    },
+    {
+        "name": "Rs",
+        "distribution": "uniform",
+        "parameters": [0.16625, 0.66500],
+        "description": "Series resistance [Ohm]",
+    },
+    {
+        "name": "Rp",
+        "distribution": "uniform",
+        "parameters": [93.75, 375.00],
+        "description": "Parallel (shunt) resistance [Ohm]",
+    },
+]
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Constantine2015": {
+        "function_id": "SolarCell",
+        "description": (
+            "Probabilistic input model for the single-diode solar cell model "
+            "from Constantine et al. (2015)"
+        ),
+        "marginals": MARGINALS_CONSTANTINE2015,
+        "copulas": None,
+    },
+}
+
+
+AVAILABLE_PARAMETERS: FunParamSpecs = {
+    "Constantine2015": {
+        "function_id": "SolarCell",
+        "description": (
+            "Parameter set for the single-diode solar cell model from "
+            "Constantine et al. (2015)"
+        ),
+        "declared_parameters": [
+            {
+                "keyword": "n_s",
+                "value": 1,
+                "type": int,
+                "description": "# of cells connected in series [-]",
+            },
+            {
+                "keyword": "v_th",
+                "value": 0.02585,
+                "type": float,
+                "description": "Thermal voltage at 25degC [V]",
+            },
+        ],
+    },
+}
+
+
+def obj_fun_root(
+    i: float,
+    v: float,
+    x: np.ndarray,
+    n_s: int,
+    v_th: float,
+) -> float:
+    """Compute the equation to get the root.
+
+    Parameters
+    ----------
+    i : float
+        The current [A]
+    v : float
+        The voltage [V]
+    x : np.ndarray
+        The input variables of the solar cell model: I_SC, I_S, n, R_S, R_P.
+    n_s : int
+        The number of cells connected in series.
+    v_th : float
+        The thermal voltage value at 25degC [V]
+
+    Returns
+    -------
+    float
+        The root objective function for implicit current; the function returns
+        0 if the current corresponds to the given voltage.
+    """
+    # Get the input variable values
+    i_sc = x[0]
+    i_s = np.exp(x[1])
+    n = x[2]
+    r_s = x[3]
+    r_p = x[4]
+
+    # Compute the photo current
+    exp_term_1 = np.exp(i_sc * r_s / n_s / n / v_th) - 1
+    i_l = i_sc + i_s * exp_term_1 + i_sc * r_s / r_p
+
+    # Compute the objective function
+    exp_term_2 = np.exp((v + i * r_s) / n_s / n / v_th) - 1
+    y = i - i_l + i_s * exp_term_2 + (v + i * r_s) / r_p
+
+    return y
+
+
+def compute_current(
+    v: float,
+    x: np.ndarray,
+    n_s: int,
+    v_th: float,
+    **kwargs,
+) -> float:
+    """Compute the current of the solar cell given a voltage value.
+
+    Parameters
+    ----------
+    v : float
+        The voltage [V]
+    x : np.ndarray
+        The input variables of the solar cell model: I_SC, I_S, n, R_S, R_P.
+    n_s : int
+        The number of cells connected in series.
+    v_th : float
+        The thermal voltage value at 25degC [V]
+
+    Returns
+    -------
+    float
+        The current that corresponds to the given voltage.
+    """
+    i0 = np.array([0.0])
+    # Find the corresponding current as the root
+    sol = root(obj_fun_root, i0, args=(v, x, n_s, v_th), **kwargs)
+    i = sol.x
+
+    return i
+
+
+def compute_power_max(
+    xx: np.ndarray,
+    n_s: int,
+    v_th: float,
+    **kwargs,
+) -> Tuple[np.ndarray, np.ndarray]:
+    """Compute the maximum power a solar cell given its design parameters.
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        A five-dimensional input values of the solar cell model given by
+        an N-by-5 array where N is the number of input values.
+    n_s : int
+        The number of cells connected in series.
+    v_th : float
+        The thermal voltage value at 25degC [V]
+    kwargs : dict
+        The additional parameters to be passed either to `root()` or
+        `minimize()`. A dictionary value of key `root` will be passed to
+        `root()`, while a dictionary value of key `minimize` will be passed to
+        `minimize()`
+
+    Returns
+    -------
+    Tuple[np.ndarray, np.ndarray]
+        A pair of arrays containing the maximum power a solar cell and the
+        corresponding voltage.
+    """
+    # Initialize output array
+    pp_max = np.empty(len(xx))
+    vv_max = np.empty(len(xx))
+
+    # Get additional arguments for root and minimize
+    root_kwargs = kwargs.get("root", {})
+    minimize_kwargs = kwargs.get("minimize", {})
+
+    for idx in range(len(xx)):
+        # Set up the objective function (negated for minimization)
+        x = xx[idx]
+
+        def _obj_fun(v, x, n_s, v_th, root_kwargs):
+            return -1 * v * compute_current(v, x, n_s, v_th, **root_kwargs)
+
+        # Minimize the objective function
+        v0 = np.array([0.0])
+        res = minimize(
+            _obj_fun,
+            v0,
+            args=(x, n_s, v_th, root_kwargs),
+            **minimize_kwargs,
+        )
+
+        pp_max[idx] = -1 * res.fun  # Negated back for the maximum value
+        vv_max[idx] = res.x
+
+    return pp_max, vv_max
+
+
+def evaluate(xx: np.ndarray, n_s: int, v_th: float, **kwargs) -> np.ndarray:
+    """Evaluate the solar cell model on a set of input values.
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        A five-dimensional input values given by an N-by-5 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.
+    """
+    yy, _ = compute_power_max(xx, n_s, v_th, **kwargs)
+
+    return yy
+
+
+class SolarCell(UQTestFunFixDimABC):
+    """Concrete implementation of the single-diode solar cell model."""
+
+    _tags = ["metamodeling", "sensitivity"]
+    _description = (
+        "Single-diode solar-cell model from Constantine et al. (2015)"
+    )
+    _available_inputs = AVAILABLE_INPUTS
+    _available_parameters = AVAILABLE_PARAMETERS
+
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/speed_reducer_shaft.py b/src/uqtestfuns/test_functions/speed_reducer_shaft.py
index 759c99a..4814f59 100644
--- a/src/uqtestfuns/test_functions/speed_reducer_shaft.py
+++ b/src/uqtestfuns/test_functions/speed_reducer_shaft.py
@@ -20,56 +20,57 @@
    Structural Safety, vol. 73, pp. 42–53, 2018.
    DOI: 10.1016/j.strusafe.2018.02.005
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 from .utils import gumbel_max_mu, gumbel_max_beta
 
 __all__ = ["SpeedReducerShaft"]
 
 
-AVAILABLE_INPUT_SPECS = {
-    "Du2004": ProbInputSpecFixDim(
-        name="SpeedReducerShaft-Du2004",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Du2004": {
+        "function_id": "SpeedReducerShaft",
+        "description": (
             "Input model for the speed reducer shaft problem "
             "from Du and Sudjianto (2004)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="D",
-                distribution="normal",
-                parameters=[39, 0.1],
-                description="Shaft diameter [mm]",
-            ),
-            UnivDistSpec(
-                name="L",
-                distribution="normal",
-                parameters=[400, 0.1],
-                description="Shaft span [mm]",
-            ),
-            UnivDistSpec(
-                name="F",
-                distribution="gumbel",
-                parameters=[gumbel_max_mu(1500, 350), gumbel_max_beta(350)],
-                description="External force [N]",
-            ),
-            UnivDistSpec(
-                name="T",
-                distribution="normal",
-                parameters=[250, 35],
-                description="Torque [Nm]",
-            ),
-            UnivDistSpec(
-                name="S",
-                distribution="uniform",
-                parameters=[70, 80],
-                description="Strength [MPa]",
-            ),
+        "marginals": [
+            {
+                "name": "D",
+                "distribution": "normal",
+                "parameters": [39, 0.1],
+                "description": "Shaft diameter [mm]",
+            },
+            {
+                "name": "L",
+                "distribution": "normal",
+                "parameters": [400, 0.1],
+                "description": "Shaft span [mm]",
+            },
+            {
+                "name": "F",
+                "distribution": "gumbel",
+                "parameters": [gumbel_max_mu(1500, 350), gumbel_max_beta(350)],
+                "description": "External force [N]",
+            },
+            {
+                "name": "T",
+                "distribution": "normal",
+                "parameters": [250, 35],
+                "description": "Torque [Nm]",
+            },
+            {
+                "name": "S",
+                "distribution": "uniform",
+                "parameters": [70, 80],
+                "description": "Strength [MPa]",
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
 
@@ -103,7 +104,7 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class SpeedReducerShaft(UQTestFunABC):
+class SpeedReducerShaft(UQTestFunFixDimABC):
     """A concrete implementation of the speed reducer shaft function."""
 
     _tags = ["reliability"]
@@ -111,8 +112,7 @@ class SpeedReducerShaft(UQTestFunABC):
         "Reliability of a shaft in a speed reducer "
         "from Du and Sudjianto (2004)"
     )
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 5
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/sulfur.py b/src/uqtestfuns/test_functions/sulfur.py
index 5c4ff40..6460a7e 100644
--- a/src/uqtestfuns/test_functions/sulfur.py
+++ b/src/uqtestfuns/test_functions/sulfur.py
@@ -58,82 +58,86 @@
    Accessed: Jan. 25, 2023
    URL: http://www.physicalgeography.net/fundamentals/8o.html.
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import MarginalSpecs, ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["Sulfur"]
 
-INPUT_MARGINALS_PENNER1994 = [  # From [3] (Table 2)
-    UnivDistSpec(
-        name="Q",
-        distribution="lognormal",
-        parameters=[np.log(71.0), np.log(1.15)],
-        description="Source strength of anthropogenic Sulfur [10^12 g/year]",
-    ),
-    UnivDistSpec(
-        name="Y",
-        distribution="lognormal",
-        parameters=[np.log(0.5), np.log(1.5)],
-        description="Fraction of SO2 oxidized to SO4(2-) aerosol [-]",
-    ),
-    UnivDistSpec(
-        name="L",
-        distribution="lognormal",
-        parameters=[np.log(5.5), np.log(1.5)],
-        description="Average lifetime of atmospheric SO4(2-) [days]",
-    ),
-    UnivDistSpec(
-        name="Psi_e",
-        distribution="lognormal",
-        parameters=[np.log(5.0), np.log(1.4)],
-        description="Aerosol mass scattering efficiency [m^2/g]",
-    ),
-    UnivDistSpec(
-        name="beta",
-        distribution="lognormal",
-        parameters=[np.log(0.3), np.log(1.3)],
-        description="Fraction of light scattered upward hemisphere [-]",
-    ),
-    UnivDistSpec(
-        name="f_Psi_e",
-        distribution="lognormal",
-        parameters=[np.log(1.7), np.log(1.2)],
-        description="Fractional increase in aerosol scattering efficiency "
+
+MARGINALS_PENNER1994: MarginalSpecs = [  # From [3] (Table 2)
+    {
+        "name": "Q",
+        "distribution": "lognormal",
+        "parameters": [np.log(71.0), np.log(1.15)],
+        "description": (
+            "Source strength of anthropogenic Sulfur [10^12 g/year]"
+        ),
+    },
+    {
+        "name": "Y",
+        "distribution": "lognormal",
+        "parameters": [np.log(0.5), np.log(1.5)],
+        "description": "Fraction of SO2 oxidized to SO4(2-) aerosol [-]",
+    },
+    {
+        "name": "L",
+        "distribution": "lognormal",
+        "parameters": [np.log(5.5), np.log(1.5)],
+        "description": "Average lifetime of atmospheric SO4(2-) [days]",
+    },
+    {
+        "name": "Psi_e",
+        "distribution": "lognormal",
+        "parameters": [np.log(5.0), np.log(1.4)],
+        "description": "Aerosol mass scattering efficiency [m^2/g]",
+    },
+    {
+        "name": "beta",
+        "distribution": "lognormal",
+        "parameters": [np.log(0.3), np.log(1.3)],
+        "description": "Fraction of light scattered upward hemisphere [-]",
+    },
+    {
+        "name": "f_Psi_e",
+        "distribution": "lognormal",
+        "parameters": [np.log(1.7), np.log(1.2)],
+        "description": "Fractional increase in aerosol scattering efficiency "
         "due to hygroscopic growth [-]",
-    ),
-    UnivDistSpec(
-        name="T^2",
-        distribution="lognormal",
-        parameters=[np.log(0.58), np.log(1.4)],
-        description="Square of atmospheric "
+    },
+    {
+        "name": "T^2",
+        "distribution": "lognormal",
+        "parameters": [np.log(0.58), np.log(1.4)],
+        "description": "Square of atmospheric "
         "transmittance above aerosol layer [-]",
-    ),
-    UnivDistSpec(
-        name="(1-Ac)",
-        distribution="lognormal",
-        parameters=[np.log(0.39), np.log(1.1)],
-        description="Fraction of earth not covered by cloud [-]",
-    ),
-    UnivDistSpec(
-        name="(1-Rs)^2",
-        distribution="lognormal",
-        parameters=[np.log(0.72), np.log(1.2)],
-        description="Square of surface coalbedo [-]",
-    ),
+    },
+    {
+        "name": "(1-Ac)",
+        "distribution": "lognormal",
+        "parameters": [np.log(0.39), np.log(1.1)],
+        "description": "Fraction of earth not covered by cloud [-]",
+    },
+    {
+        "name": "(1-Rs)^2",
+        "distribution": "lognormal",
+        "parameters": [np.log(0.72), np.log(1.2)],
+        "description": "Square of surface coalbedo [-]",
+    },
 ]
 
-AVAILABLE_INPUT_SPECS = {
-    "Penner1994": ProbInputSpecFixDim(
-        name="Sulfur-Penner1994",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Penner1994": {
+        "function_id": "Sulfur",
+        "description": (
             "Probabilistic input model for the Sulfur model "
             "from Penner et al. (1994)."
         ),
-        marginals=INPUT_MARGINALS_PENNER1994,
-        copulas=None,
-    ),
+        "marginals": MARGINALS_PENNER1994,
+        "copulas": None,
+    },
 }
 
 DEFAULT_INPUT_SELECTION = "Penner1994"
@@ -193,13 +197,12 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return dd_f
 
 
-class Sulfur(UQTestFunABC):
+class Sulfur(UQTestFunFixDimABC):
     """A concrete implementation of the Sulfur model test function."""
 
     _tags = ["metamodeling", "sensitivity"]
     _description = "Sulfur model from Charlson et al. (1992)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 9
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/undamped_oscillator.py b/src/uqtestfuns/test_functions/undamped_oscillator.py
new file mode 100644
index 0000000..e7f5649
--- /dev/null
+++ b/src/uqtestfuns/test_functions/undamped_oscillator.py
@@ -0,0 +1,229 @@
+"""
+Module with an implementation of undamped oscillator test function.
+
+The undamped oscillator function is a six-dimensional, scalar-valued test
+function that models a non-linear, undamped, single-degree-of-freedom, forced
+oscillating mechanical system.
+This function is frequently used as a test function for reliability analysis
+methods (see [1] through [6]). Additionally, in [7], the function is employed
+as a test function for metamodeling exercises.
+
+
+1. C. G. Bucher and U. Bourgund, “A fast and efficient response surface
+   approach for structural reliability problems,” Structural Safety, vol. 7,
+   no. 1, pp. 57–66, 1990.
+   DOI: 10.1016/0167-4730(90)90012-E
+2. M. R. Rajashekhar and B. R. Ellingwood, “A new look at the response surface
+   approach for reliability analysis,” Structural Safety, vol. 12, no. 3,
+   pp. 205–220, 1993.
+   DOI: 10.1016/0167-4730(93)90003-J
+3. N. Gayton, J. M. Bourinet, and M. Lemaire, “CQ2RS: a new statistical
+   approach to the response surface method for reliability analysis,”
+   Structural Safety, vol. 25, no. 1, pp. 99–121, 2003.
+   DOI: 10.1016/S0167-4730(02)00045-0
+4. L. Schueremans and D. Van Gemert, “Benefit of splines and neural networks
+   in simulation based structural reliability analysis,” Structural Safety,
+   vol. 27, no. 3, pp. 246–261, 2005.
+   DOI: 10.1016/j.strusafe.2004.11.001
+5. B. Echard, N. Gayton, and M. Lemaire, “AK-MCS: An active learning
+   reliability method combining Kriging and Monte Carlo Simulation,”
+   Structural Safety, vol. 33, no. 2, pp. 145–154, 2011.
+   DOI: 10.1016/j.strusafe.2011.01.002.
+6. B. Echard, N. Gayton, M. Lemaire, and N. Relun, “A combined Importance
+   Sampling and Kriging reliability method for small failure probabilities
+   with time-demanding numerical models,” Reliability Engineering &
+   System Safety, vol. 111, pp. 232–240, 2013.
+   DOI: 10.1016/j.ress.2012.10.008.
+8. N. Lüthen, S. Marelli, and B. Sudret, “Sparse Polynomial Chaos Expansions:
+   Literature Survey and Benchmark,” SIAM/ASA Journal of Uncertainty
+   Quantification, vol. 9, no. 2, pp. 593–649, 2021.
+   DOI: 10.1137/20M1315774
+"""
+
+import numpy as np
+
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
+
+
+__all__ = ["UndampedOscillator"]
+
+
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Gayton2003": {
+        "function_id": "UndampedOscillator",
+        "description": (
+            "Input model for the undamped non-linear oscillator "
+            "from Gayton et al. (2003) (Table 9)"
+        ),
+        "marginals": [
+            {
+                "name": "m",
+                "distribution": "normal",
+                "parameters": [1.0, 0.05],
+                "description": "Mass",
+            },
+            {
+                "name": "c1",
+                "distribution": "normal",
+                "parameters": [1.0, 0.10],
+                "description": "Spring (1) constant",
+            },
+            {
+                "name": "c2",
+                "distribution": "normal",
+                "parameters": [0.1, 0.01],
+                "description": "Spring (2) constant",
+            },
+            {
+                "name": "r",
+                "distribution": "normal",
+                "parameters": [0.5, 0.05],
+                "description": "Length of restoring force",
+            },
+            {
+                "name": "F1",
+                "distribution": "normal",
+                "parameters": [1.0, 0.2],
+                "description": "Pulse load",
+            },
+            {
+                "name": "t1",
+                "distribution": "normal",
+                "parameters": [1.0, 0.2],
+                "description": "Duration of the pulse load",
+            },
+        ],
+        "copulas": None,
+    },
+    "Echard2013-1": {
+        "function_id": "UndampedOscillator",
+        "description": (
+            "Input model for the undamped non-linear oscillator "
+            "from Echard et al. (2013) (Table 4, F1 = 0.6)"
+        ),
+        "marginals": [
+            {
+                "name": "m",
+                "distribution": "normal",
+                "parameters": [1.0, 0.05],
+                "description": "Mass",
+            },
+            {
+                "name": "c1",
+                "distribution": "normal",
+                "parameters": [1.0, 0.10],
+                "description": "Spring (1) constant",
+            },
+            {
+                "name": "c2",
+                "distribution": "normal",
+                "parameters": [0.1, 0.01],
+                "description": "Spring (2) constant",
+            },
+            {
+                "name": "r",
+                "distribution": "normal",
+                "parameters": [0.5, 0.05],
+                "description": "Length of restoring force",
+            },
+            {
+                "name": "F1",
+                "distribution": "normal",
+                "parameters": [0.6, 0.1],
+                "description": "Pulse load",
+            },
+            {
+                "name": "t1",
+                "distribution": "normal",
+                "parameters": [1.0, 0.2],
+                "description": "Duration of the pulse load",
+            },
+        ],
+        "copulas": None,
+    },
+    "Echard2013-2": {
+        "function_id": "UndampedOscillator",
+        "description": (
+            "Input model for the undamped non-linear oscillator "
+            "from Echard et al. (2013) (Table 4, F1 = 0.45)"
+        ),
+        "marginals": [
+            {
+                "name": "m",
+                "distribution": "normal",
+                "parameters": [1.0, 0.05],
+                "description": "Mass",
+            },
+            {
+                "name": "c1",
+                "distribution": "normal",
+                "parameters": [1.0, 0.10],
+                "description": "Spring (1) constant",
+            },
+            {
+                "name": "c2",
+                "distribution": "normal",
+                "parameters": [0.1, 0.01],
+                "description": "Spring (2) constant",
+            },
+            {
+                "name": "r",
+                "distribution": "normal",
+                "parameters": [0.5, 0.05],
+                "description": "Length of restoring force",
+            },
+            {
+                "name": "F1",
+                "distribution": "normal",
+                "parameters": [0.45, 0.45 / 6],
+                "description": "Pulse load",
+            },
+            {
+                "name": "t1",
+                "distribution": "normal",
+                "parameters": [1.0, 0.2],
+                "description": "Duration of the pulse load",
+            },
+        ],
+        "copulas": None,
+    },
+}
+
+
+def evaluate(xx: np.ndarray) -> np.ndarray:
+    """Evaluate the undamped oscillator test function on a set of input values.
+
+    Parameters
+    ----------
+    xx : np.ndarray
+        A two-dimensional input values given by N-by-6 arrays
+        where N is the number of input values.
+
+    Returns
+    -------
+    np.ndarray
+        The output of the test function evaluated on the input values.
+        If negative, then the system is in failed state.
+        The output is a 1-dimensional array of length N.
+    """
+    omega_0 = np.sqrt((xx[:, 1] + xx[:, 2]) / xx[:, 0])
+    term_1 = 3 * xx[:, 3]
+    term_2 = 2 * xx[:, 4] / xx[:, 0] / omega_0**2
+    term_3 = np.sin(omega_0 * xx[:, 5] / 2)
+
+    yy = term_1 - np.abs(term_2 * term_3)
+
+    return yy
+
+
+class UndampedOscillator(UQTestFunFixDimABC):
+    """A concrete implementation of the undamped oscillator test function."""
+
+    _tags = ["reliability", "metamodeling"]
+    _description = "Undamped, non-linear, single DOF oscillator"
+    _available_inputs = AVAILABLE_INPUTS
+    _available_parameters = None
+    _default_input_id = "Gayton2003"
+
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/utils.py b/src/uqtestfuns/test_functions/utils.py
index 8e7a332..4b5ad87 100644
--- a/src/uqtestfuns/test_functions/utils.py
+++ b/src/uqtestfuns/test_functions/utils.py
@@ -1,6 +1,7 @@
 """
 Utility module for the test_functions sub-package.
 """
+
 import numpy as np
 
 
diff --git a/src/uqtestfuns/test_functions/webster.py b/src/uqtestfuns/test_functions/webster.py
index 0da069e..77f315d 100644
--- a/src/uqtestfuns/test_functions/webster.py
+++ b/src/uqtestfuns/test_functions/webster.py
@@ -13,37 +13,38 @@
    Joint Program Report Series 4, 1996.
    [Online]. Available: http://globalchange.mit.edu/publication/15670
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["Webster2D"]
 
 
-AVAILABLE_INPUT_SPECS = {
-    "Webster1996": ProbInputSpecFixDim(
-        name="Webster1996",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Webster1996": {
+        "function_id": "Webster2D",
+        "description": (
             "Input specification for the 2D function "
             "from Webster et al. (1996)"
         ),
-        marginals=[
-            UnivDistSpec(
-                name="A",
-                distribution="uniform",
-                parameters=[1.0, 10.0],
-                description="None",
-            ),
-            UnivDistSpec(
-                name="B",
-                distribution="normal",
-                parameters=[2.0, 1.0],
-                description="None",
-            ),
+        "marginals": [
+            {
+                "name": "A",
+                "distribution": "uniform",
+                "parameters": [1.0, 10.0],
+                "description": None,
+            },
+            {
+                "name": "B",
+                "distribution": "normal",
+                "parameters": [2.0, 1.0],
+                "description": None,
+            },
         ],
-        copulas=None,
-    ),
+        "copulas": None,
+    },
 }
 
 
@@ -67,13 +68,12 @@ def evaluate(xx: np.ndarray):
     return yy
 
 
-class Webster2D(UQTestFunABC):
+class Webster2D(UQTestFunFixDimABC):
     """A concrete implementation of the function from Webster et al. (1996)."""
 
     _tags = ["metamodeling"]
     _description = "2D polynomial function from Webster et al. (1996)."
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 2
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/welch1992.py b/src/uqtestfuns/test_functions/welch1992.py
index d118668..80dd89d 100644
--- a/src/uqtestfuns/test_functions/welch1992.py
+++ b/src/uqtestfuns/test_functions/welch1992.py
@@ -18,36 +18,38 @@
    experiments," Technometrics, vol. 34, no. 1, pp. 15-25, 1992.
    DOI: 10.2307/1269548
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import MarginalSpecs, ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 
 __all__ = ["Welch1992"]
 
 
-INPUT_MARGINALS_WELCH1992 = [
-    UnivDistSpec(
-        name=f"x{i}",
-        distribution="uniform",
-        parameters=[-0.5, 0.5],
-        description="None",
-    )
+MARGINALS_WELCH1992: MarginalSpecs = [
+    {
+        "name": f"x{i}",
+        "distribution": "uniform",
+        "parameters": [-0.5, 0.5],
+        "description": None,
+    }
     for i in range(1, 20 + 1)
 ]
 
-AVAILABLE_INPUT_SPECS = {
-    "Welch1992": ProbInputSpecFixDim(
-        name="Welch1992",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Welch1992": {
+        "function_id": "Welch1992",
+        "description": (
             "Input specification for the test function "
             "from Welch et al. (1992)"
         ),
-        marginals=INPUT_MARGINALS_WELCH1992,
-        copulas=None,
-    ),
+        "marginals": MARGINALS_WELCH1992,
+        "copulas": None,
+    },
 }
 
+
 DEFAULT_INPUT_SELECTION = "Welch1992"
 
 
@@ -94,13 +96,12 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class Welch1992(UQTestFunABC):
+class Welch1992(UQTestFunFixDimABC):
     """A concrete implementation of the Welch et al. (1992) test function."""
 
     _tags = ["metamodeling", "sensitivity", "integration"]
     _description = "20-Dimensional function from Welch et al. (1992)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 20
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/test_functions/wing_weight.py b/src/uqtestfuns/test_functions/wing_weight.py
index 1278d57..406a256 100644
--- a/src/uqtestfuns/test_functions/wing_weight.py
+++ b/src/uqtestfuns/test_functions/wing_weight.py
@@ -19,87 +19,89 @@
    Orlando, Florida, 2020. American Institute of Aeronautics and Astronautics.
    DOI: 10.2514/6.2020-1865.
 """
+
 import numpy as np
 
-from ..core.prob_input.input_spec import UnivDistSpec, ProbInputSpecFixDim
-from ..core.uqtestfun_abc import UQTestFunABC
+from uqtestfuns.core.custom_typing import MarginalSpecs, ProbInputSpecs
+from uqtestfuns.core.uqtestfun_abc import UQTestFunFixDimABC
 from .utils import deg2rad
 
 __all__ = ["WingWeight"]
 
-INPUT_MARGINALS_FORRESTER2008 = [
-    UnivDistSpec(
-        name="Sw",
-        distribution="uniform",
-        parameters=[150.0, 200.0],
-        description="wing area [ft^2]",
-    ),
-    UnivDistSpec(
-        name="Wfw",
-        distribution="uniform",
-        parameters=[220.0, 300.0],
-        description="weight of fuel in the wing [lb]",
-    ),
-    UnivDistSpec(
-        name="A",
-        distribution="uniform",
-        parameters=[6.0, 10.0],
-        description="aspect ratio [-]",
-    ),
-    UnivDistSpec(
-        name="Lambda",
-        distribution="uniform",
-        parameters=[-10.0, 10.0],
-        description="quarter-chord sweep [degrees]",
-    ),
-    UnivDistSpec(
-        name="q",
-        distribution="uniform",
-        parameters=[16.0, 45.0],
-        description="dynamic pressure at cruise [lb/ft^2]",
-    ),
-    UnivDistSpec(
-        name="lambda",
-        distribution="uniform",
-        parameters=[0.5, 1.0],
-        description="taper ratio [-]",
-    ),
-    UnivDistSpec(
-        name="tc",
-        distribution="uniform",
-        parameters=[0.08, 0.18],
-        description="aerofoil thickness to chord ratio [-]",
-    ),
-    UnivDistSpec(
-        name="Nz",
-        distribution="uniform",
-        parameters=[2.5, 6.0],
-        description="ultimate load factor [-]",
-    ),
-    UnivDistSpec(
-        name="Wdg",
-        distribution="uniform",
-        parameters=[1700, 2500],
-        description="flight design gross weight [lb]",
-    ),
-    UnivDistSpec(
-        name="Wp",
-        distribution="uniform",
-        parameters=[0.025, 0.08],
-        description="paint weight [lb/ft^2]",
-    ),
+
+MARGINALS_FORRESTER2008: MarginalSpecs = [
+    {
+        "name": "Sw",
+        "distribution": "uniform",
+        "parameters": [150.0, 200.0],
+        "description": "wing area [ft^2]",
+    },
+    {
+        "name": "Wfw",
+        "distribution": "uniform",
+        "parameters": [220.0, 300.0],
+        "description": "weight of fuel in the wing [lb]",
+    },
+    {
+        "name": "A",
+        "distribution": "uniform",
+        "parameters": [6.0, 10.0],
+        "description": "aspect ratio [-]",
+    },
+    {
+        "name": "Lambda",
+        "distribution": "uniform",
+        "parameters": [-10.0, 10.0],
+        "description": "quarter-chord sweep [degrees]",
+    },
+    {
+        "name": "q",
+        "distribution": "uniform",
+        "parameters": [16.0, 45.0],
+        "description": "dynamic pressure at cruise [lb/ft^2]",
+    },
+    {
+        "name": "lambda",
+        "distribution": "uniform",
+        "parameters": [0.5, 1.0],
+        "description": "taper ratio [-]",
+    },
+    {
+        "name": "tc",
+        "distribution": "uniform",
+        "parameters": [0.08, 0.18],
+        "description": "aerofoil thickness to chord ratio [-]",
+    },
+    {
+        "name": "Nz",
+        "distribution": "uniform",
+        "parameters": [2.5, 6.0],
+        "description": "ultimate load factor [-]",
+    },
+    {
+        "name": "Wdg",
+        "distribution": "uniform",
+        "parameters": [1700, 2500],
+        "description": "flight design gross weight [lb]",
+    },
+    {
+        "name": "Wp",
+        "distribution": "uniform",
+        "parameters": [0.025, 0.08],
+        "description": "paint weight [lb/ft^2]",
+    },
 ]
 
-AVAILABLE_INPUT_SPECS = {
-    "Forrester2008": ProbInputSpecFixDim(
-        name="Wing-Weight-Forrester-2008",
-        description=(
+AVAILABLE_INPUTS: ProbInputSpecs = {
+    "Forrester2008": {
+        "function_id": "WingWeight",
+        "description": (
             "Probabilistic input model for the Wing Weight model "
             "from Forrester et al. (2008)."
         ),
-        marginals=INPUT_MARGINALS_FORRESTER2008,
-        copulas=None,
-    ),
+        "marginals": MARGINALS_FORRESTER2008,
+        "copulas": None,
+    },
 }
 
 
@@ -133,13 +135,12 @@ def evaluate(xx: np.ndarray) -> np.ndarray:
     return yy
 
 
-class WingWeight(UQTestFunABC):
+class WingWeight(UQTestFunFixDimABC):
     """A concrete implementation of the wing weight test function."""
 
     _tags = ["metamodeling", "sensitivity"]
     _description = "Wing weight model from Forrester et al. (2008)"
-    _available_inputs = AVAILABLE_INPUT_SPECS
+    _available_inputs = AVAILABLE_INPUTS
     _available_parameters = None
-    _default_spatial_dimension = 10
 
-    eval_ = staticmethod(evaluate)
+    evaluate = staticmethod(evaluate)  # type: ignore
diff --git a/src/uqtestfuns/utils.py b/src/uqtestfuns/utils.py
index 9d715ba..f19f4fb 100644
--- a/src/uqtestfuns/utils.py
+++ b/src/uqtestfuns/utils.py
@@ -1,6 +1,7 @@
 """
 Utility module used across package.
 """
+
 import os
 import inspect
 
diff --git a/tests/builtin_test_functions/test_ackley.py b/tests/builtin_test_functions/test_ackley.py
index aa3a460..c86f7c8 100644
--- a/tests/builtin_test_functions/test_ackley.py
+++ b/tests/builtin_test_functions/test_ackley.py
@@ -6,18 +6,19 @@
 - The tests defined in this module deals with
   the correctness of the evaluation.
 """
+
 import numpy as np
 import pytest
 
 from uqtestfuns import Ackley
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 3, 10])
-def test_optimum_value(spatial_dimension):
+@pytest.mark.parametrize("input_dimension", [1, 2, 3, 10])
+def test_optimum_value(input_dimension):
     """Test the optimum value; regardless of the dimension."""
-    ackley_fun = Ackley(spatial_dimension=spatial_dimension)
+    ackley_fun = Ackley(input_dimension=input_dimension)
 
-    xx = np.zeros((1, ackley_fun.spatial_dimension))
+    xx = np.zeros((1, ackley_fun.input_dimension))
     yy = ackley_fun(xx)
 
     # The optima of the Ackley function is at 0.0s
diff --git a/tests/builtin_test_functions/test_franke.py b/tests/builtin_test_functions/test_franke.py
index 54251db..d7185c9 100644
--- a/tests/builtin_test_functions/test_franke.py
+++ b/tests/builtin_test_functions/test_franke.py
@@ -6,6 +6,7 @@
 - The tests defined in this module deals with
   the correctness of the evaluation.
 """
+
 import numpy as np
 
 from uqtestfuns.test_functions import Franke2, Franke4, Franke5, Franke6
diff --git a/tests/builtin_test_functions/test_grammacy_1dsine.py b/tests/builtin_test_functions/test_grammacy_1dsine.py
deleted file mode 100644
index e006e06..0000000
--- a/tests/builtin_test_functions/test_grammacy_1dsine.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""
-Test module for the Grammacy1DSine test function.
-
-Notes
------
-- The tests defined in this module deals with
-  the correctness of the evaluation.
-"""
-import pytest
-
-from uqtestfuns import Gramacy1DSine
-import uqtestfuns.test_functions.gramacy2007 as gramacy_mod
-
-from conftest import assert_call
-
-# Test for different parameters to the Ishigami function
-available_parameters = list(Gramacy1DSine.available_parameters.keys())
-
-
-@pytest.fixture(params=available_parameters)
-def gramacy1dsine_fun(request):
-    gramacy1dsine = Gramacy1DSine(parameters_selection=request.param)
-
-    return gramacy1dsine
-
-
-@pytest.mark.parametrize("param_selection", available_parameters)
-def test_different_parameters(param_selection):
-    """Test selecting different built-in parameters."""
-
-    # Create an instance of Ishigami function with a specified param. selection
-    my_testfun = Gramacy1DSine(parameters_selection=param_selection)
-
-    # Assertion
-    assert (
-        my_testfun.parameters
-        == gramacy_mod.AVAILABLE_PARAMETERS[param_selection]
-    )
-
-    # Assert Call
-    xx = my_testfun.prob_input.get_sample(10)
-    assert_call(my_testfun, xx)
-
-
-def test_wrong_param_selection():
-    """Test a wrong selection of the parameters."""
-    with pytest.raises(KeyError):
-        Gramacy1DSine(parameters_selection="marelli1")
diff --git a/tests/builtin_test_functions/test_ishigami.py b/tests/builtin_test_functions/test_ishigami.py
index 172cae0..4d26490 100644
--- a/tests/builtin_test_functions/test_ishigami.py
+++ b/tests/builtin_test_functions/test_ishigami.py
@@ -6,11 +6,11 @@
 - The tests defined in this module deals with
   the correctness of the evaluation.
 """
+
 import numpy as np
 import pytest
 
 from uqtestfuns import Ishigami
-import uqtestfuns.test_functions.ishigami as ishigami_mod
 
 # Test for different parameters to the Ishigami function
 available_parameters = list(Ishigami.available_parameters.keys())
@@ -18,7 +18,7 @@
 
 @pytest.fixture(params=available_parameters)
 def ishigami_fun(request):
-    ishigami = Ishigami(parameters_selection=request.param)
+    ishigami = Ishigami(parameters_id=request.param)
 
     return ishigami
 
@@ -33,7 +33,7 @@ def test_compute_mean(ishigami_fun):
     mean_mc = np.mean(yy)
 
     # Analytical mean
-    mean_ref = ishigami_fun.parameters[0] / 2
+    mean_ref = ishigami_fun.parameters["a"] / 2
 
     # Assertion
     assert np.allclose(mean_mc, mean_ref, rtol=1e-2)
@@ -49,7 +49,7 @@ def test_compute_variance(ishigami_fun):
     var_mc = np.var(yy)
 
     # Analytical mean
-    a, b = ishigami_fun.parameters
+    a, b = ishigami_fun.parameters["a"], ishigami_fun.parameters["b"]
     var_ref = a**2 / 8 + b * np.pi**4 / 5 + b**2 * np.pi**8 / 18 + 0.5
 
     # Assertion
@@ -61,16 +61,14 @@ def test_different_parameters(param_selection):
     """Test selecting different built-in parameters."""
 
     # Create an instance of Ishigami function with a specified param. selection
-    my_testfun = Ishigami(parameters_selection=param_selection)
+    my_testfun_1 = Ishigami(parameters_id=param_selection)
+    my_testfun_2 = Ishigami(parameters_id=param_selection)
 
     # Assertion
-    assert (
-        my_testfun.parameters
-        == ishigami_mod.AVAILABLE_PARAMETERS[param_selection]
-    )
+    assert my_testfun_1.parameters == my_testfun_2.parameters
 
 
 def test_wrong_param_selection():
     """Test a wrong selection of the parameters."""
     with pytest.raises(KeyError):
-        Ishigami(parameters_selection="marelli1")
+        Ishigami(parameters_id="marelli1")
diff --git a/tests/builtin_test_functions/test_mclain.py b/tests/builtin_test_functions/test_mclain.py
index b097bd2..8383410 100644
--- a/tests/builtin_test_functions/test_mclain.py
+++ b/tests/builtin_test_functions/test_mclain.py
@@ -6,6 +6,7 @@
 - The tests defined in this module deals with
   the correctness of the evaluation.
 """
+
 import numpy as np
 
 from uqtestfuns.test_functions import (
diff --git a/tests/builtin_test_functions/test_morris2006.py b/tests/builtin_test_functions/test_morris2006.py
new file mode 100644
index 0000000..078ac1f
--- /dev/null
+++ b/tests/builtin_test_functions/test_morris2006.py
@@ -0,0 +1,73 @@
+"""
+Test module for the Morris2006 test function.
+
+Notes
+-----
+- The tests defined in this module deals with the correctness
+  of the evaluation of this particular test function.
+"""
+
+import numpy as np
+
+from uqtestfuns.test_functions import Morris2006
+
+
+def test_p_larger_than_m():
+    """Test if the parameter p is larger than the number of input dim."""
+    # Create an instance
+    fun = Morris2006(input_dimension=3)
+    fun.parameters["p"] = 3
+
+    # Generate sample
+    num_sample = 1000000
+    xx = fun.prob_input.get_sample(num_sample)
+
+    yy_1 = fun(xx)
+
+    # Replace the parameter value
+    fun.parameters["p"] = 4
+    yy_2 = fun(xx)
+
+    assert np.array_equal(yy_1, yy_2)
+
+
+def test_one_dimension():
+    """Test for one-dimensional function.
+
+    Notes
+    -----
+    - Normally Morris2006 function requires more than 1 input dimension,
+      but safeguard is in place to allow the computation of one-dimensional
+      Morris2006 function.
+    """
+    # Create an instance
+    fun = Morris2006(input_dimension=1)
+
+    # Generate sample
+    num_sample = 1000000
+    xx = fun.prob_input.get_sample(num_sample)
+
+    yy = fun(xx)
+
+    # The slope of the linear one-dimensional function is sqrt(12)
+    assert np.allclose(yy / xx[:, 0], np.sqrt(12))
+
+
+def test_inert_inputs():
+    """Test whether the remaining inputs of Morris2006 are indeed inert."""
+    # Construct two instances of test function
+    # Default parameter set to 10 inputs as important, the rest are inert
+    fun_1 = Morris2006(input_dimension=20)
+    fun_2 = Morris2006(input_dimension=30)
+
+    # Generate sample and compare both
+    num_sample = 1000000
+    xx_1 = fun_1.prob_input.get_sample(num_sample)
+    xx_2 = fun_2.prob_input.get_sample(num_sample)
+
+    yy_1 = fun_1(xx_1)
+    yy_2 = fun_2(xx_2)
+
+    # Assertions
+    assert np.allclose(np.mean(yy_1), np.mean(yy_2), rtol=1e-2)
+    assert np.allclose(np.var(yy_1), np.var(yy_2), rtol=1e-2)
diff --git a/tests/builtin_test_functions/test_otl_circuit.py b/tests/builtin_test_functions/test_otl_circuit.py
index b89c593..ef2610b 100644
--- a/tests/builtin_test_functions/test_otl_circuit.py
+++ b/tests/builtin_test_functions/test_otl_circuit.py
@@ -6,6 +6,7 @@
 - The tests defined in this module deals with the correctness
   of the evaluation of this particular test function.
 """
+
 import numpy as np
 
 from uqtestfuns.test_functions import OTLCircuit
@@ -13,8 +14,8 @@
 
 def test_inert_inputs():
     """Test whether the inputs from 'Moon' specification are indeed inert."""
-    otl_ben_ari = OTLCircuit(prob_input_selection="BenAri2007")
-    otl_moon = OTLCircuit(prob_input_selection="Moon2010")
+    otl_ben_ari = OTLCircuit(input_id="BenAri2007")
+    otl_moon = OTLCircuit(input_id="Moon2010")
 
     # Assertions: ProbInput is assigned
     assert otl_ben_ari.prob_input is not None
diff --git a/tests/builtin_test_functions/test_piston.py b/tests/builtin_test_functions/test_piston.py
index 449d0f3..0ee52a4 100644
--- a/tests/builtin_test_functions/test_piston.py
+++ b/tests/builtin_test_functions/test_piston.py
@@ -6,6 +6,7 @@
 - The tests defined in this module deals with the correctness
   of the evaluation of this particular test function.
 """
+
 import numpy as np
 
 from uqtestfuns.test_functions import Piston
@@ -13,8 +14,8 @@
 
 def test_inert_inputs():
     """Test whether the inputs from 'Moon' specification are indeed inert."""
-    piston_ben_ari = Piston(prob_input_selection="BenAri2007")
-    piston_moon = Piston(prob_input_selection="Moon2010")
+    piston_ben_ari = Piston(input_id="BenAri2007")
+    piston_moon = Piston(input_id="Moon2010")
 
     # Assert that the ProbInput is correctly attached
     assert piston_ben_ari.prob_input is not None
diff --git a/tests/builtin_test_functions/test_portfolio_3d.py b/tests/builtin_test_functions/test_portfolio_3d.py
new file mode 100644
index 0000000..a163799
--- /dev/null
+++ b/tests/builtin_test_functions/test_portfolio_3d.py
@@ -0,0 +1,87 @@
+"""
+Test module for the simple portfolio model.
+
+Notes
+-----
+- The tests defined in this module deals with
+  the correctness of the evaluation.
+"""
+
+import numpy as np
+import pytest
+
+from uqtestfuns import Portfolio3D
+
+# Test for different sets of parameters of the simple portfolio model
+available_parameters = list(Portfolio3D.available_parameters.keys())
+
+
+@pytest.fixture(params=available_parameters)
+def portfolio3d_fun(request):
+    portfolio3d = Portfolio3D(parameters_id=request.param)
+
+    return portfolio3d
+
+
+def test_compute_mean(portfolio3d_fun):
+    """Test the analytical mean."""
+
+    # Analytical mean
+    mean_inputs = np.array(
+        [x.parameters[0] for x in portfolio3d_fun.prob_input.marginals]
+    )
+    parameters = np.array(
+        [
+            portfolio3d_fun.parameters["cs"],
+            portfolio3d_fun.parameters["ct"],
+            portfolio3d_fun.parameters["cj"],
+        ]
+    )
+    mean_ref = mean_inputs @ parameters
+
+    # Assertion
+    assert np.allclose(mean_ref, 0)
+
+
+def test_compute_variance(portfolio3d_fun):
+    """Test the analytical variance."""
+
+    # Compute variance via Monte Carlo
+    xx = portfolio3d_fun.prob_input.get_sample(10000000)
+    yy = portfolio3d_fun(xx)
+
+    var_mc = np.var(yy)
+
+    # Analytical variance
+    std_inputs = np.array(
+        [x.parameters[1] for x in portfolio3d_fun.prob_input.marginals]
+    )
+    parameters = np.array(
+        [
+            portfolio3d_fun.parameters["cs"],
+            portfolio3d_fun.parameters["ct"],
+            portfolio3d_fun.parameters["cj"],
+        ]
+    )
+    var_ref = parameters**2 @ std_inputs**2
+
+    # Assertion
+    assert np.allclose(var_mc, var_ref, rtol=1e-1)
+
+
+@pytest.mark.parametrize("param_selection", available_parameters)
+def test_different_parameters(param_selection):
+    """Test selecting different built-in parameters."""
+
+    # Create an instance of Ishigami function with a specified param. selection
+    my_testfun_1 = Portfolio3D(parameters_id=param_selection)
+    my_testfun_2 = Portfolio3D(parameters_id=param_selection)
+
+    # Assertion: The parameter sets are identical
+    assert my_testfun_1.parameters == my_testfun_2.parameters
+
+
+def test_wrong_param_selection():
+    """Test a wrong selection of the parameters."""
+    with pytest.raises(KeyError):
+        Portfolio3D(parameters_id="marelli1")
diff --git a/tests/builtin_test_functions/test_sobol_g.py b/tests/builtin_test_functions/test_sobol_g.py
index 0b3f607..dae810a 100644
--- a/tests/builtin_test_functions/test_sobol_g.py
+++ b/tests/builtin_test_functions/test_sobol_g.py
@@ -6,6 +6,7 @@
 - The tests defined in this module deals with
   the correctness of the evaluation.
 """
+
 import numpy as np
 import pytest
 
@@ -17,20 +18,20 @@
 def test_wrong_param_selection():
     """Test a wrong selection of the parameters."""
     with pytest.raises(KeyError):
-        SobolG(parameters_selection="marelli1")
+        SobolG(parameters_id="marelli1")
 
 
 # ATTENTION: some parameters choice (e.g., "sobol-1")
 # can't be estimated properly with low N at high dimension
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 3, 10])
+@pytest.mark.parametrize("input_dimension", [1, 2, 3, 10])
 @pytest.mark.parametrize("params_selection", available_parameters)
-def test_compute_mean(spatial_dimension, params_selection):
+def test_compute_mean(input_dimension, params_selection):
     """Test the mean computation as the result is analytical."""
 
     # Create an instance of Sobol-G test function
     my_fun = SobolG(
-        spatial_dimension=spatial_dimension,
-        parameters_selection=params_selection,
+        input_dimension=input_dimension,
+        parameters_id=params_selection,
     )
 
     # Assert that ProbInput is correctly attached
@@ -50,15 +51,15 @@ def test_compute_mean(spatial_dimension, params_selection):
 
 
 # ATTENTION: parameters with "Sobol-1" is unstable at large dimension >= 15
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 3, 10])
+@pytest.mark.parametrize("input_dimension", [1, 2, 3, 10])
 @pytest.mark.parametrize("params_selection", available_parameters)
-def test_compute_variance(spatial_dimension, params_selection):
+def test_compute_variance(input_dimension, params_selection):
     """Test the variance computation as the result is analytical."""
 
     # Create an instance of the Sobol-G test function
     my_fun = SobolG(
-        spatial_dimension=spatial_dimension,
-        parameters_selection=params_selection,
+        input_dimension=input_dimension,
+        parameters_id=params_selection,
     )
 
     # Assert that ProbInput is correctly attached
@@ -71,10 +72,8 @@ def test_compute_variance(spatial_dimension, params_selection):
     var_mc = np.var(yy)
 
     # Analytical variance
-    params = my_fun.parameters
-    var_ref = (
-        np.prod((4 / 3 + 2 * params + params**2) / (1 + params) ** 2) - 1
-    )
+    params = my_fun.parameters["aa"]
+    var_ref = np.prod((4 / 3 + 2 * params + params**2) / (1 + params) ** 2) - 1
 
     # Assertion (no need to be very ambitious with the tolerance)
     assert np.allclose(var_mc, var_ref, rtol=1e-1)
diff --git a/tests/builtin_test_functions/test_sobol_levitan.py b/tests/builtin_test_functions/test_sobol_levitan.py
new file mode 100644
index 0000000..e16a191
--- /dev/null
+++ b/tests/builtin_test_functions/test_sobol_levitan.py
@@ -0,0 +1,95 @@
+"""
+Test module for the Sobol'-Levitan test function.
+
+Notes
+-----
+- The tests defined in this module deals with
+  the correctness of the evaluation.
+"""
+
+import numpy as np
+import pytest
+
+from uqtestfuns.test_functions import SobolLevitan
+
+available_parameters = list(SobolLevitan.available_parameters.keys())
+
+
+def test_wrong_param_selection():
+    """Test a wrong selection of the parameters."""
+    with pytest.raises(KeyError):
+        SobolLevitan(parameters_id="marelli1")
+
+
+@pytest.mark.parametrize("input_dimension", [1, 2, 3, 10, 21])
+@pytest.mark.parametrize("parameters_id", available_parameters)
+def test_compute_mean(input_dimension, parameters_id):
+    """Test the mean computation as the result is analytical."""
+
+    # Create an instance of Sobol'-Levitan test function
+    my_fun = SobolLevitan(
+        input_dimension=input_dimension,
+        parameters_id=parameters_id,
+    )
+
+    # Compute mean via Monte Carlo
+    xx = my_fun.prob_input.get_sample(1000000)
+    yy = my_fun(xx)
+
+    mean_mc = np.mean(yy)
+
+    # Analytical mean
+    mean_ref = my_fun.parameters["c0"]
+
+    # Compare it relative to the standard deviation
+    mean_mc_rel = mean_mc / np.std(yy)
+
+    # Assertion (no need to be very ambitious with the tolerance)
+    if mean_ref == 0:
+        assert np.abs(mean_mc_rel) < 1e-1
+    else:
+        assert np.allclose(mean_mc_rel, mean_ref, rtol=1e-1)
+
+
+@pytest.mark.parametrize("input_dimension", [1, 2, 3, 10, 21])
+@pytest.mark.parametrize("parameters_id", available_parameters)
+def test_compute_variance(input_dimension, parameters_id):
+    """Test the variance computation as the result is analytical."""
+
+    # Create an instance of the Sobol'-Levitan test function
+    my_fun = SobolLevitan(
+        input_dimension=input_dimension,
+        parameters_id=parameters_id,
+    )
+
+    # Compute the variance via Monte Carlo
+    xx = my_fun.prob_input.get_sample(500000)
+    yy = my_fun(xx)
+
+    var_mc = np.var(yy)
+
+    # Analytical variance
+    bb = my_fun.parameters["bb"]
+
+    def _h_m(bb):
+        out = 1.0
+        for b in bb:
+            if b == 0:
+                out *= 1.0
+            else:
+                out *= (np.exp(2 * b) - 1) / 2 / b
+        return out
+
+    def _i_m(bb):
+        out = 1.0
+        for b in bb:
+            if b == 0:
+                out *= 1.0
+            else:
+                out *= (np.exp(b) - 1) / b
+        return out
+
+    var_ref = _h_m(bb) - _i_m(bb) ** 2
+
+    # Assertion (no need to be very ambitious with the tolerance)
+    assert np.allclose(var_mc, var_ref, rtol=1e-1)
diff --git a/tests/builtin_test_functions/test_sulfur.py b/tests/builtin_test_functions/test_sulfur.py
index f0ad1c2..70c630e 100644
--- a/tests/builtin_test_functions/test_sulfur.py
+++ b/tests/builtin_test_functions/test_sulfur.py
@@ -6,6 +6,7 @@
 - The tests defined in this module deals with
   the correctness of the evaluation.
 """
+
 import numpy as np
 
 from uqtestfuns import Sulfur
diff --git a/tests/builtin_test_functions/test_test_functions.py b/tests/builtin_test_functions/test_test_functions.py
index 095f49d..54e63a1 100644
--- a/tests/builtin_test_functions/test_test_functions.py
+++ b/tests/builtin_test_functions/test_test_functions.py
@@ -8,6 +8,7 @@
   implementations not about the correctness of computation of a particular
   implementation.
 """
+
 import numpy as np
 import pytest
 import copy
@@ -42,17 +43,17 @@ def test_create_instance_with_custom_name(builtin_testfun):
     testfun_class = builtin_testfun
 
     # Get the default name of the test function
-    name = testfun_class.__name__
+    function_id = testfun_class.__name__
 
     # Create a default instance
     my_fun = testfun_class()
 
     # Assertion
-    assert my_fun.name == name
+    assert my_fun.function_id == function_id
 
     # Use custom name to create a test function
-    my_fun = testfun_class(name=name)
-    assert my_fun.name == name
+    my_fun = testfun_class(function_id=function_id)
+    assert my_fun.function_id == function_id
 
 
 def test_create_instance_with_prob_input(builtin_testfun):
@@ -105,7 +106,7 @@ def test_create_instance_with_parameters(builtin_testfun):
         my_fun_2.parameters = parameters
         assert my_fun_2.parameters is parameters
     else:
-        assert my_fun.parameters is None
+        assert len(my_fun.parameters) == 0
 
 
 def test_available_inputs(builtin_testfun):
@@ -116,7 +117,7 @@ def test_available_inputs(builtin_testfun):
     available_inputs = testfun_class.available_inputs
 
     for available_input in available_inputs:
-        assert_call(testfun_class, prob_input_selection=available_input)
+        assert_call(testfun_class, input_id=available_input)
 
 
 def test_available_parameters(builtin_testfun):
@@ -128,9 +129,7 @@ def test_available_parameters(builtin_testfun):
 
     if available_parameters is not None:
         for available_parameter in available_parameters:
-            assert_call(
-                testfun_class, parameters_selection=available_parameter
-            )
+            assert_call(testfun_class, parameters_id=available_parameter)
 
 
 def test_call_instance(builtin_testfun):
@@ -153,15 +152,35 @@ def test_str(builtin_testfun):
     # Create an instance
     my_fun = builtin_testfun()
 
+    if my_fun.variable_dimension:
+        input_dim = f"{my_fun.input_dimension} (variable)"
+    else:
+        input_dim = f"{my_fun.input_dimension} (fixed)"
+    tags = ", ".join(my_fun.tags)
     str_ref = (
-        f"Name              : {my_fun.name}\n"
-        f"Spatial dimension : {my_fun.spatial_dimension}\n"
-        f"Description       : {my_fun.description}"
+        f"Function ID      : {my_fun.function_id}\n"
+        f"Input Dimension  : {input_dim}\n"
+        f"Output Dimension : {my_fun.output_dimension}\n"
+        f"Parameterized    : {bool(my_fun.parameters)}\n"
+        f"Description      : {my_fun.description}\n"
+        f"Applications     : {tags}"
     )
 
     assert my_fun.__str__() == str_ref
 
 
+def test_str_prob_input(builtin_testfun):
+    """Test the __str__() method of the input attached to a test function."""
+    # Create an instance
+    my_fun = builtin_testfun()
+
+    # str of the ProbInput
+    my_prob_input_str = my_fun.prob_input.__str__()
+
+    # Assertion
+    assert isinstance(my_prob_input_str, str)
+
+
 def test_transform_input(builtin_testfun):
     """Test transforming a set of input values in the default unif. domain."""
 
@@ -176,7 +195,7 @@ def test_transform_input(builtin_testfun):
 
     # Transformation from the default uniform domain to the input domain
     rng = np.random.default_rng(rng_seed)
-    xx_1 = -1 + 2 * rng.random((sample_size, my_fun.spatial_dimension))
+    xx_1 = -1 + 2 * rng.random((sample_size, my_fun.input_dimension))
     xx_1 = my_fun.transform_sample(xx_1)
 
     # Directly sample from the input property
@@ -200,7 +219,7 @@ def test_transform_input_non_default(builtin_testfun):
 
     # Transformation from non-default uniform domain to the input domain
     rng = np.random.default_rng(rng_seed)
-    xx_1 = rng.random((sample_size, my_fun.spatial_dimension))
+    xx_1 = rng.random((sample_size, my_fun.input_dimension))
     xx_1 = my_fun.transform_sample(xx_1, min_value=0.0, max_value=1.0)
 
     # Directly sample from the input property
@@ -215,7 +234,7 @@ def test_evaluate_wrong_input_dim(builtin_testfun):
 
     testfun = builtin_testfun()
 
-    xx = np.random.rand(10, testfun.spatial_dimension * 2)
+    xx = np.random.rand(10, testfun.input_dimension * 2)
 
     with pytest.raises(ValueError):
         testfun(xx)
@@ -227,14 +246,14 @@ def test_evaluate_wrong_input_domain(builtin_testfun):
     testfun = builtin_testfun()
 
     # Create a sample in [-2, 2]
-    xx = -2 + 4 * np.random.rand(1000, testfun.spatial_dimension)
+    xx = -2 + 4 * np.random.rand(1000, testfun.input_dimension)
 
     with pytest.raises(ValueError):
         # By default, the transformation domain is from [-1, 1]
         testfun.transform_sample(xx)
 
     # Create sampled input values from the default and perturb them
-    xx = np.empty((100, testfun.spatial_dimension))
+    xx = np.empty((100, testfun.input_dimension))
     for i, marginal in enumerate(testfun.prob_input.marginals):
         lb = marginal.lower + 1000
         ub = marginal.upper - 1000
@@ -245,15 +264,15 @@ def test_evaluate_wrong_input_domain(builtin_testfun):
         testfun(xx)
 
 
-def test_evaluate_invalid_spatial_dim(builtin_testfun):
-    """Test if an exception is raised if invalid spatial dimension is given."""
+def test_evaluate_invalid_input_dim(builtin_testfun):
+    """Test if an exception is raised if invalid input dimension is given."""
 
-    if hasattr(builtin_testfun, "default_spatial_dimension"):
+    if hasattr(builtin_testfun, "default_input_dimension"):
         with pytest.raises(TypeError):
-            builtin_testfun(spatial_dimension="10")
+            builtin_testfun(input_dimension="10")
 
 
 def test_evaluate_invalid_input_selection(builtin_testfun):
     """Test if an exception is raised if invalid input selection is given."""
     with pytest.raises(KeyError):
-        builtin_testfun(prob_input_selection=100)
+        builtin_testfun(input_id=100)
diff --git a/tests/conftest.py b/tests/conftest.py
index 12dff3c..16d88bf 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,13 +3,14 @@
 
 All global fixtures are defined here.
 """
+
 import numpy as np
 import random
 import string
 from typing import List, Callable, Any
 
 from uqtestfuns.core.prob_input.utils import SUPPORTED_MARGINALS
-from uqtestfuns.core.prob_input.univariate_distribution import UnivDist
+from uqtestfuns.core.prob_input.marginal import Marginal
 
 MARGINALS = list(SUPPORTED_MARGINALS.keys())
 
@@ -36,7 +37,7 @@ def create_random_alphanumeric(length: int) -> str:
     return out
 
 
-def create_random_marginals(length: int) -> List[UnivDist]:
+def create_random_marginals(length: int) -> List[Marginal]:
     """Create a random list of univariate random variables.
 
     Parameters
@@ -46,7 +47,7 @@ def create_random_marginals(length: int) -> List[UnivDist]:
 
     Returns
     -------
-    List[UnivDist]
+    List[Marginal]
         List of dictionaries to specify a ProbInput instance.
     """
     marginals = []
@@ -55,6 +56,9 @@ def create_random_marginals(length: int) -> List[UnivDist]:
         distribution = random.choice(MARGINALS)
         if distribution == "beta":
             parameters = np.sort(1 + 2 * np.random.rand(4))
+        elif distribution == "exponential":
+            # Single parameter, must be strictly positive
+            parameters = 1 + np.random.rand(1)
         elif distribution == "triangular":
             parameters = np.sort(1 + 2 * np.random.rand(2))
             parameters = np.insert(
@@ -73,7 +77,7 @@ def create_random_marginals(length: int) -> List[UnivDist]:
             parameters = np.sort(1 + 2 * np.random.rand(2))
 
         marginals.append(
-            UnivDist(
+            Marginal(
                 name=f"X{i+1}",
                 distribution=distribution,
                 parameters=parameters,
diff --git a/tests/core/prob_input/test_multivariate_input.py b/tests/core/prob_input/test_prob_input.py
similarity index 52%
rename from tests/core/prob_input/test_multivariate_input.py
rename to tests/core/prob_input/test_prob_input.py
index 3f47093..e9a7ea9 100644
--- a/tests/core/prob_input/test_multivariate_input.py
+++ b/tests/core/prob_input/test_prob_input.py
@@ -1,29 +1,21 @@
 import pytest
 import numpy as np
-from tabulate import tabulate
-from typing import List, Any
 
 from uqtestfuns.core.prob_input.probabilistic_input import ProbInput
-from uqtestfuns.core.prob_input.univariate_distribution import UnivDist
-from uqtestfuns.core.prob_input.input_spec import (
-    UnivDistSpec,
-    ProbInputSpecFixDim,
-    ProbInputSpecVarDim,
-)
 from conftest import create_random_marginals
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 10, 100])
-def test_create_instance_numpy_parameters(spatial_dimension):
+@pytest.mark.parametrize("input_dimension", [1, 2, 10, 100])
+def test_create_instance_numpy_parameters(input_dimension):
     """Test the creation of a MultivariateInput instance."""
-    marginals = create_random_marginals(spatial_dimension)
+    marginals = create_random_marginals(input_dimension)
 
     my_multivariate_input = ProbInput(marginals)
 
     # Assertions
     # Test the dimensionality
-    assert my_multivariate_input.spatial_dimension == spatial_dimension
-    for i in range(spatial_dimension):
+    assert my_multivariate_input.input_dimension == input_dimension
+    for i in range(input_dimension):
         # Test the name of the marginals
         assert marginals[i].name == my_multivariate_input.marginals[i].name
 
@@ -39,10 +31,10 @@ def test_create_instance_numpy_parameters(spatial_dimension):
         )
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 10, 100])
-def test_generate_sample(spatial_dimension):
+@pytest.mark.parametrize("input_dimension", [1, 2, 10, 100])
+def test_generate_sample(input_dimension):
     """Test sample generation from an instance of MultivariateInput."""
-    marginals = create_random_marginals(spatial_dimension)
+    marginals = create_random_marginals(input_dimension)
 
     my_multivariate_input = ProbInput(marginals)
 
@@ -53,9 +45,9 @@ def test_generate_sample(spatial_dimension):
     # Test the number of sample points
     assert xx.shape[0] == sample_size
     # Test the dimensionality of the sample
-    assert xx.shape[1] == spatial_dimension
+    assert xx.shape[1] == input_dimension
     # Test the bounds
-    for i in range(spatial_dimension):
+    for i in range(input_dimension):
         assert np.min(xx[:, i]) >= my_multivariate_input.marginals[i].lower
         assert np.max(xx[:, i]) <= my_multivariate_input.marginals[i].upper
 
@@ -64,17 +56,16 @@ def test_generate_dependent_sample():
     """Test dependent sample generation (not yet supported; raise error)."""
     marginals = create_random_marginals(5)
 
-    my_multivariate_input = ProbInput(marginals)
-    my_multivariate_input.copulas = "a"
+    my_multivariate_input = ProbInput(marginals, "a")
 
     with pytest.raises(ValueError):
         my_multivariate_input.get_sample(1000)
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 5])
-def test_get_pdf_values(spatial_dimension):
+@pytest.mark.parametrize("input_dimension", [1, 5])
+def test_get_pdf_values(input_dimension):
     """Test the PDF values from an instance of MultivariateInput."""
-    marginals = create_random_marginals(spatial_dimension)
+    marginals = create_random_marginals(input_dimension)
 
     my_multivariate_input = ProbInput(marginals)
 
@@ -91,23 +82,22 @@ def test_get_dependent_pdf_values():
     """Test dependent PDF value computation (not yet supported)."""
     marginals = create_random_marginals(5)
 
-    my_multivariate_input = ProbInput(marginals)
-    my_multivariate_input.copulas = "b"
+    my_multivariate_input = ProbInput(marginals, "b")
 
     with pytest.raises(ValueError):
         my_multivariate_input.pdf(np.random.rand(2, 5))
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 10])
-def test_transform_sample(spatial_dimension):
+@pytest.mark.parametrize("input_dimension", [1, 2, 10])
+def test_transform_sample(input_dimension):
     """Test the transformation of sample values from one dist. to another."""
-    marginals_1 = create_random_marginals(spatial_dimension)
+    marginals_1 = create_random_marginals(input_dimension)
     my_multivariate_input_1 = ProbInput(marginals_1)
 
     sample_size = 5000
     xx = my_multivariate_input_1.get_sample(sample_size)
 
-    marginals_2 = create_random_marginals(spatial_dimension)
+    marginals_2 = create_random_marginals(input_dimension)
     my_multivariate_input_2 = ProbInput(marginals_2)
 
     xx_trans = my_multivariate_input_1.transform_sample(
@@ -139,7 +129,7 @@ def test_failed_transform_sample():
 def test_transform_dependent_sample():
     """Test dependent transformation (not yet supported; raise an error)."""
     marginals_1 = create_random_marginals(5)
-    my_multivariate_input_1 = ProbInput(marginals_1)
+    my_multivariate_input_1 = ProbInput(marginals_1, "a")
 
     marginals_2 = create_random_marginals(5)
     my_multivariate_input_2 = ProbInput(marginals_2)
@@ -147,86 +137,39 @@ def test_transform_dependent_sample():
     xx = np.random.rand(2, 5)
 
     with pytest.raises(ValueError):
-        my_multivariate_input_1.copulas = "a"
         my_multivariate_input_1.transform_sample(xx, my_multivariate_input_2)
 
 
 def test_str():
-    """Test __str__ method of an instance of MultivariateInput."""
-
+    """Test __str__ method of an instance of ProbInput."""
     # Create a test instance
-    marginals = create_random_marginals(2)
-    my_multivariate_input = ProbInput(marginals)
+    marginals = create_random_marginals(5)
+    my_probinput = ProbInput(marginals)
 
-    # Create the reference string
-    str_ref = f"Name         : {my_multivariate_input.name}\n"
-    str_ref += f"Spatial Dim. : {my_multivariate_input.spatial_dimension}\n"
-    str_ref += f"Description  : {my_multivariate_input.description}\n"
-    str_ref += "Marginals    :\n\n"
-    header_names = ["name", "distribution", "parameters", "description"]
-    str_ref_list: List[List] = []
-    for i, marginal in enumerate(marginals):
-        str_ref_placeholder: List[Any] = [i + 1]
-        for header_name in header_names:
-            str_ref_placeholder.append(getattr(marginal, header_name))
-        str_ref_list.append(str_ref_placeholder)
-    header_names.insert(0, "No.")
-    str_ref += tabulate(
-        str_ref_list,
-        headers=list(map(str.capitalize, header_names)),
-        stralign="center",
-    )
-    str_ref += f"\n\nCopulas      : {my_multivariate_input.copulas}"
+    # Create a string
+    my_str = str(my_probinput)
 
     # Assertion
-    assert my_multivariate_input.__str__() == str_ref
-
+    assert isinstance(my_str, str)
 
-def test_repr_html():
-    """Test _repr_html_ method of an instance of MultivariateInput."""
 
+def test_repr():
+    """Test __repr__ method of an instance of ProbInput."""
     # Create a test instance
     marginals = create_random_marginals(5)
-    my_multivariate_input = ProbInput(marginals)
+    my_probinput = ProbInput(marginals)
 
-    # Create the reference string
-    str_ref = f"<p><b>Name</b>:&nbsp;{my_multivariate_input.name}\n</p>"
-    str_ref += (
-        f"<p><b>Spatial Dimension</b>:&nbsp;"
-        f"{my_multivariate_input.spatial_dimension}\n</p>"
-    )
-    str_ref += (
-        "<p><b>Description</b>:&nbsp;"
-        f"{my_multivariate_input.description}\n</p>"
-    )
-    str_ref += "<p><b>Marginals:</b>\n</p>"
-
-    header_names = ["name", "distribution", "parameters", "description"]
-    str_ref_list: List[List] = []
-    for i, marginal in enumerate(marginals):
-        str_ref_placeholder: List[Any] = [i + 1]
-        for header_name in header_names:
-            str_ref_placeholder.append(getattr(marginal, header_name))
-        str_ref_list.append(str_ref_placeholder)
-    header_names.insert(0, "No.")
-    str_ref += tabulate(
-        str_ref_list,
-        headers=list(map(str.capitalize, header_names)),
-        stralign="center",
-        tablefmt="html",
-    )
-
-    str_ref += "\n"
-    str_ref += f"<p><b>Copulas</b>:&nbsp;{my_multivariate_input.copulas}</p>"
+    # Create a string
+    my_str = repr(my_probinput)
 
     # Assertion
-    assert my_multivariate_input._repr_html_() == str_ref
+    assert isinstance(my_str, str)
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 10, 100])
-def test_pass_random_seed(spatial_dimension):
+@pytest.mark.parametrize("input_dimension", [1, 2, 10, 100])
+def test_pass_random_seed(input_dimension):
     """Test passing random seed to the constructor."""
-    marginals = create_random_marginals(spatial_dimension)
+    marginals = create_random_marginals(input_dimension)
 
     # Create two instances with an identical seed number
     rng_seed = 42
@@ -241,10 +184,10 @@ def test_pass_random_seed(spatial_dimension):
     assert np.allclose(xx_1, xx_2)
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 10, 100])
-def test_reset_rng(spatial_dimension):
+@pytest.mark.parametrize("input_dimension", [1, 2, 10, 100])
+def test_reset_rng(input_dimension):
     """Test resetting the RNG once an instance has been created."""
-    marginals = create_random_marginals(spatial_dimension)
+    marginals = create_random_marginals(input_dimension)
 
     # Create two instances with an identical seed number
     rng_seed = 42
@@ -265,78 +208,29 @@ def test_reset_rng(spatial_dimension):
     assert np.allclose(xx_1, xx_2)
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 10, 100])
-def test_create_from_spec(spatial_dimension):
-    """Test creating an instance from specification NamedTuple"""
-    marginals: List[UnivDist] = create_random_marginals(spatial_dimension)
-
-    # Create a ProbInputSpecFixDim
-    name = "Test Name"
-    description = "Test description"
-    copulas = None
-    prob_spec = ProbInputSpecFixDim(
-        name=name,
-        description=description,
-        marginals=[
-            UnivDistSpec(
-                name=marginal.name,
-                description=marginal.description,
-                distribution=marginal.distribution,
-                parameters=marginal.parameters,  # type: ignore
-            )
-            for marginal in marginals
-        ],
-        copulas=copulas,
-    )
+@pytest.mark.parametrize("input_dimension", [1, 2, 10, 100])
+def test_set_rng_seed(input_dimension):
+    """Test setting the RNG seed after an instance has been created."""
+    marginals = create_random_marginals(input_dimension)
 
-    # Create from spec
-    my_input = ProbInput.from_spec(prob_spec)
+    # Create two instances with an identical seed number
+    my_input = ProbInput(marginals, rng_seed=42)
 
-    # Assertions
-    assert my_input.name == name
-    assert my_input.description == description
-    assert my_input.copulas == copulas
-
-    # Create a ProbInputSpecVarDim
-    def _test_vardim(spatial_dimension):
-        marginals_spec = [
-            UnivDistSpec(
-                name=f"x{i+1}",
-                description="None",
-                distribution="uniform",
-                parameters=[0, 1],
-            )
-            for i in range(spatial_dimension)
-        ]
-
-        return marginals_spec
-
-    name = "Test Name"
-    description = "Test description"
-    copulas = None
-    prob_spec_vardim = ProbInputSpecVarDim(
-        name=name,
-        description=description,
-        marginals_generator=_test_vardim,
-        copulas=copulas,
-    )
+    # Generate sample points
+    xx_1 = my_input.get_sample(1000)
+    xx_2 = my_input.get_sample(1000)
 
-    # Create from spec
-    my_input = ProbInput.from_spec(
-        prob_spec_vardim, spatial_dimension=spatial_dimension
-    )
+    # Assertion: Both samples should not be equal
+    assert not np.allclose(xx_1, xx_2)
 
-    # Assertions
-    assert my_input.name == name
-    assert my_input.description == description
-    assert my_input.copulas == copulas
-    assert my_input.spatial_dimension == spatial_dimension
+    # Reset the RNG and generate new sample points
+    my_input.rng_seed = 42
+    xx_2 = my_input.get_sample(1000)
 
-    with pytest.raises(ValueError):
-        ProbInput.from_spec(prob_spec_vardim)
+    # Assertion: Both samples should now be equal
+    assert np.allclose(xx_1, xx_2)
 
 
-#
 # def test_get_cdf_values():
 #     """Test the CDF values from an instance of UnivariateInput."""
 #     name = create_random_alphanumeric(10)
diff --git a/tests/core/prob_input/test_univariate_beta.py b/tests/core/prob_input/test_univariate_beta.py
index 4f5c62d..91e51da 100644
--- a/tests/core/prob_input/test_univariate_beta.py
+++ b/tests/core/prob_input/test_univariate_beta.py
@@ -1,10 +1,11 @@
 """
 Test module for UnivariateInput instances with a Beta distribution.
 """
+
 import pytest
 import numpy as np
 
-from uqtestfuns.core.prob_input.univariate_distribution import UnivDist
+from uqtestfuns.core.prob_input.marginal import Marginal
 from uqtestfuns.global_settings import ARRAY_FLOAT
 from conftest import create_random_alphanumeric
 
@@ -42,7 +43,7 @@ def test_wrong_number_of_parameters() -> None:
     parameters = np.sort(np.random.rand(6))
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_failed_parameter_verification() -> None:
@@ -54,19 +55,19 @@ def test_failed_parameter_verification() -> None:
     parameters = [-7.71, 10, 1, 2]
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
     # The 2nd parameter of the Beta distribution must be strictly positive!
     parameters = [7.71, -10, 1, 2]
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
     # The lower bound must be smaller than upper bound!
     parameters = [1, 2, 4, 3]
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_estimate_mean() -> None:
@@ -77,7 +78,7 @@ def test_estimate_mean() -> None:
     distribution = "beta"
     parameters = np.sort(2 * np.random.rand(4))
 
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         name=name, distribution=distribution, parameters=parameters
     )
 
@@ -102,7 +103,7 @@ def test_estimate_std() -> None:
     distribution = "beta"
     parameters = np.sort(2 * np.random.rand(4))
 
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         name=name, distribution=distribution, parameters=parameters
     )
 
diff --git a/tests/core/prob_input/test_univariate_exponential.py b/tests/core/prob_input/test_univariate_exponential.py
new file mode 100644
index 0000000..5ca8bc4
--- /dev/null
+++ b/tests/core/prob_input/test_univariate_exponential.py
@@ -0,0 +1,109 @@
+"""
+Test module specifically for Marginal instances with the exponential dist.
+"""
+
+import pytest
+import numpy as np
+
+from uqtestfuns.core.prob_input.marginal import Marginal
+from conftest import create_random_alphanumeric
+
+DISTRIBUTION = "exponential"
+
+
+def test_wrong_number_of_parameters() -> None:
+    """Test the failure of specifying invalid parameter values."""
+    name = create_random_alphanumeric(5)
+    # The distribution expects exactly 1 parameter!
+    num_params = np.random.randint(2, 10)
+    parameters = np.random.rand(num_params)
+
+    with pytest.raises(ValueError):
+        Marginal(name=name, distribution=DISTRIBUTION, parameters=parameters)
+
+
+def test_failed_parameter_verification() -> None:
+    name = create_random_alphanumeric(10)
+    # The parameter of an exponential distribution must be stricly positive!
+    parameters = [-5]
+
+    with pytest.raises(ValueError):
+        Marginal(name=name, distribution=DISTRIBUTION, parameters=parameters)
+
+
+def test_get_pdf_values() -> None:
+    """Test the PDF values from an instance of Marginal."""
+
+    name = create_random_alphanumeric(5)
+    parameters = np.sort(np.random.rand(1))
+
+    my_univariate_input = Marginal(
+        name=name, distribution=DISTRIBUTION, parameters=parameters
+    )
+
+    sample_size = 1000
+    xx = my_univariate_input.get_sample(sample_size)
+
+    # Assertions (PDF values are max. at the left bound and min. at the right)
+    assert np.all(
+        np.logical_and(
+            my_univariate_input.pdf(xx)
+            <= my_univariate_input.pdf(my_univariate_input.lower),
+            my_univariate_input.pdf(xx)
+            >= my_univariate_input.pdf(my_univariate_input.upper),
+        )
+    )
+
+
+def test_mean() -> None:
+    """Test the mean values from an instance of Marginal."""
+    name = create_random_alphanumeric(5)
+    parameters = np.sort(np.random.rand(1))
+
+    my_univariate_input = Marginal(
+        name=name, distribution=DISTRIBUTION, parameters=parameters
+    )
+
+    sample_size = 10000000
+    xx = my_univariate_input.get_sample(sample_size)
+
+    mean = np.mean(xx)
+    mean_ref = 1 / parameters[0]
+
+    assert np.isclose(mean, mean_ref, rtol=1e-02, atol=1e-02)
+
+
+def test_variance() -> None:
+    """Test the variance values from an instance of Marginal."""
+    name = create_random_alphanumeric(5)
+    parameters = np.sort(np.random.rand(1))
+
+    my_univariate_input = Marginal(
+        name=name, distribution=DISTRIBUTION, parameters=parameters
+    )
+
+    sample_size = 10000000
+    xx = my_univariate_input.get_sample(sample_size)
+
+    var = np.var(xx)
+    var_ref = 1 / (parameters[0] ** 2)
+
+    assert np.isclose(var, var_ref, rtol=1e-02, atol=1e-02)
+
+
+def test_median() -> None:
+    """Test the median values from an instance of Marginal."""
+    name = create_random_alphanumeric(5)
+    parameters = np.sort(np.random.rand(1))
+
+    my_univariate_input = Marginal(
+        name=name, distribution=DISTRIBUTION, parameters=parameters
+    )
+
+    sample_size = 10000000
+    xx = my_univariate_input.get_sample(sample_size)
+
+    median = np.median(xx)
+    median_ref = np.log(2) / parameters[0]
+
+    assert np.isclose(median, median_ref, rtol=1e-02, atol=1e-02)
diff --git a/tests/core/prob_input/test_univariate_gumbel.py b/tests/core/prob_input/test_univariate_gumbel.py
index a17023c..f102804 100644
--- a/tests/core/prob_input/test_univariate_gumbel.py
+++ b/tests/core/prob_input/test_univariate_gumbel.py
@@ -2,10 +2,11 @@
 Test module specifically for UnivariateInput instances with Gumbel (max.)
 distribution.
 """
+
 import pytest
 import numpy as np
 
-from uqtestfuns.core.prob_input.univariate_distribution import UnivDist
+from uqtestfuns.core.prob_input.marginal import Marginal
 from uqtestfuns.global_settings import ARRAY_FLOAT
 from conftest import create_random_alphanumeric
 
@@ -54,7 +55,7 @@ def test_wrong_number_of_parameters() -> None:
     parameters = np.sort(np.random.rand(5))
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_failed_parameter_verification() -> None:
@@ -65,7 +66,7 @@ def test_failed_parameter_verification() -> None:
     parameters = [7.71, -5.0]
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_estimate_mean() -> None:
@@ -74,7 +75,7 @@ def test_estimate_mean() -> None:
     parameters = np.sort(1 + 5 * np.random.rand(2))
 
     # Create an instance
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         distribution=DISTRIBUTION_NAME, parameters=parameters
     )
 
@@ -98,7 +99,7 @@ def test_estimate_variance() -> None:
     parameters = np.sort(1 + 5 * np.random.rand(2))
 
     # Create an instance
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         distribution=DISTRIBUTION_NAME, parameters=parameters
     )
 
@@ -122,7 +123,7 @@ def test_estimate_median() -> None:
     parameters = np.sort(1 + 5 * np.random.rand(2))
 
     # Create an instance
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         distribution=DISTRIBUTION_NAME, parameters=parameters
     )
 
@@ -146,7 +147,7 @@ def test_estimate_mode() -> None:
     parameters = np.sort(1 + 5 * np.random.rand(2))
 
     # Create an instance
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         distribution=DISTRIBUTION_NAME, parameters=parameters
     )
 
diff --git a/tests/core/prob_input/test_univariate_input.py b/tests/core/prob_input/test_univariate_input.py
index 345b929..b7a51d4 100644
--- a/tests/core/prob_input/test_univariate_input.py
+++ b/tests/core/prob_input/test_univariate_input.py
@@ -1,13 +1,14 @@
 """
 Test module for UnivariateInput instances.
 """
+
 import pytest
 import numpy as np
 
 from typing import Tuple, Dict, Union, Any
 from numpy.typing import ArrayLike
 
-from uqtestfuns.core.prob_input.univariate_distribution import UnivDist
+from uqtestfuns.core.prob_input.marginal import Marginal
 from uqtestfuns.core.prob_input.utils import SUPPORTED_MARGINALS
 from conftest import create_random_alphanumeric
 
@@ -18,31 +19,39 @@
 @pytest.fixture(params=MARGINALS)
 def univariate_input(
     request: Any,
-) -> Tuple[UnivDist, Dict[str, Union[str, ArrayLike]]]:
+) -> Tuple[Marginal, Dict[str, Union[str, ArrayLike]]]:
     """Test fixture, an instance of UnivariateInput."""
+
+    # All random values (distribution parameters are limited to 5 digits
+    # to avoid awkward yet unrealistic values)
     name = create_random_alphanumeric(8)
     distribution = request.param
     if request.param == "uniform":
-        parameters = np.sort(np.random.rand(2))
+        parameters = np.sort(np.round(np.random.rand(2), decimals=5))
     elif request.param == "beta":
-        parameters = np.sort(np.random.rand(4))
+        parameters = np.sort(np.round(np.random.rand(4), decimals=5))
+    elif distribution == "exponential":
+        # Single parameter, must be strictly positive
+        parameters = 1 + np.round(np.random.rand(1), decimals=5)
     elif distribution == "triangular":
-        parameters = np.sort(1 + 2 * np.random.rand(2))
+        parameters = np.sort(1 + 2 * np.round(np.random.rand(2), decimals=5))
         # Append the mid point
-        parameters = np.insert(
-            parameters, 2, np.random.uniform(parameters[0], parameters[1])
+        mid_p = np.round(
+            np.random.uniform(parameters[0], parameters[1]),
+            decimals=5,
         )
+        parameters = np.insert(parameters, 2, mid_p, axis=0)
     elif distribution in ["trunc-normal", "trunc-gumbel"]:
         # mu must be inside the bounds
-        parameters = np.sort(1 + 2 * np.random.rand(3))
+        parameters = np.sort(1 + 2 * np.round(np.random.rand(3), decimals=5))
         parameters[[0, 1]] = parameters[[1, 0]]
         # Insert sigma as the second parameter
         parameters = np.insert(parameters, 1, np.random.rand(1))
     elif distribution == "lognormal":
         # Limit the size of the parameters
-        parameters = 1 + np.random.rand(2)
+        parameters = 1 + np.round(np.random.rand(2), decimals=5)
     else:
-        parameters = 5 * np.random.rand(2)
+        parameters = 5 * np.round(np.random.rand(2), decimals=5)
         parameters[1] += 1.0
 
     specs = {
@@ -51,7 +60,7 @@ def univariate_input(
         "parameters": parameters,
     }
 
-    my_univariate_input = UnivDist(**specs)
+    my_univariate_input = Marginal(**specs)
 
     return my_univariate_input, specs
 
@@ -73,7 +82,7 @@ def test_create_instance_unsupported_marginal() -> None:
     parameters = list(np.sort(np.random.rand(2)))
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_generate_sample(univariate_input: Any) -> None:
@@ -154,7 +163,7 @@ def test_transform_sample() -> None:
     distribution_1 = "uniform"
     parameters_1 = np.sort(np.random.rand(2))
 
-    my_univariate_input_1 = UnivDist(
+    my_univariate_input_1 = Marginal(
         name=name_1, distribution=distribution_1, parameters=parameters_1
     )
 
@@ -165,7 +174,7 @@ def test_transform_sample() -> None:
     distribution_2 = "uniform"
     parameters_2 = np.sort(np.random.rand(2))
 
-    my_univariate_input_2 = UnivDist(
+    my_univariate_input_2 = Marginal(
         name=name_2, distribution=distribution_2, parameters=parameters_2
     )
 
@@ -184,7 +193,7 @@ def test_failed_transform_sample() -> None:
     distribution = "uniform"
     parameters = np.sort(np.random.rand(2))
 
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         name=name, distribution=distribution, parameters=parameters
     )
 
@@ -223,8 +232,8 @@ def test_pass_random_seed():
 
     # Create two instances with an identical seed number
     rng_seed = 42
-    my_input_1 = UnivDist("uniform", [0, 1], rng_seed=rng_seed)
-    my_input_2 = UnivDist("uniform", [0, 1], rng_seed=rng_seed)
+    my_input_1 = Marginal("uniform", [0, 1], rng_seed=rng_seed)
+    my_input_2 = Marginal("uniform", [0, 1], rng_seed=rng_seed)
 
     # Generate sample points
     xx_1 = my_input_1.get_sample(1000)
@@ -239,7 +248,7 @@ def test_reset_rng():
 
     # Create two instances with an identical seed number
     rng_seed = 42
-    my_input = UnivDist("uniform", [0, 1], rng_seed=rng_seed)
+    my_input = Marginal("uniform", [0, 1], rng_seed=rng_seed)
 
     # Generate sample points
     xx_1 = my_input.get_sample(1000)
diff --git a/tests/core/prob_input/test_univariate_logitnormal.py b/tests/core/prob_input/test_univariate_logitnormal.py
index 95b443d..7917593 100644
--- a/tests/core/prob_input/test_univariate_logitnormal.py
+++ b/tests/core/prob_input/test_univariate_logitnormal.py
@@ -1,12 +1,13 @@
 """
 Test module specifically for UnivariateInput instances with logitnormal dist.
 """
+
 import pytest
 import numpy as np
 
 from scipy.special import expit as logistic
 
-from uqtestfuns.core.prob_input.univariate_distribution import UnivDist
+from uqtestfuns.core.prob_input.marginal import Marginal
 from conftest import create_random_alphanumeric
 
 
@@ -18,7 +19,7 @@ def test_wrong_number_of_parameters() -> None:
     parameters = np.sort(np.random.rand(3))
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_failed_parameter_verification() -> None:
@@ -28,7 +29,7 @@ def test_failed_parameter_verification() -> None:
     parameters = [7.71, -10]
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_get_pdf_values() -> None:
@@ -38,7 +39,7 @@ def test_get_pdf_values() -> None:
     distribution = "logitnormal"
     parameters = np.sort(np.random.rand(2))
 
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         name=name, distribution=distribution, parameters=parameters
     )
 
@@ -62,7 +63,7 @@ def test_median() -> None:
     distribution = "logitnormal"
     parameters = np.sort(np.random.rand(2))
 
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         name=name, distribution=distribution, parameters=parameters
     )
 
diff --git a/tests/core/prob_input/test_univariate_lognormal.py b/tests/core/prob_input/test_univariate_lognormal.py
index 4430005..d107c61 100644
--- a/tests/core/prob_input/test_univariate_lognormal.py
+++ b/tests/core/prob_input/test_univariate_lognormal.py
@@ -1,10 +1,11 @@
 """
 Test module specifically for UnivariateInput instances with lognormal dist.
 """
+
 import pytest
 import numpy as np
 
-from uqtestfuns.core.prob_input.univariate_distribution import UnivDist
+from uqtestfuns.core.prob_input.marginal import Marginal
 from conftest import create_random_alphanumeric
 
 
@@ -16,7 +17,7 @@ def test_wrong_number_of_parameters() -> None:
     parameters = np.sort(np.random.rand(3))
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_failed_parameter_verification() -> None:
@@ -26,7 +27,7 @@ def test_failed_parameter_verification() -> None:
     parameters = [7.71, -10]
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_get_pdf_values() -> None:
@@ -36,7 +37,7 @@ def test_get_pdf_values() -> None:
     distribution = "lognormal"
     parameters = np.sort(np.random.rand(2))
 
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         name=name, distribution=distribution, parameters=parameters
     )
 
diff --git a/tests/core/prob_input/test_univariate_normal.py b/tests/core/prob_input/test_univariate_normal.py
index 47e2e72..a3f34e2 100644
--- a/tests/core/prob_input/test_univariate_normal.py
+++ b/tests/core/prob_input/test_univariate_normal.py
@@ -1,10 +1,11 @@
 """
 Test module specifically for UnivariateInput instances with normal dist.
 """
+
 import pytest
 import numpy as np
 
-from uqtestfuns.core.prob_input.univariate_distribution import UnivDist
+from uqtestfuns.core.prob_input.marginal import Marginal
 from conftest import create_random_alphanumeric
 
 
@@ -16,7 +17,7 @@ def test_wrong_number_of_parameters() -> None:
     parameters = np.sort(np.random.rand(10))
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_failed_parameter_verification() -> None:
@@ -27,7 +28,7 @@ def test_failed_parameter_verification() -> None:
     parameters = [7.71, -1]
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_get_pdf_values() -> None:
@@ -37,7 +38,7 @@ def test_get_pdf_values() -> None:
     distribution = "normal"
     parameters = np.sort(np.random.rand(2))
 
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         name=name, distribution=distribution, parameters=parameters
     )
 
diff --git a/tests/core/prob_input/test_univariate_triangular.py b/tests/core/prob_input/test_univariate_triangular.py
index 5c81a0a..8588c39 100644
--- a/tests/core/prob_input/test_univariate_triangular.py
+++ b/tests/core/prob_input/test_univariate_triangular.py
@@ -1,10 +1,11 @@
 """
 Test module specifically for UnivariateInput instances with triangular dist.
 """
+
 import pytest
 import numpy as np
 
-from uqtestfuns.core.prob_input.univariate_distribution import UnivDist
+from uqtestfuns.core.prob_input.marginal import Marginal
 from conftest import create_random_alphanumeric
 
 DISTRIBUTION_NAME = "triangular"
@@ -17,7 +18,7 @@ def test_wrong_number_of_parameters() -> None:
     parameters = np.sort(np.random.rand(10))
 
     with pytest.raises(ValueError):
-        UnivDist(
+        Marginal(
             name=name, distribution=DISTRIBUTION_NAME, parameters=parameters
         )
 
@@ -29,7 +30,7 @@ def test_failed_parameter_verification() -> None:
     parameters = [5, 1, 3]
 
     with pytest.raises(ValueError):
-        UnivDist(
+        Marginal(
             name=name, distribution=DISTRIBUTION_NAME, parameters=parameters
         )
 
@@ -37,7 +38,7 @@ def test_failed_parameter_verification() -> None:
     parameters = [3, 4, 10]
 
     with pytest.raises(ValueError):
-        UnivDist(
+        Marginal(
             name=name, distribution=DISTRIBUTION_NAME, parameters=parameters
         )
 
@@ -49,7 +50,7 @@ def test_estimate_mode() -> None:
     parameters[[2, 1]] = parameters[[1, 2]]
 
     # Create an instance
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         distribution=DISTRIBUTION_NAME, parameters=parameters
     )
 
diff --git a/tests/core/prob_input/test_univariate_trunc_gumbel.py b/tests/core/prob_input/test_univariate_trunc_gumbel.py
index 67c9a43..0c76271 100644
--- a/tests/core/prob_input/test_univariate_trunc_gumbel.py
+++ b/tests/core/prob_input/test_univariate_trunc_gumbel.py
@@ -1,13 +1,14 @@
 """
 Test module for UnivariateInput instances with a truncated Gumbel (max.) dist.
 """
+
 import pytest
 import numpy as np
 import scipy.integrate as integrate
 
 from scipy.stats import gumbel_r
 
-from uqtestfuns.core.prob_input.univariate_distribution import UnivDist
+from uqtestfuns.core.prob_input.marginal import Marginal
 from uqtestfuns.global_settings import ARRAY_FLOAT
 from conftest import create_random_alphanumeric
 
@@ -65,7 +66,7 @@ def test_wrong_number_of_parameters() -> None:
     parameters = np.sort(np.random.rand(5))
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_failed_parameter_verification() -> None:
@@ -76,13 +77,13 @@ def test_failed_parameter_verification() -> None:
     parameters = [7.71, -5.0, 0, 10]
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
     # The lower bound is larger than the upper bound!
     parameters = [2.71, 0.5, 5, 0]
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_estimate_mode() -> None:
@@ -94,7 +95,7 @@ def test_estimate_mode() -> None:
     parameters = np.insert(parameters, 1, np.random.rand(1))
 
     # Create an instance
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         distribution=DISTRIBUTION_NAME, parameters=parameters
     )
 
@@ -122,7 +123,7 @@ def test_estimate_median() -> None:
     parameters = np.insert(parameters, 1, np.random.rand(1))
 
     # Create an instance
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         distribution=DISTRIBUTION_NAME, parameters=parameters
     )
 
@@ -156,12 +157,12 @@ def test_untruncated() -> None:
     distribution = DISTRIBUTION_NAME
     parameters = [10, 2, -np.inf, np.inf]
 
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         distribution=distribution, parameters=parameters
     )
 
     # Create a reference Gumbel distribution
-    my_univariate_input_ref = UnivDist(
+    my_univariate_input_ref = Marginal(
         distribution="gumbel", parameters=parameters[:2]
     )
 
diff --git a/tests/core/prob_input/test_univariate_trunc_normal.py b/tests/core/prob_input/test_univariate_trunc_normal.py
index eabfbfa..dc51586 100644
--- a/tests/core/prob_input/test_univariate_trunc_normal.py
+++ b/tests/core/prob_input/test_univariate_trunc_normal.py
@@ -1,12 +1,13 @@
 """
 Test module for UnivariateInput instances with a truncated normal distribution.
 """
+
 import pytest
 import numpy as np
 
 from scipy.stats import norm
 
-from uqtestfuns.core.prob_input.univariate_distribution import UnivDist
+from uqtestfuns.core.prob_input.marginal import Marginal
 from uqtestfuns.global_settings import ARRAY_FLOAT
 from conftest import create_random_alphanumeric
 
@@ -55,7 +56,7 @@ def test_wrong_number_of_parameters() -> None:
     parameters = np.sort(np.random.rand(6))
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_failed_parameter_verification() -> None:
@@ -67,13 +68,13 @@ def test_failed_parameter_verification() -> None:
     parameters = [7.71, -10, 1, 2]
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
     # The lower bound must be smaller than upper bound!
     parameters = [3.5, 2, 4, 3]
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_estimate_mean() -> None:
@@ -88,7 +89,7 @@ def test_estimate_mean() -> None:
     # Insert sigma as the second parameter
     parameters = np.insert(parameters, 1, np.random.rand(1))
 
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         name=name, distribution=distribution, parameters=parameters
     )
 
@@ -117,7 +118,7 @@ def test_estimate_std() -> None:
     # Insert sigma as the second parameter
     parameters = np.insert(parameters, 1, np.random.rand(1))
 
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         name=name, distribution=distribution, parameters=parameters
     )
 
@@ -151,12 +152,12 @@ def test_untruncated() -> None:
     distribution = DISTRIBUTION_NAME
     parameters = [10, 2, -np.inf, np.inf]
 
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         name=name, distribution=distribution, parameters=parameters
     )
 
     # Create a reference normal distribution
-    my_univariate_input_ref = UnivDist(
+    my_univariate_input_ref = Marginal(
         distribution="normal", parameters=parameters[:2]
     )
 
diff --git a/tests/core/prob_input/test_univariate_uniform.py b/tests/core/prob_input/test_univariate_uniform.py
index 184a52a..4ef1340 100644
--- a/tests/core/prob_input/test_univariate_uniform.py
+++ b/tests/core/prob_input/test_univariate_uniform.py
@@ -1,10 +1,11 @@
 """
 Test module specifically for UnivariateInput instances with uniform dist.
 """
+
 import pytest
 import numpy as np
 
-from uqtestfuns.core.prob_input.univariate_distribution import UnivDist
+from uqtestfuns.core.prob_input.marginal import Marginal
 from conftest import create_random_alphanumeric
 
 
@@ -16,7 +17,7 @@ def test_wrong_parameters() -> None:
     parameters = np.sort(np.random.rand(1))
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_failed_parameter_verification() -> None:
@@ -27,7 +28,7 @@ def test_failed_parameter_verification() -> None:
     parameters = [10, -10]
 
     with pytest.raises(ValueError):
-        UnivDist(name=name, distribution=distribution, parameters=parameters)
+        Marginal(name=name, distribution=distribution, parameters=parameters)
 
 
 def test_get_pdf_values() -> None:
@@ -37,7 +38,7 @@ def test_get_pdf_values() -> None:
     distribution = "uniform"
     parameters = np.sort(np.random.rand(2))
 
-    my_univariate_input = UnivDist(
+    my_univariate_input = Marginal(
         name=name, distribution=distribution, parameters=parameters
     )
 
diff --git a/tests/core/test_funparams.py b/tests/core/test_funparams.py
new file mode 100644
index 0000000..b125b92
--- /dev/null
+++ b/tests/core/test_funparams.py
@@ -0,0 +1,327 @@
+"""
+Test module for FunParams class.
+"""
+
+import numpy as np
+import pytest
+import random
+import string
+
+from uqtestfuns.core import FunParams
+
+
+@pytest.fixture
+def declared_parameter():
+    return {
+        "keyword": "param",
+        "value": 10,
+        "type": int,
+        "description": "my parameter",
+    }
+
+
+@pytest.fixture
+def declared_parameter_invalid():
+    return {
+        "keyword": "01param",
+        "value": 10,
+    }
+
+
+@pytest.fixture
+def declared_parameters():
+    return [
+        {
+            "keyword": "param1",
+            "value": 10,
+            "type": int,
+            "description": "my parameter",
+        },
+        {
+            "keyword": "param2",
+            "value": 30.0,
+        },
+        {
+            "keyword": "param3",
+            "value": np.random.rand(10, 3),
+        },
+    ]
+
+
+@pytest.fixture
+def input_dict():
+    return {
+        "function_id": "test-function",
+        "parameter_id": "param-01",
+        "description": "my parameter",
+        "declared_parameters": [
+            {
+                "keyword": "param_01",
+                "value": 10,
+                "type": int,
+                "description": "my parameter",
+            },
+            {
+                "keyword": "param_02",
+                "value": 20.0,
+            },
+        ],
+    }
+
+
+def test_bool_true(declared_parameter):
+    """Test the boolean value of an instance (True)."""
+    my_params = FunParams(declared_parameters=[declared_parameter])
+
+    assert my_params
+
+
+def test_bool_false():
+    """Test the boolean value of an instance (False)."""
+    my_params = FunParams()
+
+    assert not my_params
+
+
+def test_len(declared_parameters):
+    """Test the length of an instance."""
+    my_params = FunParams(declared_parameters=declared_parameters)
+
+    assert len(my_params) == len(declared_parameters)
+
+
+class TestEquality:
+    """Series of tests for equality check."""
+
+    def test_equal_empty(self):
+        """Test the equality in value of two empty instances."""
+        my_params_1 = FunParams()
+        my_params_2 = FunParams()
+
+        assert my_params_1 == my_params_2
+
+    def test_equal(self, input_dict):
+        """Test the equality in value of two instances."""
+        my_params_1 = FunParams(**input_dict)
+        my_params_2 = FunParams(**input_dict)
+
+        assert my_params_1 == my_params_2
+
+    @pytest.mark.parametrize("other", [1, [1, 2], np.array([1])])
+    def test_other_instance(self, other):
+        """Test the equality check with other instances."""
+        my_params = FunParams()
+
+        assert my_params != other
+
+    def test_not_equal(self, declared_parameters):
+        """Test the inequality in value of two instances."""
+        my_params_1 = FunParams(declared_parameters=declared_parameters)
+        my_params_2 = FunParams(declared_parameters=declared_parameters[:-1])
+
+        assert my_params_1 != my_params_2
+
+    def test_equal_array_valued(self):
+        """Test the equality of two instances with array-valued parameter."""
+        my_params_1 = FunParams()
+        my_params_2 = FunParams()
+        xx = np.random.rand(10, 10)
+        my_params_1.add("xx", xx)
+        my_params_2.add("xx", xx)
+
+        assert my_params_1 == my_params_2
+
+    def test_inequal_array_valued(self):
+        """Test the inequality of two instances with array-valued parameter."""
+        my_params_1 = FunParams()
+        my_params_2 = FunParams()
+        my_params_1.add("xx", np.random.rand(10, 10))
+        my_params_2.add("xx", np.random.rand(5, 3))
+
+        assert my_params_1 != my_params_2
+
+
+class TestPrint:
+    """Series of tests related to print() on the instance."""
+
+    def test_print(self, input_dict):
+        """Test that print can be called on an instance."""
+        my_params = FunParams(**input_dict)
+
+        out = my_params.__str__()
+
+        assert isinstance(out, str)
+
+    def test_print_empty(self):
+        """Test that print can be called on an empty instance."""
+        my_params = FunParams()
+
+        out = my_params.__str__()
+
+        assert out == str(None)
+
+    def test_print_long_description(self, declared_parameter):
+        """Test print on an instance with long description."""
+        my_params = FunParams(
+            description=_random_string(150),
+            declared_parameters=[declared_parameter],
+        )
+
+        out = my_params.__str__()
+
+        assert isinstance(out, str)
+
+    def test_print_arbitrary(self):
+        """Test printing instance with arbitrary parameter value."""
+        # Create an instance
+        my_params = FunParams()
+        my_params.add(keyword="param", value=[120, 30])
+
+        out = my_params.__str__()
+
+        assert isinstance(out, str)
+
+    def test_print_array(self):
+        """Test printing instance with array parameter value."""
+        # Create an instance
+        my_params = FunParams()
+        my_params.add(keyword="param", value=np.random.rand(10, 3))
+
+        out = my_params.__str__()
+
+        assert isinstance(out, str)
+
+
+class TestAddParameter:
+    """Series of tests for adding parameters."""
+
+    def test_full(self, declared_parameter):
+        """Test that adding parameters with full parameter specification."""
+        # Create an empty instance
+        my_params = FunParams()
+
+        # Add parameter
+        my_params.add(**declared_parameter)
+
+        # Assertion
+        assert len(my_params) == 1
+
+    def test_repeated(self, declared_parameter):
+        """Test that adding parameter multiple times raises an exception."""
+        # Create an empty instance
+        my_params = FunParams()
+
+        # Add parameter
+        my_params.add(**declared_parameter)
+
+        # Assertion
+        with pytest.raises(KeyError):
+            my_params.add(**declared_parameter)
+
+    def test_invalid_keyword(self, declared_parameter_invalid):
+        """Test that adding a parameter w/ invalid keyword raises an exception.
+
+        Notes
+        -----
+        - A keyword must be a valid Python identifier.
+        """
+        # Create an empty instance
+        my_params = FunParams()
+
+        # Assertion
+        with pytest.raises(ValueError):
+            my_params.add(**declared_parameter_invalid)
+
+    def test_invalid_type(self):
+        """Test that adding a parameter must be consistent with the type."""
+        # Create an empty instance
+        my_params = FunParams()
+
+        # Assertion
+        with pytest.raises(TypeError):
+            my_params.add(keyword="param", value="10", type=float)
+
+
+class TestAssignParameter:
+    """Series of tests for assigning parameters after creation."""
+
+    def test_no_type(self):
+        """Test assigning a parameter with no type."""
+        # Create an instance
+        my_params = FunParams()
+        my_params.add(keyword="param", value=20)
+
+        assert my_params["param"] == 20
+
+        # Modify the value
+        my_params["param"] = "20"
+
+        assert my_params["param"] == "20"
+
+    def test_with_type(self):
+        """Test assigning a parameter with provided type."""
+        # Create an instance
+        my_params = FunParams()
+        my_params.add(keyword="param", value=20, type=int)
+
+        assert my_params["param"] == 20
+
+        # Modify the value
+        my_params["param"] = 30
+
+        assert my_params["param"] == 30
+
+    def test_invalid_type(self):
+        """Test assigning a parameter inconsistent with type"""
+        # Create an instance
+        my_params = FunParams()
+        my_params.add(keyword="param", value=20, type=int)
+
+        assert my_params["param"] == 20
+
+        with pytest.warns(UserWarning):
+            my_params["param"] = 20.0
+
+    def test_undeclared(self):
+        """Test assigning undeclared parameter."""
+        # Create an empty instance
+        my_params = FunParams()
+
+        # Assertion
+        with pytest.raises(KeyError):
+            my_params["param"] = 10
+
+
+def test_reset_value():
+    """Test resetting the value of an instance."""
+    # Create an instance
+    my_params = FunParams()
+    my_params.add(keyword="param", value=10)
+
+    # Change the value
+    my_params["param"] = 20
+
+    assert my_params["param"] != 10
+
+    my_params.reset_value()
+
+    assert my_params["param"] == 10
+
+
+def test_as_dict(declared_parameter):
+    """Test as dictionary method."""
+    my_params = FunParams()
+    my_params.add(**declared_parameter)
+
+    my_params_dict = my_params.as_dict()
+
+    key = declared_parameter["keyword"]
+    assert my_params_dict[key] == declared_parameter["value"]
+
+
+def _random_string(length):
+    """Create a random string of a given length."""
+    letters = string.ascii_letters
+    result_str = "".join(random.choice(letters) for _ in range(length))
+
+    return result_str
diff --git a/tests/core/test_input_spec.py b/tests/core/test_input_spec.py
deleted file mode 100644
index 609705b..0000000
--- a/tests/core/test_input_spec.py
+++ /dev/null
@@ -1,94 +0,0 @@
-"""
-Test module for the prob. input specification class.
-"""
-from typing import List
-
-from uqtestfuns.core.prob_input.input_spec import (
-    UnivDistSpec,
-    ProbInputSpecFixDim,
-    ProbInputSpecVarDim,
-)
-
-
-def test_marginalspec():
-    """Test the creation of MarginalSpec NamedTuple."""
-
-    name = "T0"
-    distribution = "uniform"
-    parameters = [340.0, 360.0]
-    description = "Filling gas temperature"
-
-    # Create a MarginalSpec
-    my_marginalspec = UnivDistSpec(
-        name=name,
-        distribution=distribution,
-        parameters=parameters,
-        description=description,
-    )
-
-    # Assertions
-    assert my_marginalspec.name == name
-    assert my_marginalspec.distribution == distribution
-    assert my_marginalspec.parameters == parameters
-    assert my_marginalspec.description == description
-
-
-def test_probinputspec_list():
-    """Test the creation of ProbInputSpec NamedTuple w/ list of marginals."""
-
-    # Create a list of marginals
-    marginals = _create_marginals(10)
-
-    # Create a ProbInputSpec
-    name = "Some input"
-    description = "Probabilistic input model from somewhere"
-    copulas = None
-    my_probinputspec = ProbInputSpecFixDim(
-        name=name,
-        description=description,
-        marginals=marginals,
-        copulas=copulas,
-    )
-
-    # Assertions
-    assert my_probinputspec.name == name
-    assert my_probinputspec.description == description
-    assert my_probinputspec.copulas == copulas
-    assert my_probinputspec.marginals == marginals
-
-
-def test_probinputspec_vardim():
-    """Test the creation of ProbInputSpec w/ a callable as marginal."""
-
-    # Create a list of marginals
-    marginals_gen = _create_marginals
-
-    # Create a ProbInputSpec
-    name = "Some input"
-    description = "Probabilistic input model from somewhere"
-    copulas = None
-    my_probinputspec = ProbInputSpecVarDim(
-        name=name,
-        description=description,
-        marginals_generator=marginals_gen,
-        copulas=copulas,
-    )
-
-    # Assertions
-    assert my_probinputspec.name == name
-    assert my_probinputspec.description == description
-    assert my_probinputspec.copulas == copulas
-    assert my_probinputspec.marginals_generator == marginals_gen
-
-
-def _create_marginals(spatial_dimension: int) -> List[UnivDistSpec]:
-    """Create a list of test marginals."""
-    return [
-        UnivDistSpec(
-            name=f"x{i + 1}",
-            distribution="uniform",
-            parameters=[0.0, 1.0],
-            description="None",
-        )
-        for i in range(spatial_dimension)
-    ]
diff --git a/tests/core/test_uqtestfun.py b/tests/core/test_uqtestfun.py
index db8b75a..3b929af 100644
--- a/tests/core/test_uqtestfun.py
+++ b/tests/core/test_uqtestfun.py
@@ -1,9 +1,10 @@
 """
 Test module for UQTestFun class, a generic class for generic UQ test function.
 """
+
 import pytest
 
-from uqtestfuns import UQTestFun, ProbInput
+from uqtestfuns import UQTestFun, ProbInput, FunParams
 from conftest import assert_call, create_random_marginals
 
 
@@ -15,20 +16,29 @@ def uqtestfun():
     def evaluate(x, p):
         return p * (x + 1)
 
-    parameters = 10
+    parameters = FunParams(
+        declared_parameters=[
+            {
+                "keyword": "p",
+                "value": 10.0,
+                "type": None,
+                "description": None,
+            }
+        ]
+    )
 
     my_args = {
         "evaluate": evaluate,
         "prob_input": ProbInput(input_marginals),
         "parameters": parameters,
-        "name": "Test function",
+        "function_id": "TestFunction",
     }
 
     uqtestfun_instance = UQTestFun(
         evaluate=evaluate,
         prob_input=ProbInput(input_marginals),
         parameters=parameters,
-        name="Test function",
+        function_id="TestFunction",
     )
 
     return uqtestfun_instance, my_args
@@ -40,12 +50,12 @@ def test_create_instance(uqtestfun):
     uqtestfun_instance, uqtestfun_dict = uqtestfun
 
     # Assertions
-    assert uqtestfun_instance.name == uqtestfun_dict["name"]
+    assert uqtestfun_instance.function_id == uqtestfun_dict["function_id"]
     # The original evaluate function is stored in a hidden attribute
     assert uqtestfun_instance._evaluate == uqtestfun_dict["evaluate"]
     assert (
-        uqtestfun_instance.spatial_dimension
-        == uqtestfun_dict["prob_input"].spatial_dimension
+        uqtestfun_instance.input_dimension
+        == uqtestfun_dict["prob_input"].input_dimension
     )
     assert uqtestfun_instance.parameters == uqtestfun_dict["parameters"]
 
@@ -65,8 +75,10 @@ def test_str(uqtestfun):
     uqtestfun_instance, _ = uqtestfun
 
     str_ref = (
-        f"Name              : {uqtestfun_instance.name}\n"
-        f"Spatial dimension : {uqtestfun_instance.spatial_dimension}"
+        f"Function ID      : {uqtestfun_instance.function_id}\n"
+        f"Input Dimension  : {uqtestfun_instance.input_dimension}\n"
+        f"Output Dimension : {uqtestfun_instance.output_dimension}\n"
+        f"Parameterized    : {bool(uqtestfun_instance.parameters)}"
     )
 
     assert uqtestfun_instance.__str__() == str_ref
diff --git a/tests/metafun/test_uqmetaspec.py b/tests/metafun/test_uqmetaspec.py
index 786fff5..685140c 100644
--- a/tests/metafun/test_uqmetaspec.py
+++ b/tests/metafun/test_uqmetaspec.py
@@ -1,6 +1,7 @@
 """
 Test module for instances of meta and random test function specs.
 """
+
 import itertools
 import numpy as np
 import pytest
@@ -18,25 +19,25 @@ def uqtestfunspec():
 
     # TODO: This is not a very good test because inputs should have been failed
 
-    spatial_dimension = random.randint(1, 25)
+    input_dimension = random.randint(1, 25)
 
     basis_functions = {0: lambda x: x, 1: lambda x: x**2}
     num_basis = len(basis_functions)
 
-    selected_basis = tuple(np.random.randint(0, num_basis, spatial_dimension))
+    selected_basis = tuple(np.random.randint(0, num_basis, input_dimension))
 
     effects_tuples = {}
     effects_coeffs = {}
-    for i in range(1, spatial_dimension + 1):
+    for i in range(1, input_dimension + 1):
         effects_tuples[i] = tuple(
-            itertools.combinations(np.arange(spatial_dimension), i)
+            itertools.combinations(np.arange(input_dimension), i)
         )
         effects_coeffs[i] = np.random.rand(len(effects_tuples[i]))
 
     inputs = [{"name": "X1", "distribution": "uniform", "parameters": [0, 1]}]
 
     my_args = {
-        "spatial_dimension": spatial_dimension,
+        "input_dimension": input_dimension,
         "basis_functions": basis_functions,
         "selected_basis": selected_basis,
         "effects_tuples": effects_tuples,
@@ -45,7 +46,7 @@ def uqtestfunspec():
     }
 
     uqtestfunspec_instance = UQTestFunSpec(
-        spatial_dimension=spatial_dimension,
+        input_dimension=input_dimension,
         basis_functions=basis_functions,
         selected_basis=selected_basis,
         effects_tuples=effects_tuples,
@@ -63,8 +64,8 @@ def test_create_instance(uqtestfunspec):
 
     # Assertions
     assert (
-        uqtestfunspec_instance.spatial_dimension
-        == uqtestfunspec_dict["spatial_dimension"]
+        uqtestfunspec_instance.input_dimension
+        == uqtestfunspec_dict["input_dimension"]
     )
     assert (
         uqtestfunspec_instance.basis_functions
@@ -90,7 +91,7 @@ def test_create_instance_uqmetafunspec():
     """Test creating an instance of UQMetaFunSpec."""
 
     # Create an instance
-    spatial_dimension = 3
+    input_dimension = 3
 
     basis_functions = {0: lambda x: x, 1: lambda x: x**2}
 
@@ -103,15 +104,15 @@ def test_create_instance_uqmetafunspec():
     }
 
     effects_ref = {
-        1: int(comb(spatial_dimension, 1)),
+        1: int(comb(input_dimension, 1)),
         2: 1,
     }
 
-    input_marginals = create_random_marginals(spatial_dimension)
+    input_marginals = create_random_marginals(input_dimension)
     coeffs_generator = np.random.rand
 
     metafun_spec = UQMetaFunSpec(
-        spatial_dimension,
+        input_dimension,
         basis_functions,
         effects_dict,
         input_marginals,
@@ -119,52 +120,52 @@ def test_create_instance_uqmetafunspec():
     )
 
     # Assertions
-    assert metafun_spec.spatial_dimension == spatial_dimension
+    assert metafun_spec.input_dimension == input_dimension
     assert metafun_spec.basis_functions == basis_functions
     assert metafun_spec.effects == effects_ref
     assert metafun_spec.coeffs_generator == coeffs_generator
 
 
-def _create_args_effects_dict(spatial_dimension):
+def _create_args_effects_dict(input_dimension):
     """Create a dictionary of effects-length specification."""
     effects_dict = dict()
 
-    for i in range(1, spatial_dimension + 1):
-        max_num = int(comb(spatial_dimension, i))
+    for i in range(1, input_dimension + 1):
+        max_num = int(comb(input_dimension, i))
         effects_dict[i] = np.random.randint(1, max_num + 1)
 
     return effects_dict
 
 
-def _create_effects_tuples_coeffs(spatial_dimension, coeffs_generator):
+def _create_effects_tuples_coeffs(input_dimension, coeffs_generator):
     """Create a dictionary of effect tuples and coefficients specifications."""
     effects_tuples = {}
     effects_coeffs = {}
-    for i in range(1, spatial_dimension + 1):
+    for i in range(1, input_dimension + 1):
         effects_tuples[i] = tuple(
-            itertools.combinations(np.arange(spatial_dimension), i)
+            itertools.combinations(np.arange(input_dimension), i)
         )
         effects_coeffs[i] = coeffs_generator(len(effects_tuples[i]))
 
     return effects_tuples, effects_coeffs
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 10])
-def test_get_sample_uqmetafunspec(spatial_dimension):
+@pytest.mark.parametrize("input_dimension", [1, 2, 10])
+def test_get_sample_uqmetafunspec(input_dimension):
     """Test getting a sample of function specifications realizations."""
 
     # Create an instance
-    spatial_dimension = 1
+    input_dimension = 1
 
     basis_functions = {0: lambda x: x}
 
-    effects_dict = _create_args_effects_dict(spatial_dimension)
+    effects_dict = _create_args_effects_dict(input_dimension)
 
-    inputs = create_random_marginals(spatial_dimension)
+    inputs = create_random_marginals(input_dimension)
     coeffs_generator = np.random.rand
 
     metafun_spec = UQMetaFunSpec(
-        spatial_dimension,
+        input_dimension,
         basis_functions,
         effects_dict,
         inputs,
@@ -183,12 +184,12 @@ def test_get_sample_uqmetafunspec(spatial_dimension):
 
     selected_basis = testfun_spec.selected_basis
     effects_tuples, effects_coeffs = _create_effects_tuples_coeffs(
-        spatial_dimension, coeffs_generator
+        input_dimension, coeffs_generator
     )
 
     # Comparison with test function specification created separately
     testfun_spec_ref = UQTestFunSpec(
-        spatial_dimension,
+        input_dimension,
         basis_functions,
         selected_basis,
         effects_tuples,
@@ -197,7 +198,7 @@ def test_get_sample_uqmetafunspec(spatial_dimension):
     )
 
     # Assertions
-    assert testfun_spec.spatial_dimension == testfun_spec_ref.spatial_dimension
+    assert testfun_spec.input_dimension == testfun_spec_ref.input_dimension
     # TODO: Introduce input equality
     # assert testfun_spec.inputs == testfun_spec_ref.inputs
     assert testfun_spec.selected_basis == testfun_spec_ref.selected_basis
diff --git a/tests/metafun/test_uqmetatestfun.py b/tests/metafun/test_uqmetatestfun.py
index e0d87eb..4b51b53 100644
--- a/tests/metafun/test_uqmetatestfun.py
+++ b/tests/metafun/test_uqmetatestfun.py
@@ -1,18 +1,19 @@
 """
 Test module for instances of UQMetaTestFun (meta).
 """
+
 import numpy as np
 import pytest
 
 from scipy.special import comb
 
-from uqtestfuns import UQMetaTestFun, UQTestFun, UQMetaFunSpec, UnivDist
+from uqtestfuns import UQMetaTestFun, UQTestFun, UQMetaFunSpec, Marginal
 from uqtestfuns.meta.metaspec import UQTestFunSpec
 from uqtestfuns.meta.basis_functions import BASIS_BY_ID
 from conftest import create_random_marginals, assert_call
 
 
-def _create_args_effects_dict(spatial_dimension):
+def _create_args_effects_dict(input_dimension):
     """Create a dictionary of effects-length specification.
 
     Notes
@@ -22,19 +23,19 @@ def _create_args_effects_dict(spatial_dimension):
     """
     effects_dict = dict()
 
-    for i in range(1, spatial_dimension + 1):
-        max_num = int(comb(spatial_dimension, i))
+    for i in range(1, input_dimension + 1):
+        max_num = int(comb(input_dimension, i))
         effects_dict[i] = np.random.randint(1, max_num + 1)
 
     return effects_dict
 
 
-def _create_reference_evaluate(xx: np.ndarray, parameters: UQTestFunSpec):
+def _create_reference_evaluate(xx: np.ndarray, spec: UQTestFunSpec):
     """Evaluate a test function realization with alternative way."""
-    basis_funs = parameters.basis_functions
-    selected_basis = parameters.selected_basis
-    effects_tuples = parameters.effects_tuples
-    effects_coeffs = parameters.effects_coeffs
+    basis_funs = spec.basis_functions
+    selected_basis = spec.selected_basis
+    effects_tuples = spec.effects_tuples
+    effects_coeffs = spec.effects_coeffs
 
     yy = np.zeros(xx.shape[0])
 
@@ -54,18 +55,18 @@ def _create_reference_evaluate(xx: np.ndarray, parameters: UQTestFunSpec):
 def uqmetafunspec(request):
     """Create an instance of UQMetaFunSpec."""
 
-    spatial_dimension = request.param
+    input_dimension = request.param
 
     basis_functions = {0: lambda x: x, 1: lambda x: x**2}
 
-    effects_dict = _create_args_effects_dict(spatial_dimension)
+    effects_dict = _create_args_effects_dict(input_dimension)
 
-    inputs = create_random_marginals(spatial_dimension)
+    inputs = create_random_marginals(input_dimension)
 
     coeffs_generator = np.random.rand
 
     my_args = {
-        "spatial_dimension": spatial_dimension,
+        "input_dimension": input_dimension,
         "basis_functions": basis_functions,
         "effects_dict": effects_dict,
         "inputs": inputs,
@@ -73,7 +74,7 @@ def uqmetafunspec(request):
     }
 
     my_metafun_spec = UQMetaFunSpec(
-        spatial_dimension,
+        input_dimension,
         basis_functions,
         effects_dict,
         inputs,
@@ -93,8 +94,7 @@ def test_create_instance(uqmetafunspec):
 
     # Assertions
     assert (
-        my_metafun.metafun_spec.spatial_dimension
-        == my_args["spatial_dimension"]
+        my_metafun.metafun_spec.input_dimension == my_args["input_dimension"]
     )
     assert (
         my_metafun.metafun_spec.basis_functions == my_args["basis_functions"]
@@ -110,24 +110,24 @@ def test_create_instance(uqmetafunspec):
         )
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 3, 4, 5, 10])
+@pytest.mark.parametrize("input_dimension", [1, 2, 3, 4, 5, 10])
 @pytest.mark.parametrize("input_id", [None, 0, 1, 2, 3, 4, 5, 6, 7])
-def test_create_instance_default(spatial_dimension, input_id):
+def test_create_instance_default(input_dimension, input_id):
     """Test for creating a default instance."""
 
     # Create an instance from default
-    my_metafun = UQMetaTestFun.from_default(spatial_dimension, input_id)
+    my_metafun = UQMetaTestFun.from_default(input_dimension, input_id)
 
-    if spatial_dimension == 1:
+    if input_dimension == 1:
         effects_keys_ref = [1]
-    elif spatial_dimension in [2, 3, 4]:
+    elif input_dimension in [2, 3, 4]:
         effects_keys_ref = [1, 2]
     else:
         effects_keys_ref = [1, 2, 3]
 
     # Assertions
     assert isinstance(my_metafun.metafun_spec, UQMetaFunSpec)
-    assert my_metafun.metafun_spec.spatial_dimension == spatial_dimension
+    assert my_metafun.metafun_spec.input_dimension == input_dimension
     assert effects_keys_ref == list(my_metafun.metafun_spec.effects)
     assert_call(my_metafun.metafun_spec.coeffs_generator, 10)
 
@@ -139,13 +139,13 @@ def test_create_instance_default_ranges():
     my_metafun = UQMetaTestFun.from_default(np.arange(1, 11))
 
     # Assertions
-    assert 1 <= my_metafun.metafun_spec.spatial_dimension < 11
+    assert 1 <= my_metafun.metafun_spec.input_dimension < 11
 
-    spatial_dimension = my_metafun.metafun_spec.spatial_dimension
-    print(spatial_dimension)
-    if spatial_dimension == 1:
+    input_dimension = my_metafun.metafun_spec.input_dimension
+    print(input_dimension)
+    if input_dimension == 1:
         effects_keys_ref = [1]
-    elif spatial_dimension in [2, 3, 4]:
+    elif input_dimension in [2, 3, 4]:
         effects_keys_ref = [1, 2]
     else:
         effects_keys_ref = [1, 2, 3]
@@ -160,12 +160,12 @@ def test_create_instance_default_zero_dimension():
         UQMetaTestFun.from_default(0)
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 5, 10, 100])
-def test_get_sample(spatial_dimension):
+@pytest.mark.parametrize("input_dimension", [1, 2, 5, 10, 100])
+def test_get_sample(input_dimension):
     """Test for getting a sample of UQTestFuns from the meta."""
 
     # Create an instance from default
-    my_metafun = UQMetaTestFun.from_default(spatial_dimension)
+    my_metafun = UQMetaTestFun.from_default(input_dimension)
 
     # Get sample 0
     my_testfun = my_metafun.get_sample(0)
@@ -176,9 +176,9 @@ def test_get_sample(spatial_dimension):
     my_testfun = my_metafun.get_sample(1)
     # Assertion
     assert isinstance(my_testfun, UQTestFun)
-    assert my_testfun.spatial_dimension == spatial_dimension
-    assert my_testfun.prob_input.spatial_dimension == spatial_dimension
-    assert isinstance(my_testfun.parameters, UQTestFunSpec)
+    assert my_testfun.input_dimension == input_dimension
+    assert my_testfun.prob_input.input_dimension == input_dimension
+    assert isinstance(my_testfun.parameters["spec"], UQTestFunSpec)
     assert_call(my_testfun, my_testfun.prob_input.get_sample(100))
 
     # Get sample > 1
@@ -190,23 +190,23 @@ def test_get_sample(spatial_dimension):
         assert isinstance(my_testfuns[i], UQTestFun)
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 3, 4, 5])
-def test_evaluate_sample(spatial_dimension):
+@pytest.mark.parametrize("input_dimension", [1, 2, 3, 4, 5])
+def test_evaluate_sample(input_dimension):
     """Test for evaluating a sample of test function from the meta."""
 
     basis_functions = BASIS_BY_ID
 
-    effects_dict = _create_args_effects_dict(spatial_dimension)
+    effects_dict = _create_args_effects_dict(input_dimension)
 
     input_marginals = [
-        UnivDist(distribution="uniform", parameters=[0, 1]),
+        Marginal(distribution="uniform", parameters=[0, 1]),
     ]
 
     coeffs_generator = np.random.rand
 
     # Create an instance from default
     my_metafun_spec = UQMetaFunSpec(
-        spatial_dimension,
+        input_dimension,
         basis_functions,
         effects_dict,
         input_marginals,
@@ -224,7 +224,7 @@ def test_evaluate_sample(spatial_dimension):
     xx = my_testfun.prob_input.get_sample(sample_size)
     yy = my_testfun(xx)
 
-    yy_ref = _create_reference_evaluate(xx, my_testfun.parameters)
+    yy_ref = _create_reference_evaluate(xx, **my_testfun.parameters.as_dict())
 
     # Assertion
     assert np.allclose(yy, yy_ref)
diff --git a/tests/test_list_functions.py b/tests/test_list_functions.py
index 0c54d0a..9513247 100644
--- a/tests/test_list_functions.py
+++ b/tests/test_list_functions.py
@@ -1,6 +1,7 @@
 """
 The test module for the high-level function 'list_functions()'
 """
+
 import pytest
 
 from conftest import assert_call
@@ -14,31 +15,89 @@ def test_default_call():
     assert_call(list_functions)
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 2, 10, "M"])
-@pytest.mark.parametrize("tag", SUPPORTED_TAGS)
-@pytest.mark.parametrize("tabulate", [True, False, None])
-def test_call_valid_arguments(spatial_dimension, tag, tabulate):
-    """Test function call with valid arguments."""
-    assert_call(list_functions, spatial_dimension, tag, tabulate)
+class TestValidArgument:
+    """A series of tests for calling list_functions() with valid arguments."""
+
+    @pytest.mark.parametrize("input_dimension", [1, 2, 10, "M", None])
+    def test_input_dimension(self, input_dimension):
+        """Test function call with 'input_dimension' argument."""
+        assert_call(list_functions, input_dimension=input_dimension)
+
+    @pytest.mark.parametrize("tag", SUPPORTED_TAGS)
+    def test_tag(self, tag):
+        """Test function call with 'tag' argument."""
+        assert_call(list_functions, tag=tag)
+
+    @pytest.mark.parametrize("output_dimension", [1, 2, 3, None])
+    def test_output_dimension(self, output_dimension):
+        """Test function call with 'tag' argument."""
+        assert_call(list_functions, output_dimension=output_dimension)
+
+    @pytest.mark.parametrize("parameterized", [True, False, None])
+    def test_parameterized(self, parameterized):
+        """Test function call with 'tag' argument."""
+        assert_call(list_functions, parameterized=parameterized)
+
+    @pytest.mark.parametrize("tabulate", [True, False, None])
+    def test_tabulate(self, tabulate):
+        """Test function call with 'tag' argument."""
+        assert_call(list_functions, tabulate=tabulate)
+
+
+class TestInvalidValueArgument:
+    """A series of tests for calling list_functions() w/ invalid value arg."""
 
+    @pytest.mark.parametrize("input_dimension", [-1, -2, -3, "a"])
+    def test_input_dimension(self, input_dimension):
+        """Test function call with invalid value for 'input_dimension'."""
+        with pytest.raises(ValueError):
+            list_functions(input_dimension=input_dimension)
 
-@pytest.mark.parametrize("spatial_dimension", [1, -2, -3, "a"])
-@pytest.mark.parametrize("tag", ["hello", "world"])
-def test_call_invalid_value_arguments(spatial_dimension, tag):
-    """Test function call with invalid argument values."""
+    @pytest.mark.parametrize("tag", ["hello", "world"])
+    def test_tag(self, tag):
+        """Test function call with invalid value for 'tag'."""
+        with pytest.raises(ValueError):
+            list_functions(tag=tag)
 
-    with pytest.raises(ValueError):
-        list_functions(spatial_dimension, tag)
+    @pytest.mark.parametrize("output_dimension", [-1, -2, -3])
+    def test_output_dimension(self, output_dimension):
+        """Test function call with invalid value for 'output_dimension'."""
+        with pytest.raises(ValueError):
+            list_functions(output_dimension=output_dimension)
 
 
-@pytest.mark.parametrize("spatial_dimension", [1, 1.0, [2], True])
-@pytest.mark.parametrize("tag", ["reliability", 1.0, 5.0, False])
-@pytest.mark.parametrize("tabulate", [1.0, 100, "100"])
-def test_call_invalid_type_arguments(spatial_dimension, tag, tabulate):
-    """Test function call with invalid argument types."""
+class TestInvalidTypeArgument:
+    """A series of tests for calling list_functions() w/ invalid type arg."""
 
-    with pytest.raises(TypeError):
-        list_functions(spatial_dimension, tag, tabulate)  # noqa
+    @pytest.mark.parametrize("input_dimension", [1.0, [2]])
+    def test_input_dimension(self, input_dimension):
+        """Test function call with invalid type for 'input_dimension'."""
+        with pytest.raises(TypeError):
+            list_functions(input_dimension=input_dimension)
+
+    @pytest.mark.parametrize("tag", [1.0, 5.0, False])
+    def test_tag(self, tag):
+        """Test function call with invalid type for 'tag'."""
+        with pytest.raises(TypeError):
+            list_functions(tag=tag)
+
+    @pytest.mark.parametrize("output_dimension", ["a", [2]])
+    def test_output_dimension(self, output_dimension):
+        """Test function call with invalid type for 'output_dimension'."""
+        with pytest.raises(TypeError):
+            list_functions(output_dimension=output_dimension)
+
+    @pytest.mark.parametrize("parameterized", ["a", 1, 2])
+    def test_parameterized(self, parameterized):
+        """Test function call with invalid type for 'parameterized'."""
+        with pytest.raises(TypeError):
+            list_functions(parameterized=parameterized)
+
+    @pytest.mark.parametrize("tabulate", [1.0, 100, "100"])
+    def test_tabulate(self, tabulate):
+        """Test function call with invalid type for 'tabulate'."""
+        with pytest.raises(TypeError):
+            list_functions(tabulate=tabulate)
 
 
 def test_untabulated_call():
@@ -56,3 +115,12 @@ def test_untabulated_call():
     assert len(my_classes_from_list) == len(my_classes_ref)
     for my_class in my_classes_from_list:
         assert my_class in list(my_classes_ref.values())
+
+
+def test_tablefmt_html():
+    """Test function call with 'html' as tablefmt."""
+
+    table = list_functions(tablefmt="html")
+
+    # Assertion
+    assert isinstance(table, str)