diff --git a/doc/changelog.d/6742.fixed.md b/doc/changelog.d/6742.fixed.md new file mode 100644 index 00000000000..c9393cb902c --- /dev/null +++ b/doc/changelog.d/6742.fixed.md @@ -0,0 +1 @@ +Improve Variable management in Circuit diff --git a/src/ansys/aedt/core/application/design.py b/src/ansys/aedt/core/application/design.py index e085067f6f9..7bea6b057e9 100644 --- a/src/ansys/aedt/core/application/design.py +++ b/src/ansys/aedt/core/application/design.py @@ -4057,8 +4057,12 @@ def get_evaluated_value(self, name, units=None): if self.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: if name in self.get_oo_name(app, f"Instance:{self._odesign.GetName()}"): var_obj = self.get_oo_object(app, f"Instance:{self._odesign.GetName()}/{name}") + elif name in self.get_oo_name(app, "Variables"): + var_obj = self.get_oo_object(app, f"Variables/{name}") elif name in self.get_oo_object(app, "DefinitionParameters").GetPropNames(): - val = self.get_oo_object(app, "DefinitionParameters").GetPropEvaluatedValue(name) + val = self.get_oo_object(app, "DefinitionParameters").GetPropSIValue(name) + elif self.design_type in ["Maxwell Circuit"]: + return None else: var_obj = self.get_oo_object(app, f"Variables/{name}") if var_obj: diff --git a/src/ansys/aedt/core/application/variables.py b/src/ansys/aedt/core/application/variables.py index c40593320ad..0bb6f153b66 100644 --- a/src/ansys/aedt/core/application/variables.py +++ b/src/ansys/aedt/core/application/variables.py @@ -37,10 +37,15 @@ """ +from __future__ import annotations + import ast import os import re import types +from typing import Any +from typing import Optional +from typing import Union import warnings from ansys.aedt.core.base import PyAedtBase @@ -49,13 +54,14 @@ from ansys.aedt.core.generic.constants import _resolve_unit_system from ansys.aedt.core.generic.constants import unit_system from ansys.aedt.core.generic.file_utils import open_file +from ansys.aedt.core.generic.general_methods import _retry_ntimes from ansys.aedt.core.generic.general_methods import check_numeric_equivalence from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.generic.numbers_utils import Quantity from ansys.aedt.core.generic.numbers_utils import decompose_variable_value from ansys.aedt.core.generic.numbers_utils import is_array from ansys.aedt.core.generic.numbers_utils import is_number -from ansys.aedt.core.internal.errors import GrpcApiError +from ansys.aedt.core.internal.errors import AEDTRuntimeError class CSVDataset(PyAedtBase): @@ -731,24 +737,24 @@ def _logger(self): def __init__(self, app): # Global Desktop Environment self._app = app - self._independent_design_variables = {} - self._independent_project_variables = {} - self._dependent_design_variables = {} - self._dependent_project_variables = {} + self.__independent_design_variables = {} + self.__independent_project_variables = {} + self.__dependent_design_variables = {} + self.__dependent_project_variables = {} @property def _independent_variables(self): all_independent = {} - all_independent.update(self._independent_project_variables) - all_independent.update(self._independent_design_variables) + all_independent.update(self.__independent_project_variables) + all_independent.update(self.__independent_design_variables) return all_independent @property def _dependent_variables(self): all_dependent = {} - for k, v in self._dependent_project_variables.items(): + for k, v in self.__dependent_project_variables.items(): all_dependent[k] = v - for k, v in self._dependent_design_variables.items(): + for k, v in self.__dependent_design_variables.items(): all_dependent[k] = v return all_dependent @@ -777,10 +783,10 @@ def __setitem__(self, variable, value): def _cleanup_variables(self): variables = self._get_var_list_from_aedt(self._app.odesign) + self._get_var_list_from_aedt(self._app.oproject) all_dicts = [ - self._independent_project_variables, - self._independent_design_variables, - self._dependent_project_variables, - self._dependent_design_variables, + self.__independent_project_variables, + self.__independent_design_variables, + self.__dependent_project_variables, + self.__dependent_design_variables, ] for dict_var in all_dicts: for var_name in list(dict_var.keys()): @@ -788,22 +794,13 @@ def _cleanup_variables(self): del dict_var[var_name] @pyaedt_function_handler() - def _variable_dict(self, object_list, dependent=True, independent=True): - """Retrieve the variable dictionary. + def _update_variable_dict(self, object_list): + """Update variable dictionary. Parameters ---------- object_list : list List of objects. - dependent : bool, optional - Whether to include dependent variables. The default is ``True``. - independent : bool, optional - Whether to include independent variables. The default is ``True``. - - Returns - ------- - dict - Dictionary of the specified variables. """ all_names = {} @@ -817,26 +814,47 @@ def _variable_dict(self, object_list, dependent=True, independent=True): value = Variable(variable_expression, None, si_value, all_names, name=variable_name, app=self._app) is_number_flag = is_number(value._calculated_value) if variable_name.startswith("$") and is_number_flag: - self._independent_project_variables[variable_name] = value + self.__independent_project_variables[variable_name] = value elif variable_name.startswith("$"): - self._dependent_project_variables[variable_name] = value + self.__dependent_project_variables[variable_name] = value elif is_number_flag: - self._independent_design_variables[variable_name] = value + self.__independent_design_variables[variable_name] = value else: - self._dependent_design_variables[variable_name] = value + self.__dependent_design_variables[variable_name] = value + + @pyaedt_function_handler() + def _variable_dict(self, object_list, dependent=True, independent=True): + """Retrieve the variable dictionary. + + Parameters + ---------- + object_list : list + List of objects. + dependent : bool, optional + Whether to include dependent variables. The default is ``True``. + independent : bool, optional + Whether to include independent variables. The default is ``True``. + + Returns + ------- + dict + Dictionary of the specified variables. + + """ + self._update_variable_dict(object_list) self._cleanup_variables() vars_to_output = {} dicts_to_add = [] if independent: if self._app.odesign in object_list: - dicts_to_add.append(self._independent_design_variables) + dicts_to_add.append(self.__independent_design_variables) if self._app.oproject in object_list: - dicts_to_add.append(self._independent_project_variables) + dicts_to_add.append(self.__independent_project_variables) if dependent: if self._app.odesign in object_list: - dicts_to_add.append(self._dependent_design_variables) + dicts_to_add.append(self.__dependent_design_variables) if self._app.oproject in object_list: - dicts_to_add.append(self._dependent_project_variables) + dicts_to_add.append(self.__dependent_project_variables) for dict_var in dicts_to_add: for k, v in dict_var.items(): vars_to_output[k] = v @@ -907,7 +925,7 @@ def set_variable( read_only : bool, optional Whether to set the design property or project variable to read-only. The default is ``False``. - hidden : bool, optional + hidden : bool, optional Whether to hide the design property or project variable. The default is ``False``. description : str, optional @@ -971,21 +989,23 @@ def set_variable( >>> aedtapp.variable_manager.set_variable["$p1"] == "30mm" """ - if name in self._independent_variables: - del self._independent_variables[name] - if name in self._independent_design_variables: - del self._independent_design_variables[name] - elif name in self._independent_project_variables: - del self._independent_project_variables[name] - elif name in self._dependent_variables: - del self._dependent_variables[name] - if name in self._dependent_design_variables: - del self._dependent_design_variables[name] - elif name in self._dependent_project_variables: - del self._dependent_project_variables[name] + if name in self.independent_variables: + if name in self.__independent_design_variables: + del self.__independent_design_variables[name] + elif name in self.__independent_project_variables: + del self.__independent_project_variables[name] + elif name in self.dependent_variables: + if name in self.__dependent_design_variables: + del self.__dependent_design_variables[name] + elif name in self.__dependent_project_variables: + del self.__dependent_project_variables[name] if not description: description = "" + if name in self.variables: + variable = self.variables[name] + circuit_parameter = variable.circuit_parameter + desktop_object = self.aedt_object(name) if name.startswith("$"): tab_name = "ProjectVariableTab" @@ -1125,6 +1145,7 @@ def set_variable( lower_case_vars = [var_name.lower() for var_name in var_list] if name.lower() not in lower_case_vars: return False + return True @pyaedt_function_handler(separator_name="name") @@ -1192,16 +1213,35 @@ def delete_variable(self, name): lower_case_vars = [var_name.lower() for var_name in var_list] if name.lower() in lower_case_vars: try: - desktop_object.ChangeProperty( - [ - "NAME:AllTabs", + variable = self.variables[name] + if ( + self._app._is_object_oriented_enabled() + and variable.circuit_parameter + and self._app.design_type + in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design", "Maxwell Circuit"] + ): + desktop_object.ChangeProperty( [ - f"NAME:{var_type}VariableTab", - ["NAME:PropServers", f"{var_type}Variables"], - ["NAME:DeletedProps", name], - ], - ] - ) + "NAME:AllTabs", + [ + "NAME:DefinitionParameterTab", + ["NAME:PropServers", f"Instance:{self._odesign.GetName()}"], + ["NAME:DeletedProps", name], + ], + ] + ) + else: + desktop_object.ChangeProperty( + [ + "NAME:AllTabs", + [ + f"NAME:{var_type}VariableTab", + ["NAME:PropServers", f"{var_type}Variables"], + ["NAME:DeletedProps", name], + ], + ] + ) + except Exception: # pragma: no cover self._logger.debug("Failed to change desktop object property.") else: @@ -1340,10 +1380,28 @@ class Variable(PyAedtBase): Parameters ---------- - value : float, str - Numerical value of the variable in SI units. - units : str - Units for the value. + expression : float or str + Variable expression. + units : str, optional + Unit string to enforce. If provided, must be consistent with parsed units. + si_value : float, optional + Value in SI units. If provided, it overrides the parsed/calculated value. + full_variables : dict, optional + Map of known variables for expression decomposition. + name : str, optional + Variable name in AEDT. + app : object, optional + AEDT application of type :class:`ansys.aedt.core.application`. + readonly : bool, optional + Flag controlling read only property. The default is ``False``. + hidden : bool, optional + Flags controlling hidden property. The default is ``False``. + sweep : bool, optional + Flags controlling sweep property. The default is ``True``. + postprocessing : bool, optional + Flags controlling postprocessing property. + circuit_parameter : bool, optional + Define Parameter Default variable in Circuit design. Examples -------- @@ -1367,21 +1425,21 @@ class Variable(PyAedtBase): def __init__( self, - expression, - units=None, - si_value=None, - full_variables=None, - name=None, + expression: Union[float, str], + units: Optional[str] = None, + si_value: Optional[float] = None, + full_variables: Optional[dict] = None, + name: Optional[str] = None, app=None, - readonly=False, - hidden=False, - sweep=True, - description=None, - postprocessing=False, - circuit_parameter=True, + readonly: Optional[bool] = False, + hidden: Optional[bool] = False, + sweep: Optional[bool] = True, + description: Optional[str] = None, + postprocessing: Optional[bool] = False, + circuit_parameter: Optional[bool] = True, ): - if not full_variables: - full_variables = {} + full_variables = full_variables or {} + self._variable_name = name self._app = app self._readonly = readonly @@ -1391,28 +1449,31 @@ def __init__( self._circuit_parameter = circuit_parameter self._description = description self._is_optimization_included = None - if units: - if unit_system(units): - specified_units = units - self._units = None + + # Parse expression and units self._expression = expression - self._calculated_value, self._units = decompose_variable_value(expression, full_variables) - if si_value is not None: - self._value = si_value - else: - self._value = self._calculated_value - # If units have been specified, check for a conflict and otherwise use the specified unit system - if units: - if self._units and self._units != specified_units: + self._calculated_value, parsed_units = decompose_variable_value(expression, full_variables) + self._units = parsed_units + + # Respect explicit SI value if provided + self._value = si_value if si_value is not None else self._calculated_value + + # Enforce unit specification if provided + if units is not None: + enforced_system = unit_system(units) + if not enforced_system: + raise RuntimeError(f"Unrecognized units '{units}'.") + if self._units and self._units != units: raise RuntimeError( - f"The unit specification {specified_units} is inconsistent with the identified units {self._units}." + f"The unit specification {units} is inconsistent with the identified units {self._units}." ) - self._units = specified_units + self._units = units - if not si_value and is_number(self._value): + # Convert numeric value to SI if we have a scale + if si_value is None and is_number(self._value): try: scale = AEDT_UNITS[self.unit_system][self._units] - except KeyError: + except Exception: scale = 1 if isinstance(scale, tuple): self._value = scale[0](self._value, inverse=False) @@ -1423,411 +1484,443 @@ def __init__( @property def _aedt_obj(self): - if "$" in self._variable_name and self._app: + """Return the correct AEDT object based on variable scope.""" + if self._variable_name and "$" in self._variable_name and self._app: return self._app._oproject elif self._app: return self._app._odesign return None + @property + def __has_definition_parameters(self): + """Whether the design type has DefinitionParameters or only LocalVariables.""" + if not self._app: # pragma: no cover + return False + return self._app.design_type in { + "Circuit Design", + "Twin Builder", + "HFSS 3D Layout Design", + "Maxwell Circuit", + } + + def _oo(self, obj, path): + return self._app.get_oo_object(obj, path) + + # Low-level property read/write @pyaedt_function_handler() - def _update_var(self): - if self._app: - return self._app.variable_manager.set_variable( - self._variable_name, - self._expression, - read_only=self._readonly, - hidden=self._hidden, - sweep=self._sweep, - description=self._description, - is_post_processing=self._postprocessing, - circuit_parameter=self._circuit_parameter, - ) - return False + def update_var(self): + """Push the current variable state to AEDT via variable manager.""" + if not self._app: + return False + return self._app.variable_manager.set_variable( + self._variable_name, + self._expression, + read_only=self._readonly, + hidden=self._hidden, + sweep=self._sweep, + description=self._description, + is_post_processing=self._postprocessing, + circuit_parameter=self._circuit_parameter, + ) + + def __target_container_name(self): + """Resolve the property container name for this variable.""" + name = "Variables" + if not self._app: + return name + if self.__has_definition_parameters and self.circuit_parameter: + # If the variable lives in DefinitionParameters, return that + try: + if self._variable_name in list(self._oo(self._app.odesign, "DefinitionParameters").GetPropNames()): + return "DefinitionParameters" + except Exception: # pragma: no cover + return "LocalVariables" + # Otherwise it is either LocalVariables or Variables + return "LocalVariables" + return name @pyaedt_function_handler() def _set_prop_val(self, prop, val, n_times=10): - if self._app.design_type == "Maxwell Circuit": + """Set a property value with retries, handling AEDT containers automatically.""" + if not self._app or self._app.design_type == "Maxwell Circuit": return try: - name = "Variables" - - if self._app.design_type in [ - "Circuit Design", - "Twin Builder", - "HFSS 3D Layout Design", - ]: - if self._variable_name in list( - self._app.get_oo_object(self._app.odesign, "DefinitionParameters").GetPropNames() - ): - name = "DefinitionParameters" - else: - name = "LocalVariables" - i = 0 - while i < n_times: - if name == "DefinitionParameters": - result = self._app.get_oo_object(self._aedt_obj, name).SetPropValue(prop, val) - else: - result = self._app.get_oo_object(self._aedt_obj, f"{name}/{self._variable_name}").SetPropValue( - prop, val - ) - if result: - break - i += 1 + container = self.__target_container_name() + if container == "DefinitionParameters": + prop_name, prop_to_set = prop.split("/") + self._app.odesign.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:DefinitionParameterTab", + ["NAME:PropServers", f"Instance:{self._app.odesign.GetName()}"], + [ + "NAME:ChangedProps", + [f"NAME:{self.name}", [f"NAME:{prop_name}", f"{prop_to_set}:=", val]], + ], + ], + ] + ) + else: + # Object-oriented set property value + path = ( + f"{container}/{self._variable_name}" + if container != "Variables" + else f"Variables/{self._variable_name}" + ) + _retry_ntimes(n_times, self._oo(self._aedt_obj, path).SetPropValue, prop, val) + return True except Exception: - self._app.logger.debug(f"Failed to set property '{prop}' value.") + if self._app: + raise AEDTRuntimeError(f"Failed to set property '{prop}' value.") - @pyaedt_function_handler() - def _get_prop_val(self, prop): - if self._app.design_type == "Maxwell Circuit": - return + def _get_prop_generic(self, prop, evaluated=False): + """Generic property getter. If *evaluated* is True, returns the evaluated value.""" + if not self._aedt_obj: + return None + prop = prop or self.name try: - name = "Variables" + app = self._aedt_obj + + # DefinitionParameters only available in circuit and HFSS 3D Layout design type + if self.__has_definition_parameters: + inst_name = f"Instance:{app.GetName()}" + if self.circuit_parameter: + # Definition parameters properties do not work with Object-Oriented-Programming API + obj = self._oo(app, "DefinitionParameters") + if not obj or prop != self.name: + self._app.logger.error( + "Parameter Default variable properties can not be load. AEDT API limitation." + ) + return None + return obj.GetPropEvaluatedValue(prop) if evaluated else obj.GetPropValue(prop) - if self._app.design_type in [ - "Circuit Design", - "Twin Builder", - "HFSS 3D Layout Design", - ]: - if self._variable_name in list( - self._app.get_oo_object(self._app.odesign, "DefinitionParameters").GetPropNames() - ): - return self._app.get_oo_object(self._aedt_obj, "DefinitionParameters").GetPropValue(prop) else: - name = "LocalVariables" - return self._app.get_oo_object(self._aedt_obj, f"{name}/{self._variable_name}").GetPropValue(prop) + if self.name in self._app.get_oo_name(app, inst_name): + var_obj = self._oo(app, f"{inst_name}/{self.name}") + return var_obj.GetPropEvaluatedValue(prop) if evaluated else var_obj.GetPropValue(prop) + + if self.name in self._app.get_oo_name(app, "Variables"): + var_obj = self._oo(app, f"Variables/{self.name}") + if evaluated and self._app._aedt_version <= "2024.2": # pragma: no cover + return var_obj.GetPropEvaluatedValue("EvaluatedValue") + elif evaluated and self._app._aedt_version >= "2024.2": + return var_obj.GetPropEvaluatedValue() + return var_obj.GetPropValue(prop) + + # Fallback: simple path + obj = self._oo(app, "Variables") + return obj.GetPropEvaluatedValue(prop) if evaluated else obj.GetPropValue(prop) except Exception: - self._app.logger.debug(f"Failed to get property '{prop}' value.") + if self._app: + raise AEDTRuntimeError(f"Failed to get {prop} value of parameter {self.name}.") + + @pyaedt_function_handler() + def _get_prop_val(self, prop=None): + return self._get_prop_generic(prop, evaluated=False) + @pyaedt_function_handler() + def _get_prop_evaluated_val(self, prop=None): + return self._get_prop_generic(prop, evaluated=True) + + # Public properties @property - def name(self): + def name(self) -> str: """Variable name.""" return self._variable_name @name.setter - def name(self, value): + def name(self, value: str): fallback_val = self._variable_name self._variable_name = value - if not self._update_var(): + if not self.update_var(): self._variable_name = fallback_val if self._app: - self._app.logger.error('"Failed to update property "name".') + raise AEDTRuntimeError('Failed to update property "name".') @property - def is_optimization_enabled(self): - """ "Check if optimization is enabled.""" + def is_optimization_enabled(self) -> bool: + """Whether optimization is enabled for this variable.""" return self._get_prop_val("Optimization/Included") @is_optimization_enabled.setter - def is_optimization_enabled(self, value): + def is_optimization_enabled(self, value: bool): self._set_prop_val("Optimization/Included", value, 10) @property - def optimization_min_value(self): - """ "Optimization min value.""" + def optimization_min_value(self) -> bool: + """Optimization lower bound.""" return self._get_prop_val("Optimization/Min") @optimization_min_value.setter - def optimization_min_value(self, value): + def optimization_min_value(self, value: bool): self._set_prop_val("Optimization/Min", value, 10) @property - def optimization_max_value(self): - """ "Optimization max value.""" + def optimization_max_value(self) -> bool: + """Optimization upper bound.""" return self._get_prop_val("Optimization/Max") @optimization_max_value.setter - def optimization_max_value(self, value): + def optimization_max_value(self, value: bool): self._set_prop_val("Optimization/Max", value, 10) @property - def is_sensitivity_enabled(self): - """Check if Sensitivity is enabled.""" + def is_sensitivity_enabled(self) -> bool: + """Whether sensitivity analysis is enabled.""" return self._get_prop_val("Sensitivity/Included") @is_sensitivity_enabled.setter - def is_sensitivity_enabled(self, value): + def is_sensitivity_enabled(self, value: bool): self._set_prop_val("Sensitivity/Included", value, 10) @property - def sensitivity_min_value(self): - """ "Sensitivity min value.""" + def sensitivity_min_value(self) -> bool: + """Sensitivity lower bound.""" return self._get_prop_val("Sensitivity/Min") @sensitivity_min_value.setter - def sensitivity_min_value(self, value): + def sensitivity_min_value(self, value: bool): self._set_prop_val("Sensitivity/Min", value, 10) @property - def sensitivity_max_value(self): - """ "Sensitivity max value.""" + def sensitivity_max_value(self) -> bool: + """Sensitivity upper bound.""" return self._get_prop_val("Sensitivity/Max") @sensitivity_max_value.setter - def sensitivity_max_value(self, value): + def sensitivity_max_value(self, value: bool): self._set_prop_val("Sensitivity/Max", value, 10) @property - def sensitivity_initial_disp(self): - """ "Sensitivity initial value.""" + def sensitivity_initial_disp(self) -> bool: + """Sensitivity initial displacement (if applicable).""" return self._get_prop_val("Sensitivity/IDisp") @sensitivity_initial_disp.setter - def sensitivity_initial_disp(self, value): + def sensitivity_initial_disp(self, value: bool): self._set_prop_val("Sensitivity/IDisp", value, 10) @property - def is_tuning_enabled(self): - """Check if tuning is enabled.""" + def is_tuning_enabled(self) -> bool: + """Whether tuning is enabled.""" return self._get_prop_val("Tuning/Included") @is_tuning_enabled.setter - def is_tuning_enabled(self, value): + def is_tuning_enabled(self, value: bool): self._set_prop_val("Tuning/Included", value, 10) @property - def tuning_min_value(self): - """ "Tuning min value.""" + def tuning_min_value(self) -> bool: + """Tuning lower bound.""" return self._get_prop_val("Tuning/Min") @tuning_min_value.setter - def tuning_min_value(self, value): + def tuning_min_value(self, value: bool): self._set_prop_val("Tuning/Min", value, 10) @property - def tuning_max_value(self): - """ "Tuning max value.""" + def tuning_max_value(self) -> bool: + """Tuning upper bound.""" return self._get_prop_val("Tuning/Max") @tuning_max_value.setter - def tuning_max_value(self, value): + def tuning_max_value(self, value: bool): self._set_prop_val("Tuning/Max", value, 10) @property - def tuning_step_value(self): - """ "Tuning Step value.""" + def tuning_step_value(self) -> bool: + """Tuning step value.""" return self._get_prop_val("Tuning/Step") @tuning_step_value.setter - def tuning_step_value(self, value): + def tuning_step_value(self, value: bool): self._set_prop_val("Tuning/Step", value, 10) @property - def is_statistical_enabled(self): - """Check if statistical is enabled.""" + def is_statistical_enabled(self) -> bool: + """Whether statistical analysis is enabled.""" return self._get_prop_val("Statistical/Included") @is_statistical_enabled.setter - def is_statistical_enabled(self, value): + def is_statistical_enabled(self, value: bool): self._set_prop_val("Statistical/Included", value, 10) @property - def read_only(self): - """Read-only flag value.""" + def read_only(self) -> bool: + """Current read-only flag.""" self._readonly = self._get_prop_val("ReadOnly") return self._readonly @read_only.setter - def read_only(self, value): + def read_only(self, value: bool): fallback_val = self._readonly self._readonly = value - if not self._update_var(): + if not self.update_var(): self._readonly = fallback_val if self._app: self._app.logger.error('Failed to update property "read_only".') @property - def hidden(self): - """Hidden flag value.""" + def hidden(self) -> bool: + """Current hidden flag.""" self._hidden = self._get_prop_val("Hidden") return self._hidden @hidden.setter - def hidden(self, value): + def hidden(self, value: bool): fallback_val = self._hidden self._hidden = value - if not self._update_var(): + if not self.update_var(): self._hidden = fallback_val if self._app: self._app.logger.error('Failed to update property "hidden".') @property - def sweep(self): - """Sweep flag value.""" + def sweep(self) -> bool: + """Current sweep flag.""" self._sweep = self._get_prop_val("Sweep") return self._sweep @sweep.setter - def sweep(self, value): + def sweep(self, value: bool): fallback_val = self._sweep self._sweep = value - if not self._update_var(): + if not self.update_var(): self._sweep = fallback_val if self._app: self._app.logger.error('Failed to update property "sweep".') @property - def description(self): - """Description value.""" + def description(self) -> str: + """Current description.""" self._description = self._get_prop_val("Description") return self._description @description.setter - def description(self, value): + def description(self, value: str): fallback_val = self._description self._description = value - if not self._update_var(): + if not self.update_var(): self._description = fallback_val if self._app: self._app.logger.error('Failed to update property "description".') @property - def post_processing(self): - """Postprocessing flag value.""" + def post_processing(self) -> bool: + """Whether this variable is a post-processing variable.""" if self._app: - return True if self._variable_name in self._app.variable_manager.post_processing_variables else False + return self._variable_name in self._app.variable_manager.post_processing_variables + return False @property - def circuit_parameter(self): - """Circuit parameter flag value.""" - if "$" in self._variable_name: + def circuit_parameter(self) -> bool: + """Whether this variable is a circuit parameter (for supported design types).""" + if not self._app or "$" in (self._variable_name or ""): return False - if self._app.design_type in ["HFSS 3D Layout Design", "Circuit Design", "Maxwell Circuit", "Twin Builder"]: + if self._app.design_type in [ + "HFSS 3D Layout Design", + "Circuit Design", + "Maxwell Circuit", + "Twin Builder", + ]: prop_server = f"Instance:{self._aedt_obj.GetName()}" - return ( - True - if self._variable_name in self._aedt_obj.GetProperties("DefinitionParameterTab", prop_server) - else False - ) + try: + props = self._aedt_obj.GetProperties("DefinitionParameterTab", prop_server) + return self._variable_name in props + except Exception: + return False return False @property - def expression(self): - """Expression.""" + def expression(self) -> str: + """Raw AEDT expression.""" + expression = self._expression if self._aedt_obj: - return self._aedt_obj.GetVariableValue(self._variable_name) - return + expression = self._aedt_obj.GetVariableValue(self._variable_name) + return expression @expression.setter - def expression(self, value): + def expression(self, value: str): fallback_val = self._expression self._expression = value - if not self._update_var(): + if not self.update_var(): self._expression = fallback_val if self._app: self._app.logger.error("Failed to update property Expression.") + # Values and units @property - def numeric_value(self): - """Numeric part of the expression as a float value.""" + def numeric_value(self) -> Union[float, list[Any], Any]: + """Numeric value of the expression in current units. + + If the expression is an array-like string ("[1, 2, 3]"), returns a list. + """ if is_array(self._value): return list(ast.literal_eval(self._value)) try: - var_obj = self._aedt_obj.GetChildObject("Variables").GetChildObject(self._variable_name) - evaluated_value = ( - var_obj.GetPropEvaluatedValue() - if self._app._aedt_version > "2024.2" - else var_obj.GetPropEvaluatedValue("EvaluatedValue") - ) + evaluated_value = self._get_prop_evaluated_val() if ( isinstance(evaluated_value, str) and evaluated_value.strip().startswith("[") and evaluated_value.strip().endswith("]") ): evaluated_value = ast.literal_eval(evaluated_value) + elif evaluated_value is None: + return self._value_fallback() val, _ = decompose_variable_value(evaluated_value) return val - except (Exception, TypeError, AttributeError): - if is_number(self._value): - try: - scale = AEDT_UNITS[self.unit_system][self._units] - except KeyError: - scale = 1 - if isinstance(scale, tuple): - return scale[0](self._value, True) - elif isinstance(scale, types.FunctionType): - return scale(self._value, True) - else: - return self._value / scale - else: # pragma: no cover - return self._value + except Exception: + self._value_fallback() @property - def unit_system(self): - """Unit system of the expression as a string.""" + def unit_system(self) -> str: + """Unit system name.""" return unit_system(self._units) @property - def units(self): - """Units.""" + def units(self) -> str: + """Unit string associated with the expression.""" try: - var_obj = self._aedt_obj.GetChildObject("Variables").GetChildObject(self._variable_name) - evaluated_value = ( - var_obj.GetPropEvaluatedValue() - if self._app._aedt_version > "2024.2" - else var_obj.GetPropEvaluatedValue("EvaluatedValue") - ) - - _, self._units = decompose_variable_value(evaluated_value) - return self._units - except (TypeError, AttributeError, GrpcApiError): - pass + evaluated_value = self._get_prop_evaluated_val() + if evaluated_value is None: + self._units = self._units_fallback() + else: + _, self._units = decompose_variable_value(evaluated_value) + except Exception: + self._units = self._units_fallback() return self._units @property - def value(self): - """Value.""" + def value(self) -> float: + """Current value in SI units (float).""" return self._value @property - def evaluated_value(self): - """String value. - - The numeric value with the unit is concatenated and returned as a string. The numeric display - in the modeler and the string value can differ. For example, you might see ``10mm`` in the - modeler and see ``10.0mm`` returned as the string value. - - """ - return f"{self.numeric_value}{self._units}" + def evaluated_value(self) -> Union[float, Any]: + """Concatenated numeric value and unit string.""" + if self.numeric_value is None: + return None + return f"{self.numeric_value}{self.units}" @pyaedt_function_handler() - def decompose(self): - """Decompose a variable value to a floating with its unit. + def decompose(self) -> tuple: + """Decompose the evaluated expression into a floating-point number and units. Returns ------- tuple - The float value of the variable and the units exposed as a string. - - Examples - -------- - >>> hfss = Hfss() - >>> hfss["v1"] = "3N" - >>> print(hfss.variable_manager["v1"].decompose("v1")) - >>> (3.0, "N") - + (numeric_value, units) """ return decompose_variable_value(self.evaluated_value) @pyaedt_function_handler() - def rescale_to(self, units): - """Rescale the expression to a new unit within the current unit system. - - Parameters - ---------- - units : str - Units to rescale to. - - Examples - -------- - >>> from ansys.aedt.core.application.variables import Variable - - >>> v = Variable("10W") - >>> assert v.numeric_value == 10 - >>> assert v.units == "W" - >>> v.rescale_to("kW") - >>> assert v.numeric_value == 0.01 - >>> assert v.units == "kW" + def rescale_to(self, units: str) -> Variable: + """Rescale the expression to the provided *units* within the same unit system. + Returns + ------- + :class:`ansys.aedt.core.application.variables.Variable` """ new_unit_system = unit_system(units) if new_unit_system != self.unit_system: @@ -1838,71 +1931,28 @@ def rescale_to(self, units): return self @pyaedt_function_handler() - def format(self, format): - """Retrieve the string value with the specified numerical formatting. - - Parameters - ---------- - format : str - Format for the numeric value of the string. For example, ``'06.2f'``. For - more information, see the `PyFormat documentation `_. + def format(self, fmt: str) -> str: + """Return the string value using the specified numeric format ('06.2f'). Returns ------- str - String value with the specified numerical formatting. - - Examples - -------- - >>> from ansys.aedt.core.application.variables import Variable - - >>> v = Variable("10W") - >>> assert v.format("f") == "10.000000W" - >>> assert v.format("06.2f") == "010.00W" - >>> assert v.format("6.2f") == " 10.00W" """ - return f'{self.numeric_value:" + format + "}{self._units}' + return f"{self.numeric_value:{fmt}}{self._units}" + # Arithmetic operators @pyaedt_function_handler() - def __mul__(self, other): - """Multiply the variable with a number or another variable and return a new object. + def __mul__(self, other: Union[Variable, float, int]) -> Variable: + """Multiply this variable by a number or another variable. Parameters ---------- - other : numbers.Number or variable - Object to be multiplied. + other : :class:`ansys.aedt.core.application.variables.Variable`, float or int Returns ------- - type - Variable. - - Examples - -------- - >>> from ansys.aedt.core.application.variables import Variable - - Multiply ``'Length1'`` by unitless ``'None'``` to obtain ``'Length'``. - A numerical value is also considered to be unitless. - - import ansys.aedt.core.generic.constants >>> v1 = Variable("10mm") - >>> v2 = Variable(3) - >>> result_1 = v1 * v2 - >>> result_2 = v1 * 3 - >>> assert result_1.numeric_value == 30.0 - >>> assert result_1.unit_system == "Length" - >>> assert result_2.numeric_value == result_1.numeric_value - >>> assert result_2.unit_system == "Length" - - Multiply voltage times current to obtain power. - - import ansys.aedt.core.generic.constants >>> v3 = Variable("3mA") - >>> v4 = Variable("40V") - >>> result_3 = v3 * v4 - >>> assert result_3.numeric_value == 0.12 - >>> assert result_3.units == "W" - >>> assert result_3.unit_system == "Power" - + :class:`ansys.aedt.core.application.variables.Variable` """ if not is_number(other) and not isinstance(other, Variable): raise ValueError("Multiplier must be a scalar quantity or a variable.") @@ -1913,43 +1963,27 @@ def __mul__(self, other): else: if self.unit_system == "None": return self.numeric_value * other - elif other.unit_system == "None": + if other.unit_system == "None": return other.numeric_value * self - else: - result_value = self.value * other.value - result_units = _resolve_unit_system(self.unit_system, other.unit_system, "multiply") - if not result_units: - result_units = _resolve_unit_system(other.unit_system, self.unit_system, "multiply") - + result_value = self.value * other.value + result_units = _resolve_unit_system( + self.unit_system, other.unit_system, "multiply" + ) or _resolve_unit_system(other.unit_system, self.unit_system, "multiply") return Variable(f"{result_value}{result_units}") __rmul__ = __mul__ @pyaedt_function_handler() - def __add__(self, other): - """Add the variable to another variable to return a new object. + def __add__(self, other: Union[Variable, float, int]) -> Variable: + """Add two variables with the same unit system. Parameters ---------- - other : class:`ansys.aedt.core.application.variables.Variable` - Object to be multiplied. + other : :class:`ansys.aedt.core.application.variables.Variable`, float or int Returns ------- - type - Variable. - - Examples - -------- - >>> from ansys.aedt.core.application.variables import Variable - >>> import ansys.aedt.core.generic.constants - >>> v1 = Variable("3mA") - >>> v2 = Variable("10A") - >>> result = v1 + v2 - >>> assert result.numeric_value == 10.003 - >>> assert result.units == "A" - >>> assert result.unit_system == "Current" - + :class:`ansys.aedt.core.application.variables.Variable` """ if not isinstance(other, Variable): raise ValueError("You can only add a variable with another variable.") @@ -1958,40 +1992,22 @@ def __add__(self, other): result_value = self.value + other.value result_units = SI_UNITS[self.unit_system] - # If the units of the two operands are different, return SI-Units result_variable = Variable(f"{result_value}{result_units}") - - # If the units of both operands are the same, return those units if self.units == other.units: result_variable.rescale_to(self.units) - return result_variable @pyaedt_function_handler() - def __sub__(self, other): - """Subtract another variable from the variable to return a new object. + def __sub__(self, other: Union[Variable, float, int]) -> Variable: + """Subtract two variables with the same unit system. Parameters ---------- - other : class:`ansys.aedt.core.application.variables.Variable` - Object to be subtracted. + other : :class:`ansys.aedt.core.application.variables.Variable`, float or int Returns ------- - type - Variable. - - Examples - -------- - >>> import ansys.aedt.core.generic.constants - >>> from ansys.aedt.core.application.variables import Variable - >>> v3 = Variable("3mA") - >>> v4 = Variable("10A") - >>> result_2 = v3 - v4 - >>> assert result_2.numeric_value == -9.997 - >>> assert result_2.units == "A" - >>> assert result_2.unit_system == "Current" - + :class:`ansys.aedt.core.application.variables.Variable` """ if not isinstance(other, Variable): raise ValueError("You can only subtract a variable from another variable.") @@ -2000,104 +2016,77 @@ def __sub__(self, other): result_value = self.value - other.value result_units = SI_UNITS[self.unit_system] - # If the units of the two operands are different, return SI-Units result_variable = Variable(f"{result_value}{result_units}") - - # If the units of both operands are the same, return those units if self.units == other.units: result_variable.rescale_to(self.units) - return result_variable - # Python 3.x version @pyaedt_function_handler() - def __truediv__(self, other): - """Divide the variable by a number or another variable to return a new object. + def __truediv__(self, other: Union[Variable, float, int]) -> Variable: + """Divide this variable by a number or another variable. Parameters ---------- - other : numbers.Number or variable - Object by which to divide. + other : :class:`ansys.aedt.core.application.variables.Variable`, float or int Returns ------- - type - Variable. - - Examples - -------- - Divide a variable with units ``"W"`` by a variable with units ``"V"`` and automatically - resolve the new units to ``"A"``. - - >>> from ansys.aedt.core.application.variables import Variable - >>> import ansys.aedt.core.generic.constants - >>> v1 = Variable("10W") - >>> v2 = Variable("40V") - >>> result = v1 / v2 - >>> assert result_1.numeric_value == 0.25 - >>> assert result_1.units == "A" - >>> assert result_1.unit_system == "Current" - + :class:`ansys.aedt.core.application.variables.Variable` """ if not is_number(other) and not isinstance(other, Variable): raise ValueError("Divisor must be a scalar quantity or a variable.") - if is_number(other): result_value = self.numeric_value / other result_units = self.units else: result_value = self.value / other.value result_units = _resolve_unit_system(self.unit_system, other.unit_system, "divide") - return Variable(f"{result_value}{result_units}") - # Python 2.7 version - @pyaedt_function_handler() - def __div__(self, other): - return self.__truediv__(other) - @pyaedt_function_handler() - def __rtruediv__(self, other): - """Divide another object by this object. + def __rtruediv__(self, other: Union[Variable, float, int]) -> Variable: + """Right-division: divide *other* by this variable. Parameters ---------- - other : numbers.Number or variable - Object to divide by. + other : :class:`ansys.aedt.core.application.variables.Variable`, float or int Returns ------- - type - Variable. - - Examples - -------- - Divide a number by a variable with units ``"s"`` and automatically determine that - the result is in ``"Hz"``. - - >>> import ansys.aedt.core.generic.constants - >>> from ansys.aedt.core.application.variables import Variable - >>> v = Variable("1s") - >>> result = 3.0 / v - >>> assert result.numeric_value == 3.0 - >>> assert result.units == "Hz" - >>> assert result.unit_system == "Freq" - + :class:`ansys.aedt.core.application.variables.Variable` """ if is_number(other): result_value = other / self.numeric_value result_units = _resolve_unit_system("None", self.unit_system, "divide") - else: result_value = other.numeric_value / self.numeric_value result_units = _resolve_unit_system(other.unit_system, self.unit_system, "divide") - return Variable(f"{result_value}{result_units}") - # # Python 2.7 version - # @pyaedt_function_handler() - # def __div__(self, other): - # return self.__rtruediv__(other) + @pyaedt_function_handler() + def _value_fallback(self): + # Fall back to local cached value + if is_number(self._value): + try: + scale = AEDT_UNITS[self.unit_system][self._units] + except Exception: + scale = 1 + if isinstance(scale, tuple): + return scale[0](self._value, True) + elif isinstance(scale, types.FunctionType): + return scale(self._value, True) + else: + return self._value / scale + val, _ = decompose_variable_value(self._value) + return val + + @pyaedt_function_handler() + def _units_fallback(self): + units = self._units + if not is_number(self._value): + _, units = decompose_variable_value(self._value) + self._units = units + return self._units class DataSet(PyAedtBase): diff --git a/src/ansys/aedt/core/modules/boundary/q3d_boundary.py b/src/ansys/aedt/core/modules/boundary/q3d_boundary.py index e555d706f4a..f31ac2895c4 100644 --- a/src/ansys/aedt/core/modules/boundary/q3d_boundary.py +++ b/src/ansys/aedt/core/modules/boundary/q3d_boundary.py @@ -22,10 +22,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from typing import Optional +from typing import Union import warnings from ansys.aedt.core.base import PyAedtBase from ansys.aedt.core.generic.constants import CATEGORIESQ3D +from ansys.aedt.core.generic.constants import MatrixOperationsQ3D from ansys.aedt.core.generic.constants import PlotCategoriesQ3D from ansys.aedt.core.generic.file_utils import generate_unique_name from ansys.aedt.core.generic.general_methods import filter_tuple @@ -65,17 +68,17 @@ def CATEGORIES(self): return CATEGORIESQ3D @pyaedt_function_handler() - def sources(self, is_gc_sources=True): + def sources(self, is_gc_sources: bool = True) -> list: """List of matrix sources. Parameters ---------- - is_gc_sources : bool, - In Q3d, define if to return GC sources or RL sources. Default `True`. + is_gc_sources : bool + In Q3d, define if to return GC sources or RL sources. The default is ``True``. Returns ------- - List + list """ if self.name in list(self._app.omatrix.ListReduceMatrixes()): if self._app.design_type == "Q3D Extractor": @@ -87,12 +90,12 @@ def sources(self, is_gc_sources=True): @pyaedt_function_handler() def get_sources_for_plot( self, - get_self_terms=True, - get_mutual_terms=True, - first_element_filter=None, - second_element_filter=None, - category="C", - ): + get_self_terms: bool = True, + get_mutual_terms: bool = True, + first_element_filter: Optional[str] = None, + second_element_filter: Optional[str] = None, + category: Optional[Union[str, MatrixOperationsQ3D]] = "C", + ) -> list: """Return a list of source of specified matrix ready to be used in plot reports. Parameters @@ -105,7 +108,7 @@ def get_sources_for_plot( Filter to apply to first element of equation. It accepts `*` and `?` as special characters. second_element_filter : str, optional Filter to apply to second element of equation. It accepts `*` and `?` as special characters. - category : str + category : str or :class:`ansys.aedt.core.generic.constants.MatrixOperationsQ3D`, optional Plot category name as in the report. Eg. "C" is category Capacitance. Matrix `CATEGORIES` property can be used to map available categories. @@ -144,12 +147,12 @@ def get_sources_for_plot( return list_output @property - def operations(self): + def operations(self) -> list: """List of matrix operations. Returns ------- - List + list """ if self.name in list(self._app.omatrix.ListReduceMatrixes()): self._operations = self._app.omatrix.ListReduceMatrixOperations(self.name) @@ -158,17 +161,17 @@ def operations(self): @pyaedt_function_handler() def create( self, - source_names=None, - new_net_name=None, - new_source_name=None, - new_sink_name=None, - ): + source_names: Optional[Union[str, list]] = None, + new_net_name: Optional[str] = None, + new_source_name: Optional[str] = None, + new_sink_name: Optional[str] = None, + ) -> bool: """Create a new matrix. Parameters ---------- - source_names : str, list - List or str containing the content of the matrix reduction (eg. source name). + source_names : str or list + List or str containing the content of the matrix reduction. new_net_name : str, optional Name of the new net. The default is ``None``. new_source_name : str, optional @@ -189,7 +192,7 @@ def create( return True @pyaedt_function_handler() - def delete(self): + def delete(self) -> bool: """Delete current matrix. Returns @@ -206,19 +209,19 @@ def delete(self): @pyaedt_function_handler() def add_operation( self, - operation_type, - source_names=None, - new_net_name=None, - new_source_name=None, - new_sink_name=None, - ): + operation_type: Union[str, MatrixOperationsQ3D], + source_names: Optional[Union[str, list]] = None, + new_net_name: Optional[str] = None, + new_source_name: Optional[str] = None, + new_sink_name: Optional[str] = None, + ) -> bool: """Add a new operation to existing matrix. Parameters ---------- - operation_type : str + operation_type : str or :class:`ansys.aedt.core.generic.constants.MatrixOperationsQ3D` Operation to perform - source_names : str, list + source_names : str or list, optional List or str containing the content of the matrix reduction (eg. source name). new_net_name : str, optional Name of the new net. The default is ``None``. @@ -251,33 +254,36 @@ def add_operation( @pyaedt_function_handler() def _write_command(self, source_names, new_name, new_source, new_sink): - if self._operations[-1] == "JoinSeries": - command = f"""{self._operations[-1]}('{new_name}', '{"', '".join(source_names)}')""" - elif self._operations[-1] == "JoinParallel": - command = ( - f"""{self._operations[-1]}('{new_name}', '{new_source}', '{new_sink}', '{"', '".join(source_names)}')""" - ) - elif self._operations[-1] == "JoinSelectedTerminals": - command = f"""{self._operations[-1]}('', '{"', '".join(source_names)}')""" - elif self._operations[-1] == "FloatInfinity": + operation_name = self._operations[-1] + operation_name = ( + str(operation_name.value) if isinstance(operation_name, MatrixOperationsQ3D) else operation_name + ) + + if operation_name == "JoinSeries": + command = f"""{operation_name}('{new_name}', '{"', '".join(source_names)}')""" + elif operation_name == "JoinParallel": + command = f"""{operation_name}('{new_name}', '{new_source}', '{new_sink}', '{"', '".join(source_names)}')""" + elif operation_name == "JoinSelectedTerminals": + command = f"""{operation_name}('', '{"', '".join(source_names)}')""" + elif operation_name == "FloatInfinity": command = "FloatInfinity()" - elif self._operations[-1] == "AddGround": - command = f"""{self._operations[-1]}(SelectionArray[{len(source_names)}: '{"', '".join(source_names)}'], + elif operation_name == "AddGround": + command = f"""{operation_name}(SelectionArray[{len(source_names)}: '{"', '".join(source_names)}'], OverrideInfo())""" elif ( - self._operations[-1] == "SetReferenceGround" - or self._operations[-1] == "SetReferenceGround" - or self._operations[-1] == "Float" + operation_name == "SetReferenceGround" + or operation_name == "SetReferenceGround" + or operation_name == "Float" ): - command = f"""{self._operations[-1]}(SelectionArray[{len(source_names)}: '{"', '".join(source_names)}'], + command = f"""{operation_name}(SelectionArray[{len(source_names)}: '{"', '".join(source_names)}'], OverrideInfo())""" - elif self._operations[-1] == "Parallel" or self._operations[-1] == "DiffPair": + elif operation_name == "Parallel" or operation_name == "DiffPair": id_ = 0 for el in self._app.boundaries: if el.name == source_names[0]: id_ = self._app.modeler[el.props["Objects"][0]].id - command = f"""{self._operations[-1]}(SelectionArray[{len(source_names)}: '{"', '".join(source_names)}'], + command = f"""{operation_name}(SelectionArray[{len(source_names)}: '{"', '".join(source_names)}'], OverrideInfo({id_}, '{new_name}'))""" else: - command = f"""{self._operations[-1]}('{"', '".join(source_names)}')""" + command = f"""{operation_name}('{"', '".join(source_names)}')""" return command diff --git a/tests/system/general/test_01_Design.py b/tests/system/general/test_01_Design.py index 35bddbae88a..0d603124fff 100644 --- a/tests/system/general/test_01_Design.py +++ b/tests/system/general/test_01_Design.py @@ -522,3 +522,10 @@ def test_42_save_project_with_file_name(self, aedtapp, local_scratch): def test_43_edit_notes(self, aedtapp): assert aedtapp.edit_notes("this a test") assert not aedtapp.edit_notes(1) + + def test_value_with_units(self, aedtapp): + assert aedtapp.value_with_units("10mm") == "10mm" + assert aedtapp.value_with_units("10") == "10mm" + assert aedtapp.value_with_units("10", units_system="Angle") == "10deg" + assert aedtapp.value_with_units("10", units_system="invalid") == "10" + assert aedtapp.value_with_units("A + Bmm") == "A + Bmm" diff --git a/tests/system/general/test_09_VariableManager.py b/tests/system/general/test_09_VariableManager.py index f0e6fb22d73..ebaf3aec9bf 100644 --- a/tests/system/general/test_09_VariableManager.py +++ b/tests/system/general/test_09_VariableManager.py @@ -26,473 +26,316 @@ import pytest -from ansys.aedt.core import MaxwellCircuit -from ansys.aedt.core.application.variables import Variable -from ansys.aedt.core.application.variables import generate_validation_errors -from ansys.aedt.core.generic.numbers_utils import decompose_variable_value +import ansys.aedt.core as pyaedt from ansys.aedt.core.generic.numbers_utils import is_close from ansys.aedt.core.modeler.geometry_operators import GeometryOperators -from tests.system.general.conftest import desktop_version - - -@pytest.fixture(scope="class") -def aedtapp(add_app): - app = add_app(project_name="Test_09") - return app - - -@pytest.fixture() -def validation_input(): - property_names = [ - "+X Padding Type", - "+X Padding Data", - "-X Padding Type", - "-X Padding Data", - "+Y Padding Type", - "+Y Padding Data", - "-Y Padding Type", - "-Y Padding Data", - "+Z Padding Type", - "+Z Padding Data", - "-Z Padding Type", - "-Z Padding Data", - ] - expected_settings = [ - "Absolute Offset", - "10mm", - "Percentage Offset", - "100", - "Transverse Percentage Offset", - "100", - "Percentage Offset", - "10", - "Absolute Offset", - "50mm", - "Absolute Position", - "-600mm", - ] - actual_settings = list(expected_settings) - return property_names, expected_settings, actual_settings - - -@pytest.fixture() -def validation_float_input(): - property_names = ["+X Padding Data", "-X Padding Data", "+Y Padding Data"] - expected_settings = [100, 200.1, 300] - actual_settings = list(expected_settings) - return property_names, expected_settings, actual_settings + + +@pytest.fixture( + params=[ + pyaedt.Circuit, + pyaedt.Hfss, + pyaedt.Maxwell3d, + pyaedt.Maxwell2d, + pyaedt.Q2d, + pyaedt.Hfss3dLayout, + pyaedt.Rmxprt, + pyaedt.TwinBuilder, + ], + ids=[ + "circuit", + "hfss", + "maxwell_3d", + "maxwell_2d", + "q2d", + "hfss3d_layout", + "rmxprt", + "twin_builder", + ], +) +def app(request, add_app): + app = add_app(application=request.param) + yield app + app.close_project(app.project_name) + + +@pytest.fixture +def hfss_app(add_app): + app = add_app(application=pyaedt.Hfss) + yield app + app.close_project(app.project_name) + + +@pytest.fixture +def maxwell_circuit_app(add_app): + app = add_app(application=pyaedt.MaxwellCircuit) + yield app + app.close_project(app.project_name) class TestClass: - @pytest.fixture(autouse=True) - def init(self, aedtapp, local_scratch): - self.aedtapp = aedtapp - self.local_scratch = local_scratch - - def test_01_set_globals(self): - var = self.aedtapp.variable_manager - self.aedtapp["$Test_Global1"] = "5rad" - self.aedtapp["$Test_Global2"] = -1.0 - self.aedtapp["$Test_Global3"] = "0" - self.aedtapp["$Test_Global4"] = "$Test_Global2*$Test_Global1" - independent = self.aedtapp._variable_manager.independent_variable_names - dependent = self.aedtapp._variable_manager.dependent_variable_names - val = var["$Test_Global4"] - assert val.numeric_value == -5.0 + def test_set_project_variables(self, hfss_app): + hfss_app["$Test_Global1"] = "5rad" + hfss_app["$Test_Global2"] = -1.0 + hfss_app["$Test_Global3"] = "0" + hfss_app["$Test_Global4"] = "$Test_Global2*$Test_Global1" + independent = hfss_app._variable_manager.independent_variable_names + dependent = hfss_app._variable_manager.dependent_variable_names + assert hfss_app.variable_manager.variables["$Test_Global4"].numeric_value == -5.0 assert "$Test_Global1" in independent assert "$Test_Global2" in independent assert "$Test_Global3" in independent assert "$Test_Global4" in dependent - self.aedtapp["$test"] = "1mm" - self.aedtapp["$test2"] = "$test" - assert "$test2" in self.aedtapp.variable_manager.dependent_project_variable_names - assert "$test" in self.aedtapp.variable_manager.independent_project_variable_names - del self.aedtapp["$test2"] - assert "$test2" not in self.aedtapp.variable_manager.variables - del self.aedtapp["$test"] - assert "$test" not in self.aedtapp.variable_manager.variables - - def test_01_set_var_simple(self): - var = self.aedtapp.variable_manager - self.aedtapp["Var1"] = "1rpm" - var_1 = self.aedtapp["Var1"] + hfss_app["$test"] = "1mm" + hfss_app["$test2"] = "$test" + assert "$test2" in hfss_app.variable_manager.dependent_project_variable_names + assert "$test" in hfss_app.variable_manager.independent_project_variable_names + del hfss_app["$test2"] + assert "$test2" not in hfss_app.variable_manager.variables + del hfss_app["$test"] + assert "$test" not in hfss_app.variable_manager.variables + + def test_set_var_simple(self, app): + var = app.variable_manager + app["Var1"] = "1rpm" + var_1 = app["Var1"] var_2 = var["Var1"].expression assert var_1 == var_2 assert is_close(var["Var1"].numeric_value, 1.0) - self.aedtapp["test"] = "1mm" - self.aedtapp["test2"] = "test" - assert "test2" in self.aedtapp.variable_manager.dependent_design_variable_names - del self.aedtapp["test2"] - assert "test2" not in self.aedtapp.variable_manager.variables - del self.aedtapp["test"] - assert "test" not in self.aedtapp.variable_manager.variables - - def test_02_test_formula(self): - self.aedtapp["Var1"] = 3 - self.aedtapp["Var2"] = "12deg" - self.aedtapp["Var3"] = "Var1 * Var2" - - self.aedtapp["$PrjVar1"] = "2*pi" - self.aedtapp["$PrjVar2"] = 45 - self.aedtapp["$PrjVar3"] = "sqrt(34 * $PrjVar2/$PrjVar1 )" - - v = self.aedtapp.variable_manager - for var_name in v.variable_names: - print(f"{var_name} = {self.aedtapp[var_name]}") + # Test all properties + assert var["Var1"].evaluated_value == "1.0rpm" + + if app.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: + assert var["Var1"].description is None + else: + assert var["Var1"].description == "" + + if app.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: + assert var["Var1"].hidden is None + else: + assert not var["Var1"].hidden + + if app.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: + assert var["Var1"].read_only is None + else: + assert not var["Var1"].read_only + + assert is_close(var["Var1"].value, 0.104719, relative_tolerance=1e-4) + + assert var["Var1"].expression == "1rpm" + + if app.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: + assert var["Var1"].is_optimization_enabled is None + else: + assert not var["Var1"].is_optimization_enabled + + if app.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: + assert var["Var1"].is_sensitivity_enabled is None + else: + assert not var["Var1"].is_sensitivity_enabled + + if app.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: + assert var["Var1"].is_statistical_enabled is None + else: + assert not var["Var1"].is_statistical_enabled + + assert not var["Var1"].post_processing + + if app.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: + assert var["Var1"].sweep is None + else: + assert var["Var1"].sweep + + assert var["Var1"].units == "rpm" + + app["test"] = "1mm" + app["test2"] = "test" + assert "test2" in app.variable_manager.dependent_design_variable_names + del app["test2"] + assert "test2" not in app.variable_manager.variables + del app["test"] + assert "test" not in app.variable_manager.variables + + def test_test_formula(self, app): + app["Var1"] = 3 + app["Var2"] = "12deg" + app["Var3"] = "Var1 * Var2" + + assert app.variable_manager.variables["Var3"].numeric_value == 36.0 + if app.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: + assert app.variable_manager.variables["Var3"].circuit_parameter + else: + assert not app.variable_manager.variables["Var3"].circuit_parameter + + assert app.variable_manager.variables["Var3"].units == "deg" + + app["$PrjVar1"] = "2*pi" + app["$PrjVar2"] = 45 + app["$PrjVar3"] = "sqrt(34 * $PrjVar2/$PrjVar1 )" + tol = 1e-9 c2pi = math.pi * 2.0 - assert abs(v["$PrjVar1"].numeric_value - c2pi) < tol - assert abs(v["$PrjVar3"].numeric_value - math.sqrt(34 * 45.0 / c2pi)) < tol - assert abs(v["Var3"].numeric_value - 3.0 * 12.0) < tol - assert v["Var3"].units == "deg" + assert abs(app.variable_manager.variables["$PrjVar1"].numeric_value - c2pi) < tol + assert abs(app.variable_manager.variables["$PrjVar3"].numeric_value - math.sqrt(34 * 45.0 / c2pi)) < tol + assert abs(app.variable_manager.variables["Var3"].numeric_value - 3.0 * 12.0) < tol + assert app.variable_manager.variables["Var3"].units == "deg" - def test_03_test_evaluated_value(self): - self.aedtapp["p1"] = "10mm" - self.aedtapp["p2"] = "20mm" - self.aedtapp["p3"] = "p1 * p2" - v = self.aedtapp.variable_manager + def test_evaluated_value(self, app): + app["p1"] = "10mm" + app.variable_manager.set_variable("p2", "20mm", circuit_parameter=False) + app["p3"] = "p1 * p2" + v = app.variable_manager eval_p3_nom = v._app.get_evaluated_value("p3") assert is_close(eval_p3_nom, 0.0002) + eval_p3_nom_mm = v._app.get_evaluated_value("p3", "mm") assert is_close(eval_p3_nom_mm, 0.2) - v_app = self.aedtapp.variable_manager - assert v_app["p1"].sweep - v_app["p1"].sweep = False - assert not v_app["p1"].sweep - assert not v_app["p1"].read_only - v_app["p1"].read_only = True - assert v_app["p1"].read_only - assert not v_app["p1"].hidden - v_app["p1"].hidden = True - assert v_app["p1"].hidden + + v_app = app.variable_manager + assert v_app["p2"].sweep + v_app["p2"].sweep = False + assert not v_app["p2"].sweep + + if app.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: + # Circuit parameter + assert v_app["p1"].sweep is None + v_app["p1"].sweep = False + assert v_app["p1"] is not None + + assert not v_app["p2"].read_only + v_app["p2"].read_only = True + assert v_app["p2"].read_only + + if app.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: + # Circuit parameter + assert v_app["p1"].read_only is None + v_app["p1"].read_only = True + assert v_app["p1"] is not None + + assert not v_app["p2"].hidden + v_app["p2"].hidden = True + assert v_app["p2"].hidden + + if app.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: + # Circuit parameter + assert v_app["p1"].hidden is None + v_app["p1"].hidden = False + assert v_app["p1"] is not None + assert v_app["p2"].description == "" v_app["p2"].description = "myvar" assert v_app["p2"].description == "myvar" + + if app.design_type in ["Circuit Design", "Twin Builder", "HFSS 3D Layout Design"]: + # Circuit parameter + assert v_app["p1"].description is None + v_app["p1"].description = False + assert v_app["p1"] is not None + assert v_app["p2"].expression == "20mm" v_app["p2"].expression = "5rad" assert v_app["p2"].expression == "5rad" - def test_04_set_variable(self): - assert self.aedtapp.variable_manager.set_variable("p1", expression="10mm") - assert self.aedtapp["p1"] == "10mm" - assert self.aedtapp.variable_manager.set_variable("p1", expression="20mm", overwrite=False) - assert self.aedtapp["p1"] == "10mm" - assert self.aedtapp.variable_manager.set_variable("p1", expression="30mm") - assert self.aedtapp["p1"] == "30mm" - assert self.aedtapp.variable_manager.set_variable( + assert v_app["p1"].expression == "10mm" + v_app["p1"].expression = "5mm" + assert v_app["p1"].expression == "5mm" + + def test_set_variable(self, app): + assert app.variable_manager.set_variable("p1", expression="10mm", circuit_parameter=False) + assert app["p1"] == "10mm" + assert not app.variable_manager.variables["p1"].circuit_parameter + assert app.variable_manager.variables["p1"].numeric_value == 10.0 + assert app.variable_manager.set_variable("p1", expression="20mm", overwrite=False) + assert app["p1"] == "10mm" + assert app.variable_manager.set_variable("p1", expression="30mm") + assert app["p1"] == "30mm" + + assert app.variable_manager.set_variable( name="p2", expression="10mm", read_only=True, hidden=True, description="This is a description of this variable", ) - assert self.aedtapp.variable_manager.set_variable("$p1", expression="10mm") - assert self.aedtapp.variable_manager.set_variable("$p1", expression="12mm") - - def test_05_variable_class(self): - v = Variable("4mm") - num_value = v.numeric_value - assert num_value == 4.0 - - v = v.rescale_to("meter") - assert v.evaluated_value == "0.004meter" - assert v.numeric_value == 0.004 - assert v.value == v.numeric_value - - v = Variable("100cel") - assert v.numeric_value == 100.0 - assert v.evaluated_value == "100.0cel" - assert v.value == 373.15 - v.rescale_to("fah") - assert v.numeric_value == 212.0 - - v = Variable("30dBW") - assert v.numeric_value == 30.0 - assert v.evaluated_value == "30.0dBW" - assert v.value == 1000 - v.rescale_to("megW") - assert v.numeric_value == 0.001 - assert v.evaluated_value == "0.001megW" - assert v.value == 1000 - - v = Variable("10dBm") - assert v.numeric_value == 10.0 - assert v.evaluated_value == "10.0dBm" - assert v.value == 0.01 - v.rescale_to("W") - assert v.numeric_value == 0.01 - assert v.evaluated_value == "0.01W" - assert v.value == 0.01 - - def test_06_multiplication(self): - v1 = Variable("10mm") - v2 = Variable(3) - v3 = Variable("3mA") - v4 = Variable("40V") - v5 = Variable("100NewtonMeter") - v6 = Variable("1000rpm") - tol = 1e-4 - result_1 = v1 * v2 - result_2 = v2 * v3 - result_3 = v3 * v4 - result_4 = v4 * v3 - result_5 = v4 * 24.0 * v3 - result_6 = v5 * v6 - result_7 = v6 * v5 - result_8 = (v5 * v6).rescale_to("kW") - assert result_1.numeric_value == 30.0 - assert result_1.unit_system == "Length" - - assert result_2.numeric_value == 9.0 - assert result_2.units == "mA" - assert result_2.unit_system == "Current" - - assert result_3.numeric_value == 0.12 - assert result_3.units == "W" - assert result_3.unit_system == "Power" - - assert result_4.numeric_value == 0.12 - assert result_4.units == "W" - assert result_4.unit_system == "Power" - - assert result_5.numeric_value == 2.88 - assert result_5.units == "W" - assert result_5.unit_system == "Power" - - assert abs(result_6.numeric_value - 10471.9755) / result_6.numeric_value < tol - assert result_6.units == "W" - assert result_6.unit_system == "Power" - - assert abs(result_7.numeric_value - 10471.9755) / result_4.numeric_value < tol - assert result_7.units == "W" - assert result_7.unit_system == "Power" - - assert abs(result_8.numeric_value - 10.4719755) / result_8.numeric_value < tol - assert result_8.units == "kW" - assert result_8.unit_system == "Power" - - def test_07_addition(self): - v1 = Variable("10mm") - v2 = Variable(3) - v3 = Variable("3mA") - v4 = Variable("10A") - with pytest.raises(ValueError): - v1 + v2 - - with pytest.raises(ValueError): - v2 + v1 - result_1 = v2 + v2 - result_2 = v3 + v4 - result_3 = v3 + v3 - - assert result_1.numeric_value == 6.0 - assert result_1.unit_system == "None" - - assert result_2.numeric_value == 10.003 - assert result_2.units == "A" - assert result_2.unit_system == "Current" - - assert result_3.numeric_value == 6.0 - assert result_3.units == "mA" - assert result_3.unit_system == "Current" - - def test_08_subtraction(self): - v1 = Variable("10mm") - v2 = Variable(3) - v3 = Variable("3mA") - v4 = Variable("10A") - - with pytest.raises(ValueError): - v1 - v2 - - with pytest.raises(ValueError): - v2 - v1 - - result_1 = v2 - v2 - result_2 = v3 - v4 - result_3 = v3 - v3 - - assert result_1.numeric_value == 0.0 - assert result_1.unit_system == "None" - - assert result_2.numeric_value == -9.997 - assert result_2.units == "A" - assert result_2.unit_system == "Current" - - assert result_3.numeric_value == 0.0 - assert result_3.units == "mA" - assert result_3.unit_system == "Current" - - def test_09_specify_units(self): - # Scaling of the unit system "Angle" - angle = Variable("1rad") - angle.rescale_to("deg") - assert is_close(angle.numeric_value, 57.29577951308232) - angle.rescale_to("degmin") - assert is_close(angle.numeric_value, 57.29577951308232 * 60.0) - angle.rescale_to("degsec") - assert is_close(angle.numeric_value, 57.29577951308232 * 3600.0) - - # Convert 200Hz to Angular speed numerically - omega = Variable(200 * math.pi * 2, "rad_per_sec") - assert omega.unit_system == "AngularSpeed" - assert is_close(omega.value, 1256.6370614359173) - omega.rescale_to("rpm") - assert is_close(omega.numeric_value, 12000.0) - omega.rescale_to("rev_per_sec") - assert is_close(omega.numeric_value, 200.0) - - # test speed times time equals diestance - v = Variable("100m_per_sec") - assert v.unit_system == "Speed" - v.rescale_to("feet_per_sec") - assert is_close(v.numeric_value, 328.08398950131) - v.rescale_to("feet_per_min") - assert is_close(v.numeric_value, 328.08398950131 * 60) - v.rescale_to("miles_per_sec") - assert is_close(v.numeric_value, 0.06213711723534) - v.rescale_to("miles_per_minute") - assert is_close(v.numeric_value, 3.72822703412) - v.rescale_to("miles_per_hour") - assert is_close(v.numeric_value, 223.69362204724) - - t = Variable("20s") - distance = v * t - assert distance.unit_system == "Length" - assert distance.evaluated_value == "2000.0meter" - distance.rescale_to("in") - assert is_close(distance.numeric_value, 2000 / 0.0254) - - def test_10_division(self): - """ - 'Power_divide_Voltage': 'Current', - 'Power_divide_Current': 'Voltage', - 'Power_divide_AngularSpeed': 'Torque', - 'Power_divide_Torque': 'Angular_Speed', - 'Angle_divide_AngularSpeed': 'Time', - 'Angle_divide_Time': 'AngularSpeed', - 'Voltage_divide_Current': 'Resistance', - 'Voltage_divide_Resistance': 'Current', - 'Resistance_divide_AngularSpeed': 'Inductance', - 'Resistance_divide_Inductance': 'AngularSpeed', - 'None_divide_Freq': 'Time', - 'None_divide_Time': 'Freq', - 'Length_divide_Time': 'Speed', - 'Length_divide_Speed': 'Time' - """ - v1 = Variable("10W") - v2 = Variable("40V") - v3 = Variable("1s") - v4 = Variable("5mA") - v5 = Variable("100NewtonMeter") - v6 = Variable("1000rpm") - tol = 1e-4 - - result_1 = v1 / v2 - assert result_1.numeric_value == 0.25 - assert result_1.units == "A" - assert result_1.unit_system == "Current" - - result_2 = v2 / result_1 - assert result_2.numeric_value == 160.0 - assert result_2.units == "ohm" - assert result_2.unit_system == "Resistance" - - result_3 = 3 / v3 - assert result_3.numeric_value == 3.0 - assert result_3.units == "Hz" - assert result_3.unit_system == "Freq" - - result_4 = v3 / 2 - assert abs(result_4.numeric_value - 0.5) < tol - assert result_4.units == "s" - assert result_4.unit_system == "Time" - - result_5 = v4 / v5 - assert abs(result_5.numeric_value - 0.00005) < tol - assert result_5.units == "" - assert result_5.unit_system == "None" - - result_6 = v1 / v5 + v6 - assert abs(result_6.numeric_value - 104.8198) / result_6.numeric_value < tol - assert result_6.units == "rad_per_sec" - assert result_6.unit_system == "AngularSpeed" - - def test_11_delete_variable(self): - assert self.aedtapp.variable_manager.delete_variable("Var1") - - def test_12_decompose_variable_value(self): - assert decompose_variable_value("3.123456m") == (3.123456, "m") - assert decompose_variable_value("3m") == (3, "m") - assert decompose_variable_value("3") == (3, "") - assert decompose_variable_value("3.") == (3.0, "") - assert decompose_variable_value("3.123456m2") == (3.123456, "m2") - assert decompose_variable_value("3.123456Nm-2") == (3.123456, "Nm-2") - assert decompose_variable_value("3.123456kg2m2") == (3.123456, "kg2m2") - assert decompose_variable_value("3.123456kgm2") == (3.123456, "kgm2") - - def test_13_postprocessing(self): - v1 = self.aedtapp.variable_manager.set_variable("test_post1", 10, is_post_processing=True) + assert app.variable_manager.set_variable("$p1", expression="10mm") + assert app.variable_manager.set_variable("$p1", expression="12mm") + + def test_delete_variable(self, app): + app["Var1"] = 1 + assert app.variable_manager.delete_variable("Var1") + + def test_postprocessing(self, app): + if app.design_type == "Twin Builder": + pytest.skip("Twin Builder is crashing for this test.") + + v1 = app.variable_manager.set_variable("test_post1", 10, is_post_processing=True, circuit_parameter=False) assert v1 - assert not self.aedtapp.variable_manager.set_variable("test2", "v1+1") - assert self.aedtapp.variable_manager.set_variable("test2", "test_post1+1", is_post_processing=True) + assert not app.variable_manager.set_variable("test2", "v1+1") + assert app.variable_manager.set_variable( + "test2", "test_post1+1", is_post_processing=True, circuit_parameter=False + ) x1 = GeometryOperators.parse_dim_arg( - self.aedtapp.variable_manager["test2"].evaluated_value, variable_manager=self.aedtapp.variable_manager + app.variable_manager["test2"].evaluated_value, variable_manager=app.variable_manager ) - assert x1 == 11 - - def test_14_intrinsics(self): - self.aedtapp["fc"] = "Freq" - assert self.aedtapp["fc"] == "Freq" - assert self.aedtapp.variable_manager.dependent_variables["fc"].units == self.aedtapp.units.frequency - - def test_15_arrays(self): - self.aedtapp["arr_index"] = 0 - self.aedtapp["arr1"] = "[1, 2, 3]" - self.aedtapp["arr2"] = [1, 2, 3] - self.aedtapp["getvalue1"] = "arr1[arr_index]" - self.aedtapp["getvalue2"] = "arr2[arr_index]" - assert self.aedtapp.variable_manager["getvalue1"].numeric_value == 1.0 - assert self.aedtapp.variable_manager["getvalue2"].numeric_value == 1.0 - - def test_16_maxwell_circuit_variables(self): - mc = MaxwellCircuit(version=desktop_version) - mc["var2"] = "10mm" - assert mc["var2"] == "10mm" - v_circuit = mc.variable_manager + assert x1 == 11.0 + + def test_intrinsics(self, app): + app["fc"] = "Freq" + assert app["fc"] == "Freq" + assert app.variable_manager.dependent_variables["fc"].units == app.units.frequency + + def test_arrays(self, app): + app.variable_manager.set_variable("arr_index", expression=0, circuit_parameter=False) + app.variable_manager.set_variable("arr1", expression="[1, 2, 3]", circuit_parameter=False) + app.variable_manager.set_variable("arr2", expression=[1, 2, 3], circuit_parameter=False) + app.variable_manager.set_variable("arr_index", expression=0, circuit_parameter=False) + + app["getvalue1"] = "arr1[arr_index]" + app["getvalue2"] = "arr2[arr_index]" + assert app.variable_manager["getvalue1"].numeric_value == 1.0 + assert app.variable_manager["getvalue2"].numeric_value == 1.0 + + def test_maxwell_circuit_variables(self, maxwell_circuit_app): + maxwell_circuit_app["var2"] = "10mm" + assert maxwell_circuit_app["var2"] == "10mm" + v_circuit = maxwell_circuit_app.variable_manager var_circuit = v_circuit.variable_names assert "var2" in var_circuit assert v_circuit.independent_variables["var2"].units == "mm" - mc["var3"] = "10deg" - mc["var4"] = "10rad" - assert mc["var3"] == "10deg" - assert mc["var4"] == "10rad" - - def test_17_project_variable_operation(self): - self.aedtapp["$my_proj_test"] = "1mm" - self.aedtapp["$my_proj_test2"] = 2 - self.aedtapp["$my_proj_test3"] = "$my_proj_test*$my_proj_test2" - assert self.aedtapp.variable_manager["$my_proj_test3"].units == "mm" - assert self.aedtapp.variable_manager["$my_proj_test3"].numeric_value == 2.0 - - def test_18_test_optimization_properties(self): + maxwell_circuit_app["var3"] = "10deg" + maxwell_circuit_app["var4"] = "10rad" + assert maxwell_circuit_app["var3"] == "10deg" + assert maxwell_circuit_app["var4"] == "10rad" + + def test_project_variable_operation(self, app): + app["$my_proj_test"] = "1mm" + app["$my_proj_test2"] = 2 + app["$my_proj_test3"] = "$my_proj_test*$my_proj_test2" + assert app.variable_manager["$my_proj_test3"].units == "mm" + assert app.variable_manager["$my_proj_test3"].numeric_value == 2.0 + + def test_test_optimization_properties(self, app): var = "v1" - self.aedtapp[var] = "10mm" - v = self.aedtapp.variable_manager + app.variable_manager.set_variable(var, "10mm", circuit_parameter=False) + v = app.variable_manager assert not v[var].is_optimization_enabled v[var].is_optimization_enabled = True assert v[var].is_optimization_enabled assert v[var].optimization_min_value == "5mm" - v[var].optimization_min_value = "1m" + v[var].optimization_min_value = "1mm" + assert v[var].optimization_min_value == "1mm" + assert v[var].optimization_max_value == "15mm" v[var].optimization_max_value = "14mm" assert v[var].optimization_max_value == "14mm" + assert not v[var].is_tuning_enabled v[var].is_tuning_enabled = True assert v[var].is_tuning_enabled + assert v[var].tuning_min_value == "5mm" v[var].tuning_min_value = "4mm" assert v[var].tuning_max_value == "15mm" @@ -516,10 +359,17 @@ def test_18_test_optimization_properties(self): v[var].sensitivity_initial_disp = "0.5mm" assert v[var].sensitivity_initial_disp == "0.5mm" - def test_19_test_optimization_global_properties(self): + if app.design_type == "Circuit Design": + # Circuit parameter is not available in optimetrics + app.variable_manager.set_variable("v2", "20mm", circuit_parameter=True) + v = app.variable_manager + assert not v["v2"].is_optimization_enabled + v["v2"].is_optimization_enabled = True + + def test_optimization_global_properties(self, hfss_app): var = "$v1" - self.aedtapp[var] = "10mm" - v = self.aedtapp.variable_manager + hfss_app[var] = "10mm" + v = hfss_app.variable_manager assert not v[var].is_optimization_enabled v[var].is_optimization_enabled = True assert v[var].is_optimization_enabled @@ -554,139 +404,27 @@ def test_19_test_optimization_global_properties(self): v[var].sensitivity_initial_disp = "0.5mm" assert v[var].sensitivity_initial_disp == "0.5mm" - def test_20_variable_with_units(self): - self.aedtapp["v1"] = "3mm" - self.aedtapp["v2"] = "2*v1" - assert self.aedtapp.variable_manager.decompose("v1") == (3.0, "mm") - assert self.aedtapp.variable_manager.decompose("v2") == (6.0, "mm") - assert self.aedtapp.variable_manager["v2"].decompose() == (6.0, "mm") - assert self.aedtapp.variable_manager.decompose("5mm") == (5.0, "mm") - - def test_21_test_validator_exact_match(self, validation_input): - property_names, expected_settings, actual_settings = validation_input - validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) - assert len(validation_errors) == 0 - - def test_22_test_validator_tolerance(self, validation_input): - property_names, expected_settings, actual_settings = validation_input - - # Small difference should produce no validation errors - actual_settings[1] = "10.0000000001mm" - actual_settings[3] = "100.0000000001" - actual_settings[5] = "100.0000000001" - validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) - - assert len(validation_errors) == 0 - - def test_23_test_validator_invalidate_offset_type(self, validation_input): - property_names, expected_settings, actual_settings = validation_input - - # Are expected to be "Absolute Offset" - actual_settings[0] = "Percentage Offset" - - validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) - - assert len(validation_errors) == 1 - - def test_24_test_validator_invalidate_value(self, validation_input): - property_names, expected_settings, actual_settings = validation_input - - # Above tolerance - actual_settings[1] = "10.000002mm" - - validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) - - assert len(validation_errors) == 1 - - def test_25_test_validator_invalidate_unit(self, validation_input): - property_names, expected_settings, actual_settings = validation_input - - actual_settings[1] = "10in" - - validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) - - assert len(validation_errors) == 1 - - def test_26_test_validator_invalidate_multiple(self, validation_input): - property_names, expected_settings, actual_settings = validation_input - - actual_settings[0] = "Percentage Offset" - actual_settings[1] = "22mm" - actual_settings[2] = "Transverse Percentage Offset" - - validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) - - assert len(validation_errors) == 3 - - def test_27_test_validator_invalidate_wrong_type(self, validation_input): - property_names, expected_settings, actual_settings = validation_input - - actual_settings[1] = "nonnumeric" - - validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) - - assert len(validation_errors) == 1 - - def test_28_test_validator_float_type(self, validation_float_input): - property_names, expected_settings, actual_settings = validation_float_input - - validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) - - assert len(validation_errors) == 0 - - def test_29_test_validator_float_type_tolerance(self, validation_float_input): - property_names, expected_settings, actual_settings = validation_float_input - - # Set just below the tolerance to pass the check - actual_settings[0] *= 1 + 0.99 * 1e-9 - actual_settings[1] *= 1 - 0.99 * 1e-9 - actual_settings[2] *= 1 + 0.99 * 1e-9 - - validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) - - assert len(validation_errors) == 0 - - def test_30_test_validator_float_type_invalidate(self, validation_float_input): - property_names, expected_settings, actual_settings = validation_float_input - - # Set just above the tolerance to fail the check - actual_settings[0] *= 1 + 1.01 * 1e-9 - actual_settings[1] *= 1 + 1.01 * 1e-9 - actual_settings[2] *= 1 + 1.01 * 1e-9 - - validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) - - assert len(validation_errors) == 3 - - def test_31_test_validator_float_type_invalidate(self, validation_float_input): - property_names, expected_settings, actual_settings = validation_float_input - - actual_settings[0] *= 2 - - validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) - - assert len(validation_errors) == 1 - - def test_32_delete_unused_variables(self): - self.aedtapp.insert_design("used_variables") - self.aedtapp["used_var"] = "1mm" - self.aedtapp["unused_var"] = "1mm" - self.aedtapp["$project_used_var"] = "1" - self.aedtapp.modeler.create_rectangle(0, ["used_var", "used_var", "used_var"], [10, 20]) - mat1 = self.aedtapp.materials.add_material("new_copper2") + def test_variable_with_units(self, app): + app["v1"] = "3mm" + app["v2"] = "2*v1" + assert app.variable_manager.decompose("v1") == (3.0, "mm") + assert app.variable_manager.decompose("v2") == (6.0, "mm") + assert app.variable_manager["v2"].decompose() == (6.0, "mm") + assert app.variable_manager.decompose("5mm") == (5.0, "mm") + + def test_delete_unused_variables(self, hfss_app): + hfss_app.insert_design("used_variables") + hfss_app["used_var"] = "1mm" + hfss_app["unused_var"] = "1mm" + hfss_app["$project_used_var"] = "1" + hfss_app.modeler.create_rectangle(0, ["used_var", "used_var", "used_var"], [10, 20]) + mat1 = hfss_app.materials.add_material("new_copper2") mat1.permittivity = "$project_used_var" - assert self.aedtapp.variable_manager.is_used("used_var") - assert not self.aedtapp.variable_manager.is_used("unused_var") - assert self.aedtapp.variable_manager.delete_variable("unused_var") - self.aedtapp["unused_var"] = "1mm" - number_of_variables = len(self.aedtapp.variable_manager.variable_names) - assert self.aedtapp.variable_manager.delete_unused_variables() - new_number_of_variables = len(self.aedtapp.variable_manager.variable_names) + assert hfss_app.variable_manager.is_used("used_var") + assert not hfss_app.variable_manager.is_used("unused_var") + assert hfss_app.variable_manager.delete_variable("unused_var") + hfss_app["unused_var"] = "1mm" + number_of_variables = len(hfss_app.variable_manager.variable_names) + assert hfss_app.variable_manager.delete_unused_variables() + new_number_of_variables = len(hfss_app.variable_manager.variable_names) assert number_of_variables != new_number_of_variables - - def test_33_value_with_units(self): - assert self.aedtapp.value_with_units("10mm") == "10mm" - assert self.aedtapp.value_with_units("10") == "10mm" - assert self.aedtapp.value_with_units("10", units_system="Angle") == "10deg" - assert self.aedtapp.value_with_units("10", units_system="invalid") == "10" - assert self.aedtapp.value_with_units("A + Bmm") == "A + Bmm" diff --git a/tests/system/general/test_16_3d_stackup.py b/tests/system/general/test_16_3d_stackup.py index 9e679909df8..d00652ab223 100644 --- a/tests/system/general/test_16_3d_stackup.py +++ b/tests/system/general/test_16_3d_stackup.py @@ -45,7 +45,7 @@ def init(self, aedtapp, st, local_scratch): self.local_scratch = local_scratch def test_01_create_stackup(self): - self.st.dielectic_x_postion = "10mm" + self.st.dielectic_x_position = "10mm" gnd = self.st.add_ground_layer("gnd1") self.st.add_dielectric_layer("diel1", thickness=1) assert self.st.thickness.numeric_value == 1.035 @@ -143,7 +143,9 @@ def test_05_polygon(self): assert poly4 def test_05_resize(self): + self.st.application.variable_manager.variables["dielectric_x_position"].value assert self.st.resize(20) + self.st.application.variable_manager.variables["dielectric_x_position"].value assert self.st.dielectric_x_position self.st.dielectric_x_position = "10mm" assert self.st.dielectric_x_position.evaluated_value == "10.0mm" diff --git a/tests/unit/test_variable.py b/tests/unit/test_variable.py new file mode 100644 index 00000000000..c3ddc149296 --- /dev/null +++ b/tests/unit/test_variable.py @@ -0,0 +1,421 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2025 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. + +import math + +import pytest + +from ansys.aedt.core.application.variables import Variable +from ansys.aedt.core.application.variables import generate_validation_errors +from ansys.aedt.core.generic.numbers_utils import decompose_variable_value +from ansys.aedt.core.generic.numbers_utils import is_close + + +@pytest.fixture() +def validation_input(): + property_names = [ + "+X Padding Type", + "+X Padding Data", + "-X Padding Type", + "-X Padding Data", + "+Y Padding Type", + "+Y Padding Data", + "-Y Padding Type", + "-Y Padding Data", + "+Z Padding Type", + "+Z Padding Data", + "-Z Padding Type", + "-Z Padding Data", + ] + expected_settings = [ + "Absolute Offset", + "10mm", + "Percentage Offset", + "100", + "Transverse Percentage Offset", + "100", + "Percentage Offset", + "10", + "Absolute Offset", + "50mm", + "Absolute Position", + "-600mm", + ] + actual_settings = list(expected_settings) + return property_names, expected_settings, actual_settings + + +@pytest.fixture() +def validation_float_input(): + property_names = ["+X Padding Data", "-X Padding Data", "+Y Padding Data"] + expected_settings = [100, 200.1, 300] + actual_settings = list(expected_settings) + return property_names, expected_settings, actual_settings + + +class TestClass: + def test_variable_class(self): + v = Variable("4mm") + num_value = v.numeric_value + assert num_value == 4.0 + + v = v.rescale_to("meter") + assert v.evaluated_value == "0.004meter" + assert v.numeric_value == 0.004 + assert v.value == v.numeric_value + + v = Variable("100cel") + assert v.numeric_value == 100.0 + assert v.evaluated_value == "100.0cel" + assert v.value == 373.15 + v.rescale_to("fah") + assert v.numeric_value == 212.0 + + v = Variable("30dBW") + assert v.numeric_value == 30.0 + assert v.evaluated_value == "30.0dBW" + assert v.value == 1000 + v.rescale_to("megW") + assert v.numeric_value == 0.001 + assert v.evaluated_value == "0.001megW" + assert v.value == 1000 + + v = Variable("10dBm") + assert v.numeric_value == 10.0 + assert v.evaluated_value == "10.0dBm" + assert v.value == 0.01 + v.rescale_to("W") + assert v.numeric_value == 0.01 + assert v.evaluated_value == "0.01W" + assert v.value == 0.01 + + def test_multiplication(self): + v1 = Variable("10mm") + v2 = Variable(3) + v3 = Variable("3mA") + v4 = Variable("40V") + v5 = Variable("100NewtonMeter") + v6 = Variable("1000rpm") + tol = 1e-4 + result_1 = v1 * v2 + + result_2 = v2 * v3 + result_3 = v3 * v4 + result_4 = v4 * v3 + result_5 = v4 * 24.0 * v3 + result_6 = v5 * v6 + result_7 = v6 * v5 + result_8 = (v5 * v6).rescale_to("kW") + assert result_1.numeric_value == 30.0 + assert result_1.unit_system == "Length" + + assert result_2.numeric_value == 9.0 + assert result_2.units == "mA" + assert result_2.unit_system == "Current" + + assert result_3.numeric_value == 0.12 + assert result_3.units == "W" + assert result_3.unit_system == "Power" + + assert result_4.numeric_value == 0.12 + assert result_4.units == "W" + assert result_4.unit_system == "Power" + + assert result_5.numeric_value == 2.88 + assert result_5.units == "W" + assert result_5.unit_system == "Power" + + assert abs(result_6.numeric_value - 10471.9755) / result_6.numeric_value < tol + assert result_6.units == "W" + assert result_6.unit_system == "Power" + + assert abs(result_7.numeric_value - 10471.9755) / result_4.numeric_value < tol + assert result_7.units == "W" + assert result_7.unit_system == "Power" + + assert abs(result_8.numeric_value - 10.4719755) / result_8.numeric_value < tol + assert result_8.units == "kW" + assert result_8.unit_system == "Power" + + def test_addition(self): + v1 = Variable("10mm") + v2 = Variable(3) + v3 = Variable("3mA") + v4 = Variable("10A") + with pytest.raises(ValueError): + _ = v1 + v2 + + with pytest.raises(ValueError): + _ = v2 + v1 + result_1 = v2 + v2 + result_2 = v3 + v4 + result_3 = v3 + v3 + + assert result_1.numeric_value == 6.0 + assert result_1.unit_system == "None" + + assert result_2.numeric_value == 10.003 + assert result_2.units == "A" + assert result_2.unit_system == "Current" + + assert result_3.numeric_value == 6.0 + assert result_3.units == "mA" + assert result_3.unit_system == "Current" + + def test_subtraction(self): + v1 = Variable("10mm") + v2 = Variable(3) + v3 = Variable("3mA") + v4 = Variable("10A") + + with pytest.raises(ValueError): + _ = v1 - v2 + + with pytest.raises(ValueError): + _ = v2 - v1 + + result_1 = v2 - v2 + result_2 = v3 - v4 + result_3 = v3 - v3 + + assert result_1.numeric_value == 0.0 + assert result_1.unit_system == "None" + + assert result_2.numeric_value == -9.997 + assert result_2.units == "A" + assert result_2.unit_system == "Current" + + assert result_3.numeric_value == 0.0 + assert result_3.units == "mA" + assert result_3.unit_system == "Current" + + def test_specify_units(self): + # Scaling of the unit system "Angle" + angle = Variable("1rad") + angle.rescale_to("deg") + assert is_close(angle.numeric_value, 57.29577951308232) + angle.rescale_to("degmin") + assert is_close(angle.numeric_value, 57.29577951308232 * 60.0) + angle.rescale_to("degsec") + assert is_close(angle.numeric_value, 57.29577951308232 * 3600.0) + + # Convert 200Hz to Angular speed numerically + omega = Variable(200 * math.pi * 2, "rad_per_sec") + assert omega.unit_system == "AngularSpeed" + assert is_close(omega.value, 1256.6370614359173) + omega.rescale_to("rpm") + assert is_close(omega.numeric_value, 12000.0) + omega.rescale_to("rev_per_sec") + assert is_close(omega.numeric_value, 200.0) + + # test speed times time equals diestance + v = Variable("100m_per_sec") + assert v.unit_system == "Speed" + v.rescale_to("feet_per_sec") + assert is_close(v.numeric_value, 328.08398950131) + v.rescale_to("feet_per_min") + assert is_close(v.numeric_value, 328.08398950131 * 60) + v.rescale_to("miles_per_sec") + assert is_close(v.numeric_value, 0.06213711723534) + v.rescale_to("miles_per_minute") + assert is_close(v.numeric_value, 3.72822703412) + v.rescale_to("miles_per_hour") + assert is_close(v.numeric_value, 223.69362204724) + + t = Variable("20s") + distance = v * t + assert distance.unit_system == "Length" + assert distance.evaluated_value == "2000.0meter" + distance.rescale_to("in") + assert is_close(distance.numeric_value, 2000 / 0.0254) + + def test_division(self): + """ + 'Power_divide_Voltage': 'Current', + 'Power_divide_Current': 'Voltage', + 'Power_divide_AngularSpeed': 'Torque', + 'Power_divide_Torque': 'Angular_Speed', + 'Angle_divide_AngularSpeed': 'Time', + 'Angle_divide_Time': 'AngularSpeed', + 'Voltage_divide_Current': 'Resistance', + 'Voltage_divide_Resistance': 'Current', + 'Resistance_divide_AngularSpeed': 'Inductance', + 'Resistance_divide_Inductance': 'AngularSpeed', + 'None_divide_Freq': 'Time', + 'None_divide_Time': 'Freq', + 'Length_divide_Time': 'Speed', + 'Length_divide_Speed': 'Time' + """ + v1 = Variable("10W") + v2 = Variable("40V") + v3 = Variable("1s") + v4 = Variable("5mA") + v5 = Variable("100NewtonMeter") + v6 = Variable("1000rpm") + tol = 1e-4 + + result_1 = v1 / v2 + assert result_1.numeric_value == 0.25 + assert result_1.units == "A" + assert result_1.unit_system == "Current" + + result_2 = v2 / result_1 + assert result_2.numeric_value == 160.0 + assert result_2.units == "ohm" + assert result_2.unit_system == "Resistance" + + result_3 = 3 / v3 + assert result_3.numeric_value == 3.0 + assert result_3.units == "Hz" + assert result_3.unit_system == "Freq" + + result_4 = v3 / 2 + assert abs(result_4.numeric_value - 0.5) < tol + assert result_4.units == "s" + assert result_4.unit_system == "Time" + + result_5 = v4 / v5 + assert abs(result_5.numeric_value - 0.00005) < tol + assert result_5.units == "" + assert result_5.unit_system == "None" + + result_6 = v1 / v5 + v6 + assert abs(result_6.numeric_value - 104.8198) / result_6.numeric_value < tol + assert result_6.units == "rad_per_sec" + assert result_6.unit_system == "AngularSpeed" + + def test_decompose_variable_value(self): + assert decompose_variable_value("3.123456m") == (3.123456, "m") + assert decompose_variable_value("3m") == (3, "m") + assert decompose_variable_value("3") == (3, "") + assert decompose_variable_value("3.") == (3.0, "") + assert decompose_variable_value("3.123456m2") == (3.123456, "m2") + assert decompose_variable_value("3.123456Nm-2") == (3.123456, "Nm-2") + assert decompose_variable_value("3.123456kg2m2") == (3.123456, "kg2m2") + assert decompose_variable_value("3.123456kgm2") == (3.123456, "kgm2") + + def test_validator_exact_match(self, validation_input): + property_names, expected_settings, actual_settings = validation_input + validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) + assert len(validation_errors) == 0 + + def test_validator_tolerance(self, validation_input): + property_names, expected_settings, actual_settings = validation_input + + # Small difference should produce no validation errors + actual_settings[1] = "10.0000000001mm" + actual_settings[3] = "100.0000000001" + actual_settings[5] = "100.0000000001" + validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) + + assert len(validation_errors) == 0 + + def test_validator_invalidate_offset_type(self, validation_input): + property_names, expected_settings, actual_settings = validation_input + + # Are expected to be "Absolute Offset" + actual_settings[0] = "Percentage Offset" + + validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) + + assert len(validation_errors) == 1 + + def test_validator_invalidate_value(self, validation_input): + property_names, expected_settings, actual_settings = validation_input + + # Above tolerance + actual_settings[1] = "10.000002mm" + + validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) + + assert len(validation_errors) == 1 + + def test_validator_invalidate_unit(self, validation_input): + property_names, expected_settings, actual_settings = validation_input + + actual_settings[1] = "10in" + + validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) + + assert len(validation_errors) == 1 + + def test_validator_invalidate_multiple(self, validation_input): + property_names, expected_settings, actual_settings = validation_input + + actual_settings[0] = "Percentage Offset" + actual_settings[1] = "22mm" + actual_settings[2] = "Transverse Percentage Offset" + + validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) + + assert len(validation_errors) == 3 + + def test_validator_invalidate_wrong_type(self, validation_input): + property_names, expected_settings, actual_settings = validation_input + + actual_settings[1] = "nonnumeric" + + validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) + + assert len(validation_errors) == 1 + + def test_validator_float_type(self, validation_float_input): + property_names, expected_settings, actual_settings = validation_float_input + + validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) + + assert len(validation_errors) == 0 + + def test_validator_float_type_tolerance(self, validation_float_input): + property_names, expected_settings, actual_settings = validation_float_input + + # Set just below the tolerance to pass the check + actual_settings[0] *= 1 + 0.99 * 1e-9 + actual_settings[1] *= 1 - 0.99 * 1e-9 + actual_settings[2] *= 1 + 0.99 * 1e-9 + + validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) + + assert len(validation_errors) == 0 + + def test_validator_float_type_invalidate(self, validation_float_input): + property_names, expected_settings, actual_settings = validation_float_input + + # Set just above the tolerance to fail the check + actual_settings[0] *= 1 + 1.01 * 1e-9 + actual_settings[1] *= 1 + 1.01 * 1e-9 + actual_settings[2] *= 1 + 1.01 * 1e-9 + + validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) + + assert len(validation_errors) == 3 + + def test_validator_float_type_invalidate_zeros(self, validation_float_input): + property_names, expected_settings, actual_settings = validation_float_input + + actual_settings[0] *= 2 + + validation_errors = generate_validation_errors(property_names, expected_settings, actual_settings) + + assert len(validation_errors) == 1