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/src/farn/cli/farn.py b/src/farn/cli/farn.py index 5b71f1a3..46480cfe 100755 --- a/src/farn/cli/farn.py +++ b/src/farn/cli/farn.py @@ -147,7 +147,9 @@ def main(): # Configure Logging # ..to console - log_level_console: str = "INFO" # default would usually be 'WARNING', but for farn it makes sense to set default level to 'INFO' + log_level_console: str = ( + "INFO" # default would usually be 'WARNING', but for farn it makes sense to set default level to '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 diff --git a/src/farn/core/case.py b/src/farn/core/case.py index fb00a439..5265cda1 100644 --- a/src/farn/core/case.py +++ b/src/farn/core/case.py @@ -225,7 +225,7 @@ def add_parameters( MutableSequence[Parameter], MutableMapping[str, str], None ] = None, ): - """Add extra parameters manually""" + """Add extra parameters manually.""" if isinstance(parameters, MutableSequence) and isinstance( parameters[0], Parameter ): @@ -290,16 +290,15 @@ def add_parameters( self, parameters: Union[ MutableSequence[Parameter], MutableMapping[str, str], None - ] = None, + ] = None, ): - '''how can this run? - ''' + """How can this run?.""" _cases: List[Case] = deepcopy(self) for case in _cases: - case.add_parameters(parameters) - + _ = case.add_parameters(parameters) + return False - + def to_pandas( self, use_path_as_index: bool = True, diff --git a/src/farn/sampling/sampling.py b/src/farn/sampling/sampling.py index 449a2a03..5deeaf5e 100644 --- a/src/farn/sampling/sampling.py +++ b/src/farn/sampling/sampling.py @@ -41,6 +41,7 @@ def __init__(self, seed: Union[int, None] = None): 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 @@ -50,6 +51,13 @@ def __init__(self, seed: Union[int, None] = None): def _set_up_known_sampling_types(self): self.known_sampling_types = { + "factorial": { + "required_args": [ + "_names", + "_ranges", + "_listOfSamples", + ] + }, "fixed": { "required_args": [ "_names", @@ -95,16 +103,6 @@ def _set_up_known_sampling_types(self): "_includeBoundingBox", ], }, - "arbitrary": { - "required_args": [ - "_names", - "_ranges", - "_numberOfSamples", - "_distributionName", # uniform|normal|exp... better to have a dedicated name in known_sampling_types - "_distributionParameters", # mu|sigma|skew|camber not applicsble for uniform - "_includeBoundingBox", # required - ] - }, "hilbertCurve": { "required_args": [ "_names", @@ -122,6 +120,7 @@ def set_sampling_type(self, sampling_type: str): """Set the sampling type. Valid values: + "factorial" "fixed" "linSpace" "uniformLhs" @@ -197,40 +196,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]] = {} # Assert that the values per parameter are provided as a list for item in self.sampling_parameters["_values"]: if not isinstance(item, Sequence): - msg: str = "_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 ValueError(msg) @@ -243,7 +268,9 @@ def _generate_samples_using_fixed_sampling(self) -> Dict[str, List[Any]]: for number_of_values in number_of_values_per_parameter ) if not all_parameters_have_same_number_of_values: - msg: str = "_values: The number of values per parameter need to be the same for all parameters. However, they are different." + msg: str = ( + "_values: The number of values per parameter need to be the same for all parameters. However, they are different." + ) logger.error(msg) raise ValueError(msg) @@ -258,7 +285,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] @@ -275,15 +302,15 @@ 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: ndarray[Any, Any] = self._generate_values_using_uniform_lhs_sampling() + values: ndarray[Any, Any] = self._generate_values_using_uniform_lhs() self._write_values_into_samples_dict(values, samples) 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: @@ -300,7 +327,7 @@ def _generate_samples_using_normal_lhs_sampling(self) -> Dict[str, List[Any]]: self.mean = self.sampling_parameters["_mu"] self.std = self.sampling_parameters["_sigma"] - values: ndarray[Any, Any] = self._generate_values_using_normal_lhs_sampling() + values: ndarray[Any, Any] = self._generate_values_using_normal_lhs() # Clipping (optional. Clipping will only be performed if sampling parameter "_ranges" is defined.) # NOTE: In current implementation, sampled values exceeding a parameters valid range @@ -322,63 +349,24 @@ 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"]) samples: Dict[str, List[Any]] = self._generate_samples_dict() - values: ndarray[Any, Any] = self._generate_values_using_sobol_sampling() + values: ndarray[Any, Any] = self._generate_values_using_sobol() self._write_values_into_samples_dict(values, samples) 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 - - 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) # check this need! - - 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.maxIterationDepth = 15 - values: ndarray[Any, Any] = 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 @@ -389,7 +377,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) -> ndarray[Any, Any]: + 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 from scipy.stats import uniform @@ -416,7 +432,7 @@ def _generate_values_using_uniform_lhs_sampling(self) -> ndarray[Any, Any]: return sample_set - def _generate_values_using_normal_lhs_sampling(self) -> ndarray[Any, Any]: + def _generate_values_using_normal_lhs(self) -> ndarray[Any, Any]: """Gaussnormal LHS.""" from pyDOE3 import lhs from scipy.stats import norm @@ -440,7 +456,7 @@ def _generate_values_using_normal_lhs_sampling(self) -> ndarray[Any, Any]: return sample_set - def _generate_values_using_sobol_sampling(self) -> ndarray[Any, Any]: + def _generate_values_using_sobol(self) -> ndarray[Any, Any]: from scipy.stats import qmc from scipy.stats.qmc import Sobol @@ -468,7 +484,7 @@ def _generate_values_using_sobol_sampling(self) -> ndarray[Any, Any]: return sample_set - def _generate_values_using_hilbert_sampling(self) -> ndarray[Any, Any]: + 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) @@ -498,15 +514,21 @@ def _generate_values_using_hilbert_sampling(self) -> ndarray[Any, Any]: if "_iterationDepth" in self.sampling_parameters.keys(): if not isinstance(self.sampling_parameters["_iterationDepth"], int): - msg: str = 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: str = f'_iterationDepth {self.sampling_parameters["_iterationDepth"]} given in farnDict is beyond the limit of {self.maxIterationDepth}...\n\t\tsetting to {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}' + ) logger.warning(msg) self.iteration_depth = self.maxIterationDepth elif self.sampling_parameters["_iterationDepth"] < self.minIterationDepth: - 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: str = ( + f'_iterationDepth {self.sampling_parameters["_iterationDepth"]} given in farnDict is below the limit of {self.minIterationDepth}...\n\t\tsetting to {self.minIterationDepth}' + ) logger.warning(msg) self.iteration_depth = self.minIterationDepth else: diff --git a/tests/cli/test_farn_cli.py b/tests/cli/test_farn_cli.py index 8ae20dd1..43c1f6b5 100644 --- a/tests/cli/test_farn_cli.py +++ b/tests/cli/test_farn_cli.py @@ -21,7 +21,9 @@ class CliArgs: verbose: bool = False log: Union[str, None] = None log_level: str = field(default_factory=lambda: "WARNING") - farnDict: Union[str, None] = field(default_factory=lambda: "test_farnDict") # noqa: N815 + farnDict: Union[str, None] = field( + default_factory=lambda: "test_farnDict" + ) # noqa: N815 sample: bool = False generate: bool = False execute: Union[str, None] = None diff --git a/tests/test_cases.py b/tests/test_cases.py index d4b94552..5c36efb2 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -57,7 +57,9 @@ def _assert_type_and_equality(cases: Cases, case_list_assert: List[Case]): assert isinstance(cases, Cases) -def _assert_sequence(cases: Cases, case_assert_1: Case, case_assert_2: Case, case_assert_3: Case): +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 @@ -67,7 +69,9 @@ 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) + 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 +84,9 @@ 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) + 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 +99,9 @@ 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) + df_assert: DataFrame = _create_dataframe( + use_path_as_index=True, parameters_only=False + ) # Execute df: DataFrame = cases.to_pandas() # Assert @@ -106,7 +114,9 @@ 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) + 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 +150,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) @@ -274,7 +286,9 @@ def test_filter_level_0_valid_only(): 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 @@ -292,7 +306,9 @@ def test_filter_level_1_valid_only(): 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 @@ -310,7 +326,9 @@ def test_filter_level_minus_1_valid_only(): 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 @@ -328,7 +346,9 @@ def test_filter_default_arguments(): 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_farn.py b/tests/test_farn.py index f3d7a7d5..048c128c 100644 --- a/tests/test_farn.py +++ b/tests/test_farn.py @@ -35,11 +35,21 @@ def test_create_samples(): 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(): @@ -121,8 +131,12 @@ def test_execute(caplog: LogCaptureFixture): _ = 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"python -m farn.cli.farn {sampled_file.name} --execute testwinvar" + ) + _ = os.system( + f"python -m farn.cli.farn {sampled_file.name} --execute printwinenv" + ) # Assert @@ -172,7 +186,9 @@ def test_sample_exclude_filtering(caplog: LogCaptureFixture): 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_sampling.py b/tests/test_sampling.py index cb365340..6d785ef8 100644 --- a/tests/test_sampling.py +++ b/tests/test_sampling.py @@ -96,7 +96,13 @@ def test_linSpace_sampling_one_parameter(): "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(): "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(): 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)