From 268780f66acd492ffa64035929293d2340c44a20 Mon Sep 17 00:00:00 2001 From: Damar Wicaksono Date: Wed, 6 Nov 2024 17:51:44 +0100 Subject: [PATCH] `output_dimension` is now property of UQTestFunBareABC Introduce the `output_dimension` property for UQ test functions and update related tests and documentations. Furthermore, the high-level function `list_functions()` is updated to display additional information related to output dimension and parameterization in grid format. This commit should resolve Issue #345. --- CHANGELOG.md | 6 + README.md | 68 ++-- docs/fundamentals/integration.md | 2 +- docs/fundamentals/metamodeling.md | 2 +- docs/fundamentals/optimization.md | 2 +- docs/fundamentals/reliability.md | 2 +- docs/fundamentals/sensitivity.md | 2 +- docs/test-functions/available.md | 5 +- src/uqtestfuns/core/uqtestfun_abc.py | 32 +- src/uqtestfuns/helpers.py | 352 +++++++++++------- .../test_test_functions.py | 9 +- .../core/prob_input/test_univariate_input.py | 23 +- tests/core/test_uqtestfun.py | 7 +- tests/test_list_functions.py | 117 ++++-- 14 files changed, 410 insertions(+), 219 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34b4a57..cda84f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `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 @@ -23,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- `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`. diff --git a/README.md b/README.md index 0f477f7..438bb21 100644 --- a/README.md +++ b/README.md @@ -33,17 +33,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 +59,35 @@ 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 +Output Dimension : 1 +Parameterized : False +Description : Borehole function from Harper and Gupta (1983) ``` 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 : +Name : Borehole-Harper-1983 +Input Dimension : 8 +Description : Probabilistic input model of the Borehole model from + Harper and Gupta (1983). +Marginals : - No. Name Distribution Parameters Description - + 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 : None ``` A sample of input values can be generated from the input model: diff --git a/docs/fundamentals/integration.md b/docs/fundamentals/integration.md index 743914e..4b62a28 100644 --- a/docs/fundamentals/integration.md +++ b/docs/fundamentals/integration.md @@ -35,5 +35,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 13b6b20..240435a 100644 --- a/docs/fundamentals/metamodeling.md +++ b/docs/fundamentals/metamodeling.md @@ -58,5 +58,5 @@ using the ``tag`` parameter: import uqtestfuns as uqtf -uqtf.list_functions(tag="metamodeling") +uqtf.list_functions(tag="metamodeling", tablefmt="html") ``` diff --git a/docs/fundamentals/optimization.md b/docs/fundamentals/optimization.md index 7eb478a..f5950a3 100644 --- a/docs/fundamentals/optimization.md +++ b/docs/fundamentals/optimization.md @@ -31,5 +31,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/reliability.md b/docs/fundamentals/reliability.md index bf3cdee..269fe60 100644 --- a/docs/fundamentals/reliability.md +++ b/docs/fundamentals/reliability.md @@ -39,7 +39,7 @@ using the ``tag`` parameter: import uqtestfuns as uqtf -uqtf.list_functions(tag="reliability") +uqtf.list_functions(tag="reliability", tablefmt="html") ``` ## About reliability analysis diff --git a/docs/fundamentals/sensitivity.md b/docs/fundamentals/sensitivity.md index ded2250..3b01a35 100644 --- a/docs/fundamentals/sensitivity.md +++ b/docs/fundamentals/sensitivity.md @@ -45,5 +45,5 @@ using the ``tag`` parameter: import uqtestfuns as uqtf -uqtf.list_functions(tag="sensitivity") +uqtf.list_functions(tag="sensitivity", tablefmt="html") ``` diff --git a/docs/test-functions/available.md b/docs/test-functions/available.md index 9ca1e3c..aa3bed4 100644 --- a/docs/test-functions/available.md +++ b/docs/test-functions/available.md @@ -69,12 +69,13 @@ regardless of their typical applications. | {ref}`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/src/uqtestfuns/core/uqtestfun_abc.py b/src/uqtestfuns/core/uqtestfun_abc.py index 705bda1..2779307 100644 --- a/src/uqtestfuns/core/uqtestfun_abc.py +++ b/src/uqtestfuns/core/uqtestfun_abc.py @@ -64,6 +64,7 @@ def __init__( self.prob_input = prob_input self._parameters = parameters self._function_id = function_id + self._output_dimension: Optional[int] = None @property def prob_input(self) -> ProbInput: @@ -105,6 +106,21 @@ def input_dimension(self) -> int: """The input dimension of the UQ test function.""" return self.prob_input.input_dimension + @property + 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, xx: np.ndarray, @@ -151,9 +167,10 @@ def transform_sample( def __str__(self): out = ( - f"Function ID : {self.function_id}\n" - f"Input Dimension : {self.input_dimension}\n" - f"Parameterized : {bool(self.parameters)}" + 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 @@ -380,10 +397,11 @@ def description(cls) -> Optional[str]: def __str__(self): out = ( - f"Function ID : {self.function_id}\n" - f"Input Dimension : {self.input_dimension}\n" - f"Parameterized : {bool(self.parameters)}\n" - f"Description : {self.description}" + 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)}\n" + f"Description : {self.description}" ) return out diff --git a/src/uqtestfuns/helpers.py b/src/uqtestfuns/helpers.py index 4b452d4..8116cc0 100644 --- a/src/uqtestfuns/helpers.py +++ b/src/uqtestfuns/helpers.py @@ -17,11 +17,43 @@ __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( 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 @@ -34,9 +66,16 @@ def list_functions( 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 ------- @@ -52,125 +91,64 @@ def list_functions( """ # --- Parse input arguments - _verify_input_args(input_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 input dimension - if input_dimension: - available_classes_from_dimension = _get_functions_from_dimension( - available_classes_dict, input_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", - "Input Dim.", - "Parameterized", - "Application", - "Description", - ] - else: - header_names = [ - "No.", - "Constructor", - "Input Dim.", - "Parameterized", - "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_input_dimension: - default_input_dimension = "M" - else: - default_input_dimension = available_class.default_input_dimension - - description = available_class.description - is_parameterized = ( - True if available_class.available_parameters else False - ) + # --- Get the arguments for tabulate + print_attribs, header_names, colalign, maxcolwidth = _get_table_formatting( + input_dimension, + tag, + output_dimension, + parameterized, + ) - if tag is None: - tags = ", ".join(available_class.tags) - value = [ - idx + 1, - f"{available_class_name}()", - f"{default_input_dimension}", - f"{is_parameterized}", - tags, - f"{description}", - ] - else: - value = [ - idx + 1, - f"{available_class_name}()", - f"{default_input_dimension}", - f"{is_parameterized}", - 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", - "center", - "left", - ), + tablefmt=tablefmt, + colalign=colalign, + maxcolwidths=maxcolwidth, ) + return table else: table = tbl( values, headers=header_names, - stralign="center", - colalign=("center", "center", "center", "center", "left"), + tablefmt=tablefmt, + colalign=colalign, + maxcolwidths=maxcolwidth, ) - - print(table) + print(table) return None @@ -178,6 +156,8 @@ def list_functions( def _verify_input_args( 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. @@ -185,30 +165,33 @@ def _verify_input_args( Parameters ---------- input_dimension : Optional[Union[str, int]] - The number of input dimension to filter the list. + 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 ``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 ``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 'input_dimension' @@ -241,7 +224,26 @@ 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))): @@ -250,42 +252,124 @@ def _verify_input_args( ) -def _get_functions_from_dimension( - available_classes: dict, - input_dimension: Union[int, str], -) -> List[str]: - """Get the function keys that satisfy the input 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(input_dimension, str): - input_dimension = input_dimension.lower() + return data - for ( - available_class_name, - available_class_path, - ) in available_classes.items(): - default_input_dimension = available_class_path.default_input_dimension - if not default_input_dimension: - default_input_dimension = "m" - if default_input_dimension == input_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 + + +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"]} - for ( - available_class_name, - available_class_path, - ) in available_classes.items(): - tags = available_class_path.tags + return data - if tag in tags: - values.append(available_class_name) + +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 + default_input_dimension = class_path.default_input_dimension + if default_input_dimension is None: + default_input_dimension = "M" + else: + default_input_dimension = instance.input_dimension + + data[available_class] = { + "constructor": available_class + "()", + "input_dim": default_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/tests/builtin_test_functions/test_test_functions.py b/tests/builtin_test_functions/test_test_functions.py index 733aa2c..a2fa9d7 100644 --- a/tests/builtin_test_functions/test_test_functions.py +++ b/tests/builtin_test_functions/test_test_functions.py @@ -155,10 +155,11 @@ def test_str(builtin_testfun): my_fun = builtin_testfun() str_ref = ( - f"Function ID : {my_fun.function_id}\n" - f"Input Dimension : {my_fun.input_dimension}\n" - f"Parameterized : {bool(my_fun.parameters)}\n" - f"Description : {my_fun.description}" + f"Function ID : {my_fun.function_id}\n" + f"Input Dimension : {my_fun.input_dimension}\n" + f"Output Dimension : {my_fun.output_dimension}\n" + f"Parameterized : {bool(my_fun.parameters)}\n" + f"Description : {my_fun.description}" ) assert my_fun.__str__() == str_ref diff --git a/tests/core/prob_input/test_univariate_input.py b/tests/core/prob_input/test_univariate_input.py index f0abcf5..b7a51d4 100644 --- a/tests/core/prob_input/test_univariate_input.py +++ b/tests/core/prob_input/test_univariate_input.py @@ -21,32 +21,37 @@ def univariate_input( request: Any, ) -> 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.random.rand(1) + 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 = { diff --git a/tests/core/test_uqtestfun.py b/tests/core/test_uqtestfun.py index c6fe36d..5430d4f 100644 --- a/tests/core/test_uqtestfun.py +++ b/tests/core/test_uqtestfun.py @@ -68,9 +68,10 @@ def test_str(uqtestfun): uqtestfun_instance, _ = uqtestfun str_ref = ( - f"Function ID : {uqtestfun_instance.function_id}\n" - f"Input Dimension : {uqtestfun_instance.input_dimension}\n" - f"Parameterized : {bool(uqtestfun_instance.parameters)}" + 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/test_list_functions.py b/tests/test_list_functions.py index e9e194e..9513247 100644 --- a/tests/test_list_functions.py +++ b/tests/test_list_functions.py @@ -15,31 +15,89 @@ def test_default_call(): assert_call(list_functions) -@pytest.mark.parametrize("input_dimension", [1, 2, 10, "M"]) -@pytest.mark.parametrize("tag", SUPPORTED_TAGS) -@pytest.mark.parametrize("tabulate", [True, False, None]) -def test_call_valid_arguments(input_dimension, tag, tabulate): - """Test function call with valid arguments.""" - assert_call(list_functions, input_dimension, tag, tabulate) - - -@pytest.mark.parametrize("input_dimension", [1, -2, -3, "a"]) -@pytest.mark.parametrize("tag", ["hello", "world"]) -def test_call_invalid_value_arguments(input_dimension, tag): - """Test function call with invalid argument values.""" - - with pytest.raises(ValueError): - list_functions(input_dimension, tag) - - -@pytest.mark.parametrize("input_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(input_dimension, tag, tabulate): - """Test function call with invalid argument types.""" - - with pytest.raises(TypeError): - list_functions(input_dimension, tag, tabulate) # noqa +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("tag", ["hello", "world"]) + def test_tag(self, tag): + """Test function call with invalid value for 'tag'.""" + with pytest.raises(ValueError): + list_functions(tag=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) + + +class TestInvalidTypeArgument: + """A series of tests for calling list_functions() w/ invalid type arg.""" + + @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(): @@ -57,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)