diff --git a/docs/source/fileFormat.farnDict.md b/docs/source/fileFormat.farnDict.md index 8b72ca97..2eced3d9 100644 --- a/docs/source/fileFormat.farnDict.md +++ b/docs/source/fileFormat.farnDict.md @@ -19,12 +19,20 @@ A farnDict | _layers | dict | dict defining all layers. Each layer represents one nest level in the folder structure that will be generated by farn. | |  \ | dict | unique key defining a layer. It serves as basename for all case folders in the nest level corresponding with that layer. | |   _sampling | dict | dict defining sampling-type and -parameters of a layer | -|    _type | string | sampling type. Choices currently implemented are {'fixed', 'linSpace', 'uniformLhs', 'normalLhs', 'sobol', 'hilbertCurve'} | +|    _type | string | sampling type. Choices currently implemented are: | +| | | factorial: Sklearn full-factorial implementation, re-normailized to given ranges. Note that a factorial can also be achieved by cascaded linSpace layers, introducing additional hierarchy. | +| | | fixed: List of fixed values. | +| | | linSpace: Linear spacing of one dimension. Note that it places n points covering the outer limits as given in _ranges. | +| | | uniformLhs: Uniformly distributed latin-hypercube sampling. | +| | | normalLhs: Gaussian normal distributed latin-hypercube sampling. | +| | | sobol: Sobol sampling. Note that for a lower auto correlation, the parameter _onset can be given defining a later starting point in the sequence. | +| | | hilbertCurve: Hilbert multi-dimensional space-filling curve implementation. Note that the _numberOfSamples points are interpolated between start end end point. | |    _names | list[string] | list naming all variables / parameters being varied in this layer. For each variable / parameter named here, sampled values will be generated. | |    _values | list[list[*float]] | (required for sampling type 'fixed'): List containing lists of fixed values. For each parameter name defined in _names, one list of fixed values must exist, i.e. the number of lists in _values must match the number of parameter names defined in _names. The number of values can freely be chosen. However, all lists in _values must have the same number of values. | |    _ranges | list[list[float, float]] | (required for sampling types 'linSpace', 'uniformLhs' and 'hilbertCurve'): List containing ranges. A range is defined through the lower and upper boundary value for the related parameter name, given as tuple (minimum, maximum). For each parameter name defined in _names, one range tuple must exist. | |    _numberOfSamples | int | (required for sampling types 'linSpace', 'uniformLhs' and 'hilbertCurve'): Number of samples to be generated. In case of 'linSpace', boundary values are included if an odd number of samples is given. In case of 'uniformLHS', the given number of samples will be generated within range (=between lower and upper boundary), excluding the boundaries themselves. | -|    _includeBoundingBox | bool | (optional, for sampling type 'uniformLhs', 'sobol' and 'hilbertCurve'): Defines whether the lower and upper boundary values of each parameter name shall be added as additional samples. If missing, defaults to FALSE. | +|    _listOfSamples | list[int] | (required for sampling type 'factorial'): Number of samples to be generated per dimension. The total _numberOfSamples is calculated automatically. Note that each entry per dimension has to be larger or equal 2 for proper function of factorial. Note that the _includeBoundingBox parameter is obsolete and must not be given here. | +|    _includeBoundingBox | bool | (optional, for sampling type 'uniformLhs', 'sobol' and 'hilbertCurve'): Defines whether the lower and upper boundary values of each parameter name shall be added as additional samples. If missing, defaults to FALSE. Note that if invoked for sampling type 'hilbertCurve', the parameter adds two coinciding sample points: the starting and the end point of the distribution. | |    _iterationDepth | int | (optional, for sampling type 'hilbertCurve'): Defines the hilbert iteration depth, default: 10. | |   _condition | dict | (optional) a condition allows to define a filter expression to include or exclude specific samples. (see [Filtering of Cases](#filtering-of-cases)) | |    _filter | string | filter expression (see [Filter Expression](#filter-expression)) | diff --git a/pyproject.toml b/pyproject.toml index 53f3b284..bc9083f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ dependencies = [ "pillow>=12.1", "pyDOE3>=1.6.2", "psutil>=7.2", + "scikit-learn>=1.6", "hilbertcurve>=2.0", "dictIO>=0.4.4", "ospx>=0.3.5", @@ -119,7 +120,7 @@ required-environments = [ [project.scripts] -farn = "farn.cli.__main__:main" +farn = "farn.cli.farn:main" batchProcess = "farn.cli.batchProcess:main" diff --git a/src/farn/cli/__main__.py b/src/farn/cli/__main__.py old mode 100644 new mode 100755 index e3ea01da..aeda1501 --- a/src/farn/cli/__main__.py +++ b/src/farn/cli/__main__.py @@ -154,6 +154,7 @@ def main() -> None: # ..to console # NOTE: Default would usually be 'WARNING', but for farn it makes sense to set default level to 'INFO' log_level_console: str = "INFO" + ) if any([args.quiet, args.verbose]): log_level_console = "ERROR" if args.quiet else log_level_console log_level_console = "DEBUG" if args.verbose else log_level_console @@ -179,6 +180,8 @@ def main() -> None: # Check whether farn dict file exists if not farn_dict_file.is_file(): logger.error(f"farn: File {farn_dict_file} not found.") + # easter egg: Generate Barnsley fern + # _generate_barnsley_fern() return # Print the parsed commandline arguments for documentation and debugging purposes. @@ -225,10 +228,15 @@ def _generate_barnsley_fern() -> None: """ import tempfile # noqa: PLC0415 import tkinter as tk # noqa: PLC0415 + import tempfile + import tkinter as tk from numpy import random # noqa: PLC0415 from PIL import Image # noqa: PLC0415 from PIL.ImageDraw import ImageDraw # noqa: PLC0415 + from numpy import random + from PIL import Image + from PIL.ImageDraw import ImageDraw def t1(p: tuple[float, float]) -> tuple[float, float]: """1%.""" diff --git a/src/farn/cli/__main__.py.orig b/src/farn/cli/__main__.py.orig new file mode 100644 index 00000000..a22592e0 --- /dev/null +++ b/src/farn/cli/__main__.py.orig @@ -0,0 +1,329 @@ +#!/usr/bin/env python +"""farn command line interface.""" + +import argparse +import logging +import pprint +from importlib import metadata +from pathlib import Path + +from farn import run_farn +from farn.utils.logging import configure_logging + +logger = logging.getLogger(__name__) + + +def _get_version() -> str: + """Return the installed package version, or a safe fallback if unavailable.""" + try: + return metadata.version("farn") + except metadata.PackageNotFoundError: + # Fallback when package metadata is not available (e.g. running from source) + return "farn (version unknown)" + + +def _argparser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="farn", + usage="%(prog)s farn_dict_file [options [args]]", + epilog="_________________farn___________________", + prefix_chars="-", + add_help=True, + description=( + "Run the sampling for all layers as configured in farn dict file," + "generate the corresponding case folder structure and" + "execute user-defined shell command sets in all case folders." + ), + ) + + _ = parser.add_argument( + "farn_dict_file", + metavar="farn_dict_file", + type=str, + help="name of the dict file containing the farn configuration.", + ) + + _ = parser.add_argument( + "-s", + "--sample", + action="store_true", + help=( + "read farn dict file, run the sampling defined for each layer " + "and save the sampled farn dict file with prefix sampled." + ), + default=False, + required=False, + ) + + _ = parser.add_argument( + "-g", + "--generate", + action="store_true", + help="generate the folder structure that spawns all layers and cases defined in farn dict file", + default=False, + required=False, + ) + + _ = parser.add_argument( + "-e", + "--execute", + metavar="command", + action="store", + type=str, + help=( + "execute the given command set in all case folders.\n" + "The command set must be defined in the commands section of the applicable layer in farn dict file." + ), + default=None, + required=False, + ) + + _ = parser.add_argument( + "-b", + "--batch", + action="store_true", + help="Executes the given command set in batch mode, i.e. asynchronously", + default=False, + required=False, + ) + + _ = parser.add_argument( + "--test", + action="store_true", + help="Run only first case and exit. (note: --test is most useful in combination with --execute)", + default=False, + required=False, + ) + + console_verbosity = parser.add_mutually_exclusive_group(required=False) + + _ = console_verbosity.add_argument( + "-q", + "--quiet", + action="store_true", + help=("console output will be quiet."), + default=False, + ) + + _ = console_verbosity.add_argument( + "-v", + "--verbose", + action="store_true", + help=("console output will be verbose."), + default=False, + ) + + _ = parser.add_argument( + "--log", + action="store", + type=str, + help="name of log file. If specified, this will activate logging to file.", + default=None, + required=False, + ) + + _ = parser.add_argument( + "--log-level", + action="store", + type=str, + help="log level applied to logging to file.", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + default="WARNING", + required=False, + ) + + _ = parser.add_argument( + "-V", + "--version", + action="version", + version=_get_version(), + ) + + return parser + + +def main() -> None: + """Entry point for console script as configured in pyproject.toml. + + Runs the command line interface and parses arguments and options entered on the console. + """ + parser = _argparser() + args = parser.parse_args() + + # Configure Logging + # ..to console +<<<<<<< HEAD:src/farn/cli/farn.py + log_level_console: str = ( + "INFO" # default would usually be 'WARNING', but for farn it makes sense to set default level to 'INFO' + ) +======= + # NOTE: Default would usually be 'WARNING', but for farn it makes sense to set default level to 'INFO' + log_level_console: str = "INFO" +>>>>>>> origin/main:src/farn/cli/__main__.py + if any([args.quiet, args.verbose]): + log_level_console = "ERROR" if args.quiet else log_level_console + log_level_console = "DEBUG" if args.verbose else log_level_console + # ..to file + log_file: Path | None = Path(args.log) if args.log else None + log_level_file: str = args.log_level + configure_logging(log_level_console, log_file, log_level_file) + + farn_dict_file: Path = Path(args.farn_dict_file) + sample: bool = args.sample + generate: bool = args.generate + command: str | None = args.execute + batch: bool = args.batch + test: bool = args.test + + # Ensure that at least one of the arguments {sample, generate, command} is given. + # (minimum one of them is required) + # Drop an error if not. + if not (sample or generate or command): + parser.print_help() + logger.error("farn: none of the required options given: '--sample' or '--generate' or '--execute'") + + # Check whether farn dict file exists + if not farn_dict_file.is_file(): + logger.error(f"farn: File {farn_dict_file} not found.") + return + + # Print the parsed commandline arguments for documentation and debugging purposes. + # The arguments will be split into one argument per line, if possible. + # If extracting a mapping from `args` fails, fall back to its string representation. + _indent: str = " " * 13 + try: + _arg_mapping = vars(args) + except TypeError: + _arg_mapping = {"args": str(args)} + _formatted_args = pprint.pformat(_arg_mapping, sort_dicts=True) + _indented_args = "\n".join(f"{_indent}{line}" for line in _formatted_args.splitlines()) + logger.info( + "Start farn.py with following arguments:\n%s\n", + _indented_args, + ) + + # Invoke API + _ = run_farn( + farn_dict_file=farn_dict_file, + sample=sample, + generate=generate, + command=command, + batch=batch, + test=test, + ) + + +def _generate_barnsley_fern() -> None: + """ + easter egg: Barnsley fern. + + Barnsley Fern: + ┌ ┐ ┌ ┐ ┌ ┐ + | a b | | x | | e | + ƒ(x,y) = | | | | + | | + | c d | | y | | f | + └ ┘ └ ┘ └ ┘ + w a b c d e f p Portion generated + ƒ1 0 0 0 0.16 0 0 0.01 Stem + ƒ2 0.85 0.04 -0.04 0.85 0 1.60 0.85 Successively smaller leaflets + ƒ3 0.20 -0.26 0.23 0.22 0 1.60 0.07 Largest left-hand leaflet + ƒ4 -0.15 0.28 0.26 0.24 0 0.44 0.07 Largest right-hand leaflet + """ + import tempfile # noqa: PLC0415 + import tkinter as tk # noqa: PLC0415 + + from numpy import random # noqa: PLC0415 + from PIL import Image # noqa: PLC0415 + from PIL.ImageDraw import ImageDraw # noqa: PLC0415 + + def t1(p: tuple[float, float]) -> tuple[float, float]: + """1%.""" + return (0.0, 0.16 * p[1]) + + def t2(p: tuple[float, float]) -> tuple[float, float]: + """85%.""" + return (0.85 * p[0] + 0.04 * p[1], -0.04 * p[0] + 0.85 * p[1] + 1.6) + + def t3(p: tuple[float, float]) -> tuple[float, float]: + """7%.""" + return (0.2 * p[0] - 0.26 * p[1], 0.23 * p[0] + 0.22 * p[1] + 1.6) + + def t4(p: tuple[float, float]) -> tuple[float, float]: + """7%.""" + return (-0.15 * p[0] + 0.28 * p[1], 0.26 * p[0] + 0.24 * p[1] + 0.44) + + x_size = 1024 + y_size = 1024 + im = Image.new("RGBA", (x_size, x_size)) + draw = ImageDraw(im) + + p: tuple[float, float] = (0, 0) + end = 20000 + ii = 0 + scale = 100 + x_offset = 512 + + rng = random.default_rng() + rnd = rng.random() + rnd2 = rng.normal(1, 0) + e = 1 + s = 0 + rnd3 = ( + rng.normal(e, s), + rng.normal(e, s), + rng.normal(e, s), + ) + while ii < end: + rnd = rng.random() + rnd2 = rng.normal(1, 0) + if ii % 1 == 0: + rnd3 = ( + rng.normal(e, s), + rng.normal(e, s), + rng.normal(e, s), + ) + rgb = [148, 204, 48] + if rnd <= (0.01 * rnd2): + p = t1(p) + elif rnd > (0.01 * rnd2) and rnd <= (0.86 * rnd2): + p = t2(p) + elif rnd > (0.86 * rnd2) and rnd <= (0.93 * rnd2): + p = t3(p) + else: + p = t4(p) + draw.point( + (p[0] * scale + x_offset, p[1] * scale), + fill=( + int(rgb[0] * rnd3[0]), + int(rgb[1] * rnd3[1]), + int(rgb[2] * rnd3[2]), + ), + ) + + ii += 1 + + del draw + + with tempfile.TemporaryDirectory() as temp_dir: + temp_file = Path(temp_dir) / "splash.png" + im.save(temp_file) + + root = tk.Tk() + root.overrideredirect(boolean=True) + screen_width = root.winfo_screenwidth() + screen_height = root.winfo_screenheight() + root.geometry( + newGeometry=f"{x_size}x{y_size}+{int(screen_width / 2 - x_size / 2)}+{int(screen_height / 2 - y_size / 2)}" + ) + image = tk.PhotoImage(file=temp_file) + canvas = tk.Canvas(root, height=y_size, width=x_size, bg="dark slate gray") + _ = canvas.create_image(x_size / 2, y_size / 2, image=image) # pyright: ignore[reportUnknownMemberType] + canvas.pack() + _ = root.after(3000, root.destroy) + root.mainloop() + + return + + +if __name__ == "__main__": + main() diff --git a/src/farn/core/case.py b/src/farn/core/case.py index 1a9f9899..bb1fd8fe 100644 --- a/src/farn/core/case.py +++ b/src/farn/core/case.py @@ -289,7 +289,7 @@ def add_parameters( """Manually add extra parameters.""" _cases: list[Case] = deepcopy(self) for case in _cases: - case.add_parameters(parameters) + _ = case.add_parameters(parameters) return diff --git a/src/farn/core/case.py.orig b/src/farn/core/case.py.orig new file mode 100644 index 00000000..94e93c90 --- /dev/null +++ b/src/farn/core/case.py.orig @@ -0,0 +1,827 @@ +<<<<<<< HEAD +# pyright: reportUnknownMemberType=false +import logging +import re +from copy import deepcopy +from enum import IntEnum +from pathlib import Path +from typing import ( + Any, + Dict, + List, + MutableMapping, + MutableSequence, + Sequence, + Set, + Union, +) + +import numpy as np +from dictIO.utils.path import relative_path +from numpy import ndarray +from pandas import DataFrame, Series + +from farn.core import Parameter + +__ALL__ = [ + "CaseStatus", + "Case", + "Cases", +] + +logger = logging.getLogger(__name__) + + +class CaseStatus(IntEnum): + """Enumeration class allowing an algorithm that processes cases, i.e. a simulator or case processor, + to indicate the state a case iscurrently in. + """ + + NONE = 0 + FAILURE = 1 + PREPARED = 10 + RUNNING = 20 + SUCCESS = 30 + + +class Case: + """Dataclass holding case attributes. + + Case holds all relevant attributes needed by farn to process cases, e.g. + - condition + - parameter names and associated values + - commands + - .. + """ + + def __init__( + self, + case: str = "", + layer: str = "", + level: int = 0, + no_of_samples: int = 0, + index: int = 0, + path: Union[Path, None] = None, + is_leaf: bool = False, + condition: Union[MutableMapping[str, str], None] = None, + parameters: Union[MutableSequence[Parameter], None] = None, + command_sets: Union[MutableMapping[str, List[str]], None] = None, + ): + self.case: Union[str, None] = case + self.layer: Union[str, None] = layer + self.level: int = level + self.no_of_samples: int = no_of_samples + self.index: int = index + self.path: Path = path or Path.cwd() + self.is_leaf: bool = is_leaf + self.condition: MutableMapping[str, str] = condition or {} + self.parameters: MutableSequence[Parameter] = parameters or [] + self.command_sets: MutableMapping[str, List[str]] = command_sets or {} + self.status: CaseStatus = CaseStatus.NONE + + @property + def is_valid(self) -> bool: + """Evaluates whether the case matches the configured filter expression. + + A case is considered valid if it fulfils the filter citeria configured in farnDict for the respective layer. + + Returns + ------- + bool + result of validity check. True indicates the case is valid, False not valid. + """ + + # Check whether the '_condition' element is defined. Without it, case is in any case considered valid. + if not self.condition: + return True + + # Check whether filter expression is defined. + # If filter expression is missing, condition cannot be evaluated but case is, by default, still considered valid. + filter_expression = ( + self.condition["_filter"] if "_filter" in self.condition else None + ) + if not filter_expression: + logger.warning( + f"Layer {self.layer}: _condition element found but no _filter element defined therein. " + f"As the filter expression is missing, the condition cannot be evalued. Case {self.case} is hence considered valid. " + ) + return True + + # Check whether optional argument '_action' is defined. Use default action, if not. + action = self.condition["_action"] if "_action" in self.condition else None + if not action: + logger.warning( + f"Layer {self.layer}: No _action defined in _condition element. Default action 'exclude' is used. " + ) + action = "exclude" + + # Check for formal errors that lead to invalidity + if not self.parameters: + logger.warning( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is invalid: " + f"A filter expression {filter_expression} is defined, but no parameters exist. " + ) + return False + for parameter in self.parameters: + if not parameter.name: + logger.warning( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is invalid: " + f"A filter expression {filter_expression} is defined, " + f"but at least one parameter name is missing. " + ) + return False + if not parameter.value: + logger.warning( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is invalid: " + f"A filter expression {filter_expression} is defined and parameter names exist, " + f"but parameter values are missing. " + f"Parameter name: {parameter.name} " + f"Parameter value: None " + ) + return False + + # transfer a white list of case properties to locals() for subsequent filtering + available_vars: Set[str] = set() + for attribute in dir(self): + try: + if attribute in [ + "case", + "layer", + "level", + "index", + "path" "is_leaf", + "no_of_samples", + "condition", + "command_sets", + ]: + locals()[attribute] = eval(f"self.{attribute}") + available_vars.add(attribute) + except Exception: + logger.exception( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is invalid: " + f"Reading case property '{attribute}' failed." + ) + return False + + # Read all parameter names and their associated values defined in current case, and assign them to local in-memory variables + for parameter in self.parameters: + if parameter.name and not re.match("^_", parameter.name): + try: + exec(f"{parameter.name} = {parameter.value}") + available_vars.add(parameter.name) + except Exception: + logger.exception( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is invalid: " + f"Reading parameter {parameter.name} with value {parameter.value} failed. " + ) + return False + + logger.debug( + f"Layer {self.layer}, available filter variables in current scope: {'{'+', '.join(available_vars)+'}'}" + ) + + # Evaluate filter expression + filter_expression_evaluates_to_true = False + try: + filter_expression_evaluates_to_true = eval(filter_expression) + except Exception: + # In case evaluation of the filter expression fails, processing will not stop. + # However, a warning will be logged and the respective case will be considered valid. + logger.warning( + f"Layer {self.layer}, case {self.case} evaluation of the filter expression failed:\n" + f"\tOne or more of the variables used in the filter expression are not defined or not accessible in the current layer.\n" + f"\t\tLayer: {self.layer}\n" + f"\t\tLevel: {self.level}\n" + f"\t\tCase: {self.case}\n" + f"\t\tFilter expression: {filter_expression}\n" + f"\t\tParameter names: {[parameter.name for parameter in self.parameters]}\n" + f"\t\tParameter values: {[parameter.value for parameter in self.parameters]} " + ) + + # Finally: Determine case validity based on filter expression and action + if action == "exclude": + if filter_expression_evaluates_to_true: + logger.debug( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is invalid:\n" + f"\tThe filter expression '{filter_expression}' evaluated to True.\n" + f"\tAction '{action}' performed. Case {self.case} excluded." + ) + return False + return True + if action == "include": + if filter_expression_evaluates_to_true: + logger.debug( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is valid:\n" + f"\tThe filter expression '{filter_expression}' evaluated to True.\n" + f"\tAction '{action}' performed. Case {self.case} included." + ) + return True + return False + + return True + + def add_parameters( + self, + parameters: Union[ + MutableSequence[Parameter], MutableMapping[str, str], None + ] = None, + ): + """Add extra parameters manually.""" + if isinstance(parameters, MutableSequence) and isinstance( + parameters[0], Parameter + ): + self.parameters.extend(parameters) + + elif isinstance(parameters, MutableMapping): + self.parameters.extend( + Parameter(parameter_name, parameter_value) + for parameter_name, parameter_value in parameters.items() + ) + + else: + logger.error( + f"Layer {self.layer}, case {self.case} add_parameters failed:\n" + f"\tWrong input data format for additional parameters.\n" + ) + exit(1) + + return True + + def to_dict(self) -> Dict[str, Any]: + """Return a dict with all case attributes. + + Returns + ------- + Dict[str, Any] + dict with all case attributes + """ + return { + "case": self.case, + "layer": self.layer, + "level": self.level, + "index": self.index, + "path": self.path, + "is_leaf": self.is_leaf, + "no_of_samples": self.no_of_samples, + "condition": self.condition, + "parameters": { + parameter.name: parameter.value for parameter in self.parameters or [] + }, + "commands": self.command_sets, + "status": self.status, + } + + def __str__(self): + return str(self.to_dict()) + + def __eq__(self, __o: object) -> bool: + return str(self) == str(__o) + + +class Cases(List[Case]): + """Container Class for Cases. + + Inherits from List[Case] and can hence be transparently used as a Python list type. + However, Cases extends its list base class by two convenience methods: + to_pandas() and to_numpy(), which turn the list of Case objects + into a pandas DataFrame or numpy ndarray, respectively. + """ + + def add_parameters( + self, + parameters: Union[ + MutableSequence[Parameter], MutableMapping[str, str], None + ] = None, + ): + """How can this run?.""" + _cases: List[Case] = deepcopy(self) + for case in _cases: + _ = case.add_parameters(parameters) + + return False + + def to_pandas( + self, + use_path_as_index: bool = True, + parameters_only: bool = False, + ) -> DataFrame: + """Return cases as a pandas Dataframe. + + Returns a DataFrame with case properties and case specific parameter values of all cases. + + Parameters + ---------- + use_path_as_index : bool, optional + turn path column into index column, by default True + parameters_only : bool, optional + reduce DataFrame to contain only the case's parameter values, by default False + + Returns + ------- + DataFrame + DataFrame with case properties and case specific parameter values of all cases. + """ + indices: List[int] = [] + + _cases: List[Case] = deepcopy(self) + for _index, case in enumerate(_cases): + indices.append(_index) + case.path = relative_path(Path.cwd(), case.path) + if case.parameters: + for parameter in case.parameters: + if not parameter.name: + parameter.name = "NA" + + series: Dict[str, Series] = { # pyright: ignore + "case": Series(data=None, dtype=np.dtype(str), name="case"), + "path": Series(data=None, dtype=np.dtype(str), name="path"), + } + + for _index, case in enumerate(_cases): + if case.case: + series["case"].loc[_index] = case.case # pyright: ignore + series["path"].loc[_index] = str(case.path) # pyright: ignore + if case.parameters: + for parameter in case.parameters: + if parameter.name not in series: + series[parameter.name] = Series( + data=None, + dtype=parameter.dtype, # pyright: ignore + name=parameter.name, + ) + if parameter.value is not None: + series[parameter.name].loc[ + _index + ] = parameter.value # pyright: ignore + + if parameters_only: + _ = series.pop("case") + if not use_path_as_index: + _ = series.pop("path") + + df_X = DataFrame(data=series) # noqa: N806 + + if use_path_as_index: + df_X.set_index("path", inplace=True) + + return df_X + + def to_numpy(self) -> ndarray[Any, Any]: + """Return parameter values of all cases as a 2-dimensional numpy array. + + Returns + ------- + ndarray[Any, Any] + 2-dimensional numpy array with case specific parameter values of all cases. + """ + df_X: DataFrame = self.to_pandas(parameters_only=True) # noqa: N806 + array: ndarray[Any, Any] = df_X.to_numpy() + return array + + def filter( + self, + levels: Union[int, Sequence[int]] = -1, + valid_only: bool = True, + ) -> "Cases": + """Return a sub-set of cases according to the passed in selection criteria. + + Parameters + ---------- + levels : Union[int, Sequence[int]], optional + return all cases of a distinct level, or a sequence of levels. + level=-1 returns the last level (the leaf cases), by default -1 + valid_only : bool, optional + return only valid cases, i.e cases which pass a filter expression + defined for the case's layer, by default True + + Returns + ------- + Cases + Cases object containing all cases that match the selection criteria. + """ + _levels: List[int] = [levels] if isinstance(levels, int) else list(levels) + filtered_cases: List[Case] + filtered_cases = [ + case + for case in self + if case.level in _levels or (case.is_leaf and -1 in _levels) + ] + + if valid_only: + filtered_cases = [case for case in filtered_cases if case.is_valid] + + return Cases(filtered_cases) +======= +# pyright: reportUnknownMemberType=false +# ruff: noqa: N806 +from __future__ import annotations + +import logging +import re +import sys +from collections.abc import MutableMapping, MutableSequence, Sequence +from copy import deepcopy +from enum import IntEnum +from pathlib import Path +from typing import ( + Any, +) + +import numpy as np +from dictIO.utils.path import relative_path +from numpy import ndarray +from pandas import DataFrame, Series + +from farn.core.parameter import Parameter + +__all__ = [ + "Case", + "CaseStatus", + "Cases", +] + +logger = logging.getLogger(__name__) + + +class CaseStatus(IntEnum): + """Enumeration class allowing an algorithm that processes cases, i.e. a simulator or case processor, + to indicate the state a case iscurrently in. + """ + + NONE = 0 + FAILURE = 1 + PREPARED = 10 + RUNNING = 20 + SUCCESS = 30 + + +class Case: + """Dataclass holding case attributes. + + Case holds all relevant attributes needed by farn to process cases, e.g. + - condition + - parameter names and associated values + - commands + - .. + """ + + def __init__( # noqa: PLR0913 + self, + case: str = "", + layer: str = "", + level: int = 0, + no_of_samples: int = 0, + index: int = 0, + path: Path | None = None, + *, + is_leaf: bool = False, + condition: MutableMapping[str, str] | None = None, + parameters: MutableSequence[Parameter] | None = None, + command_sets: MutableMapping[str, list[str]] | None = None, + ) -> None: + self.case: str | None = case + self.layer: str | None = layer + self.level: int = level + self.no_of_samples: int = no_of_samples + self.index: int = index + self.path: Path = path or Path.cwd() + self.is_leaf: bool = is_leaf + self.condition: MutableMapping[str, str] = condition or {} + self.parameters: MutableSequence[Parameter] = parameters or [] + self.command_sets: MutableMapping[str, list[str]] = command_sets or {} + self.status: CaseStatus = CaseStatus.NONE + + @property + def is_valid(self) -> bool: + """Evaluates whether the case matches the configured filter expression. + + A case is considered valid if it fulfils the filter citeria + configured in the farn dict file for the respective layer. + + Returns + ------- + bool + result of validity check. True indicates the case is valid, False not valid. + """ + # Check whether the '_condition' element is defined. Without it, case is in any case considered valid. + if not self.condition: + return True + + # Check whether filter expression is defined. + # If filter expression is missing, condition cannot be evaluated but case is, by default, + # still considered valid. + filter_expression = self.condition.get("_filter", None) + if not filter_expression: + logger.warning( + f"Layer {self.layer}: _condition element found but no _filter element defined therein. " + f"As the filter expression is missing, the condition cannot be evalued. " + f"Case {self.case} is hence considered valid." + ) + return True + + # Check whether optional argument '_action' is defined. Use default action, if not. + action = self.condition.get("_action", None) + if not action: + logger.warning( + f"Layer {self.layer}: No _action defined in _condition element. Default action 'exclude' is used. " + ) + action = "exclude" + + # Check for formal errors that lead to invalidity + if not self.parameters: + logger.warning( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is invalid: " + f"A filter expression {filter_expression} is defined, but no parameters exist. " + ) + return False + for parameter in self.parameters: + if not parameter.name: + logger.warning( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is invalid: " + f"A filter expression {filter_expression} is defined, " + f"but at least one parameter name is missing. " + ) + return False + if not parameter.value: + logger.warning( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is invalid: " + f"A filter expression {filter_expression} is defined and parameter names exist, " + f"but parameter values are missing. " + f"Parameter name: {parameter.name} " + f"Parameter value: None " + ) + return False + + # transfer a white list of case properties to frame.f_locals for subsequent filtering + available_vars: set[str] = set() + for attribute in dir(self): + try: + if attribute in [ + "case", + "layer", + "level", + "index", + "pathis_leaf", + "no_of_samples", + "condition", + "command_sets", + ]: + sys._getframe().f_locals[attribute] = eval(f"self.{attribute}") # noqa: S307, SLF001 # type: ignore[reportPrivateUsage] + available_vars.add(attribute) + except Exception: + logger.exception( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is invalid: " + f"Reading case property '{attribute}' failed." + ) + return False + + # Read all parameter names and their associated values defined in current case + # and assign them to local in-memory variables. + for parameter in self.parameters: + if parameter.name and not re.match("^_", parameter.name): + try: + sys._getframe().f_locals[parameter.name] = parameter.value # noqa: SLF001 # type: ignore[reportPrivateUsage] + available_vars.add(parameter.name) + except Exception: + logger.exception( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is invalid: " + f"Reading parameter {parameter.name} with value {parameter.value} failed. " + ) + return False + + logger.debug( + f"Layer {self.layer}, available filter variables in current scope: {'{' + ', '.join(available_vars) + '}'}" + ) + + # Evaluate filter expression + filter_expression_evaluates_to_true = False + try: + filter_expression_evaluates_to_true = eval(filter_expression) # noqa: S307 + except Exception: # noqa: BLE001 + # In case evaluation of the filter expression fails, processing will not stop. + # However, a warning will be logged and the respective case will be considered valid. + logger.warning( + f"Layer {self.layer}, case {self.case} evaluation of the filter expression failed:\n" + f"\tOne or more of the variables used in the filter expression are not defined " + f"or not accessible in the current layer.\n" + f"\t\tLayer: {self.layer}\n" + f"\t\tLevel: {self.level}\n" + f"\t\tCase: {self.case}\n" + f"\t\tFilter expression: {filter_expression}\n" + f"\t\tParameter names: {[parameter.name for parameter in self.parameters]}\n" + f"\t\tParameter values: {[parameter.value for parameter in self.parameters]} " + ) + + # Finally: Determine case validity based on filter expression and action + if action == "exclude": + if filter_expression_evaluates_to_true: + logger.debug( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is invalid:\n" + f"\tThe filter expression '{filter_expression}' evaluated to True.\n" + f"\tAction '{action}' performed. Case {self.case} excluded." + ) + return False + return True + if action == "include": + if filter_expression_evaluates_to_true: + logger.debug( + f"Layer {self.layer}, case {self.case} validity check: case {self.case} is valid:\n" + f"\tThe filter expression '{filter_expression}' evaluated to True.\n" + f"\tAction '{action}' performed. Case {self.case} included." + ) + return True + return False + + return True + + def add_parameters( + self, + parameters: MutableSequence[Parameter] | MutableMapping[str, str] | None = None, + ) -> None: + """Manually add extra parameters.""" + if isinstance(parameters, MutableSequence): + self.parameters.extend(parameters) + + elif isinstance(parameters, MutableMapping): + self.parameters.extend( + Parameter(parameter_name, parameter_value) for parameter_name, parameter_value in parameters.items() + ) + + else: + logger.error( + f"Layer {self.layer}, case {self.case} add_parameters failed:\n" + f"\tWrong input data format for additional parameters.\n" + ) + + return + + def to_dict(self) -> dict[str, Any]: + """Return a dict with all case attributes. + + Returns + ------- + Dict[str, Any] + dict with all case attributes + """ + return { + "case": self.case, + "layer": self.layer, + "level": self.level, + "index": self.index, + "path": self.path, + "is_leaf": self.is_leaf, + "no_of_samples": self.no_of_samples, + "condition": self.condition, + "parameters": {parameter.name: parameter.value for parameter in self.parameters or []}, + "commands": self.command_sets, + "status": self.status, + } + + def __str__(self) -> str: + return str(self.to_dict()) + + def __eq__(self, other: object) -> bool: + return str(self) == str(other) + + def __hash__(self) -> int: + return hash(str(self)) + + +class Cases(list[Case]): + """Container Class for Cases. + + Inherits from List[Case] and can hence be transparently used as a Python list type. + However, Cases extends its list base class by two convenience methods: + to_pandas() and to_numpy(), which turn the list of Case objects + into a pandas DataFrame or numpy ndarray, respectively. + """ + + def add_parameters( + self, + parameters: MutableSequence[Parameter] | MutableMapping[str, str] | None = None, + ) -> None: + """Manually add extra parameters.""" + _cases: list[Case] = deepcopy(self) + for case in _cases: + case.add_parameters(parameters) + + return + + def to_pandas( + self, + *, + use_path_as_index: bool = True, + parameters_only: bool = False, + ) -> DataFrame: + """Return cases as a pandas Dataframe. + + Returns a DataFrame with case properties and case specific parameter values of all cases. + + Parameters + ---------- + use_path_as_index : bool, optional + turn path column into index column, by default True + parameters_only : bool, optional + reduce DataFrame to contain only the case's parameter values, by default False + + Returns + ------- + DataFrame + DataFrame with case properties and case specific parameter values of all cases. + """ + indices: list[int] = [] + + _cases: list[Case] = deepcopy(self) + for _index, case in enumerate(_cases): + indices.append(_index) + case.path = relative_path(Path.cwd(), case.path) + if case.parameters: + for parameter in case.parameters: + if not parameter.name: + parameter.name = "NA" + + series: dict[str, Series] = { + "case": Series(data=None, dtype=np.dtype(str), name="case"), + "path": Series(data=None, dtype=np.dtype(str), name="path"), + } + + for _index, case in enumerate(_cases): + # TODO @CLAROS: Check whether we can replace .loc[_index] with .iloc[_index] + # and .loc[_index] with .at[_index] + # CLAROS, 2024-10-24 + if case.case: + series["case"].loc[_index] = case.case # type: ignore[call-overload, reportCallIssue] + series["path"].loc[_index] = str(case.path) # type: ignore[call-overload, reportCallIssue] + if case.parameters: + for parameter in case.parameters: + if parameter.name not in series: + if parameter.dtype is not None: + series[parameter.name] = Series( + data=None, + dtype=parameter.dtype, + name=parameter.name, + ) + else: + series[parameter.name] = Series( + data=None, + name=parameter.name, + ) + + if parameter.value is not None: + series[parameter.name].loc[_index] = parameter.value # type: ignore[call-overload, reportCallIssue] + + if parameters_only: + _ = series.pop("case") + if not use_path_as_index: + _ = series.pop("path") + + df_X = DataFrame(data=series) + + if use_path_as_index: + df_X = df_X.set_index("path") + + return df_X + + def to_numpy(self) -> ndarray[tuple[int, int], np.dtype[np.float64]]: + """Return parameter values of all cases as a 2-dimensional numpy array. + + Returns + ------- + ndarray[tuple[int, int], np.dtype[np.float64 | np.int32]] + 2-dimensional numpy array with case specific parameter values of all cases. + """ + df_X: DataFrame = self.to_pandas(parameters_only=True) + array: ndarray[tuple[int, int], np.dtype[np.float64]] = df_X.to_numpy().astype(np.float64) + return array + + def filter( + self, + levels: int | Sequence[int] = -1, + *, + valid_only: bool = True, + ) -> Cases: + """Return a sub-set of cases according to the passed in selection criteria. + + Parameters + ---------- + levels : Union[int, Sequence[int]], optional + return all cases of a distinct level, or a sequence of levels. + level=-1 returns the last level (the leaf cases), by default -1 + valid_only : bool, optional + return only valid cases, i.e cases which pass a filter expression + defined for the case's layer, by default True + + Returns + ------- + Cases + Cases object containing all cases that match the selection criteria. + """ + _levels: list[int] = [levels] if isinstance(levels, int) else list(levels) + filtered_cases: list[Case] + filtered_cases = [case for case in self if case.level in _levels or (case.is_leaf and -1 in _levels)] + + if valid_only: + filtered_cases = [case for case in filtered_cases if case.is_valid] + + return Cases(filtered_cases) +>>>>>>> origin/main diff --git a/src/farn/farn.py b/src/farn/farn.py index c9ddf6c2..d267b49f 100644 --- a/src/farn/farn.py +++ b/src/farn/farn.py @@ -76,7 +76,6 @@ def run_farn( FileNotFoundError if farn_dict_file does not exist """ - # sourcery skip: extract-method # Make sure farn_dict_file argument is of type Path. If not, cast it to Path type. farn_dict_file = farn_dict_file if isinstance(farn_dict_file, Path) else Path(farn_dict_file) diff --git a/src/farn/sampling/discrete.py b/src/farn/sampling/discrete.py index cf5b4618..375ae9e3 100644 --- a/src/farn/sampling/discrete.py +++ b/src/farn/sampling/discrete.py @@ -38,6 +38,7 @@ def __init__( self.number_of_samples: int = 0 self.number_of_bb_samples: int = 0 self.leading_zeros: int = 0 + self.list_of_samples: list[int] = [] self.iteration_depth: int self.minIterationDepth: int @@ -47,6 +48,13 @@ def __init__( def _set_up_known_sampling_types(self) -> None: self.known_sampling_types = { + "factorial": { + "required_args": [ + "_names", + "_ranges", + "_listOfSamples", + ] + }, "fixed": { "required_args": [ "_names", @@ -92,16 +100,6 @@ def _set_up_known_sampling_types(self) -> None: "_includeBoundingBox", ], }, - "arbitrary": { - "required_args": [ - "_names", - "_ranges", - "_numberOfSamples", - "_distributionName", # uniform|normal|exp... - "_distributionParameters", # mu|sigma|skew|camber not applicable for uniform - "_includeBoundingBox", # required - ] - }, "hilbertCurve": { "required_args": [ "_names", @@ -119,6 +117,7 @@ def set_sampling_type(self, sampling_type: str) -> None: """Set the sampling type. Valid values: + "factorial" "fixed" "linSpace" "uniformLhs" @@ -193,40 +192,66 @@ def generate_samples(self) -> dict[str, list[Any]]: """ samples: dict[str, list[Any]] = {} - if self.sampling_type == "fixed": - samples = self._generate_samples_using_fixed_sampling() + if self.sampling_type == "factorial": + samples = self._generate_samples_using_factorial() + + elif self.sampling_type == "fixed": + samples = self._generate_samples_using_fixed() elif self.sampling_type == "linSpace": - samples = self._generate_samples_using_linspace_sampling() + samples = self._generate_samples_using_linspace() elif self.sampling_type == "uniformLhs": - samples = self._generate_samples_using_uniform_lhs_sampling() + samples = self._generate_samples_using_uniform_lhs() + + # elif self.sampling_type == "uniformRnd": + # samples = self._generate_samples_using_uniform_rnd() elif self.sampling_type == "normalLhs": - samples = self._generate_samples_using_normal_lhs_sampling() + samples = self._generate_samples_using_normal_lhs() - elif self.sampling_type == "sobol": - samples = self._generate_samples_using_sobol_sampling() + # elif self.sampling_type == "normalRnd": + # samples = self._generate_samples_using_normal_rnd() - elif self.sampling_type == "arbitrary": - samples = self._generate_samples_using_arbitrary_sampling() + elif self.sampling_type == "sobol": + samples = self._generate_samples_using_sobol() elif self.sampling_type == "hilbertCurve": - samples = self._generate_samples_using_hilbert_sampling() + samples = self._generate_samples_using_hilbert() else: raise NotImplementedError(f"{self.sampling_type} not implemented yet.") return samples - def _generate_samples_using_fixed_sampling(self) -> dict[str, list[Any]]: + def _generate_samples_using_factorial(self) -> Dict[str, List[Any]]: + _ = self._check_length_matches_number_of_names("_ranges") + self.list_of_samples = list(self.sampling_parameters["_listOfSamples"]) + if any(x <= 1 for x in self.list_of_samples): + logger.error( + f"Factorial does not work for dimensions populated with less than 2 values: {self.list_of_samples}" + ) + exit(1) + # to continue using _determine_number_of_samples, _numberOfSamples is generated + self.sampling_parameters["_numberOfSamples"] = int( + np.prod(self.list_of_samples) + ) + samples: Dict[str, List[Any]] = self._generate_samples_dict() + values: ndarray[Any, Any] = self._generate_values_using_factorial() + self._write_values_into_samples_dict(values, samples) + + return samples + + def _generate_samples_using_fixed(self) -> Dict[str, List[Any]]: _ = self._check_length_matches_number_of_names("_values") samples: dict[str, list[Any]] = {} msg: str # Assert that the values per parameter are provided as a list for item in self.sampling_parameters["_values"]: if not isinstance(item, Sequence): - msg = "_values: The values per parameter need to be provided as a list of values." + msg: str = ( + "_values: The values per parameter need to be provided as a list of values." + ) logger.error(msg) raise TypeError(msg) @@ -255,7 +280,7 @@ def _generate_samples_using_fixed_sampling(self) -> dict[str, list[Any]]: return samples - def _generate_samples_using_linspace_sampling(self) -> dict[str, list[Any]]: + def _generate_samples_using_linspace(self) -> Dict[str, List[Any]]: _ = self._check_length_matches_number_of_names("_ranges") samples: dict[str, list[Any]] = self._generate_samples_dict() self.minVals = [x[0] for x in self.ranges] @@ -272,7 +297,7 @@ def _generate_samples_using_linspace_sampling(self) -> dict[str, list[Any]]: return samples - def _generate_samples_using_uniform_lhs_sampling(self) -> dict[str, list[Any]]: + def _generate_samples_using_uniform_lhs(self) -> Dict[str, List[Any]]: _ = self._check_length_matches_number_of_names("_ranges") samples: dict[str, list[Any]] = self._generate_samples_dict() values: np.ndarray[Any, np.dtype[np.float64]] = self._generate_values_using_uniform_lhs_sampling() @@ -280,7 +305,7 @@ def _generate_samples_using_uniform_lhs_sampling(self) -> dict[str, list[Any]]: return samples - def _generate_samples_using_normal_lhs_sampling(self) -> dict[str, list[Any]]: + def _generate_samples_using_normal_lhs(self) -> Dict[str, List[Any]]: """LHS using gaussian normal distributions. required input arguments: @@ -318,7 +343,7 @@ def _generate_samples_using_normal_lhs_sampling(self) -> dict[str, list[Any]]: return samples - def _generate_samples_using_sobol_sampling(self) -> dict[str, list[Any]]: + def _generate_samples_using_sobol(self) -> Dict[str, List[Any]]: _ = self._check_length_matches_number_of_names("_ranges") self.onset = int(self.sampling_parameters["_onset"]) @@ -328,50 +353,14 @@ def _generate_samples_using_sobol_sampling(self) -> dict[str, list[Any]]: return samples - def _generate_samples_using_arbitrary_sampling(self) -> dict[str, list[Any]]: - """ - Purpose: To perform a sampling based on the pre-drawn sample. - Pre-requisite: - 1. Since the most fitted distribution is unknown, it shall be found by using the fitter module. - 2. fitter module provides: 1) the name of the most fitted distribution, 2) relavant parameters - 3. relavent parameters mostly comprises with 3 components: 1) skewness 2) location 3) scale - 4. At this moment, those prerequisites shall be provided as arguments. This could be modified later - 5. refer to commented example below. - """ - _ = self._check_length_matches_number_of_names("_ranges") - - samples: dict[str, list[Any]] = self._generate_samples_dict() - self.minVals = [x[0] for x in self.ranges] - self.maxVals = [x[1] for x in self.ranges] - - import scipy.stats # noqa: F401, PLC0415 - - distribution_name: Sequence[str] - distribution_parameters: Sequence[Any] - for index, _ in enumerate(self.fields): - distribution_name = self.sampling_parameters["_distributionName"] - distribution_parameters = self.sampling_parameters["_distributionParameters"] - - _eval_command = f"scipy.stats.{distribution_name[index]}" - dist = eval(_eval_command) # noqa: S307 - - samples[self.fields[index]] = dist.rvs( - *distribution_parameters[index], - size=self.number_of_samples, - ).tolist() - - # requires if self.kwargs['_includeBoundingBox'] is True: as well - - return samples - - def _generate_samples_using_hilbert_sampling(self) -> dict[str, list[Any]]: + def _generate_samples_using_hilbert(self) -> Dict[str, List[Any]]: _ = self._check_length_matches_number_of_names("_ranges") samples: dict[str, list[Any]] = self._generate_samples_dict() # Depending on implementation - self.minIterationDepth = 3 + self.minIterationDepth = 5 self.maxIterationDepth = 15 - - values: np.ndarray[Any, np.dtype[np.float64]] = self._generate_values_using_hilbert_sampling() + + values: ndarray[Any, Any] = self._generate_values_using_hilbert() self._write_values_into_samples_dict(values, samples) return samples @@ -382,7 +371,35 @@ def _generate_samples_dict(self) -> dict[str, list[Any]]: self._generate_case_names(samples_dict) return samples_dict - def _generate_values_using_uniform_lhs_sampling(self) -> np.ndarray[Any, np.dtype[np.float64]]: + def _generate_values_using_factorial(self) -> ndarray[Any, Any]: + """Full factorial, normalized and scaled to ranges.""" + from pyDOE3 import fullfact + from sklearn.preprocessing import normalize + + ff_distribution = fullfact(self.list_of_samples) + + _range_lower_bounds: ndarray[Any, Any] = np.array( + [range[0] for range in self.ranges] + ) + _range_upper_bounds: ndarray[Any, Any] = np.array( + [range[1] for range in self.ranges] + ) + loc: ndarray[Any, Any] = _range_lower_bounds + scale: ndarray[Any, Any] = _range_upper_bounds - _range_lower_bounds + values, norms = normalize( + ff_distribution.T, norm="l1", axis=1, return_norm=True + ) + sample_set: ndarray[Any, Any] = ( + values.T + * norms + / (self.list_of_samples - np.ones(len(self.list_of_samples))) + * scale + + loc + ) + + return sample_set + + def _generate_values_using_uniform_lhs(self) -> ndarray[Any, Any]: """Uniform LHS.""" from pyDOE3 import lhs # noqa: PLC0415 from scipy.stats import uniform # noqa: PLC0415 @@ -406,7 +423,7 @@ def _generate_values_using_uniform_lhs_sampling(self) -> np.ndarray[Any, np.dtyp return sample_set - def _generate_values_using_normal_lhs_sampling(self) -> np.ndarray[Any, np.dtype[np.float64]]: + def _generate_values_using_normal_lhs(self) -> ndarray[Any, Any]: """Gaussnormal LHS.""" from pyDOE3 import lhs # noqa: PLC0415 from scipy.stats import norm # noqa: PLC0415 @@ -436,10 +453,11 @@ def _generate_values_using_normal_lhs_sampling(self) -> np.ndarray[Any, np.dtype return sample_set - def _generate_values_using_sobol_sampling(self) -> np.ndarray[Any, np.dtype[np.float64]]: + def _generate_values_using_sobol(self) -> ndarray[Any, Any]: from scipy.stats import qmc # noqa: PLC0415 from scipy.stats.qmc import Sobol # noqa: PLC0415 - + #todo: check compare with sobol_seq + sobol_engine: Sobol = Sobol( d=self.number_of_fields, scramble=False, @@ -464,7 +482,7 @@ def _generate_values_using_sobol_sampling(self) -> np.ndarray[Any, np.dtype[np.f return sample_set - def _generate_values_using_hilbert_sampling(self) -> np.ndarray[Any, np.dtype[np.float64]]: + def _generate_values_using_hilbert(self) -> ndarray[Any, Any]: """Source hilbertcurve pypi pkg or numpy it showed that hilbertcurve is a better choice and more precise with a higher iteration depth (<=15) pypi pkg Decimals is required for proper function up to (<=15) @@ -493,21 +511,23 @@ def _generate_values_using_hilbert_sampling(self) -> np.ndarray[Any, np.dtype[np if "_iterationDepth" in self.sampling_parameters: if not isinstance(self.sampling_parameters["_iterationDepth"], int): - msg = f"_iterationDepth was not given as integer: {self.sampling_parameters['_iterationDepth']}." + msg: str = ( + f'_iterationDepth was not given as integer: {self.sampling_parameters["_iterationDepth"]}.' + ) logger.error(msg) raise ValueError(msg) if self.sampling_parameters["_iterationDepth"] > self.maxIterationDepth: - msg = ( - f"_iterationDepth {self.sampling_parameters['_iterationDepth']} " - f"given in farn dict file is beyond the limit of {self.maxIterationDepth}...\n" + msg: str = ( + f'_iterationDepth {self.sampling_parameters["_iterationDepth"]} given in farnDict is beyond the limit of {self.maxIterationDepth}...\n\t\tsetting to {self.maxIterationDepth}' + ) f"\t\tsetting to {self.maxIterationDepth}" ) logger.warning(msg) self.iteration_depth = self.maxIterationDepth elif self.sampling_parameters["_iterationDepth"] < self.minIterationDepth: - msg = ( - f"_iterationDepth {self.sampling_parameters['_iterationDepth']} " - f"given in farn dict file is below the limit of {self.minIterationDepth}...\n" + msg: str = ( + f'_iterationDepth {self.sampling_parameters["_iterationDepth"]} given in farnDict is below the limit of {self.minIterationDepth}...\n\t\tsetting to {self.minIterationDepth}' + ) f"\t\tsetting to {self.minIterationDepth}" ) logger.warning(msg) @@ -516,7 +536,18 @@ def _generate_values_using_hilbert_sampling(self) -> np.ndarray[Any, np.dtype[np self.iteration_depth = self.sampling_parameters["_iterationDepth"] else: self.iteration_depth = 10 - + msg: str = (f'_iterationDepth not given in farnDict...\n\tsetting to {self.iteration_depth}') + logger.warning(msg) + + corners: int = 2**self.number_of_fields + multiply_of_corners: int = number_of_continuous_samples % corners + if multiply_of_corners in [0, 1, corners-1]: + nearly: str = 'exactly ' if multiply_of_corners == 0 else 'nearly ' + msg: str = ( + f'The number of samples ({number_of_continuous_samples}) is {nearly}a multiple of the corners for a {self.number_of_fields}-dim tessereact ({corners}) and thus the sampling set might be correlated...\n\tconsider changing the number of samples.' + ) + logger.warning(msg) + hc = HilbertCurve(self.iteration_depth, self.number_of_fields, n_procs=0) # -1: all threads logger.info( @@ -542,7 +573,7 @@ def _generate_values_using_hilbert_sampling(self) -> np.ndarray[Any, np.dtype[np int_distribution: np.ndarray[Any, np.dtype[np.int32]] = np.trunc(distribution) hilbert_points: Iterable[Iterable[int]] = hc.points_from_distances(int_distribution) - + _points: list[Iterable[float]] = [] interpolation_hits = 0 for hpt, _dst, _idst in zip(hilbert_points, distribution, int_distribution, strict=False): @@ -582,7 +613,7 @@ def _generate_values_using_hilbert_sampling(self) -> np.ndarray[Any, np.dtype[np points.max(axis=0), reverse=True, ) - + # Upscale points from unit hypercube to bounds range_lower_bounds: np.ndarray[Any, np.dtype[np.float64]] = np.array([r[0] for r in self.ranges]) range_upper_bounds: np.ndarray[Any, np.dtype[np.float64]] = np.array([r[1] for r in self.ranges]) @@ -591,7 +622,7 @@ def _generate_values_using_hilbert_sampling(self) -> np.ndarray[Any, np.dtype[np range_lower_bounds, range_upper_bounds, ) - + return sample_set def _determine_number_of_samples(self) -> None: diff --git a/src/farn/sampling/discrete.py.orig b/src/farn/sampling/discrete.py.orig new file mode 100644 index 00000000..5b337652 --- /dev/null +++ b/src/farn/sampling/discrete.py.orig @@ -0,0 +1,875 @@ +import logging +import math +from collections.abc import Generator, Iterable, Mapping, Sequence +from typing import Any + +import numpy as np + +logger = logging.getLogger(__name__) + + +class DiscreteSampling: + """Class providing methods to run a discrete sampling of a specific layer, + i.e. of all variables defined in the given layer. + """ + + def __init__( + self, + seed: int | None = None, + ) -> None: + self.layer_name: str = "" + self.sampling_parameters: Mapping[str, Any] = {} + self.fields: list[str] = [] + self.values: list[Sequence[Any]] = [] + self.ranges: Sequence[Sequence[Any]] = [] + self.bounding_box: list[list[float]] = [] + self.minVals: list[Any] = [] + self.maxVals: list[Any] = [] + self.case_names: list[str] = [] + self.mean: Sequence[float] = [] + self.std: float | Sequence[float] | Sequence[Sequence[float]] = 0.0 + self.onset: int = 0 + + self.sampling_type: str = "" + self.known_sampling_types: dict[str, dict[str, list[str]]] = {} + self._set_up_known_sampling_types() + + self.number_of_fields: int = 0 + self.number_of_samples: int = 0 + self.number_of_bb_samples: int = 0 + self.leading_zeros: int = 0 + self.list_of_samples: list[int] = [] + + self.iteration_depth: int + self.minIterationDepth: int + self.maxIterationDepth: int + self.include_bounding_box: bool = False + self.seed: int | None = seed + + def _set_up_known_sampling_types(self) -> None: + self.known_sampling_types = { + "factorial": { + "required_args": [ + "_names", + "_ranges", + "_listOfSamples", + ] + }, + "fixed": { + "required_args": [ + "_names", + "_values", + ] + }, + "linSpace": { + "required_args": [ + "_names", + "_ranges", + "_numberOfSamples", + ] + }, + "uniformLhs": { + "required_args": [ + "_names", + "_ranges", + "_numberOfSamples", + ], + "optional_args": [ + "_includeBoundingBox", + ], + }, + "normalLhs": { + "required_args": [ + "_names", + "_numberOfSamples", + "_mu", + "_sigma", + ], + "optional_args": [ + "_ranges", + ], + }, + "sobol": { + "required_args": [ + "_names", + "_ranges", + "_numberOfSamples", # determines overall sobol set (+ _onset) + "_onset", # skip first sobol points and start at _onset number + ], + "optional_args": [ + "_includeBoundingBox", + ], + }, +<<<<<<< HEAD:src/farn/sampling/sampling.py +======= + "arbitrary": { + "required_args": [ + "_names", + "_ranges", + "_numberOfSamples", + "_distributionName", # uniform|normal|exp... + "_distributionParameters", # mu|sigma|skew|camber not applicable for uniform + "_includeBoundingBox", # required + ] + }, +>>>>>>> origin/main:src/farn/sampling/discrete.py + "hilbertCurve": { + "required_args": [ + "_names", + "_ranges", + "_numberOfSamples", + ], + "optional_args": [ + "_includeBoundingBox", + "_iterationDepth", + ], + }, + } + + def set_sampling_type(self, sampling_type: str) -> None: + """Set the sampling type. + + Valid values: + "factorial" + "fixed" + "linSpace" + "uniformLhs" + "normalLhs" + "sobol" + "arbitrary" + "hilbertCurve" + """ + if sampling_type in self.known_sampling_types: + self.sampling_type = sampling_type + else: + msg: str = f"sampling type {sampling_type} not implemented yet" + logger.error(msg) + raise ValueError(msg) + + def set_sampling_parameters( + self, + sampling_parameters: Mapping[str, Any], + layer_name: str = "", + ) -> None: + """Set the sampling parameters. + + The passed-in sampling parameters will be validated. + Upon successful validation, the sampling is configured using the provided parameters. + """ + self.layer_name = layer_name + self.sampling_parameters = sampling_parameters + + # check if all required arguments are provided + # TODO @CLAROS: check argument types + for kwarg in self.known_sampling_types[self.sampling_type]["required_args"]: + if kwarg not in self.sampling_parameters: + msg: str = ( + f"The following required argument is missing to configure " + f"sampling type {self.sampling_type}: {kwarg}" + ) + logger.error(msg) + + # read all fields + for name in self.sampling_parameters["_names"]: + self.fields.append(name) + + # determine the dimension (=number of fields) + self.number_of_fields = len(self.fields) + + # ranges + if "_ranges" in self.sampling_parameters and self._check_consistency_of_ranges( + self.sampling_parameters["_ranges"] + ): + self.ranges = self.sampling_parameters["_ranges"] + + # extra bounding box samples are not treated by lhs algorithm, however part of the lists + self.number_of_samples = 0 + self.number_of_bb_samples = 0 + + def generate_samples(self) -> dict[str, list[Any]]: + """Return a dict with all generated samples for the layer this sampling is run on. + + The first element in the returned dict contains the case names generated. + All following elements (second to last) contain the values sampled + for each variable defined in the layer this sampling is run on. + I.e. + "names": (case_name_1, case_name_2, .., case_name_n) + "variable_1": (value_1, value_2, .., value_n) + ... + "variable_m": (value_1, value_2, .., value_n) + + Returns + ------- + Dict[str, List[Any]] + the dict with all generated samples + """ + samples: dict[str, list[Any]] = {} + + if self.sampling_type == "factorial": + samples = self._generate_samples_using_factorial() + + elif self.sampling_type == "fixed": + samples = self._generate_samples_using_fixed() + + elif self.sampling_type == "linSpace": + samples = self._generate_samples_using_linspace() + + elif self.sampling_type == "uniformLhs": + samples = self._generate_samples_using_uniform_lhs() + + # elif self.sampling_type == "uniformRnd": + # samples = self._generate_samples_using_uniform_rnd() + + elif self.sampling_type == "normalLhs": + samples = self._generate_samples_using_normal_lhs() + + # elif self.sampling_type == "normalRnd": + # samples = self._generate_samples_using_normal_rnd() + + elif self.sampling_type == "sobol": + samples = self._generate_samples_using_sobol() + + elif self.sampling_type == "hilbertCurve": + samples = self._generate_samples_using_hilbert() + + else: + raise NotImplementedError(f"{self.sampling_type} not implemented yet.") + + return samples + +<<<<<<< HEAD:src/farn/sampling/sampling.py + def _generate_samples_using_factorial(self) -> Dict[str, List[Any]]: + _ = self._check_length_matches_number_of_names("_ranges") + self.list_of_samples = list(self.sampling_parameters["_listOfSamples"]) + if any(x <= 1 for x in self.list_of_samples): + logger.error( + f"Factorial does not work for dimensions populated with less than 2 values: {self.list_of_samples}" + ) + exit(1) + # to continue using _determine_number_of_samples, _numberOfSamples is generated + self.sampling_parameters["_numberOfSamples"] = int( + np.prod(self.list_of_samples) + ) + samples: Dict[str, List[Any]] = self._generate_samples_dict() + values: ndarray[Any, Any] = self._generate_values_using_factorial() + self._write_values_into_samples_dict(values, samples) + + return samples + + def _generate_samples_using_fixed(self) -> Dict[str, List[Any]]: +======= + def _generate_samples_using_fixed_sampling(self) -> dict[str, list[Any]]: +>>>>>>> origin/main:src/farn/sampling/discrete.py + _ = self._check_length_matches_number_of_names("_values") + samples: dict[str, list[Any]] = {} + msg: str + # Assert that the values per parameter are provided as a list + for item in self.sampling_parameters["_values"]: + if not isinstance(item, Sequence): +<<<<<<< HEAD:src/farn/sampling/sampling.py + msg: str = ( + "_values: The values per parameter need to be provided as a list of values." + ) +======= + msg = "_values: The values per parameter need to be provided as a list of values." +>>>>>>> origin/main:src/farn/sampling/discrete.py + logger.error(msg) + raise TypeError(msg) + + # Assert that the number of values per parameter is the same for all parameters + number_of_values_per_parameter: list[int] = [len(item) for item in self.sampling_parameters["_values"]] + all_parameters_have_same_number_of_values: bool = all( + number_of_values == number_of_values_per_parameter[0] # (breakline) + for number_of_values in number_of_values_per_parameter + ) + if not all_parameters_have_same_number_of_values: +<<<<<<< HEAD:src/farn/sampling/sampling.py + msg: str = ( + "_values: The number of values per parameter need to be the same for all parameters. However, they are different." +======= + msg = ( + "_values: The number of values per parameter need to be the same for all parameters. " + "However, they are different." +>>>>>>> origin/main:src/farn/sampling/discrete.py + ) + logger.error(msg) + raise ValueError(msg) + + self.number_of_samples = number_of_values_per_parameter[0] + self.leading_zeros = int(math.log10(self.number_of_samples) - 1.0e-06) + 1 + self._generate_case_names(samples) + + self.values = self.sampling_parameters["_values"] + + for index, field in enumerate(self.fields): + samples[field] = list(self.values[index]) + + return samples + +<<<<<<< HEAD:src/farn/sampling/sampling.py + def _generate_samples_using_linspace(self) -> Dict[str, List[Any]]: +======= + def _generate_samples_using_linspace_sampling(self) -> dict[str, list[Any]]: +>>>>>>> origin/main:src/farn/sampling/discrete.py + _ = self._check_length_matches_number_of_names("_ranges") + samples: dict[str, list[Any]] = self._generate_samples_dict() + self.minVals = [x[0] for x in self.ranges] + self.maxVals = [x[1] for x in self.ranges] + + for index, _ in enumerate(self.fields): + samples[self.fields[index]] = list( + np.linspace( + self.minVals[index], + self.maxVals[index], + self.number_of_samples, + ) + ) + + return samples + +<<<<<<< HEAD:src/farn/sampling/sampling.py + def _generate_samples_using_uniform_lhs(self) -> Dict[str, List[Any]]: + _ = self._check_length_matches_number_of_names("_ranges") + samples: Dict[str, List[Any]] = self._generate_samples_dict() + values: ndarray[Any, Any] = self._generate_values_using_uniform_lhs() +======= + def _generate_samples_using_uniform_lhs_sampling(self) -> dict[str, list[Any]]: + _ = self._check_length_matches_number_of_names("_ranges") + samples: dict[str, list[Any]] = self._generate_samples_dict() + values: np.ndarray[Any, np.dtype[np.float64]] = self._generate_values_using_uniform_lhs_sampling() +>>>>>>> origin/main:src/farn/sampling/discrete.py + self._write_values_into_samples_dict(values, samples) + + return samples + +<<<<<<< HEAD:src/farn/sampling/sampling.py + def _generate_samples_using_normal_lhs(self) -> Dict[str, List[Any]]: +======= + def _generate_samples_using_normal_lhs_sampling(self) -> dict[str, list[Any]]: +>>>>>>> origin/main:src/farn/sampling/discrete.py + """LHS using gaussian normal distributions. + + required input arguments: + * _names: required names template + * _numberOfSamples: required how many samples + * _mu: required absolute location vector of distribution center point (origin) + * _sigma: variation (vector), or required scalar, optional vector + """ + _ = self._check_length_matches_number_of_names("_mu") + if isinstance(self.sampling_parameters["_sigma"], Sequence): + _ = self._check_length_matches_number_of_names("_sigma") + + samples: dict[str, list[Any]] = self._generate_samples_dict() + self.mean = self.sampling_parameters["_mu"] + self.std = self.sampling_parameters["_sigma"] + +<<<<<<< HEAD:src/farn/sampling/sampling.py + values: ndarray[Any, Any] = self._generate_values_using_normal_lhs() +======= + values: np.ndarray[Any, np.dtype[np.float64]] = self._generate_values_using_normal_lhs_sampling() +>>>>>>> origin/main:src/farn/sampling/discrete.py + + # Clipping + # (optional. Clipping will only be performed if sampling parameter "_ranges" is defined.) + # NOTE: In current implementation, sampled values exceeding a parameters valid range + # are not discarded but reset to the respective range upper or lower bound. + # If real clipping shall be implemented, it would require discarding exceeding values. + # As the number of values that would need to be discarded is different + # for each individual parameter (dimension), + # the necessary clipping logic will quickly become complex. + # Hence the somewhat simpler approach for now, where exceeding values + # simply get reset to the range bounderies. + if self.ranges: + range_lower_bounds: np.ndarray[Any, np.dtype[np.float64]] = np.array([r[0] for r in self.ranges]) + range_upper_bounds: np.ndarray[Any, np.dtype[np.float64]] = np.array([r[1] for r in self.ranges]) + values = np.clip(values, range_lower_bounds, range_upper_bounds) + + self._write_values_into_samples_dict(values, samples) + + return samples + +<<<<<<< HEAD:src/farn/sampling/sampling.py + def _generate_samples_using_sobol(self) -> Dict[str, List[Any]]: + _ = self._check_length_matches_number_of_names("_ranges") + self.onset = int(self.sampling_parameters["_onset"]) + + samples: Dict[str, List[Any]] = self._generate_samples_dict() + values: ndarray[Any, Any] = self._generate_values_using_sobol() +======= + def _generate_samples_using_sobol_sampling(self) -> dict[str, list[Any]]: + _ = self._check_length_matches_number_of_names("_ranges") + self.onset = int(self.sampling_parameters["_onset"]) + + samples: dict[str, list[Any]] = self._generate_samples_dict() + values: np.ndarray[Any, np.dtype[np.float64]] = self._generate_values_using_sobol_sampling() +>>>>>>> origin/main:src/farn/sampling/discrete.py + self._write_values_into_samples_dict(values, samples) + + return samples + +<<<<<<< HEAD:src/farn/sampling/sampling.py + def _generate_samples_using_hilbert(self) -> Dict[str, List[Any]]: +======= + def _generate_samples_using_arbitrary_sampling(self) -> dict[str, list[Any]]: + """ + Purpose: To perform a sampling based on the pre-drawn sample. + Pre-requisite: + 1. Since the most fitted distribution is unknown, it shall be found by using the fitter module. + 2. fitter module provides: 1) the name of the most fitted distribution, 2) relavant parameters + 3. relavent parameters mostly comprises with 3 components: 1) skewness 2) location 3) scale + 4. At this moment, those prerequisites shall be provided as arguments. This could be modified later + 5. refer to commented example below. + """ + _ = self._check_length_matches_number_of_names("_ranges") + + samples: dict[str, list[Any]] = self._generate_samples_dict() + self.minVals = [x[0] for x in self.ranges] + self.maxVals = [x[1] for x in self.ranges] + + import scipy.stats # noqa: F401, PLC0415 + + distribution_name: Sequence[str] + distribution_parameters: Sequence[Any] + for index, _ in enumerate(self.fields): + distribution_name = self.sampling_parameters["_distributionName"] + distribution_parameters = self.sampling_parameters["_distributionParameters"] + + _eval_command = f"scipy.stats.{distribution_name[index]}" + dist = eval(_eval_command) # noqa: S307 + + samples[self.fields[index]] = dist.rvs( + *distribution_parameters[index], + size=self.number_of_samples, + ).tolist() + + # requires if self.kwargs['_includeBoundingBox'] is True: as well + + return samples + + def _generate_samples_using_hilbert_sampling(self) -> dict[str, list[Any]]: +>>>>>>> origin/main:src/farn/sampling/discrete.py + _ = self._check_length_matches_number_of_names("_ranges") + samples: dict[str, list[Any]] = self._generate_samples_dict() + # Depending on implementation + self.minIterationDepth = 5 + self.maxIterationDepth = 15 +<<<<<<< HEAD:src/farn/sampling/sampling.py + + values: ndarray[Any, Any] = self._generate_values_using_hilbert() +======= + + values: np.ndarray[Any, np.dtype[np.float64]] = self._generate_values_using_hilbert_sampling() +>>>>>>> origin/main:src/farn/sampling/discrete.py + self._write_values_into_samples_dict(values, samples) + + return samples + + def _generate_samples_dict(self) -> dict[str, list[Any]]: + samples_dict: dict[str, list[Any]] = {} + self._determine_number_of_samples() + self._generate_case_names(samples_dict) + return samples_dict + +<<<<<<< HEAD:src/farn/sampling/sampling.py + def _generate_values_using_factorial(self) -> ndarray[Any, Any]: + """Full factorial, normalized and scaled to ranges.""" + from pyDOE3 import fullfact + from sklearn.preprocessing import normalize + + ff_distribution = fullfact(self.list_of_samples) + + _range_lower_bounds: ndarray[Any, Any] = np.array( + [range[0] for range in self.ranges] + ) + _range_upper_bounds: ndarray[Any, Any] = np.array( + [range[1] for range in self.ranges] + ) + loc: ndarray[Any, Any] = _range_lower_bounds + scale: ndarray[Any, Any] = _range_upper_bounds - _range_lower_bounds + values, norms = normalize( + ff_distribution.T, norm="l1", axis=1, return_norm=True + ) + sample_set: ndarray[Any, Any] = ( + values.T + * norms + / (self.list_of_samples - np.ones(len(self.list_of_samples))) + * scale + + loc + ) + + return sample_set + + def _generate_values_using_uniform_lhs(self) -> ndarray[Any, Any]: +======= + def _generate_values_using_uniform_lhs_sampling(self) -> np.ndarray[Any, np.dtype[np.float64]]: +>>>>>>> origin/main:src/farn/sampling/discrete.py + """Uniform LHS.""" + from pyDOE3 import lhs # noqa: PLC0415 + from scipy.stats import uniform # noqa: PLC0415 + + lhs_distribution: np.ndarray[Any, np.dtype[np.float64]] | None = lhs( + n=self.number_of_fields, + samples=self.number_of_samples - self.number_of_bb_samples, + criterion="corr" if self.number_of_fields > 1 else None, + random_state=self.seed, + ) + if lhs_distribution is None: + msg: str = "lhs_distribution is None. This should not happen. Check pyDOE3.lhs function." + logger.error(msg) + raise ValueError(msg) + _range_lower_bounds: np.ndarray[Any, np.dtype[np.float64]] = np.array([r[0] for r in self.ranges]) + _range_upper_bounds: np.ndarray[Any, np.dtype[np.float64]] = np.array([r[1] for r in self.ranges]) + loc: np.ndarray[Any, np.dtype[np.float64]] = _range_lower_bounds + scale: np.ndarray[Any, np.dtype[np.float64]] = _range_upper_bounds - _range_lower_bounds + + sample_set: np.ndarray[Any, np.dtype[np.float64]] = uniform(loc=loc, scale=scale).ppf(lhs_distribution) + + return sample_set + +<<<<<<< HEAD:src/farn/sampling/sampling.py + def _generate_values_using_normal_lhs(self) -> ndarray[Any, Any]: +======= + def _generate_values_using_normal_lhs_sampling(self) -> np.ndarray[Any, np.dtype[np.float64]]: +>>>>>>> origin/main:src/farn/sampling/discrete.py + """Gaussnormal LHS.""" + from pyDOE3 import lhs # noqa: PLC0415 + from scipy.stats import norm # noqa: PLC0415 + + lhs_distribution: np.ndarray[Any, np.dtype[np.float64]] | None = lhs( + n=self.number_of_fields, + samples=self.number_of_samples - self.number_of_bb_samples, + criterion="corr" if self.number_of_fields > 1 else None, + random_state=self.seed, + ) + if lhs_distribution is None: + msg: str = "lhs_distribution is None. This should not happen. Check pyDOE3.lhs function." + logger.error(msg) + raise ValueError(msg) + # criterion: a string that tells lhs how to sample the points + # (default: None, which simply randomizes the points within the intervals) + # - center|c: center the points within the sampling intervals + # - maximin|m: maximize the minimum distance between points, but place the point + # in a randomized location within its interval + # - centermaximin|cm: same as “maximin”, but centered within the intervals + # - correlation|corr: minimize the maximum correlation coefficient + + # std of type scalar (scale) or vector (stretch, scale), no rotation + _std: np.ndarray[Any, np.dtype[np.float64]] = np.array(self.std) + + sample_set: np.ndarray[Any, np.dtype[np.float64]] = norm(loc=self.mean, scale=_std).ppf(lhs_distribution) + + return sample_set + +<<<<<<< HEAD:src/farn/sampling/sampling.py + def _generate_values_using_sobol(self) -> ndarray[Any, Any]: + from scipy.stats import qmc + from scipy.stats.qmc import Sobol + #todo: check compare with sobol_seq + +======= + def _generate_values_using_sobol_sampling(self) -> np.ndarray[Any, np.dtype[np.float64]]: + from scipy.stats import qmc # noqa: PLC0415 + from scipy.stats.qmc import Sobol # noqa: PLC0415 + +>>>>>>> origin/main:src/farn/sampling/discrete.py + sobol_engine: Sobol = Sobol( + d=self.number_of_fields, + scramble=False, + rng=None, + ) + + if self.onset > 0: + _ = sobol_engine.fast_forward(n=self.onset) + + points: np.ndarray[Any, np.dtype[np.float64]] = sobol_engine.random( + n=self.number_of_samples - self.number_of_bb_samples, + ) + + # Upscale points from unit hypercube to bounds + range_lower_bounds: np.ndarray[Any, np.dtype[np.float64]] = np.array([r[0] for r in self.ranges]) + range_upper_bounds: np.ndarray[Any, np.dtype[np.float64]] = np.array([r[1] for r in self.ranges]) + sample_set: np.ndarray[Any, np.dtype[np.float64]] = qmc.scale( + points, + range_lower_bounds, + range_upper_bounds, + ) + + return sample_set + +<<<<<<< HEAD:src/farn/sampling/sampling.py + def _generate_values_using_hilbert(self) -> ndarray[Any, Any]: +======= + def _generate_values_using_hilbert_sampling(self) -> np.ndarray[Any, np.dtype[np.float64]]: +>>>>>>> origin/main:src/farn/sampling/discrete.py + """Source hilbertcurve pypi pkg or numpy + it showed that hilbertcurve is a better choice and more precise with a higher iteration depth (<=15) + pypi pkg Decimals is required for proper function up to (<=15) + numpy approach instead has only (<=10). + """ + # sourcery skip: extract-duplicate-method + from math import modf # noqa: PLC0415 + + from scipy.stats import qmc # noqa: PLC0415 + + msg: str + try: + from decimal import Decimal # noqa: PLC0415 + except ImportError: + msg = "no module named Decimal" + logger.exception(msg) + raise + try: + from hilbertcurve.hilbertcurve import HilbertCurve # noqa: PLC0415 + except ImportError: + msg = "no module named HilbertCurve" + logger.exception(msg) + raise + + number_of_continuous_samples: int = self.number_of_samples - self.number_of_bb_samples + + if "_iterationDepth" in self.sampling_parameters: + if not isinstance(self.sampling_parameters["_iterationDepth"], int): +<<<<<<< HEAD:src/farn/sampling/sampling.py + msg: str = ( + f'_iterationDepth was not given as integer: {self.sampling_parameters["_iterationDepth"]}.' + ) + logger.error(msg) + raise ValueError(msg) + if self.sampling_parameters["_iterationDepth"] > self.maxIterationDepth: + msg: str = ( + f'_iterationDepth {self.sampling_parameters["_iterationDepth"]} given in farnDict is beyond the limit of {self.maxIterationDepth}...\n\t\tsetting to {self.maxIterationDepth}' +======= + msg = f"_iterationDepth was not given as integer: {self.sampling_parameters['_iterationDepth']}." + logger.error(msg) + raise ValueError(msg) + if self.sampling_parameters["_iterationDepth"] > self.maxIterationDepth: + msg = ( + f"_iterationDepth {self.sampling_parameters['_iterationDepth']} " + f"given in farn dict file is beyond the limit of {self.maxIterationDepth}...\n" + f"\t\tsetting to {self.maxIterationDepth}" +>>>>>>> origin/main:src/farn/sampling/discrete.py + ) + logger.warning(msg) + self.iteration_depth = self.maxIterationDepth + elif self.sampling_parameters["_iterationDepth"] < self.minIterationDepth: +<<<<<<< HEAD:src/farn/sampling/sampling.py + msg: str = ( + f'_iterationDepth {self.sampling_parameters["_iterationDepth"]} given in farnDict is below the limit of {self.minIterationDepth}...\n\t\tsetting to {self.minIterationDepth}' +======= + msg = ( + f"_iterationDepth {self.sampling_parameters['_iterationDepth']} " + f"given in farn dict file is below the limit of {self.minIterationDepth}...\n" + f"\t\tsetting to {self.minIterationDepth}" +>>>>>>> origin/main:src/farn/sampling/discrete.py + ) + logger.warning(msg) + self.iteration_depth = self.minIterationDepth + else: + self.iteration_depth = self.sampling_parameters["_iterationDepth"] + else: + self.iteration_depth = 10 +<<<<<<< HEAD:src/farn/sampling/sampling.py + msg: str = (f'_iterationDepth not given in farnDict...\n\tsetting to {self.iteration_depth}') + logger.warning(msg) + + corners: int = 2**self.number_of_fields + multiply_of_corners: int = number_of_continuous_samples % corners + if multiply_of_corners in [0, 1, corners-1]: + nearly: str = 'exactly ' if multiply_of_corners == 0 else 'nearly ' + msg: str = ( + f'The number of samples ({number_of_continuous_samples}) is {nearly}a multiple of the corners for a {self.number_of_fields}-dim tessereact ({corners}) and thus the sampling set might be correlated...\n\tconsider changing the number of samples.' + ) + logger.warning(msg) + + hc = HilbertCurve( + self.iteration_depth, self.number_of_fields, n_procs=0 + ) # -1: all threads +======= + + hc = HilbertCurve(self.iteration_depth, self.number_of_fields, n_procs=0) # -1: all threads +>>>>>>> origin/main:src/farn/sampling/discrete.py + + logger.info( + f"The number of hilbert points is {hc.max_h}, " + f"the number of continuous samples is {number_of_continuous_samples}" + ) + if hc.max_h <= int(10.0 * number_of_continuous_samples): + logger.warning( + 'Try to set or increase "_iterationDepth" gradually to achieve ' + 'a number of hilbert points of about 10-times higher than "_numberOfSamples".' + ) + + distribution: np.ndarray[Any, np.dtype[np.float64]] = np.array( + [ + Decimal(x) + for x in np.linspace( + int(hc.min_h), + int(hc.max_h), + number_of_continuous_samples, + ) + ] + ) + int_distribution: np.ndarray[Any, np.dtype[np.int32]] = np.trunc(distribution) + +<<<<<<< HEAD:src/farn/sampling/sampling.py + hilbert_points = hc.points_from_distances(int_distribution) + + _points: Iterable[Iterable[float]] = [] +======= + hilbert_points: Iterable[Iterable[int]] = hc.points_from_distances(int_distribution) + + _points: list[Iterable[float]] = [] +>>>>>>> origin/main:src/farn/sampling/discrete.py + interpolation_hits = 0 + for hpt, _dst, _idst in zip(hilbert_points, distribution, int_distribution, strict=False): + dst = float(_dst) + idst = int(_idst) + if dst == idst: + _points.append(hpt) + else: + # interpolation starts: use idst to find integer neighbour of dst + # nn: next neighbour + dst_nn = idst + 1 + pt_from_dst = hpt + pt_from_dst_nn = hc.point_from_distance(dst_nn) + + # find the index where both discrete points are different and interpolate that index + # and create the new real-valued point + point: list[float] = [] + for i, j in zip(pt_from_dst, pt_from_dst_nn, strict=False): + if i != j: + # non-matching index found, e.g. + # points are in the same dimension and need to be interpolated alongside + smaller_index = min(i, j) + fraction, _ = modf(dst) + # add the component to real-valued vector + point.append(float(smaller_index) + fraction) + interpolation_hits += 1 + else: + point.append(float(i)) + + _points.append(point) + points: np.ndarray[Any, np.dtype[np.float64]] = np.array(_points).astype(np.float64) + + # Downscale points from hilbert space to unit hypercube [0,1)*d +<<<<<<< HEAD:src/farn/sampling/sampling.py + points = qmc.scale(points, points.min(axis=0), points.max(axis=0), reverse=True) # type: ignore + +======= + points = qmc.scale( + points, + points.min(axis=0), + points.max(axis=0), + reverse=True, + ) + +>>>>>>> origin/main:src/farn/sampling/discrete.py + # Upscale points from unit hypercube to bounds + range_lower_bounds: np.ndarray[Any, np.dtype[np.float64]] = np.array([r[0] for r in self.ranges]) + range_upper_bounds: np.ndarray[Any, np.dtype[np.float64]] = np.array([r[1] for r in self.ranges]) + sample_set: np.ndarray[Any, np.dtype[np.float64]] = qmc.scale( + points, + range_lower_bounds, + range_upper_bounds, + ) +<<<<<<< HEAD:src/farn/sampling/sampling.py + range_upper_bounds: ndarray[Any, Any] = np.array( + [range[1] for range in self.ranges] + ) + sample_set: ndarray[Any, Any] = qmc.scale(points, range_lower_bounds, range_upper_bounds) # type: ignore + +======= + +>>>>>>> origin/main:src/farn/sampling/discrete.py + return sample_set + + def _determine_number_of_samples(self) -> None: + if "_includeBoundingBox" in self.sampling_parameters and isinstance( + self.sampling_parameters["_includeBoundingBox"], bool + ): + self.include_bounding_box = self.sampling_parameters["_includeBoundingBox"] + self.number_of_samples = int(self.sampling_parameters["_numberOfSamples"]) + if self.include_bounding_box is True: + self.number_of_bb_samples = int(2**self.number_of_fields) + self.number_of_samples += self.number_of_bb_samples + self.leading_zeros = int(math.log10(self.number_of_samples) - 1e-06) + 1 + + def _generate_case_names( + self, + samples: dict[str, list[Any]], + ) -> None: + _format_specifier: str = f"0{self.leading_zeros}d" + self.case_names = [f"{self.layer_name}_{format(i, _format_specifier)}" for i in range(self.number_of_samples)] + samples["_case_name"] = self.case_names + + def _check_length_matches_number_of_names( + self, + parameter_name: str, + ) -> bool: + # check that size of a list/ vector equals the size of _names + if len(self.sampling_parameters[parameter_name]) != len(self.sampling_parameters["_names"]): + msg: str = f"lists _names and {parameter_name}: lengths of entries do not match" + logger.error(msg) + return False + return True + + def _check_consistency_of_ranges(self, ranges: Sequence[Sequence[Any]]) -> bool: + for item in ranges: + if len(item) != 2: # noqa: PLR2004 + logger.error("The structure of min and max values in _ranges is inconsistent.") + return False + return self._check_length_matches_number_of_names("_ranges") + + def _create_bounding_box(self) -> None: + import itertools # noqa: PLC0415 + + tmp: list[float | Sequence[float] | Sequence[Any]] = [] + if len(self.sampling_parameters["_ranges"]) == 1: + # (only one single parameter defined) + tmp = list(self.sampling_parameters["_ranges"][0]) + else: + # permutate boundaries of all paramaters + tmp = list( + itertools.product( + self.sampling_parameters["_ranges"][0], + self.sampling_parameters["_ranges"][1], + ) + ) + for field_index in range(1, self.number_of_fields - 1): + tmp = list(itertools.product(tmp, self.sampling_parameters["_ranges"][field_index + 1])) + self.bounding_box = [] + for item in tmp: + if isinstance(item, Iterable): + self.bounding_box.append(list(self._flatten(item))) + else: + self.bounding_box.append([item]) + return + + def _write_values_into_samples_dict( + self, + values: np.ndarray[Any, np.dtype[np.float64]], + samples: dict[str, list[Any]], + ) -> None: + if self.include_bounding_box is True: + self._create_bounding_box() + values = np.concatenate( + ( + np.array(self.bounding_box), + values, + ), + axis=0, + ) + for index, _ in enumerate(self.fields): + samples[self.fields[index]] = values.T[index].tolist() + return + + def _flatten( + self, + iterable: Sequence[Any], + ) -> Generator[Any, Any, Any]: + """Flattens sequence... happens why?.""" + for element in iterable: + if isinstance(element, Sequence) and not isinstance(element, str | bytes): + yield from self._flatten(element) + else: + yield element diff --git a/tests/cli/test_farn_cli.py b/tests/cli/test_farn_cli.py index 9bfdcffe..fe75aa44 100644 --- a/tests/cli/test_farn_cli.py +++ b/tests/cli/test_farn_cli.py @@ -6,8 +6,8 @@ import pytest -from farn.cli import __main__ -from farn.cli.__main__ import _argparser, _get_version, main +from farn.cli import farn +from farn.cli.farn import _argparser, _get_version, main # *****Test commandline interface (CLI)************************************************************ @@ -166,8 +166,8 @@ def fake_run_farn( ): pass - monkeypatch.setattr(__main__, "configure_logging", fake_configure_logging) - monkeypatch.setattr(__main__, "run_farn", fake_run_farn) + monkeypatch.setattr(farn, "configure_logging", fake_configure_logging) + monkeypatch.setattr(farn, "run_farn", fake_run_farn) # Execute if isinstance(expected, ConfigureLoggingArgs): args_expected: ConfigureLoggingArgs = expected @@ -247,7 +247,7 @@ def fake_run_farn( args.batch = batch args.test = test - monkeypatch.setattr(__main__, "run_farn", fake_run_farn) + monkeypatch.setattr(farn, "run_farn", fake_run_farn) # Execute if isinstance(expected, ApiArgs): args_expected: ApiArgs = expected diff --git a/tests/cli/test_farn_cli.py.orig b/tests/cli/test_farn_cli.py.orig new file mode 100644 index 00000000..0839a493 --- /dev/null +++ b/tests/cli/test_farn_cli.py.orig @@ -0,0 +1,270 @@ +# pyright: reportPrivateUsage=false +import sys +from argparse import ArgumentError +from dataclasses import dataclass, field +from pathlib import Path + +import pytest + +from farn.cli import __main__ +from farn.cli.__main__ import _argparser, _get_version, main + +# *****Test commandline interface (CLI)************************************************************ + + +@dataclass() +class CliArgs: + # Expected default values for the CLI arguments when farn gets called via the commandline + quiet: bool = False + verbose: bool = False + log: str | None = None + log_level: str = field(default_factory=lambda: "WARNING") +<<<<<<< HEAD + farnDict: Union[str, None] = field( + default_factory=lambda: "test_farnDict" + ) # noqa: N815 +======= + farn_dict_file: str | None = field(default_factory=lambda: "test_farnDict") +>>>>>>> origin/main + sample: bool = False + generate: bool = False + execute: str | None = None + test: bool = False + + +@pytest.mark.parametrize( + "inputs, expected", + [ + ([], ArgumentError), + (["test_farnDict"], CliArgs()), + (["test_farnDict", "-q"], CliArgs(quiet=True)), + (["test_farnDict", "--quiet"], CliArgs(quiet=True)), + (["test_farnDict", "-v"], CliArgs(verbose=True)), + (["test_farnDict", "--verbose"], CliArgs(verbose=True)), + (["test_farnDict", "-qv"], ArgumentError), + (["test_farnDict", "--log", "logFile"], CliArgs(log="logFile")), + (["test_farnDict", "--log"], ArgumentError), + (["test_farnDict", "--log-level", "INFO"], CliArgs(log_level="INFO")), + (["test_farnDict", "--log-level"], ArgumentError), + (["test_farnDict", "-s"], CliArgs(sample=True)), + (["test_farnDict", "--sample"], CliArgs(sample=True)), + (["test_farnDict", "-g"], CliArgs(generate=True)), + (["test_farnDict", "--generate"], CliArgs(generate=True)), + (["test_farnDict", "-e", "command"], CliArgs(execute="command")), + (["test_farnDict", "--execute", "command"], CliArgs(execute="command")), + ( + ["test_farnDict", "--execute", "command name with spaces"], + CliArgs(execute="command name with spaces"), + ), + (["test_farnDict", "--execute"], ArgumentError), + (["test_farnDict", "--test"], CliArgs(test=True)), + (["test_farnDict", "-t"], ArgumentError), + ], +) +def test_cli( + inputs: list[str], + expected: CliArgs | type, + monkeypatch: pytest.MonkeyPatch, +): + # sourcery skip: no-conditionals-in-tests + # sourcery skip: no-loop-in-tests + # Prepare + monkeypatch.setattr(sys, "argv", ["farn", *inputs]) + parser = _argparser() + # Execute + if isinstance(expected, CliArgs): + args_expected: CliArgs = expected + args = parser.parse_args() + # Assert args + for key in args_expected.__dataclass_fields__: + assert args.__getattribute__(key) == args_expected.__getattribute__(key) + elif issubclass(expected, Exception): + exception: type = expected + # Assert that expected exception is raised + with pytest.raises((exception, SystemExit)): + args = parser.parse_args() + else: + raise TypeError + + +@pytest.mark.parametrize("flag", ["-V", "--version"]) +def test_cli_version( + flag: str, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +): + # Prepare + monkeypatch.setattr(sys, "argv", ["farn", flag]) + parser = _argparser() + # Execute & Assert + with pytest.raises(SystemExit) as exc_info: + _ = parser.parse_args() + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert _get_version() in captured.out + + +# *****Ensure the CLI correctly configures logging************************************************* + + +@dataclass() +class ConfigureLoggingArgs: + # Values that main() is expected to pass to ConfigureLogging() by default when configuring the logging + # Note: 'INFO' deviates from standard 'WARNING', but was decided intentionally for farn + log_level_console: str = field(default_factory=lambda: "INFO") + log_file: Path | None = None + log_level_file: str = field(default_factory=lambda: "WARNING") + + +@pytest.mark.parametrize( + "inputs, expected", + [ + ([], ArgumentError), + (["test_farnDict"], ConfigureLoggingArgs()), + (["test_farnDict", "-q"], ConfigureLoggingArgs(log_level_console="ERROR")), + (["test_farnDict", "--quiet"], ConfigureLoggingArgs(log_level_console="ERROR")), + (["test_farnDict", "-v"], ConfigureLoggingArgs(log_level_console="DEBUG")), + ( + ["test_farnDict", "--verbose"], + ConfigureLoggingArgs(log_level_console="DEBUG"), + ), + (["test_farnDict", "-qv"], ArgumentError), + ( + ["test_farnDict", "--log", "logFile"], + ConfigureLoggingArgs(log_file=Path("logFile")), + ), + (["test_farnDict", "--log"], ArgumentError), + ( + ["test_farnDict", "--log-level", "INFO"], + ConfigureLoggingArgs(log_level_file="INFO"), + ), + (["test_farnDict", "--log-level"], ArgumentError), + ], +) +def test_logging_configuration( + inputs: list[str], + expected: ConfigureLoggingArgs | type, + monkeypatch: pytest.MonkeyPatch, +): + # sourcery skip: no-conditionals-in-tests + # sourcery skip: no-loop-in-tests + # Prepare + monkeypatch.setattr(sys, "argv", ["farn", *inputs]) + args: ConfigureLoggingArgs = ConfigureLoggingArgs() + + def fake_configure_logging( + log_level_console: str, + log_file: Path | None, + log_level_file: str, + ): + args.log_level_console = log_level_console + args.log_file = log_file + args.log_level_file = log_level_file + + def fake_run_farn( + farn_dict_file: Path, + *, + sample: bool, + generate: bool, + command: str | None, + batch: bool, + test: bool, + ): + pass + + monkeypatch.setattr(__main__, "configure_logging", fake_configure_logging) + monkeypatch.setattr(__main__, "run_farn", fake_run_farn) + # Execute + if isinstance(expected, ConfigureLoggingArgs): + args_expected: ConfigureLoggingArgs = expected + main() + # Assert args + for key in args_expected.__dataclass_fields__: + assert args.__getattribute__(key) == args_expected.__getattribute__(key) + elif issubclass(expected, Exception): + exception: type = expected + # Assert that expected exception is raised + with pytest.raises((exception, SystemExit)): + main() + else: + raise TypeError + + +# *****Ensure the CLI correctly invokes the API**************************************************** + + +@dataclass() +class ApiArgs: + # Values that main() is expected to pass to run_farn() by default when invoking the API + farn_dict_file: Path = field(default_factory=lambda: Path("test_farnDict")) + sample: bool = False + generate: bool = False + command: str | None = None + batch: bool = False + test: bool = False + + +@pytest.mark.parametrize( + "inputs, expected", + [ + ([], ArgumentError), + (["test_farnDict"], ApiArgs()), + (["test_farnDict", "-s"], ApiArgs(sample=True)), + (["test_farnDict", "--sample"], ApiArgs(sample=True)), + (["test_farnDict", "-g"], ApiArgs(generate=True)), + (["test_farnDict", "--generate"], ApiArgs(generate=True)), + (["test_farnDict", "-e", "command"], ApiArgs(command="command")), + (["test_farnDict", "--execute", "command"], ApiArgs(command="command")), + ( + ["test_farnDict", "--execute", "command name with spaces"], + ApiArgs(command="command name with spaces"), + ), + (["test_farnDict", "--execute"], ArgumentError), + (["test_farnDict", "-b"], ApiArgs(batch=True)), + (["test_farnDict", "--batch"], ApiArgs(batch=True)), + (["test_farnDict", "--test"], ApiArgs(test=True)), + (["test_farnDict", "-t"], ArgumentError), + ], +) +def test_api_invokation( + inputs: list[str], + expected: ApiArgs | type, + monkeypatch: pytest.MonkeyPatch, +): + # sourcery skip: no-conditionals-in-tests + # sourcery skip: no-loop-in-tests + # Prepare + monkeypatch.setattr(sys, "argv", ["farn", *inputs]) + args: ApiArgs = ApiArgs() + + def fake_run_farn( + farn_dict_file: Path, + *, + sample: bool = False, + generate: bool = False, + command: str | None = None, + batch: bool = False, + test: bool = False, + ): + args.farn_dict_file = farn_dict_file + args.sample = sample + args.generate = generate + args.command = command + args.batch = batch + args.test = test + + monkeypatch.setattr(__main__, "run_farn", fake_run_farn) + # Execute + if isinstance(expected, ApiArgs): + args_expected: ApiArgs = expected + main() + # Assert args + for key in args_expected.__dataclass_fields__: + assert args.__getattribute__(key) == args_expected.__getattribute__(key) + elif issubclass(expected, Exception): + exception: type = expected + # Assert that expected exception is raised + with pytest.raises((exception, SystemExit)): + main() + else: + raise TypeError diff --git a/tests/sampled.farnDict-exceptionlistfiltering b/tests/sampled.farnDict-exceptionlistfiltering new file mode 100644 index 00000000..bf283c45 --- /dev/null +++ b/tests/sampled.farnDict-exceptionlistfiltering @@ -0,0 +1,112 @@ +/*---------------------------------*- C++ -*----------------------------------*\ +filetype dictionary; coding utf-8; version 0.1; local --; purpose --; +\*----------------------------------------------------------------------------*/ +_environment +{ + CASEDIR cases-exceptionlistfiltering; + DUMPDIR dump; + LOGDIR logs; + RESULTDIR results; + TEMPLATEDIR template; +} +_layers +{ + firstParameter + { + _sampling + { + _type fixed; + _names + ( + param0 + ); + _values + ( + ( + 0.5 0.75 1.0 + ) + ); + } + _condition + { + _filter 'index != 1'; + _action exclude; + } + _commands + { + echo0 + ( + 'echo param0' 'echo %cd%' pwd + ); + } + _comment 'level 0 firstParmeter'; + _samples + { + _names + ( + firstParameter_0 firstParameter_1 firstParameter_2 + ); + param0 + ( + 0.5 0.75 1.0 + ); + } + } + lhsVariation + { + _sampling + { + _type uniformLhs; + _names + ( + param1 param2 param3 + ); + _ranges + ( + ( + -10 10 + ) + ( + -5 5 + ) + ( + 0 100 + ) + ); + _includeBoundingBox false; + _numberOfSamples 5; + } + _condition + { + _filter 'abs(param0 * param1) >= 3.5'; + _action exclude; + } + _commands + { + echo1 + ( + 'echo param1 param2 param3' 'echo %cd%' pwd + ); + } + _comment 'level 1 lhsVariation, start -g with no filtering, continue -e with given exception list'; + _samples + { + _names + ( + lhsVariation_0 lhsVariation_1 lhsVariation_2 lhsVariation_3 lhsVariation_4 + ); + param1 + ( + 8.660631464752942 -8.763635497359642 1.6119062285186807 2.607277455447246 -2.0268941395832467 + ); + param2 + ( + -0.09672668187936395 -2.3286949592789643 1.6761712157975852 3.0611142909477316 -4.414154033060077 + ); + param3 + ( + 73.28674957139499 93.7189967654611 33.33352273366535 5.469904312265501 49.03259513367585 + ); + } + } +} diff --git a/tests/sampled.farnDict-initialfiltering b/tests/sampled.farnDict-initialfiltering new file mode 100644 index 00000000..52d4f280 --- /dev/null +++ b/tests/sampled.farnDict-initialfiltering @@ -0,0 +1,107 @@ +/*---------------------------------*- C++ -*----------------------------------*\ +filetype dictionary; coding utf-8; version 0.1; local --; purpose --; +\*----------------------------------------------------------------------------*/ +_environment +{ + CASEDIR cases-initialfiltering; + DUMPDIR dump; + LOGDIR logs; + RESULTDIR results; + TEMPLATEDIR template; +} +_layers +{ + firstParameter + { + _sampling + { + _type fixed; + _names + ( + param0 + ); + _values + ( + ( + 0.5 1.0 + ) + ); + } + _condition + { + _filter 'abs(param0 * param1) >= 1'; + _action exclude; + } + _commands + { + echo0 + ( + 'echo param0' 'echo %cd%' pwd + ); + } + _comment 'level 0 firstParmeter'; + _samples + { + _names + ( + firstParameter_0 firstParameter_1 + ); + param0 + ( + 0.5 1.0 + ); + } + } + lhsVariation + { + _sampling + { + _type uniformLhs; + _names + ( + param1 param2 param3 + ); + _ranges + ( + ( + -10 10 + ) + ( + -5 5 + ) + ( + 0 100 + ) + ); + _includeBoundingBox false; + _numberOfSamples 5; + } + _commands + { + echo1 + ( + 'echo param1 param2 param3' 'echo %cd%' pwd + ); + } + _comment 'level 1 lhsVariation'; + _samples + { + _names + ( + lhsVariation_0 lhsVariation_1 lhsVariation_2 lhsVariation_3 lhsVariation_4 + ); + param1 + ( + -4.483089985885793 6.633942522390214 -8.082607754935667 0.5871269015797882 3.9197648401023972 + ); + param2 + ( + 3.9316308857384605 -4.838660157051919 -1.095067786106358 1.241979896163027 -0.5323609531789382 + ); + param3 + ( + 63.7174530981745 52.56203633727421 20.547098447720487 15.664527924018614 96.0543856901311 + ); + } + } +} diff --git a/tests/sampled.farnDict-nofiltering b/tests/sampled.farnDict-nofiltering new file mode 100644 index 00000000..d8d8198d --- /dev/null +++ b/tests/sampled.farnDict-nofiltering @@ -0,0 +1,102 @@ +/*---------------------------------*- C++ -*----------------------------------*\ +filetype dictionary; coding utf-8; version 0.1; local --; purpose --; +\*----------------------------------------------------------------------------*/ +_environment +{ + CASEDIR cases-nofiltering; + DUMPDIR dump; + LOGDIR logs; + RESULTDIR results; + TEMPLATEDIR template; +} +_layers +{ + firstParameter + { + _sampling + { + _type fixed; + _names + ( + param0 + ); + _values + ( + ( + 0.5 1.0 + ) + ); + } + _commands + { + echo0 + ( + 'echo param0' 'echo %cd%' pwd + ); + } + _comment 'level 0 firstParmeter'; + _samples + { + _names + ( + firstParameter_0 firstParameter_1 + ); + param0 + ( + 0.5 1.0 + ); + } + } + lhsVariation + { + _sampling + { + _type uniformLhs; + _names + ( + param1 param2 param3 + ); + _ranges + ( + ( + -10 10 + ) + ( + -5 5 + ) + ( + 0 100 + ) + ); + _includeBoundingBox false; + _numberOfSamples 5; + } + _commands + { + echo1 + ( + 'echo param1 param2 param3' 'echo %cd%' pwd + ); + } + _comment 'level 1 lhsVariation'; + _samples + { + _names + ( + lhsVariation_0 lhsVariation_1 lhsVariation_2 lhsVariation_3 lhsVariation_4 + ); + param1 + ( + -6.2164810327097815 6.595698302847342 0.6461314908912623 -4.758791428007402 2.392309345297292 + ); + param2 + ( + 0.07786167768863539 4.367779924973641 -2.741230601424664 -3.0591201944884996 2.2934498709493294 + ); + param3 + ( + 28.8614246131807 54.28379898596809 15.115992344765672 71.1473117951479 88.70769315856471 + ); + } + } +} diff --git a/tests/sampled.test_farnDict b/tests/sampled.test_farnDict new file mode 100644 index 00000000..c76bd32d --- /dev/null +++ b/tests/sampled.test_farnDict @@ -0,0 +1,52 @@ +/*---------------------------------*- C++ -*----------------------------------*\ +filetype dictionary; coding utf-8; version 0.1; local --; purpose --; +\*----------------------------------------------------------------------------*/ +_environment +{ + CASEDIR cases; + DUMPDIR dump; + LOGDIR logs; + RESULTDIR results; + TEMPLATEDIR template; +} +_layers +{ + layer1 + { + _samples + { + _names + ( + layer1_000 layer1_001 layer1_002 + ); + param1 + ( + 0 1 2 + ); + } + _commands + { + testwinvar + ( + 'echo variable %CASEDIR%' + ); + testlinvar + ( + "echo variable $CASEDIR" + ); + printwinenv + ( + 'echo environment & printenv | findstr CASEDIR' + ); + printlinenv + ( + 'echo environment ; printenv | grep CASEDIR' + ); + parse + ( + 'dictParser paramDict --verbose --log logs\dictParser.log --log-level INFO' + ); + } + _comment 'test some numbers'; + } +} diff --git a/tests/sampled.test_farnDict_1layer b/tests/sampled.test_farnDict_1layer new file mode 100644 index 00000000..ea151c51 --- /dev/null +++ b/tests/sampled.test_farnDict_1layer @@ -0,0 +1,67 @@ +/*---------------------------------*- C++ -*----------------------------------*\ +filetype dictionary; coding utf-8; version 0.1; local --; purpose --; +\*----------------------------------------------------------------------------*/ +_environment +{ + CASEDIR cases-1layer; + DUMPDIR dump; + LOGDIR logs; + RESULTDIR results; + TEMPLATEDIR template; +} +_layers +{ + layer0 + { + _sampling + { + _type fixed; + _names + ( + param0 + ); + _values + ( + ( + 1 2 3 + ) + ); + } + _condition + { + _action exclude; + _filter 'case in ["layer0_1"]'; + } + _commands + { + testwinvar + ( + 'echo variable %CASEDIR%' + ); + testlinvar + ( + "echo variable $CASEDIR" + ); + printwinenv + ( + 'echo environment & printenv | findstr CASEDIR' + ); + printlinenv + ( + 'echo environment ; printenv | grep CASEDIR' + ); + } + _samples + { + _names + ( + layer0_0 layer0_1 layer0_2 + ); + param0 + ( + 1 2 3 + ); + } + _comment 'level 0 for basename layer0'; + } +} diff --git a/tests/sampled.test_farnDict_2layer b/tests/sampled.test_farnDict_2layer new file mode 100644 index 00000000..aaa3cdf8 --- /dev/null +++ b/tests/sampled.test_farnDict_2layer @@ -0,0 +1,96 @@ +/*---------------------------------*- C++ -*----------------------------------*\ +filetype dictionary; coding utf-8; version 0.1; local --; purpose --; +\*----------------------------------------------------------------------------*/ +_environment +{ + CASEDIR cases-2layer; + DUMPDIR dump; + LOGDIR logs; + RESULTDIR results; + TEMPLATEDIR template; +} +_layers +{ + layer1 + { + _sampling + { + _type fixed; + _names + ( + param0 + ); + _values + ( + ( + 1 2 3 + ) + ); + } + _samples + { + _names + ( + layer1_0 layer1_1 layer1_2 + ); + param0 + ( + 1 2 3 + ); + } + _comment 'level 0 for basename layer1'; + } + layer2 + { + _sampling + { + _type fixed; + _names + ( + param1 + ); + _values + ( + ( + 2 4 8 + ) + ); + } + _condition + { + _action exclude; + _filter 'case in ["layer2_0", "layer2_1"]'; + } + _commands + { + testwinvar + ( + 'echo variable %CASEDIR%' + ); + testlinvar + ( + "echo variable $CASEDIR" + ); + printwinenv + ( + 'echo environment & printenv | findstr CASEDIR' + ); + printlinenv + ( + 'echo environment ; printenv | grep CASEDIR' + ); + } + _samples + { + _names + ( + layer2_0 layer2_1 layer2_2 + ); + param1 + ( + 2 4 8 + ); + } + _comment 'level 1 for basename layer2'; + } +} diff --git a/tests/test_cases.py b/tests/test_cases.py index 5ee49157..812c0ec1 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -67,7 +67,9 @@ def test_to_pandas_range_index() -> None: # Prepare case_1, case_2, case_3 = _create_cases() cases: Cases = Cases([case_1, case_2, case_3]) - df_assert: DataFrame = _create_dataframe(use_path_as_index=False, parameters_only=False) + df_assert: DataFrame = _create_dataframe( + use_path_as_index=False, parameters_only=False + ) # Execute df: DataFrame = cases.to_pandas(use_path_as_index=False) # Assert @@ -80,7 +82,9 @@ def test_to_pandas_range_index_parameters_only() -> None: # Prepare case_1, case_2, case_3 = _create_cases() cases: Cases = Cases([case_1, case_2, case_3]) - df_assert: DataFrame = _create_dataframe(use_path_as_index=False, parameters_only=True) + df_assert: DataFrame = _create_dataframe( + use_path_as_index=False, parameters_only=True + ) # Execute df: DataFrame = cases.to_pandas(use_path_as_index=False, parameters_only=True) # Assert @@ -93,7 +97,9 @@ def test_to_pandas_path_index() -> None: # Prepare case_1, case_2, case_3 = _create_cases() cases: Cases = Cases([case_1, case_2, case_3]) - df_assert: DataFrame = _create_dataframe(use_path_as_index=True, parameters_only=False) + df_assert: DataFrame = _create_dataframe( + use_path_as_index=True, parameters_only=False + ) # Execute df: DataFrame = cases.to_pandas() # Assert @@ -106,7 +112,9 @@ def test_to_pandas_path_index_parameters_only() -> None: # Prepare case_1, case_2, case_3 = _create_cases() cases: Cases = Cases([case_1, case_2, case_3]) - df_assert: DataFrame = _create_dataframe(use_path_as_index=True, parameters_only=True) + df_assert: DataFrame = _create_dataframe( + use_path_as_index=True, parameters_only=True + ) # Execute df: DataFrame = cases.to_pandas(parameters_only=True) # Assert @@ -140,7 +148,9 @@ def _create_cases() -> tuple[Case, Case, Case]: parameter_31 = Parameter("param_1", 31.1) parameter_32 = Parameter("param_2", 32.2) parameter_33 = Parameter("param_3", 33.3) - case_3: Case = Case(case="case_3", parameters=[parameter_31, parameter_32, parameter_33]) + case_3: Case = Case( + case="case_3", parameters=[parameter_31, parameter_32, parameter_33] + ) return (case_1, case_2, case_3) @@ -278,7 +288,9 @@ def test_filter_level_0_valid_only() -> None: case_dir: Path = Path.cwd() cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) cases_not_modified_assert: Cases = deepcopy(cases) - cases_filtered_assert: Cases = Cases([case for case in cases if case.level == 0 and case.is_valid]) + cases_filtered_assert: Cases = Cases( + [case for case in cases if case.level == 0 and case.is_valid] + ) # Execute cases_filtered: Cases = cases.filter(0, valid_only=True) # Assert @@ -296,7 +308,9 @@ def test_filter_level_1_valid_only() -> None: case_dir: Path = Path.cwd() cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) cases_not_modified_assert: Cases = deepcopy(cases) - cases_filtered_assert: Cases = Cases([case for case in cases if case.level == 1 and case.is_valid]) + cases_filtered_assert: Cases = Cases( + [case for case in cases if case.level == 1 and case.is_valid] + ) # Execute cases_filtered: Cases = cases.filter(1, valid_only=True) # Assert @@ -314,7 +328,9 @@ def test_filter_level_minus_1_valid_only() -> None: case_dir: Path = Path.cwd() cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) cases_not_modified_assert: Cases = deepcopy(cases) - cases_filtered_assert: Cases = Cases([case for case in cases if case.is_leaf and case.is_valid]) + cases_filtered_assert: Cases = Cases( + [case for case in cases if case.is_leaf and case.is_valid] + ) # Execute cases_filtered: Cases = cases.filter(-1, valid_only=True) # Assert @@ -332,7 +348,9 @@ def test_filter_default_arguments() -> None: case_dir: Path = Path.cwd() cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) cases_not_modified_assert: Cases = deepcopy(cases) - cases_filtered_assert: Cases = Cases([case for case in cases if case.is_leaf and case.is_valid]) + cases_filtered_assert: Cases = Cases( + [case for case in cases if case.is_leaf and case.is_valid] + ) # Execute cases_filtered: Cases = cases.filter() # Assert diff --git a/tests/test_cases.py.orig b/tests/test_cases.py.orig new file mode 100644 index 00000000..79a75e63 --- /dev/null +++ b/tests/test_cases.py.orig @@ -0,0 +1,703 @@ +<<<<<<< HEAD +# pyright: reportUnknownMemberType=false +# pyright: reportArgumentType=false + +from copy import deepcopy +from pathlib import Path +from typing import Any, List, Tuple + +import numpy as np +from dictIO import CppDict, DictReader +from dictIO.utils.path import relative_path +from numpy import ndarray +from pandas import DataFrame + +from farn import create_cases, create_samples +from farn.core import Case, Cases, Parameter + + +def test_cases(): + # Prepare + case_1, case_2, case_3 = _create_cases() + case_list_assert: List[Case] = [case_1, case_2, case_3] + # Execute + cases: Cases = Cases([case_1, case_2, case_3]) + cases_by_append: Cases = Cases() + cases_by_append.append(case_1) + cases_by_append.append(case_2) + cases_by_append.append(case_3) + cases_by_extend: Cases = Cases() + cases_by_extend.extend([case_1, case_2, case_3]) + # Assert + assert len(cases) == 3 + assert len(cases_by_append) == 3 + assert len(cases_by_extend) == 3 + _assert_type_and_equality(cases, case_list_assert) + _assert_type_and_equality(cases_by_append, case_list_assert) + _assert_type_and_equality(cases_by_extend, case_list_assert) + _assert_sequence(cases, case_1, case_2, case_3) + _assert_sequence(cases_by_append, case_1, case_2, case_3) + _assert_sequence(cases_by_extend, case_1, case_2, case_3) + assert len(cases[0].parameters) == 1 + assert len(cases[1].parameters) == 2 + assert len(cases[2].parameters) == 3 + assert cases[2].parameters[0].name == "param_1" # type: ignore + assert cases[2].parameters[1].name == "param_2" # type: ignore + assert cases[2].parameters[2].name == "param_3" # type: ignore + assert cases[2].parameters[0].value == 31.1 # type: ignore + assert cases[2].parameters[1].value == 32.2 # type: ignore + assert cases[2].parameters[2].value == 33.3 # type: ignore + assert cases[0].case == "case_1" + assert cases[1].case == "case_2" + assert cases[2].case == "case_3" + + +def _assert_type_and_equality(cases: Cases, case_list_assert: List[Case]): + assert cases == case_list_assert + assert isinstance(cases, List) + assert isinstance(cases, Cases) + + +def _assert_sequence( + cases: Cases, case_assert_1: Case, case_assert_2: Case, case_assert_3: Case +): + assert cases[0] is case_assert_1 + assert cases[1] is case_assert_2 + assert cases[2] is case_assert_3 + + +def test_to_pandas_range_index(): + # Prepare + case_1, case_2, case_3 = _create_cases() + cases: Cases = Cases([case_1, case_2, case_3]) + df_assert: DataFrame = _create_dataframe( + use_path_as_index=False, parameters_only=False + ) + # Execute + df: DataFrame = cases.to_pandas(use_path_as_index=False) + # Assert + assert df.shape == df_assert.shape + assert df.shape == (3, 5) + assert df.equals(df_assert) + + +def test_to_pandas_range_index_parameters_only(): + # Prepare + case_1, case_2, case_3 = _create_cases() + cases: Cases = Cases([case_1, case_2, case_3]) + df_assert: DataFrame = _create_dataframe( + use_path_as_index=False, parameters_only=True + ) + # Execute + df: DataFrame = cases.to_pandas(use_path_as_index=False, parameters_only=True) + # Assert + assert df.shape == df_assert.shape + assert df.shape == (3, 3) + assert df.equals(df_assert) + + +def test_to_pandas_path_index(): + # Prepare + case_1, case_2, case_3 = _create_cases() + cases: Cases = Cases([case_1, case_2, case_3]) + df_assert: DataFrame = _create_dataframe( + use_path_as_index=True, parameters_only=False + ) + # Execute + df: DataFrame = cases.to_pandas() + # Assert + assert df.shape == df_assert.shape + assert df.shape == (3, 4) + assert df.equals(df_assert) + + +def test_to_pandas_path_index_parameters_only(): + # Prepare + case_1, case_2, case_3 = _create_cases() + cases: Cases = Cases([case_1, case_2, case_3]) + df_assert: DataFrame = _create_dataframe( + use_path_as_index=True, parameters_only=True + ) + # Execute + df: DataFrame = cases.to_pandas(parameters_only=True) + # Assert + assert df.shape == df_assert.shape + assert df.shape == (3, 3) + assert df.equals(df_assert) + + +def test_to_numpy(): + # Prepare + case_1, case_2, case_3 = _create_cases() + cases: Cases = Cases([case_1, case_2, case_3]) + array_assert: ndarray[Any, Any] = _create_ndarray() + # Execute + array: ndarray[Any, Any] = cases.to_numpy() + # Assert + assert array.shape == array_assert.shape + assert array.shape == (3, 3) + assert str(array) == str(array_assert) + + +def _create_cases() -> Tuple[Case, Case, Case]: + parameter_11 = Parameter("param_1", 11.1) + parameter_12 = Parameter("param_2", 12.2) # noqa: F841 + parameter_13 = Parameter("param_3", 13.3) # noqa: F841 + case_1: Case = Case(case="case_1", parameters=[parameter_11]) + parameter_21 = Parameter("param_1", 21.1) + parameter_22 = Parameter("param_2", 22.2) + parameter_23 = Parameter("param_3", 23.3) # noqa: F841 + case_2: Case = Case(case="case_2", parameters=[parameter_21, parameter_22]) + parameter_31 = Parameter("param_1", 31.1) + parameter_32 = Parameter("param_2", 32.2) + parameter_33 = Parameter("param_3", 33.3) + case_3: Case = Case( + case="case_3", parameters=[parameter_31, parameter_32, parameter_33] + ) + return (case_1, case_2, case_3) + + +def _create_dataframe(use_path_as_index: bool, parameters_only: bool) -> DataFrame: + cwd: Path = Path.cwd() + path: str = str(relative_path(cwd, cwd)) + index: List[int] = [0, 1, 2] + columns: List[str] = ["case", "path", "param_1", "param_2", "param_3"] + values: List[List[Any]] + values = [ + ["case_1", path, 11.1, None, None], + ["case_2", path, 21.1, 22.2, None], + ["case_3", path, 31.1, 32.2, 33.3], + ] + df: DataFrame = DataFrame(data=values, index=index, columns=columns) + if parameters_only: + df.drop(["case"], axis=1, inplace=True) + if not use_path_as_index: + df.drop(["path"], axis=1, inplace=True) + if use_path_as_index: + df.set_index("path", inplace=True) + return df + + +def _create_ndarray() -> ndarray[Any, Any]: + array: ndarray[Any, Any] = np.array( + [ + [11.1, np.nan, np.nan], + [21.1, 22.2, np.nan], + [31.1, 32.2, 33.3], + ] + ) + return array + + +def test_filter_all(): + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: CppDict = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_all_assert: Cases = deepcopy(cases) + # Execute + cases_all: Cases = cases.filter([0, 1], valid_only=False) + # Assert + assert isinstance(cases_all, Cases) + assert len(cases_all) == len(cases_all_assert) + assert cases_all == cases_all_assert + assert cases == cases_not_modified_assert + + +def test_filter_level_0(): + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: CppDict = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases([case for case in cases if case.level == 0]) + # Execute + cases_filtered: Cases = cases.filter(0, valid_only=False) + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert + + +def test_filter_level_1(): + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: CppDict = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases([case for case in cases if case.level == 1]) + # Execute + cases_filtered: Cases = cases.filter(1, valid_only=False) + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert + + +def test_filter_level_minus_1(): + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: CppDict = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases([case for case in cases if case.is_leaf]) + # Execute + cases_filtered: Cases = cases.filter(-1, valid_only=False) + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert + + +def test_filter_all_valid_only(): + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: CppDict = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_all_assert: Cases = Cases([case for case in cases if case.is_valid]) + # Execute + cases_all: Cases = cases.filter([0, 1], valid_only=True) + # Assert + assert isinstance(cases_all, Cases) + assert len(cases_all) == len(cases_all_assert) + assert cases_all == cases_all_assert + assert cases == cases_not_modified_assert + + +def test_filter_level_0_valid_only(): + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: CppDict = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases( + [case for case in cases if case.level == 0 and case.is_valid] + ) + # Execute + cases_filtered: Cases = cases.filter(0, valid_only=True) + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert + + +def test_filter_level_1_valid_only(): + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: CppDict = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases( + [case for case in cases if case.level == 1 and case.is_valid] + ) + # Execute + cases_filtered: Cases = cases.filter(1, valid_only=True) + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert + + +def test_filter_level_minus_1_valid_only(): + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: CppDict = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases( + [case for case in cases if case.is_leaf and case.is_valid] + ) + # Execute + cases_filtered: Cases = cases.filter(-1, valid_only=True) + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert + + +def test_filter_default_arguments(): + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: CppDict = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases( + [case for case in cases if case.is_leaf and case.is_valid] + ) + # Execute + cases_filtered: Cases = cases.filter() + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert +======= +# pyright: reportUnknownMemberType=false +# pyright: reportArgumentType=false + +from copy import deepcopy +from pathlib import Path +from typing import Any + +import numpy as np +from dictIO import DictReader, SDict +from dictIO.utils.path import relative_path +from numpy import ndarray +from pandas import DataFrame + +from farn import create_cases, create_samples +from farn.core import Case, Cases, Parameter + + +def test_cases() -> None: + # Prepare + case_1, case_2, case_3 = _create_cases() + case_list_assert: list[Case] = [case_1, case_2, case_3] + # Execute + cases: Cases = Cases([case_1, case_2, case_3]) + cases_by_append: Cases = Cases() + cases_by_append.append(case_1) + cases_by_append.append(case_2) + cases_by_append.append(case_3) + cases_by_extend: Cases = Cases() + cases_by_extend.extend([case_1, case_2, case_3]) + # Assert + assert len(cases) == 3 + assert len(cases_by_append) == 3 + assert len(cases_by_extend) == 3 + _assert_type_and_equality(cases, case_list_assert) + _assert_type_and_equality(cases_by_append, case_list_assert) + _assert_type_and_equality(cases_by_extend, case_list_assert) + _assert_sequence(cases, case_1, case_2, case_3) + _assert_sequence(cases_by_append, case_1, case_2, case_3) + _assert_sequence(cases_by_extend, case_1, case_2, case_3) + assert len(cases[0].parameters) == 1 + assert len(cases[1].parameters) == 2 + assert len(cases[2].parameters) == 3 + assert cases[2].parameters[0].name == "param_1" + assert cases[2].parameters[1].name == "param_2" + assert cases[2].parameters[2].name == "param_3" + assert cases[2].parameters[0].value == 31.1 + assert cases[2].parameters[1].value == 32.2 + assert cases[2].parameters[2].value == 33.3 + assert cases[0].case == "case_1" + assert cases[1].case == "case_2" + assert cases[2].case == "case_3" + + +def _assert_type_and_equality(cases: Cases, case_list_assert: list[Case]) -> None: + assert cases == case_list_assert + assert isinstance(cases, list) + assert isinstance(cases, Cases) + + +def _assert_sequence(cases: Cases, case_assert_1: Case, case_assert_2: Case, case_assert_3: Case) -> None: + assert cases[0] is case_assert_1 + assert cases[1] is case_assert_2 + assert cases[2] is case_assert_3 + + +def test_to_pandas_range_index() -> None: + # Prepare + case_1, case_2, case_3 = _create_cases() + cases: Cases = Cases([case_1, case_2, case_3]) + df_assert: DataFrame = _create_dataframe(use_path_as_index=False, parameters_only=False) + # Execute + df: DataFrame = cases.to_pandas(use_path_as_index=False) + # Assert + assert df.shape == df_assert.shape + assert df.shape == (3, 5) + assert df.equals(df_assert) + + +def test_to_pandas_range_index_parameters_only() -> None: + # Prepare + case_1, case_2, case_3 = _create_cases() + cases: Cases = Cases([case_1, case_2, case_3]) + df_assert: DataFrame = _create_dataframe(use_path_as_index=False, parameters_only=True) + # Execute + df: DataFrame = cases.to_pandas(use_path_as_index=False, parameters_only=True) + # Assert + assert df.shape == df_assert.shape + assert df.shape == (3, 3) + assert df.equals(df_assert) + + +def test_to_pandas_path_index() -> None: + # Prepare + case_1, case_2, case_3 = _create_cases() + cases: Cases = Cases([case_1, case_2, case_3]) + df_assert: DataFrame = _create_dataframe(use_path_as_index=True, parameters_only=False) + # Execute + df: DataFrame = cases.to_pandas() + # Assert + assert df.shape == df_assert.shape + assert df.shape == (3, 4) + assert df.equals(df_assert) + + +def test_to_pandas_path_index_parameters_only() -> None: + # Prepare + case_1, case_2, case_3 = _create_cases() + cases: Cases = Cases([case_1, case_2, case_3]) + df_assert: DataFrame = _create_dataframe(use_path_as_index=True, parameters_only=True) + # Execute + df: DataFrame = cases.to_pandas(parameters_only=True) + # Assert + assert df.shape == df_assert.shape + assert df.shape == (3, 3) + assert df.equals(df_assert) + + +def test_to_numpy() -> None: + # Prepare + case_1, case_2, case_3 = _create_cases() + cases: Cases = Cases([case_1, case_2, case_3]) + array_assert: np.ndarray[Any, np.dtype[np.float64]] = _create_ndarray() + # Execute + array: ndarray[tuple[int, int], np.dtype[np.float64]] = cases.to_numpy() + # Assert + assert array.shape == array_assert.shape + assert array.shape == (3, 3) + assert str(array) == str(array_assert) + + +def _create_cases() -> tuple[Case, Case, Case]: + parameter_11 = Parameter("param_1", 11.1) + parameter_12 = Parameter("param_2", 12.2) # noqa: F841 + parameter_13 = Parameter("param_3", 13.3) # noqa: F841 + case_1: Case = Case(case="case_1", parameters=[parameter_11]) + parameter_21 = Parameter("param_1", 21.1) + parameter_22 = Parameter("param_2", 22.2) + parameter_23 = Parameter("param_3", 23.3) # noqa: F841 + case_2: Case = Case(case="case_2", parameters=[parameter_21, parameter_22]) + parameter_31 = Parameter("param_1", 31.1) + parameter_32 = Parameter("param_2", 32.2) + parameter_33 = Parameter("param_3", 33.3) + case_3: Case = Case(case="case_3", parameters=[parameter_31, parameter_32, parameter_33]) + return (case_1, case_2, case_3) + + +def _create_dataframe( + *, + use_path_as_index: bool, + parameters_only: bool, +) -> DataFrame: + cwd: Path = Path.cwd() + path: str = str(relative_path(cwd, cwd)) + index: list[int] = [0, 1, 2] + columns: list[str] = ["case", "path", "param_1", "param_2", "param_3"] + values: list[list[Any]] + values = [ + ["case_1", path, 11.1, None, None], + ["case_2", path, 21.1, 22.2, None], + ["case_3", path, 31.1, 32.2, 33.3], + ] + data: DataFrame = DataFrame(data=values, index=index, columns=columns) + if parameters_only: + data = data.drop(["case"], axis=1) + if not use_path_as_index: + data = data.drop(["path"], axis=1) + if use_path_as_index: + data = data.set_index("path") + return data + + +def _create_ndarray() -> np.ndarray[Any, np.dtype[np.float64]]: + array: np.ndarray[Any, np.dtype[np.float64]] = np.array( + [ + [11.1, np.nan, np.nan], + [21.1, 22.2, np.nan], + [31.1, 32.2, 33.3], + ] + ) + return array + + +def test_filter_all() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: SDict[str, Any] = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_all_assert: Cases = deepcopy(cases) + # Execute + cases_all: Cases = cases.filter([0, 1], valid_only=False) + # Assert + assert isinstance(cases_all, Cases) + assert len(cases_all) == len(cases_all_assert) + assert cases_all == cases_all_assert + assert cases == cases_not_modified_assert + + +def test_filter_level_0() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: SDict[str, Any] = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases([case for case in cases if case.level == 0]) + # Execute + cases_filtered: Cases = cases.filter(0, valid_only=False) + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert + + +def test_filter_level_1() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: SDict[str, Any] = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases([case for case in cases if case.level == 1]) + # Execute + cases_filtered: Cases = cases.filter(1, valid_only=False) + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert + + +def test_filter_level_minus_1() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: SDict[str, Any] = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases([case for case in cases if case.is_leaf]) + # Execute + cases_filtered: Cases = cases.filter(-1, valid_only=False) + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert + + +def test_filter_all_valid_only() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: SDict[str, Any] = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_all_assert: Cases = Cases([case for case in cases if case.is_valid]) + # Execute + cases_all: Cases = cases.filter([0, 1], valid_only=True) + # Assert + assert isinstance(cases_all, Cases) + assert len(cases_all) == len(cases_all_assert) + assert cases_all == cases_all_assert + assert cases == cases_not_modified_assert + + +def test_filter_level_0_valid_only() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: SDict[str, Any] = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases([case for case in cases if case.level == 0 and case.is_valid]) + # Execute + cases_filtered: Cases = cases.filter(0, valid_only=True) + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert + + +def test_filter_level_1_valid_only() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: SDict[str, Any] = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases([case for case in cases if case.level == 1 and case.is_valid]) + # Execute + cases_filtered: Cases = cases.filter(1, valid_only=True) + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert + + +def test_filter_level_minus_1_valid_only() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: SDict[str, Any] = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases([case for case in cases if case.is_leaf and case.is_valid]) + # Execute + cases_filtered: Cases = cases.filter(-1, valid_only=True) + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert + + +def test_filter_default_arguments() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + farn_dict: SDict[str, Any] = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + cases: Cases = create_cases(farn_dict, case_dir, valid_only=False) + cases_not_modified_assert: Cases = deepcopy(cases) + cases_filtered_assert: Cases = Cases([case for case in cases if case.is_leaf and case.is_valid]) + # Execute + cases_filtered: Cases = cases.filter() + # Assert + assert isinstance(cases_filtered, Cases) + assert len(cases_filtered) == len(cases_filtered_assert) + assert cases_filtered == cases_filtered_assert + assert cases == cases_not_modified_assert +>>>>>>> origin/main diff --git a/tests/test_farn.py b/tests/test_farn.py index a897eb4a..0e9f2063 100644 --- a/tests/test_farn.py +++ b/tests/test_farn.py @@ -35,11 +35,21 @@ def test_create_samples() -> None: assert "_samples" in farn_dict["_layers"]["cp"] assert "_samples" in farn_dict["_layers"]["hilbert"] assert "_samples" in farn_dict["_layers"]["mp"] - assert len(farn_dict["_layers"]["gp"]) == len(sampled_farn_dict_assert["_layers"]["gp"]) - assert len(farn_dict["_layers"]["lhsvar"]) == len(sampled_farn_dict_assert["_layers"]["lhsvar"]) - assert len(farn_dict["_layers"]["cp"]) == len(sampled_farn_dict_assert["_layers"]["cp"]) - assert len(farn_dict["_layers"]["hilbert"]) == len(sampled_farn_dict_assert["_layers"]["hilbert"]) - assert len(farn_dict["_layers"]["mp"]) == len(sampled_farn_dict_assert["_layers"]["mp"]) + assert len(farn_dict["_layers"]["gp"]) == len( + sampled_farn_dict_assert["_layers"]["gp"] + ) + assert len(farn_dict["_layers"]["lhsvar"]) == len( + sampled_farn_dict_assert["_layers"]["lhsvar"] + ) + assert len(farn_dict["_layers"]["cp"]) == len( + sampled_farn_dict_assert["_layers"]["cp"] + ) + assert len(farn_dict["_layers"]["hilbert"]) == len( + sampled_farn_dict_assert["_layers"]["hilbert"] + ) + assert len(farn_dict["_layers"]["mp"]) == len( + sampled_farn_dict_assert["_layers"]["mp"] + ) def test_create_cases() -> None: @@ -168,7 +178,9 @@ def test_sample_exclude_filtering(caplog: pytest.LogCaptureFixture) -> None: out: str = caplog.text.rstrip() # Assert assert "The filter expression 'index != 1' evaluated to True." in out - assert "The filter expression 'abs(param0 * param1) >= 3.5' evaluated to True." in out + assert ( + "The filter expression 'abs(param0 * param1) >= 3.5' evaluated to True." in out + ) assert "Action 'exclude' performed. Case lhsVariation_" in out diff --git a/tests/test_farn.py.orig b/tests/test_farn.py.orig new file mode 100644 index 00000000..fc2b0838 --- /dev/null +++ b/tests/test_farn.py.orig @@ -0,0 +1,299 @@ +import os +from pathlib import Path +from typing import Any + +import pytest +from dictIO import DictReader, SDict + +from farn import create_cases, create_samples, run_farn +from farn.core import Cases + + +def test_sample() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_v4") + sampled_file = Path(f"sampled.{farn_dict_file.name}") + assert not sampled_file.exists() + # Execute + _ = run_farn(farn_dict_file, sample=True) + # Assert + assert sampled_file.exists() + + +def test_create_samples() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_v4") + farn_dict: SDict[str, Any] = DictReader.read(farn_dict_file, comments=False) + sampled_file = Path(f"sampled.{farn_dict_file.name}") + _ = run_farn(farn_dict_file, sample=True) + sampled_farn_dict_assert: SDict[str, Any] = DictReader.read(sampled_file) + # Execute + create_samples(farn_dict) + # Assert + assert "_samples" in farn_dict["_layers"]["gp"] + assert "_samples" in farn_dict["_layers"]["lhsvar"] + assert "_samples" in farn_dict["_layers"]["cp"] + assert "_samples" in farn_dict["_layers"]["hilbert"] + assert "_samples" in farn_dict["_layers"]["mp"] + assert len(farn_dict["_layers"]["gp"]) == len( + sampled_farn_dict_assert["_layers"]["gp"] + ) + assert len(farn_dict["_layers"]["lhsvar"]) == len( + sampled_farn_dict_assert["_layers"]["lhsvar"] + ) + assert len(farn_dict["_layers"]["cp"]) == len( + sampled_farn_dict_assert["_layers"]["cp"] + ) + assert len(farn_dict["_layers"]["hilbert"]) == len( + sampled_farn_dict_assert["_layers"]["hilbert"] + ) + assert len(farn_dict["_layers"]["mp"]) == len( + sampled_farn_dict_assert["_layers"]["mp"] + ) + + +def test_create_cases() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_no_filtering") + farn_dict: SDict[str, Any] = DictReader.read(farn_dict_file, comments=False) + create_samples(farn_dict) + case_dir: Path = Path.cwd() + # Execute + cases: Cases = create_cases(farn_dict, case_dir, valid_only=True) + # Assert + assert isinstance(cases, Cases) + assert len(cases) == 12 + + +def test_generate(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict") + sampled_file = Path(f"sampled.{farn_dict_file.name}") + _ = run_farn(farn_dict_file, sample=True) + caplog.clear() + # Execute + _ = run_farn(sampled_file, generate=True) + # Assert + assert Path("cases").exists() + assert Path("cases/layer1_0").exists() + assert Path("cases/layer1_1").exists() + assert Path("cases/layer1_2").exists() + + +def test_regenerate(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict") + sampled_file = Path(f"sampled.{farn_dict_file.name}") + _ = run_farn(farn_dict_file, sample=True) + caplog.clear() + # Execute + _ = run_farn(sampled_file, generate=True) + _ = run_farn(sampled_file, generate=True) + # Assert + assert Path("cases").exists() + assert Path("cases/layer1_0").exists() + assert Path("cases/layer1_1").exists() + assert Path("cases/layer1_2").exists() + + +def test_always_distribute_parameters() -> None: + # Prepare + farn_dict_file = Path("test_farnDict_always_distribute") + sampled_file = Path(f"sampled.{farn_dict_file.name}") + _ = run_farn(farn_dict_file, sample=True) + # Execute + _ = run_farn(sampled_file, generate=True) + # Assert + assert Path("cases").exists() + assert Path("cases/linspaceLayer_0").exists() + assert Path("cases/linspaceLayer_0").exists() + # test one output + param_dict_file = Path("cases/linspaceLayer_0/paramDict") + param_dict: SDict[str, Any] = DictReader.read(param_dict_file, comments=False) + + assert param_dict["param0"] == 0.0 + assert param_dict["param1"] == 1.0 + assert param_dict["param2"] == 2.0 + + +# @TODO: There is nothing actually asserted in this test. -> Frank to check. +# CLAROS, 2022-05-13 +def test_execute(caplog: pytest.LogCaptureFixture) -> None: + # sourcery skip: no-conditionals-in-tests + # Prepare + farn_dict_file = Path("test_farnDict") + sampled_file = Path(f"sampled.{farn_dict_file.name}") + _ = run_farn(farn_dict_file, sample=True) + _ = run_farn(sampled_file, generate=True) + caplog.clear() + # Execute +<<<<<<< HEAD + if platform.system() == "Linux": + _ = os.system("farn.py sampled.test_farnDict -e testlinvar") + _ = os.system("farn.py sampled.test_farnDict -e printlinenv") + else: + _ = os.system( + f"python -m farn.cli.farn {sampled_file.name} --execute testwinvar" + ) + _ = os.system( + f"python -m farn.cli.farn {sampled_file.name} --execute printwinenv" + ) +======= + _ = os.system(f"farn {sampled_file.name} --execute testwinvar") # noqa: S605 + _ = os.system(f"farn {sampled_file.name} --execute printwinenv") # noqa: S605 +>>>>>>> origin/main + # Assert + + +def test_sample_logging_verbosity_default(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict_no_filtering") + # Execute + _ = run_farn(farn_dict_file, sample=True) + out: str = caplog.text.rstrip() + # Assert + assert "Successfully listed 10 valid cases. 0 invalid case was excluded." in out + + +def test_generate_logging_verbosity_default(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict_no_filtering") + sampled_file = Path(f"sampled.{farn_dict_file.name}") + _ = run_farn(farn_dict_file, sample=True) + caplog.clear() + # Execute + _ = run_farn(sampled_file, generate=True) + out: str = caplog.text.rstrip() + # Assert + assert "Successfully created 10 paramDict files in 10 case folders." in out + assert "creating case folder" not in out + + +def test_sample_failed_filtering(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict_failed_filtering") + sampled_file = Path(f"sampled.{farn_dict_file.name}") + # Execute + _ = run_farn(farn_dict_file, sample=True) + out: str = caplog.text.rstrip() + # Assert + # sampled dict should still have been written, although filtering could not successfully be executed + assert sampled_file.exists() + assert "evaluation of the filter expression failed" in out + + +def test_sample_exclude_filtering(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict_exclude_filtering") + caplog.set_level("DEBUG") + # Execute + _ = run_farn(farn_dict_file, sample=True) + out: str = caplog.text.rstrip() + # Assert + assert "The filter expression 'index != 1' evaluated to True." in out + assert ( + "The filter expression 'abs(param0 * param1) >= 3.5' evaluated to True." in out + ) + assert "Action 'exclude' performed. Case lhsVariation_" in out + + +def test_sample_filtering_one_layer_filter_layer(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict_one_layer_filter_layer") + # Execute + _ = run_farn(farn_dict_file, sample=True) + out: str = caplog.text.rstrip() + # Assert + assert "Successfully listed 2 valid cases. 1 invalid case was excluded." in out + + +def test_generate_filtering_one_layer_filter_layer(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict_one_layer_filter_layer") + sampled_file = Path(f"sampled.{farn_dict_file.name}") + _ = run_farn(farn_dict_file, sample=True) + caplog.clear() + # Execute + _ = run_farn(sampled_file, generate=True) + out: str = caplog.text.rstrip() + # Assert + # case folder 'layer0_1' must not exist + assert not Path("cases_one_layer/layer0_1").exists() + assert "Successfully listed 2 valid cases. 1 invalid case was excluded." in out + assert "Successfully created 2 case folders." in out + assert "Successfully created 2 paramDict files in 2 case folders." in out + + +def test_sample_filtering_one_layer_filter_param(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict_one_layer_filter_param") + # Execute + _ = run_farn(farn_dict_file, sample=True) + out: str = caplog.text.rstrip() + # Assert + assert "Successfully listed 2 valid cases. 1 invalid case was excluded." in out + + +def test_generate_filtering_one_layer_filter_param(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict_one_layer_filter_param") + sampled_file = Path(f"sampled.{farn_dict_file.name}") + _ = run_farn(farn_dict_file, sample=True) + caplog.clear() + # Execute + _ = run_farn(sampled_file, generate=True) + out: str = caplog.text.rstrip() + # Assert + assert Path("cases_one_layer").exists() + assert "Successfully created 2 paramDict files in 2 case folders." in out + + +def test_sample_filtering_two_layers_filter_layer(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict_two_layers_filter_layer") + # Execute + _ = run_farn(farn_dict_file, sample=True) + out: str = caplog.text.rstrip() + # Assert + assert "Successfully listed 3 valid cases. 6 invalid cases were excluded." in out + + +def test_generate_filtering_two_layers_filter_layer(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict_two_layers_filter_layer") + sampled_file = Path(f"sampled.{farn_dict_file.name}") + _ = run_farn(farn_dict_file, sample=True) + caplog.clear() + # Execute + _ = run_farn(sampled_file, generate=True) + out: str = caplog.text.rstrip() + # Assert + # case folder 'layer1_0/layers_0' must not exist + assert not Path("cases_two_layers/layer1_0/layers_0").exists() + assert "Successfully listed 3 valid cases. 6 invalid cases were excluded." in out + assert "Successfully created 6 case folders." in out + assert "Successfully created 3 paramDict files in 3 case folders." in out + + +def test_sample_filtering_two_layers_filter_param(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict_two_layers_filter_param") + # Execute + _ = run_farn(farn_dict_file, sample=True) + out: str = caplog.text.rstrip() + # Assert + assert "Successfully listed 3 valid cases. 6 invalid cases were excluded." in out + + +def test_generate_filtering_two_layers_filter_param(caplog: pytest.LogCaptureFixture) -> None: + # Prepare + farn_dict_file = Path("test_farnDict_two_layers_filter_param") + sampled_file = Path(f"sampled.{farn_dict_file.name}") + _ = run_farn(farn_dict_file, sample=True) + caplog.clear() + # Execute + _ = run_farn(sampled_file, generate=True) + out: str = caplog.text.rstrip() + # Assert + assert Path("cases_two_layers").exists() + assert "Successfully created 3 paramDict files in 3 case folders." in out diff --git a/tests/test_sampling.py b/tests/test_sampling.py index e0ed63d7..76c71413 100644 --- a/tests/test_sampling.py +++ b/tests/test_sampling.py @@ -96,7 +96,13 @@ def test_linSpace_sampling_one_parameter() -> None: "param1", } assert len(samples["_case_name"]) == 5 - assert samples["_case_name"] == ["layer0_0", "layer0_1", "layer0_2", "layer0_3", "layer0_4"] + assert samples["_case_name"] == [ + "layer0_0", + "layer0_1", + "layer0_2", + "layer0_3", + "layer0_4", + ] assert len(samples["param1"]) == 5 assert np.allclose(samples["param1"], [0.5, 0.6, 0.7, 0.8, 0.9]) @@ -123,7 +129,13 @@ def test_linSpace_sampling_two_parameters() -> None: "param2", } assert len(samples["_case_name"]) == 5 - assert samples["_case_name"] == ["layer0_0", "layer0_1", "layer0_2", "layer0_3", "layer0_4"] + assert samples["_case_name"] == [ + "layer0_0", + "layer0_1", + "layer0_2", + "layer0_3", + "layer0_4", + ] assert len(samples["param1"]) == 5 assert np.allclose(samples["param1"], [0.5, 0.6, 0.7, 0.8, 0.9]) assert len(samples["param2"]) == 5 @@ -1454,3 +1466,92 @@ def test_hilbertCurve_sampling_three_parameters_including_bounding_box() -> None assert np.allclose(samples["param2"], param2_values_expected) assert len(samples["param3"]) == 28 assert np.allclose(samples["param3"], param3_values_expected) + + +def test_factorial_sampling_three_parameters(): + # Prepare + sampling: DiscreteSampling = DiscreteSampling() + sampling.set_sampling_type(sampling_type="factorial") + sampling.set_sampling_parameters( + sampling_parameters={ + "_names": ["param1", "param2", "param3"], + "_ranges": [(-10.0, 10.0), (0.0, 3.5), (0.0, 1.1)], + "_listOfSamples": [3, 2, 2], + }, + layer_name="layer0", + ) + case_names_expected: List[str] = [ + "layer0_00", + "layer0_01", + "layer0_02", + "layer0_03", + "layer0_04", + "layer0_05", + "layer0_06", + "layer0_07", + "layer0_08", + "layer0_09", + "layer0_10", + "layer0_11", + ] + param1_values_expected: List[float] = [ + -10.0, + 0.0, + 10.0, + -10.0, + 0.0, + 10.0, + -10.0, + 0.0, + 10.0, + -10.0, + 0.0, + 10.0, + ] + param2_values_expected: List[float] = [ + 0.0, + 0.0, + 0.0, + 3.5, + 3.5, + 3.5, + 0.0, + 0.0, + 0.0, + 3.5, + 3.5, + 3.5, + ] + param3_values_expected: List[float] = [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.1, + 1.1, + 1.1, + 1.1, + 1.1, + 1.1, + ] + + # Execute + samples: Dict[str, List[Any]] = sampling.generate_samples() + # Assert + assert len(samples) == 4 + assert samples.keys() == { + "_case_name", + "param1", + "param2", + "param3", + } + assert len(samples["_case_name"]) == 12 + assert samples["_case_name"] == case_names_expected + assert len(samples["param1"]) == 12 + assert np.allclose(samples["param1"], param1_values_expected) + assert len(samples["param2"]) == 12 + assert np.allclose(samples["param2"], param2_values_expected) + assert len(samples["param3"]) == 12 + assert np.allclose(samples["param3"], param3_values_expected)