From 77790a0cd052322e4c2dc882c094effe21238e3d Mon Sep 17 00:00:00 2001 From: Ronny Fahlberg <86288939+rfahlberg@users.noreply.github.com> Date: Tue, 17 Dec 2024 08:36:41 +0100 Subject: [PATCH] MAINT: OptimizationParameter: Fix wrapping of reference_value and range. (#374) --- src/ansys/optislang/core/json_utils.py | 75 ++++ .../optislang/core/project_parametric.py | 387 ++++++++++-------- src/ansys/optislang/core/utils.py | 2 +- tests/tcp/test_tcp_managers.py | 36 +- tests/test_project_parametric_classes.py | 123 ++---- 5 files changed, 373 insertions(+), 250 deletions(-) create mode 100644 src/ansys/optislang/core/json_utils.py diff --git a/src/ansys/optislang/core/json_utils.py b/src/ansys/optislang/core/json_utils.py new file mode 100644 index 000000000..50127098f --- /dev/null +++ b/src/ansys/optislang/core/json_utils.py @@ -0,0 +1,75 @@ +# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Json utilities module.""" + +from typing import Any + + +def _get_enum_value(enum_value: Any, default_value: str = "", throw_on_error: bool = True) -> str: + """Get value of an enum encoded in oSL Json. + + Parameters + ---------- + enum_value : Any + The enum encoded in oSL Json. Can be either a plain string + or a dict in the format ``{ "enum" : [], "value" : "" }``. + default_value : str + The default value if not present or convertible. + + Returns + ------- + str + The decoded enum value. If not present or convertible and throw_on_error is False, + ``default_value`` is returned, otherwise, an exception it raised. + Raises + ------ + TypeError + Raised when the decoded enum_value is not a string. + ValueError + Raised when the value for the ``string`` is invalid. + """ + if isinstance(enum_value, dict): + value = enum_value.get("value") + if value is None: + if throw_on_error: + raise ValueError( + f"Cannot decode {enum_value} as enum: Doesn't contain the 'value' entry." + ) + else: + return default_value + elif isinstance(value, str): + return value + else: + if throw_on_error: + raise TypeError( + f"Cannot decode {enum_value} as enum: 'value' entry is not of type str." + ) + else: + return default_value + elif isinstance(enum_value, str): + return enum_value + else: + if throw_on_error: + raise TypeError(f"Cannot decode {enum_value} as enum: str or dict expected.") + else: + return default_value diff --git a/src/ansys/optislang/core/project_parametric.py b/src/ansys/optislang/core/project_parametric.py index 029eb851f..8295ddc8e 100644 --- a/src/ansys/optislang/core/project_parametric.py +++ b/src/ansys/optislang/core/project_parametric.py @@ -28,6 +28,7 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union import uuid +from ansys.optislang.core.json_utils import _get_enum_value from ansys.optislang.core.utils import enum_from_str @@ -715,11 +716,11 @@ def _extract_criterion_properties_from_dict(criterion_dict: dict) -> Dict[str, A raise TypeError("Unsupported format of criterion dictionary.") name = criterion_dict["First"] criterion_dict = criterion_dict["Second"] - criterion = ComparisonType.from_str(criterion_dict["type"]["value"]) + criterion = ComparisonType.from_str(_get_enum_value(criterion_dict["type"])) expression = criterion_dict["lhs"] expression_value_type = ( CriterionValueType.from_str( - criterion_dict["lhs_value"].get("kind", {}).get("value", "UNINITIALIZED") + _get_enum_value(criterion_dict["lhs_value"].get("kind", {}), "UNINITIALIZED", False) ) if criterion_dict.get("lhs_value") else None @@ -743,7 +744,7 @@ def _extract_criterion_properties_from_dict(criterion_dict: dict) -> Dict[str, A limit_expression = criterion_dict.get("rhs") # optional limit_expression_value_type = ( CriterionValueType.from_str( - criterion_dict["rhs_value"].get("kind", {}).get("value", "UNINITIALIZED") + _get_enum_value(criterion_dict["rhs_value"].get("kind", {}), "UNINITIALIZED", False) ) if criterion_dict.get("rhs_value") else None @@ -768,7 +769,7 @@ def _extract_criterion_properties_from_dict(criterion_dict: dict) -> Dict[str, A value_type = value_type = ( CriterionValueType.from_str( - criterion_dict["value"].get("kind", {}).get("value", "UNINITIALIZED") + _get_enum_value(criterion_dict["value"].get("kind", {}), "UNINITIALIZED", False) ) if criterion_dict.get("value") else None @@ -933,14 +934,14 @@ def _parse_str_to_vector(string: str): return vector_list @staticmethod - def _lhs_rhs_value_to_dict( + def _value_to_dict( value: Union[bool, float, complex, list, dict, None], value_type: CriterionValueType ) -> dict: - """Convert given value to `lhs_value` or `rhs_value` dictionary.""" + """Convert given value to dictionary.""" if value_type == CriterionValueType.UNINITIALIZED: - return {"kind": {"value": None}} + return {"kind": None} - value_dict = {"kind": {"value": value_type.name.lower()}} + value_dict = {"kind": value_type.name.lower()} if value_type == CriterionValueType.SCALAR: if isinstance(value, complex): value_dict.update( @@ -958,32 +959,6 @@ def _lhs_rhs_value_to_dict( value_dict.update({value_type.name.lower(): value}) return value_dict - @staticmethod - def _value_to_dict( - value: Union[bool, float, complex, list, dict, None], value_type: CriterionValueType - ) -> dict: - """Convert given value to dictionary.""" - if value_type == CriterionValueType.UNINITIALIZED: - return {"kind": {"value": None}} - - value_dict = {"kind": {"value": value_type.name.lower()}} - if value_type == CriterionValueType.SCALAR: - if isinstance(value, complex): - value_dict["kind"].update( - {value_type.name.lower(): {"imag": value.imag, "real": value.real}} - ) - else: - value_dict["kind"].update({value_type.name.lower(): {"real": value}}) - elif value_type == CriterionValueType.SIGNAL or value_type == CriterionValueType.XYDATA: - value_dict["kind"].update({"matrix": value[0], "vector": value[1]}) - elif ( - value_type == CriterionValueType.BOOL - or value_type == CriterionValueType.MATRIX - or value_type == CriterionValueType.VECTOR - ): - value_dict["kind"].update({value_type.name.lower(): value}) - return value_dict - class ConstraintCriterion(Criterion): """Stores constraint criterion data.""" @@ -1151,15 +1126,15 @@ def to_dict(self) -> dict: "First": self.name, "Second": { "lhs": self.expression, - "lhs_value": Criterion._lhs_rhs_value_to_dict( + "lhs_value": Criterion._value_to_dict( value=self.expression_value, value_type=self.expression_value_type ), "need_eval": False, "rhs": self.limit_expression, - "rhs_value": Criterion._lhs_rhs_value_to_dict( + "rhs_value": Criterion._value_to_dict( value=self.limit_expression_value, value_type=self.limit_expression_value_type ), - "type": {"value": self.criterion.name.lower()}, + "type": self.criterion.name.lower(), "value": Criterion._value_to_dict(value=self.value, value_type=self.value_type), }, } @@ -1346,17 +1321,15 @@ def to_dict(self) -> dict: "First": self.name, "Second": { "lhs": self.expression, - "lhs_value": Criterion._lhs_rhs_value_to_dict( + "lhs_value": Criterion._value_to_dict( value=self.expression_value, value_type=self.expression_value_type ), "need_eval": False, "rhs": self.limit_expression, - "rhs_value": Criterion._lhs_rhs_value_to_dict( + "rhs_value": Criterion._value_to_dict( value=self.expression_value, value_type=self.expression_value_type ), - "type": { - "value": self.criterion.name.lower(), - }, + "type": self.criterion.name.lower(), "value": Criterion._value_to_dict(value=self.value, value_type=self.value_type), }, } @@ -1472,10 +1445,10 @@ def to_dict(self) -> dict: "lhs_value": None, "need_eval": False, "rhs": self.expression, - "rhs_value": Criterion._lhs_rhs_value_to_dict( + "rhs_value": Criterion._value_to_dict( self.expression_value, self.expression_value_type ), - "type": {"value": self.criterion.name.lower()}, + "type": self.criterion.name.lower(), "value": Criterion._value_to_dict(self.value, self.value_type), }, } @@ -1584,10 +1557,10 @@ def to_dict(self) -> dict: "lhs_value": None, "need_eval": False, "rhs": self.expression, - "rhs_value": Criterion._lhs_rhs_value_to_dict( + "rhs_value": Criterion._value_to_dict( self.expression_value, self.expression_value_type ), - "type": {"value": self.criterion.name.lower()}, + "type": self.criterion.name.lower(), "value": Criterion._value_to_dict(self.value, self.value_type), }, } @@ -1703,7 +1676,7 @@ class Parameter: def __init__( self, name: str = "", - reference_value: Optional[Union[bool, float, str]] = None, + reference_value: Optional[Union[bool, float, str, int]] = None, id: Optional[str] = None, const: bool = False, type_: Union[ParameterType, str] = ParameterType.DETERMINISTIC, @@ -1714,7 +1687,7 @@ def __init__( ---------- name: str, optional Name of the parameter. By default ``""``. - reference_value: Optional[Union[bool, float, str]], optional + reference_value: Optional[Union[bool, float, str, int]], optional Parameter's reference value. By default ``None``. id: str, optional Parameter's unique id. By default ``None``. @@ -1849,20 +1822,20 @@ def const(self, is_const: bool) -> None: @property def reference_value( self, - ) -> Union[bool, float, str, None]: + ) -> Optional[Union[bool, float, str, int]]: """Reference value of the parameter.""" return self.__reference_value @reference_value.setter def reference_value( self, - reference_value: Union[bool, float, str, None], + reference_value: Optional[Union[bool, float, str, int]], ) -> None: """Set the reference value of the parameter. Parameters ---------- - reference_value: Union[bool, float, str, None] + reference_value: Optional[Union[bool, float, str, int]] Reference value of the parameter. Raises @@ -1959,42 +1932,44 @@ def _extract_parameter_properties_from_dict(par_dict: dict) -> dict: "name": par_dict["name"], "reference_value": par_dict["reference_value"], "reference_value_type": ( - par_dict["deterministic_property"].get("domain_type", {}).get("value", None) + ParameterValueType.from_str( + _get_enum_value(par_dict["deterministic_property"].get("domain_type", {})) + ) if par_dict.get("deterministic_property") else None ), "id": par_dict["id"], "const": par_dict["const"], - "type": ParameterType.from_str(par_dict["type"]["value"]), - "operation": par_dict.get("dependency_expression", None), + "type": ParameterType.from_str(_get_enum_value(par_dict["type"])), + "operation": par_dict.get("dependency_expression", ""), "deterministic_resolution": ( ParameterResolution.from_str( - par_dict["deterministic_property"].get("kind", {}).get("value", None) + _get_enum_value(par_dict["deterministic_property"].get("kind", {})) ) if par_dict.get("deterministic_property") - else None + else ParameterResolution.CONTINUOUS ), "stochastic_resolution": ( ParameterResolution.from_str( - par_dict["stochastic_property"].get("kind", {}).get("value", None) + _get_enum_value(par_dict["stochastic_property"].get("kind", {})) ) if par_dict.get("stochastic_property") - else None + else ParameterResolution.MARGINALDISTRIBUTION ), "distribution_type": ( DistributionType.from_str( - par_dict.get("stochastic_property", {}).get("type", {}).get("value", None) + _get_enum_value(par_dict["stochastic_property"].get("type", {})) ) if par_dict.get("stochastic_property") - else None + else DistributionType.NORMAL ), "distribution_parameters": ( - tuple(par_dict["stochastic_property"]["distribution_parameters"]) + par_dict["stochastic_property"]["distribution_parameters"] if par_dict.get("stochastic_property", {}).get("distribution_parameters") else None ), "statistical_moments": ( - tuple(par_dict["stochastic_property"]["statistical_moments"]) + par_dict["stochastic_property"]["statistical_moments"] if par_dict.get("stochastic_property", {}).get( "statistical_moments", ) @@ -2008,10 +1983,10 @@ def _extract_parameter_properties_from_dict(par_dict: dict) -> dict: par_dict.get("deterministic_property", {}).get("lower_bound", None), par_dict.get("deterministic_property", {}).get("upper_bound", None), ) - # discrete values otherwise, stored as ([val1, val2, val3 ..]) + # discrete values otherwise, stored as [val1, val2, val3 ..] elif properties_dict["deterministic_resolution"] is not None: - properties_dict["range"] = ( - tuple(par_dict.get("deterministic_property", {}).get("discrete_states", [])), + properties_dict["range"] = par_dict.get("deterministic_property", {}).get( + "discrete_states", [] ) else: properties_dict["range"] = None @@ -2029,7 +2004,7 @@ def __init__( self, name: str = "", operation: str = "0", - reference_value: Optional[Union[bool, float, str, Tuple[Any, ParameterValueType]]] = None, + reference_value: Optional[Union[bool, float, str, int]] = None, id: Optional[str] = None, const: bool = False, ) -> None: @@ -2041,7 +2016,7 @@ def __init__( Name of the parameter. By default ``""``. operation: str, optional Mathematic expression to evaluate. By default ``"0"``. - reference_value: Optional[Union[bool, float, str, Tuple[Any, ParameterValueType]]], optional + reference_value: Optional[Union[bool, float, str, int]], optional Reference value of the parameter. By default ``None``. id: str, optional Unique ID of the parameter. A unique Id is automatically generated if not specified. @@ -2125,7 +2100,7 @@ def to_dict(self) -> dict: "name": self.name, "reference_value": None, "removable": True, - "type": {"value": self.type.name.lower()}, + "type": self.type.name.lower(), "unit": "", } @@ -2152,11 +2127,11 @@ class MixedParameter(Parameter): def __init__( self, name: str = "", - reference_value: float = 0, + reference_value: float = 0.0, id: Optional[str] = None, const: bool = False, deterministic_resolution: Union[ParameterResolution, str] = ParameterResolution.CONTINUOUS, - range: Union[Sequence[float, float], Sequence[Sequence[float]]] = (-1, 1), + range: Union[Tuple[float, float], Sequence[float]] = (-1.0, 1.0), stochastic_resolution: Union[ ParameterResolution, str ] = ParameterResolution.MARGINALDISTRIBUTION, @@ -2172,23 +2147,23 @@ def __init__( name: str, optional Name of the parameter. By default ``""``. reference_value: float, optional - Parameter's reference value. By default ``0``. + Parameter's reference value. By default ``0.0``. id: str, optional Parameter's unique id. A unique Id is automatically generated if not specified. const: bool, optional Determines whether is parameter constant. By default ``False``. deterministic_resolution: Union[ParameterResolution, str], optional Parameter's deterministic resolution. By default ``ParameterResolution.CONTINUOUS``. - range: Union[Sequence[float, float], Sequence[Sequence[float]]], optional - Either 2 values specifying range or list of discrete values. By default ``(-1, 1)``. + range: Union[Tuple[float, float], Sequence[float]], optional + Either 2 values specifying range or list of discrete values. By default ``(-1.0, 1.0)``. stochastic_resolution: Union[ParameterResolution, str], optional Parameter's stochastic resolution. By default ``ParameterResolution.MARGINALDISTRIBUTION``. distribution_type: Union[DistributionType, str], optional Parameter's distribution type. By default ``DistributionType.NORMAL``. - distribution_parameters: Optional[Sequence[float, ...]], optional + distribution_parameters: Optional[Sequence[float]], optional Distribution's parameters. By default ``None``. - statistical_moments: Optional[Sequence[float, ...]], optional + statistical_moments: Optional[Sequence[float]], optional Distribution's statistical moments. By default ``None``. cov: Optional[float], optional Distribution's COV. By default ``None``. @@ -2201,18 +2176,33 @@ def __init__( type_=ParameterType.MIXED, ) self.__reference_value_type = ParameterValueType.REAL - self.deterministic_resolution = deterministic_resolution + self.deterministic_resolution = ( + ParameterResolution.from_str(deterministic_resolution) + if isinstance(deterministic_resolution, str) + else deterministic_resolution + ) self.range = range - self.stochastic_resolution = stochastic_resolution - self.distribution_type = distribution_type - self.distribution_parameters = ( - tuple(distribution_parameters) if distribution_parameters is not None else None + self.stochastic_resolution = ( + ParameterResolution.from_str(stochastic_resolution) + if isinstance(stochastic_resolution, str) + else stochastic_resolution ) - self.statistical_moments = ( - tuple(statistical_moments) if statistical_moments is not None else None + self.distribution_type = ( + DistributionType.from_str(distribution_type) + if isinstance(distribution_type, str) + else distribution_type ) + self.distribution_parameters = distribution_parameters + self.statistical_moments = statistical_moments self.cov = cov + if ( + self.distribution_parameters is None + and self.statistical_moments is None + and self.cov is None + ): + self.statistical_moments = [reference_value, 1.0] + def __eq__(self, other) -> bool: """Compare properties of two instances of the ``MixedParameter`` class. @@ -2259,6 +2249,38 @@ def __deepcopy__(self, memo) -> MixedParameter: cov=self.cov, ) + @property + def reference_value( + self, + ) -> Optional[Union[bool, float, str, int]]: + """Reference value of the parameter.""" + return self.__reference_value + + @reference_value.setter + def reference_value( + self, + reference_value: float, + ) -> None: + """Set the reference value of the parameter. + + Parameters + ---------- + reference_value: float + Reference value of the parameter. + + Raises + ------ + TypeError + Raised when the type for the reference value is invalid. + """ + if isinstance(reference_value, float): + self.__reference_value = reference_value + else: + raise TypeError( + "Type of ``reference_value`` must be ``float`` but type: " + f"``{type(reference_value)}`` was given." + ) + @property def reference_value_type(self) -> ParameterValueType: """Type of the reference value.""" @@ -2296,23 +2318,20 @@ def deterministic_resolution( ) @property - def range(self) -> Union[Tuple[float, float], Tuple[Tuple[float, ...]]]: + def range(self) -> Union[Tuple[float, float], Sequence[float]]: """Range of the mixed parameter.""" return self.__range @range.setter - def range(self, range: Union[Sequence[float, float], Sequence[Sequence[float]]]) -> None: + def range(self, range: Union[Tuple[float, float], Sequence[float]]) -> None: """Set the range of the mixed parameter. Parameters ---------- - range : Union[Sequence[float, float], Sequence[Sequence[float]]] + range : Union[Tuple[float, float], Sequence[float]] Range of the mixed parameter. """ - if not isinstance(range[0], (int, float)): - self.__range = (tuple(range[0]),) - else: - self.__range = tuple(range) + self.__range = range @property def stochastic_resolution(self) -> ParameterResolution: @@ -2373,26 +2392,23 @@ def distribution_type(self, distribution_type: Union[DistributionType, str]) -> ) @property - def distribution_parameters(self) -> Union[Tuple[float], None]: + def distribution_parameters(self) -> Optional[Sequence[float]]: """Parameters of the distribution.""" return self.__distribution_parameters @distribution_parameters.setter - def distribution_parameters(self, parameters: Union[Sequence[float], None]): + def distribution_parameters(self, parameters: Optional[Sequence[float]]): """Set the parameters of the distribution. Parameters ---------- - parameters : Sequence[float] + parameters : Optional[Sequence[float]] Parameters of the distribution. """ - if parameters is not None: - self.__distribution_parameters = tuple(parameters) - else: - self.__distribution_parameters = None + self.__distribution_parameters = parameters @property - def statistical_moments(self) -> Optional[Tuple[float]]: + def statistical_moments(self) -> Optional[Sequence[float]]: """Statistical moments of the distribution.""" return self.__statistical_moments @@ -2405,10 +2421,7 @@ def statistical_moments(self, moments: Optional[Sequence[float]]): moments : Optional[Sequence[float]] Statistical moments of the distribution. """ - if moments is not None: - self.__statistical_moments = tuple(moments) - else: - self.__statistical_moments = None + self.__statistical_moments = moments @property def cov(self) -> Optional[float]: @@ -2434,16 +2447,18 @@ def to_dict(self) -> dict: dict Input dictionary for the optiSLang server. """ - if len(self.range) == 1: - range_dict = {"discrete_states": self.range[0]} - else: + range_dict: Dict[str, Any] = {} + if self.deterministic_resolution == ParameterResolution.CONTINUOUS: range_dict = { "lower_bound": self.range[0], "upper_bound": self.range[1], } + else: + range_dict = {"discrete_states": self.range} + stochastic_property: Dict[str, Any] = {} stochastic_property = { - "kind": {"value": self.stochastic_resolution.name.lower()}, - "type": {"value": self.distribution_type.name.lower()}, + "kind": self.stochastic_resolution.name.lower(), + "type": self.distribution_type.name.lower(), } if self.distribution_parameters is not None: stochastic_property["distribution_parameters"] = self.distribution_parameters @@ -2451,26 +2466,27 @@ def to_dict(self) -> dict: stochastic_property["statistical_moments"] = self.statistical_moments if self.cov is not None: stochastic_property["cov"] = self.cov + deterministic_property: Dict[str, Any] = { + "domain_type": self.reference_value_type.name.lower(), + "kind": self.deterministic_resolution.name.lower(), + } + deterministic_property.update(range_dict) output_dict = { "active": True, "const": self.const if self.const is not None else False, - "deterministic_property": { - "domain_type": {"value": self.reference_value_type.name.lower()}, - "kind": {"value": self.deterministic_resolution.name.lower()}, - }, + "deterministic_property": deterministic_property, "modifiable": False, "name": self.name, - "reference_value": self.reference_value if self.reference_value else 0, + "reference_value": self.reference_value, "removable": True, "stochastic_property": stochastic_property, - "type": {"value": self.type.name.lower()}, + "type": self.type.name.lower(), "unit": "", } if self.id is not None: output_dict["id"] = self.id - output_dict["deterministic_property"].update(range_dict) return output_dict def __str__(self) -> str: @@ -2498,12 +2514,12 @@ class OptimizationParameter(Parameter): def __init__( self, name: str = "", - reference_value: Union[bool, float, str, None] = 0, + reference_value: Optional[Union[bool, float, str, int]] = 0.0, reference_value_type: ParameterValueType = ParameterValueType.REAL, id: Optional[str] = None, const: bool = False, deterministic_resolution: Union[ParameterResolution, str] = ParameterResolution.CONTINUOUS, - range: Union[Sequence[float, float], Sequence[Sequence[float]]] = (-1, 1), + range: Union[Tuple[float, float], Sequence[Union[bool, float, str, int]]] = (-1.0, 1.0), ) -> None: """Create a new instance of ``OptimizationParameter``. @@ -2511,8 +2527,8 @@ def __init__( ---------- name: str, optional Name of the parameter. By default ``""``. - reference_value: Union[bool, float, str, None], optional - Parameter's reference value. By default ``0``. + reference_value: Union[bool, float, str, int], optional + Parameter's reference value. By default ``0.0``. reference_value_type: ParameterValueType, optional Type of the reference value. By default ``ParameterValueType.REAL``. id: str, optional @@ -2521,8 +2537,8 @@ def __init__( Determines whether is parameter constant. By default ``False``. deterministic_resolution: Union[ParameterResolution, str], optional Parameter's deterministic resolution. By default ``ParameterResolution.CONTINUOUS``. - range: Union[Sequence[float, float], Sequence[Sequence[float]]], optional - Either 2 values specifying range or list of discrete values. By default ``(-1, 1)``. + range: Union[Tuple[float, float], Sequence[Union[bool, float, str, int]]], optional + Either 2 values specifying range or list of discrete values. By default ``(-1.0, 1.0)``. """ super().__init__( name=name, @@ -2532,7 +2548,11 @@ def __init__( type_=ParameterType.DETERMINISTIC, ) self.reference_value_type = reference_value_type - self.deterministic_resolution = deterministic_resolution + self.deterministic_resolution = ( + ParameterResolution.from_str(deterministic_resolution) + if isinstance(deterministic_resolution, str) + else deterministic_resolution + ) self.range = range def __eq__(self, other) -> bool: @@ -2584,7 +2604,7 @@ def reference_value_type(self, type_: Union[ParameterValueType, str]) -> None: Parameters ---------- type_ : Union[ParameterValueType, str] - Type of the reference value. + Type of the reference value. Raises ------ @@ -2633,23 +2653,22 @@ def deterministic_resolution( ) @property - def range(self) -> Union[Tuple[float, float], Tuple[Tuple[float, ...]]]: + def range(self) -> Union[Tuple[float, float], Sequence[Union[bool, float, str, int]]]: """Range of the optimization parameter.""" return self.__range @range.setter - def range(self, range: Union[Sequence[float, float], Sequence[Sequence[float]]]) -> None: + def range( + self, range: Union[Tuple[float, float], Sequence[Union[bool, float, str, int]]] + ) -> None: """Set the range of the optimization parameter. Parameters ---------- - range: Union[Sequence[float, float], Sequence[Sequence[float]]] + range: Union[Tuple[float, float], Sequence[Union[bool, float, str, int]]] Range of the optimization parameter. """ - if not isinstance(range[0], (float, int)): - self.__range = (tuple(range[0]),) - else: - self.__range = tuple(range) + self.__range = range def to_dict(self) -> dict: """Convert an instance of the ``OptimizationParameter`` to a dictionary. @@ -2659,30 +2678,32 @@ def to_dict(self) -> dict: dict Input dictionary for the optiSLang server. """ - if len(self.range) == 1: - range_dict = {"discrete_states": self.range[0]} - else: + range_dict: Dict[str, Any] = {} + if self.deterministic_resolution == ParameterResolution.CONTINUOUS: range_dict = { "lower_bound": self.range[0], "upper_bound": self.range[1], } + else: + range_dict = {"discrete_states": self.range} + deterministic_property: Dict[str, Any] = { + "domain_type": self.reference_value_type.name.lower(), + "kind": self.deterministic_resolution.name.lower(), + } + deterministic_property.update(range_dict) output_dict = { "active": True, "const": self.const if self.const is not None else False, - "deterministic_property": { - "domain_type": {"value": self.reference_value_type.name.lower()}, - "kind": {"value": self.deterministic_resolution.name.lower()}, - }, + "deterministic_property": deterministic_property, "modifiable": False, "name": self.name, "reference_value": self.reference_value, "removable": True, - "type": {"value": self.type.name.lower()}, + "type": self.type.name.lower(), "unit": "", } if self.id is not None: output_dict["id"] = self.id - output_dict["deterministic_property"].update(range_dict) return output_dict def __str__(self) -> str: @@ -2705,7 +2726,7 @@ class StochasticParameter(Parameter): def __init__( self, name: str = "", - reference_value: float = 0, + reference_value: float = 0.0, id: Optional[str] = None, const: bool = False, stochastic_resolution: Union[ @@ -2723,7 +2744,7 @@ def __init__( name: str, optional Name of the parameter. By default ``""``. reference_value: float, optional - Parameter's reference value. By default ``0``. + Parameter's reference value. By default ``0.0``. id: str, optional Parameter's unique id. A unique Id is automatically generated if not specified. const: bool, optional @@ -2733,9 +2754,9 @@ def __init__( By default ``ParameterResolution.MARGINALDISTRIBUTION``. distribution_type: Union[DistributionType, str], optional Parameter's distribution type. By default ``DistributionType.NORMAL``. - distribution_parameters: Optional[Sequence[float, ...]], optional + distribution_parameters: Optional[Sequence[float]], optional Distribution's parameters. Defaults to ``None``. - statistical_moments: Optional[Sequence[float, ...]], optional + statistical_moments: Optional[Sequence[float]], optional Distribution's statistical moments. Defaults to ``None``. cov: Optional[float], optional Distribution's COV. Defaults to ``None``. @@ -2748,16 +2769,27 @@ def __init__( type_=ParameterType.STOCHASTIC, ) self.__reference_value_type = ParameterValueType.REAL - self.stochastic_resolution = stochastic_resolution - self.distribution_type = distribution_type - self.distribution_parameters = ( - tuple(distribution_parameters) if distribution_parameters is not None else None + self.stochastic_resolution = ( + ParameterResolution.from_str(stochastic_resolution) + if isinstance(stochastic_resolution, str) + else stochastic_resolution ) - self.statistical_moments = ( - tuple(statistical_moments) if statistical_moments is not None else None + self.distribution_type = ( + DistributionType.from_str(distribution_type) + if isinstance(distribution_type, str) + else distribution_type ) + self.distribution_parameters = distribution_parameters + self.statistical_moments = statistical_moments self.cov = cov + if ( + self.distribution_parameters is None + and self.statistical_moments is None + and self.cov is None + ): + self.statistical_moments = [reference_value, 1.0] + def __eq__(self, other) -> bool: r"""Compare properties of two instances of the ``StochasticParameter`` class. @@ -2800,6 +2832,38 @@ def __deepcopy__(self, memo) -> StochasticParameter: cov=self.cov, ) + @property + def reference_value( + self, + ) -> Optional[Union[bool, float, str, int]]: + """Reference value of the parameter.""" + return self.__reference_value + + @reference_value.setter + def reference_value( + self, + reference_value: float, + ) -> None: + """Set the reference value of the parameter. + + Parameters + ---------- + reference_value: float + Reference value of the parameter. + + Raises + ------ + TypeError + Raised when the type for the reference value is invalid. + """ + if isinstance(reference_value, float): + self.__reference_value = reference_value + else: + raise TypeError( + "Type of ``reference_value`` must be ``float`` but type: " + f"``{type(reference_value)}`` was given." + ) + @property def reference_value_type(self) -> ParameterValueType: """Type of the reference value``.""" @@ -2864,42 +2928,36 @@ def distribution_type(self, distribution_type: Union[DistributionType, str]) -> ) @property - def distribution_parameters(self) -> Union[Tuple[float], None]: + def distribution_parameters(self) -> Optional[Sequence[float]]: """Parameters of the distribution.""" return self.__distribution_parameters @distribution_parameters.setter - def distribution_parameters(self, parameters: Union[Sequence[float], None]): + def distribution_parameters(self, parameters: Optional[Sequence[float]]): """Set the parameters of the distribution. Parameters ---------- - parameters : Sequence[float] + parameters : Optional[Sequence[float]] Parameters of the distribution. """ - if parameters is not None: - self.__distribution_parameters = tuple(parameters) - else: - self.__distribution_parameters = None + self.__distribution_parameters = parameters @property - def statistical_moments(self) -> Union[Tuple[float], None]: + def statistical_moments(self) -> Optional[Sequence[float]]: """Statistical moments of the distribution.""" return self.__statistical_moments @statistical_moments.setter - def statistical_moments(self, moments: Union[Sequence[float], None]): + def statistical_moments(self, moments: Optional[Sequence[float]]): """Set the statistical moments of the distribution. Parameters ---------- - moments : Sequence[float] + moments : Optional[Sequence[float]] Statistical moments of the distribution. """ - if moments is not None: - self.__statistical_moments = tuple(moments) - else: - self.__statistical_moments = None + self.__statistical_moments = moments @property def cov(self) -> Optional[float]: @@ -2925,9 +2983,10 @@ def to_dict(self) -> dict: dict Input dictionary for the optiSLang server. """ + stochastic_property: Dict[str, Any] = {} stochastic_property = { - "kind": {"value": self.stochastic_resolution.name.lower()}, - "type": {"value": self.distribution_type.name.lower()}, + "kind": self.stochastic_resolution.name.lower(), + "type": self.distribution_type.name.lower(), } if self.distribution_parameters is not None: stochastic_property["distribution_parameters"] = self.distribution_parameters @@ -2943,7 +3002,7 @@ def to_dict(self) -> dict: "reference_value": self.reference_value, "removable": True, "stochastic_property": stochastic_property, - "type": {"value": self.type.name.lower()}, + "type": self.type.name.lower(), "unit": "", } if self.id is not None: diff --git a/src/ansys/optislang/core/utils.py b/src/ansys/optislang/core/utils.py index e5da2c580..791dadd73 100644 --- a/src/ansys/optislang/core/utils.py +++ b/src/ansys/optislang/core/utils.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -"""Utitilies module.""" +"""Utilities module.""" from __future__ import annotations import collections diff --git a/tests/tcp/test_tcp_managers.py b/tests/tcp/test_tcp_managers.py index 1e7cd632a..4f1976c9a 100644 --- a/tests/tcp/test_tcp_managers.py +++ b/tests/tcp/test_tcp_managers.py @@ -205,18 +205,40 @@ def test_add_parameter(optislang: Optislang): "default_stochastic": StochasticParameter(name="default_stochastic"), "custom_optimization": OptimizationParameter( name="custom_optimization", - reference_value=3, + reference_value=3.0, const=True, deterministic_resolution=ParameterResolution.NOMINALDISCRETE, - range=[[1, 2, 3]], + range=[1.0, 2.0, 3.0], ), "custom_mixed": MixedParameter( - name="custom_mixed", reference_value=10, distribution_type=DistributionType.CHI_SQUARE + name="custom_mixed", reference_value=10.0, distribution_type=DistributionType.CHI_SQUARE ), "custom_dependent": DependentParameter( name="custom_dependent", operation="default_optimization+custom_optimization" ), - "custom_stochastic": StochasticParameter(name="custom_stochastic", reference_value=12), + "custom_stochastic": StochasticParameter( + name="custom_stochastic", + reference_value=12.0, + statistical_moments=[12.0], + cov=0.5, + ), + "custom_stochastic_2": StochasticParameter( + name="custom_stochastic_2", + reference_value=12.0, + statistical_moments=[12.0, 6.0], + ), + "custom_stochastic_3": StochasticParameter( + name="custom_stochastic_3", + reference_value=12.0, + distribution_parameters=[12.0, 6.0], + ), + "custom_mixed_2": MixedParameter( + name="custom_mixed_2", + reference_value=10.0, + distribution_type=DistributionType.TRIANGULAR, + distribution_parameters=[6.0, 12.0, 18.0], + range=(0.0, 20.0), + ), } for parameter in parameter_dict.values(): parameter_manager.add_parameter(parameter=parameter) @@ -224,7 +246,7 @@ def test_add_parameter(optislang: Optislang): parameter_manager.add_parameter(parameter=MixedParameter("custom_mixed")) parameters = parameter_manager.get_parameters() - assert len(parameters) == 8 + assert len(parameters) == 11 for parameter in parameters: if parameter_dict.get(parameter.name): @@ -278,7 +300,7 @@ def test_modify_parameter(optislang: Optislang): ) with pytest.raises(NameError): parameter_manager.modify_parameter( - MixedParameter(name="xxx", reference_value=15, const=False) + MixedParameter(name="xxx", reference_value=15.0, const=False) ) modified_parameter = [ parameter for parameter in parameter_manager.get_parameters() if parameter.name == "a" @@ -307,7 +329,7 @@ def test_modify_parameter_property(optislang: Optislang): ][0] assert isinstance(modified_parameter, StochasticParameter) assert modified_parameter.type == ParameterType.STOCHASTIC - assert modified_parameter.reference_value == 10 + assert modified_parameter.reference_value == 10.0 assert modified_parameter.const == False diff --git a/tests/test_project_parametric_classes.py b/tests/test_project_parametric_classes.py index b6f5f3c57..04e58243a 100644 --- a/tests/test_project_parametric_classes.py +++ b/tests/test_project_parametric_classes.py @@ -71,7 +71,7 @@ "name": "dependent", "reference_value": 0.0, "removable": True, - "type": {"value": "dependent"}, + "type": "dependent", "unit": "", } OPTIMIZATION_PARAMETER = OptimizationParameter( @@ -81,8 +81,8 @@ "active": True, "const": False, "deterministic_property": { - "domain_type": {"value": "real"}, - "kind": {"value": "continuous"}, + "domain_type": "real", + "kind": "continuous", "lower_bound": -1.0, "upper_bound": 1.0, }, @@ -91,7 +91,7 @@ "name": "optimization", "reference_value": 0.0, "removable": True, - "type": {"value": "deterministic"}, + "type": "deterministic", "unit": "", } STOCHASTIC_PARAMETER = StochasticParameter( @@ -106,11 +106,11 @@ "reference_value": 0.0, "removable": True, "stochastic_property": { - "kind": {"value": "marginaldistribution"}, + "kind": "marginaldistribution", "statistical_moments": [0.0, 1.0], - "type": {"value": "normal"}, + "type": "normal", }, - "type": {"value": "stochastic"}, + "type": "stochastic", "unit": "", } STOCHASTIC_PARAMETER_2 = StochasticParameter( @@ -159,8 +159,8 @@ "active": True, "const": False, "deterministic_property": { - "domain_type": {"value": "real"}, - "kind": {"value": "continuous"}, + "domain_type": "real", + "kind": "continuous", "lower_bound": -1.0, "upper_bound": 1.0, }, @@ -170,11 +170,11 @@ "reference_value": 0.0, "removable": True, "stochastic_property": { - "kind": {"value": "marginaldistribution"}, + "kind": "marginaldistribution", "statistical_moments": [0.0, 1.0], - "type": {"value": "normal"}, + "type": "normal", }, - "type": {"value": "mixed"}, + "type": "mixed", "unit": "", } CONSTRAINT_CRITERION = ConstraintCriterion( @@ -191,35 +191,17 @@ "Second": { "lhs": "0", "lhs_value": { - "kind": { - "enum": ["uninitialized", "bool", "scalar", "vector", "matrix", "signal", "xydata"], - "value": "scalar", - }, + "kind": "scalar", "scalar": {"imag": 0.0, "real": 0.0}, }, "need_eval": False, "rhs": "0", "rhs_value": { - "kind": { - "enum": ["uninitialized", "bool", "scalar", "vector", "matrix", "signal", "xydata"], - "value": "scalar", - }, + "kind": "scalar", "scalar": {"imag": 0.0, "real": 0.0}, }, - "type": { - "enum": [ - "ignore", - "min", - "max", - "lessequal", - "equal", - "greaterequal", - "lesslimitstate", - "greaterlimitstate", - ], - "value": "lessequal", - }, - "value": {"kind": {"enum": [...], "value": "scalar"}, "scalar": {"imag": 0.0, "real": 0.0}}, + "type": "lessequal", + "value": {"kind": "scalar", "scalar": {"imag": 0.0, "real": 0.0}}, }, } OBJECTIVE_CRITERION = ObjectiveCriterion( @@ -276,10 +258,7 @@ "Second": { "lhs": "0", "lhs_value": { - "kind": { - "enum": ["uninitialized", "bool", "scalar", "vector", "matrix", "signal", "xydata"], - "value": "scalar", - }, + "kind": "scalar", "scalar": {"imag": 0.0, "real": 0.0}, }, "need_eval": False, @@ -291,19 +270,7 @@ }, "scalar": {"imag": 0.0, "real": 0.0}, }, - "type": { - "enum": [ - "ignore", - "min", - "max", - "lessequal", - "equal", - "greaterequal", - "lesslimitstate", - "greaterlimitstate", - ], - "value": "lesslimitstate", - }, + "type": "lesslimitstate", "value": { "kind": { "enum": ["uninitialized", "bool", "scalar", "vector", "matrix", "signal", "xydata"], @@ -647,7 +614,7 @@ def test_parameter(): parameter.id = "xxx-12-55" assert parameter.id == "xxx-12-55" parameter.const = False - assert parameter.const == False + assert parameter.const is False with pytest.raises(AttributeError): parameter.type = ParameterType.DEPENDENT @@ -680,7 +647,7 @@ def test_parameter(): mixed_parameter_from_dict = Parameter.from_dict(MIXED_PARAMETER_DICT) assert isinstance(mixed_parameter_from_dict, MixedParameter) with pytest.raises((ValueError, KeyError)): - Parameter.from_dict({"type": {"value": "invalid"}}) + Parameter.from_dict({"type": "invalid"}) def test_optimization_parameter(): @@ -717,9 +684,9 @@ def test_optimization_parameter(): optimization_parameter_copy.deterministic_resolution = ParameterResolution.CONTINUOUS assert optimization_parameter_copy.deterministic_resolution == ParameterResolution.CONTINUOUS - optimization_parameter_copy.range = [[1, 2, 3]] - assert optimization_parameter_copy.range == ((1, 2, 3),) - optimization_parameter_copy.range = [1, 2] + optimization_parameter_copy.range = [1, 2, 3] + assert optimization_parameter_copy.range == [1, 2, 3] + optimization_parameter_copy.range = (1, 2) assert optimization_parameter_copy.range == (1, 2) print(optimization_parameter_from_dict) @@ -741,10 +708,10 @@ def test_stochastic_parameter(): assert isinstance(stochastic_parameter_copy.reference_value_type, ParameterValueType) assert isinstance(stochastic_parameter_copy.stochastic_resolution, ParameterResolution) assert isinstance(stochastic_parameter_copy.distribution_type, DistributionType) - assert isinstance(stochastic_parameter_copy.statistical_moments, tuple) + assert isinstance(stochastic_parameter_copy.statistical_moments, list) - assert stochastic_parameter_copy.distribution_parameters == None - assert stochastic_parameter_copy.cov == None + assert stochastic_parameter_copy.distribution_parameters is None + assert stochastic_parameter_copy.cov is None with pytest.raises(AttributeError): stochastic_parameter_copy.reference_value_type = "REAL" @@ -752,7 +719,7 @@ def test_stochastic_parameter(): with pytest.raises(TypeError): stochastic_parameter_copy.stochastic_resolution = ["deterministic"] - stochastic_parameter_copy.stochastic_resolution == "marginaldistribution" + stochastic_parameter_copy.stochastic_resolution = "marginaldistribution" assert ( stochastic_parameter_copy.stochastic_resolution == ParameterResolution.MARGINALDISTRIBUTION ) @@ -769,9 +736,9 @@ def test_stochastic_parameter(): assert stochastic_parameter_copy.distribution_type == DistributionType.BERNOULLI stochastic_parameter_copy.statistical_moments = [1, 2] - assert stochastic_parameter_copy.statistical_moments == (1, 2) + assert stochastic_parameter_copy.statistical_moments == [1, 2] stochastic_parameter_copy.statistical_moments = [1] - assert stochastic_parameter_copy.statistical_moments == (1,) + assert stochastic_parameter_copy.statistical_moments == [1] print(stochastic_parameter_from_dict) @@ -792,7 +759,7 @@ def test_stochastic_parameter_2(): assert isinstance(stochastic_parameter_copy.reference_value_type, ParameterValueType) assert isinstance(stochastic_parameter_copy.stochastic_resolution, ParameterResolution) assert isinstance(stochastic_parameter_copy.distribution_type, DistributionType) - assert isinstance(stochastic_parameter_copy.statistical_moments, tuple) + assert isinstance(stochastic_parameter_copy.statistical_moments, list) assert isinstance(stochastic_parameter_copy.cov, float) assert stochastic_parameter_copy.distribution_parameters == None @@ -803,7 +770,7 @@ def test_stochastic_parameter_2(): with pytest.raises(TypeError): stochastic_parameter_copy.stochastic_resolution = ["deterministic"] - stochastic_parameter_copy.stochastic_resolution == "marginaldistribution" + stochastic_parameter_copy.stochastic_resolution = "marginaldistribution" assert ( stochastic_parameter_copy.stochastic_resolution == ParameterResolution.MARGINALDISTRIBUTION ) @@ -820,9 +787,9 @@ def test_stochastic_parameter_2(): assert stochastic_parameter_copy.distribution_type == DistributionType.BERNOULLI stochastic_parameter_copy.statistical_moments = [1, 2] - assert stochastic_parameter_copy.statistical_moments == (1, 2) + assert stochastic_parameter_copy.statistical_moments == [1, 2] stochastic_parameter_copy.statistical_moments = [1] - assert stochastic_parameter_copy.statistical_moments == (1,) + assert stochastic_parameter_copy.statistical_moments == [1] print(stochastic_parameter_from_dict) @@ -843,7 +810,7 @@ def test_stochastic_parameter_3(): assert isinstance(stochastic_parameter_copy.reference_value_type, ParameterValueType) assert isinstance(stochastic_parameter_copy.stochastic_resolution, ParameterResolution) assert isinstance(stochastic_parameter_copy.distribution_type, DistributionType) - assert isinstance(stochastic_parameter_copy.distribution_parameters, tuple) + assert isinstance(stochastic_parameter_copy.distribution_parameters, list) assert stochastic_parameter_copy.statistical_moments == None assert stochastic_parameter_copy.cov == None @@ -854,7 +821,7 @@ def test_stochastic_parameter_3(): with pytest.raises(TypeError): stochastic_parameter_copy.stochastic_resolution = ["deterministic"] - stochastic_parameter_copy.stochastic_resolution == "marginaldistribution" + stochastic_parameter_copy.stochastic_resolution = "marginaldistribution" assert ( stochastic_parameter_copy.stochastic_resolution == ParameterResolution.MARGINALDISTRIBUTION ) @@ -871,9 +838,9 @@ def test_stochastic_parameter_3(): assert stochastic_parameter_copy.distribution_type == DistributionType.BERNOULLI stochastic_parameter_copy.distribution_parameters = [1, 2] - assert stochastic_parameter_copy.distribution_parameters == (1, 2) + assert stochastic_parameter_copy.distribution_parameters == [1, 2] stochastic_parameter_copy.distribution_parameters = [1] - assert stochastic_parameter_copy.distribution_parameters == (1,) + assert stochastic_parameter_copy.distribution_parameters == [1] print(stochastic_parameter_from_dict) @@ -896,7 +863,7 @@ def test_mixed_parameter(): assert isinstance(mixed_parameter_copy.range, tuple) assert isinstance(mixed_parameter_copy.stochastic_resolution, ParameterResolution) assert isinstance(mixed_parameter_copy.distribution_type, DistributionType) - assert isinstance(mixed_parameter_copy.statistical_moments, tuple) + assert isinstance(mixed_parameter_copy.statistical_moments, list) assert mixed_parameter_copy.distribution_parameters == None @@ -911,14 +878,14 @@ def test_mixed_parameter(): mixed_parameter_copy.deterministic_resolution = ParameterResolution.CONTINUOUS assert mixed_parameter_copy.deterministic_resolution == ParameterResolution.CONTINUOUS - mixed_parameter_copy.range = [[1, 2, 3]] - assert mixed_parameter_copy.range == ((1, 2, 3),) - mixed_parameter_copy.range = [1, 2] + mixed_parameter_copy.range = [1, 2, 3] + assert mixed_parameter_copy.range == [1, 2, 3] + mixed_parameter_copy.range = (1, 2) assert mixed_parameter_copy.range == (1, 2) with pytest.raises(TypeError): mixed_parameter_copy.stochastic_resolution = ["deterministic"] - mixed_parameter_copy.stochastic_resolution == "marginaldistribution" + mixed_parameter_copy.stochastic_resolution = "marginaldistribution" assert mixed_parameter_copy.stochastic_resolution == ParameterResolution.MARGINALDISTRIBUTION mixed_parameter_copy.stochastic_resolution = ParameterResolution.EMPIRICAL_CONTINUOUS assert mixed_parameter_copy.stochastic_resolution == ParameterResolution.EMPIRICAL_CONTINUOUS @@ -931,9 +898,9 @@ def test_mixed_parameter(): assert mixed_parameter_copy.distribution_type == DistributionType.BERNOULLI mixed_parameter_copy.distribution_parameters = [1, 2] - assert mixed_parameter_copy.distribution_parameters == (1, 2) + assert mixed_parameter_copy.distribution_parameters == [1, 2] mixed_parameter_copy.distribution_parameters = [1] - assert mixed_parameter_copy.distribution_parameters == (1,) + assert mixed_parameter_copy.distribution_parameters == [1] print(mixed_parameter_from_dict) @@ -1009,7 +976,7 @@ def test_criterion(): assert isinstance(variable_criterion_from_dict, VariableCriterion) with pytest.raises(TypeError): - Criterion.from_dict({"Second": {"type": {"value": "invalid"}}}) + Criterion.from_dict({"Second": {"type": "invalid"}}) with pytest.raises(AttributeError): criterion.type = CriterionType.LIMIT_STATE