diff --git a/doc/source/Resources/pyaedt_installer_from_aedt.py b/doc/source/Resources/pyaedt_installer_from_aedt.py index 6b4451a9c00..12abb5d58e7 100644 --- a/doc/source/Resources/pyaedt_installer_from_aedt.py +++ b/doc/source/Resources/pyaedt_installer_from_aedt.py @@ -39,6 +39,20 @@ VENV_DIR_PREFIX = ".pyaedt_env" + +""" +It is possible create Python virtual environment in a specific directory by setting variable VENV_DIR. +For example, +VENV_DIR = "e:/pyaedt_env" +""" +VENV_DIR = None +if not VENV_DIR: + if is_windows: + VENV_DIR = os.path.join(os.environ["APPDATA"], VENV_DIR_PREFIX) + else: + VENV_DIR = os.path.join(os.environ["HOME"], VENV_DIR_PREFIX) + + DISCLAIMER = ( "This script will download and install certain third-party software and/or " "open-source software (collectively, 'Third-Party Software'). Such Third-Party " @@ -99,10 +113,10 @@ def run_pyinstaller_from_c_python(oDesktop): # Add PyAEDT tabs in AEDT # Virtual environment path and Python executable if is_windows: - venv_dir = os.path.join(os.environ["APPDATA"], VENV_DIR_PREFIX, python_version_new) + venv_dir = os.path.join(VENV_DIR, python_version_new) python_exe = os.path.join(venv_dir, "Scripts", "python.exe") else: - venv_dir = os.path.join(os.environ["HOME"], VENV_DIR_PREFIX, python_version_new) + venv_dir = os.path.join(VENV_DIR, python_version_new) python_exe = os.path.join(venv_dir, "bin", "python") pyaedt_path = os.path.join(venv_dir, "Lib", "site-packages", "ansys", "aedt", "core") if is_linux: @@ -207,11 +221,11 @@ def install_pyaedt(): python_version = "3_7" if is_windows: - venv_dir = Path(os.environ["APPDATA"], VENV_DIR_PREFIX, python_version) + venv_dir = Path(VENV_DIR, python_version) python_exe = venv_dir / "Scripts" / "python.exe" pip_exe = venv_dir / "Scripts" / "pip.exe" else: - venv_dir = Path(os.environ["HOME"], VENV_DIR_PREFIX, python_version) + venv_dir = Path(VENV_DIR, python_version) python_exe = venv_dir / "bin" / "python" pip_exe = venv_dir / "bin" / "pip" os.environ["ANSYSEM_ROOT{}".format(args.version)] = args.edt_root diff --git a/src/ansys/aedt/core/application/aedt_objects.py b/src/ansys/aedt/core/application/aedt_objects.py index e0ff57e368c..46c92d2436d 100644 --- a/src/ansys/aedt/core/application/aedt_objects.py +++ b/src/ansys/aedt/core/application/aedt_objects.py @@ -81,6 +81,7 @@ def __init__(self, desktop=None, project=None, design=None, is_inherithed=False) self._o_symbol_manager = None self._opadstackmanager = None self._oradfield = None + self._onetwork_data_explorer = None @property def oradfield(self): @@ -429,3 +430,15 @@ def o_model_manager(self): if not self._o_model_manager and self.odefinition_manager: self._o_model_manager = self.odefinition_manager.GetManager("Model") return self._o_model_manager + + @property + def onetwork_data_explorer(self): + """Network data explorer module. + + References + ---------- + >>> oDesktop.GetTool("NdExplorer") + """ + if not self._onetwork_data_explorer: + self._onetwork_data_explorer = self._odesktop.GetTool("NdExplorer") + return self._onetwork_data_explorer diff --git a/src/ansys/aedt/core/circuit.py b/src/ansys/aedt/core/circuit.py index 227f32c4168..63edd35b2a5 100644 --- a/src/ansys/aedt/core/circuit.py +++ b/src/ansys/aedt/core/circuit.py @@ -192,7 +192,6 @@ def __init__( remove_lock=remove_lock, ) ScatteringMethods.__init__(self, self) - self.onetwork_data_explorer = self._desktop.GetTool("NdExplorer") def _init_from_design(self, *args, **kwargs): self.__init__(*args, **kwargs) diff --git a/src/ansys/aedt/core/desktop.py b/src/ansys/aedt/core/desktop.py index fddcb0ca821..c7df255fc9f 100644 --- a/src/ansys/aedt/core/desktop.py +++ b/src/ansys/aedt/core/desktop.py @@ -516,8 +516,8 @@ def __init__( if getattr(self, "_initialized", None) is not None and self._initialized: try: self.grpc_plugin.recreate_application(True) - except Exception: # nosec - pass + except Exception: + pyaedt_logger.debug("Failed to recreate application.") return else: self._initialized = True diff --git a/src/ansys/aedt/core/generic/general_methods.py b/src/ansys/aedt/core/generic/general_methods.py index 560ce0aa529..5bdf80d4933 100644 --- a/src/ansys/aedt/core/generic/general_methods.py +++ b/src/ansys/aedt/core/generic/general_methods.py @@ -404,7 +404,7 @@ def open_file(file_path, file_options="r", encoding=None, override_existing=True Parameters ---------- - file_path : str + file_path : str or Path Full absolute path to the file (either local or remote). file_options : str, optional Options for opening the file. diff --git a/src/ansys/aedt/core/hfss.py b/src/ansys/aedt/core/hfss.py index e7405089188..70a5d3a7feb 100644 --- a/src/ansys/aedt/core/hfss.py +++ b/src/ansys/aedt/core/hfss.py @@ -232,7 +232,6 @@ def __init__( remove_lock=remove_lock, ) ScatteringMethods.__init__(self, self) - self.onetwork_data_explorer = self.odesktop.GetTool("NdExplorer") self._field_setups = [] self.component_array = {} self.component_array_names = list(self.get_oo_name(self.odesign, "Model")) diff --git a/src/ansys/aedt/core/hfss3dlayout.py b/src/ansys/aedt/core/hfss3dlayout.py index 82562193d2e..9bb05b7c92e 100644 --- a/src/ansys/aedt/core/hfss3dlayout.py +++ b/src/ansys/aedt/core/hfss3dlayout.py @@ -187,7 +187,6 @@ def __init__( remove_lock=remove_lock, ) ScatteringMethods.__init__(self, self) - self.onetwork_data_explorer = self.odesktop.GetTool("NdExplorer") def _init_from_design(self, *args, **kwargs): self.__init__(*args, **kwargs) diff --git a/src/ansys/aedt/core/modeler/circuits/primitives_nexxim.py b/src/ansys/aedt/core/modeler/circuits/primitives_nexxim.py index 8494f0ff950..284a623e6e7 100644 --- a/src/ansys/aedt/core/modeler/circuits/primitives_nexxim.py +++ b/src/ansys/aedt/core/modeler/circuits/primitives_nexxim.py @@ -22,7 +22,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import os +from pathlib import Path import re import secrets import time @@ -314,8 +314,9 @@ def connect_components_in_series(self, assignment, use_wire=True): if component in [cmp.id, cmp.name, cmp.composed_name]: comps.append(cmp) break + if len(comps) < 2: + raise RuntimeError("At least two components have to be passed.") i = 0 - assert len(comps) > 1, "At least two components have to be passed." while i < (len(comps) - 1): comps[i].pins[-1].connect_to_component(comps[i + 1].pins[0], use_wire=use_wire) i += 1 @@ -352,7 +353,8 @@ def connect_components_in_parallel(self, assignment): if component in [cmp.id, cmp.name, cmp.composed_name]: comps.append(cmp) break - assert len(comps) > 1, "At least two components have to be passed." + if len(comps) < 2: + raise RuntimeError("At least two components have to be passed.") comps[0].pins[0].connect_to_component([i.pins[0] for i in comps[1:]]) terminal_to_connect = [cmp for cmp in comps if len(cmp.pins) >= 2] if len(terminal_to_connect) > 1: @@ -1616,14 +1618,14 @@ def _add_subcircuit_link( Name of the subcircuit HFSS link. pin_names : list List of the pin names. - source_project_path : str + source_project_path : str or Path Path to the source project. source_design_name : str Name of the design. solution_name : str, optional Name of the solution and sweep. The default is ``"Setup1 : Sweep"``. - image_subcircuit_path : str, optional + image_subcircuit_path : str or Path or None Path of the Picture used in Circuit. Default is an HFSS Picture exported automatically. model_type : str, optional @@ -1647,6 +1649,8 @@ def _add_subcircuit_link( >>> oComponentManager.Add >>> oDesign.AddCompInstance """ + if isinstance(source_project_path, str): + source_project_path = Path(source_project_path) model = "hfss" owner = "HFSS" icon_file = "hfss.bmp" @@ -1668,19 +1672,20 @@ def _add_subcircuit_link( hspice_customization = self._get_comp_custom_settings(3, 1, 2, 3, 0, 0, "False", "", 3) if image_subcircuit_path: - _, file_extension = os.path.splitext(image_subcircuit_path) - if file_extension != ".gif" or file_extension != ".bmp" or file_extension != ".jpg": + if isinstance(image_subcircuit_path, str): + image_subcircuit_path = Path(image_subcircuit_path) + if image_subcircuit_path.suffix not in [".gif", ".bmp", ".jpg"]: image_subcircuit_path = None warnings.warn("Image extension is not valid. Use default image instead.") if not image_subcircuit_path: - image_subcircuit_path = os.path.normpath( - os.path.join(self._modeler._app.desktop_install_dir, "syslib", "Bitmaps", icon_file) - ) + image_subcircuit_path = ( + Path(self._modeler._app.desktop_install_dir) / "syslib" / "Bitmaps" / icon_file + ).resolve() filename = "" comp_name_aux = generate_unique_name(source_design_name) WB_SystemID = source_design_name - if not self._app.project_file == source_project_path: - filename = source_project_path + if Path(self._app.project_file) != source_project_path: + filename = str(source_project_path) comp_name_aux = comp_name WB_SystemID = "" @@ -1699,7 +1704,7 @@ def _add_subcircuit_link( "Description:=", "", "ImageFile:=", - image_subcircuit_path, + str(image_subcircuit_path), "SymbolPinConfiguration:=", 0, ["NAME:PortInfoBlk"], @@ -1946,7 +1951,7 @@ def set_sim_solution_on_hfss_subcircuit(self, component, solution_name="Setup1 : @pyaedt_function_handler() def _edit_link_definition_hfss_subcircuit(self, component, edited_prop): - """Generic function to set the link definition for an hfss subcircuit.""" + """Generic function to set the link definition for a HFSS subcircuit.""" if isinstance(component, str): complist = component.split(";") elif isinstance(component, CircuitComponent): @@ -1955,16 +1960,21 @@ def _edit_link_definition_hfss_subcircuit(self, component, edited_prop): complist = self.components[component].composed_name.split(";") else: raise AttributeError("Wrong Component Input") - complist2 = complist[0].split("@") - arg = ["NAME:AllTabs"] - arg1 = ["NAME:Model"] - arg2 = ["NAME:PropServers", "Component@" + str(complist2[1])] - arg3 = ["NAME:ChangedProps", edited_prop] - - arg1.append(arg2) - arg1.append(arg3) - arg.append(arg1) + arg = [ + "NAME:AllTabs", + [ + "NAME:Model", + [ + "NAME:PropServers", + "Component@" + str(complist[0].split("@")[1]), + ], + [ + "NAME:ChangedProps", + edited_prop, + ], + ], + ] self._app._oproject.ChangeProperty(arg) return True @@ -2016,7 +2026,7 @@ def create_component_from_spicemodel( Parameters ---------- - input_file : str + input_file : str or Path Path to .lib file. model : str, optional Model name to import. If `None` the first subckt in the lib file will be placed. @@ -2038,12 +2048,15 @@ def create_component_from_spicemodel( Examples -------- + >>> from pathlib import Path >>> from ansys.aedt.core import Circuit >>> cir = Circuit(version="2023.2") - >>> model = os.path.join("Your path", "test.lib") + >>> model = Path("Your path") / "test.lib" >>> cir.modeler.schematic.create_component_from_spicemodel(input_file=model,model="GRM1234",symbol="nexx_cap") >>> cir.release_desktop(False, False) """ + if isinstance(input_file, str): + input_file = Path(input_file) models = self._parse_spice_model(input_file) if not model and models: model = models[0] @@ -2061,12 +2074,11 @@ def create_component_from_spicemodel( else: arg2.append([False, "", "", False]) arg.append(arg2) - self.o_component_manager.ImportModelsFromFile(input_file.replace("\\", "/"), arg) + self.o_component_manager.ImportModelsFromFile(input_file.as_posix(), arg) if create_component: return self.create_component(None, component_library=None, component_name=model, location=location) - else: - return True + return True @pyaedt_function_handler(model_path="input_file", solution_name="solution") def add_siwave_dynamic_link(self, input_file, solution=None, simulate_solutions=False): @@ -2074,7 +2086,7 @@ def add_siwave_dynamic_link(self, input_file, solution=None, simulate_solutions= Parameters ---------- - input_file : str + input_file : str or Path Full path to the .siw file. solution : str, optional Solution name. @@ -2086,18 +2098,20 @@ def add_siwave_dynamic_link(self, input_file, solution=None, simulate_solutions= :class:`ansys.aedt.core.modeler.circuits.object_3d_circuit.CircuitComponent` Circuit Component Object. """ - assert os.path.exists(input_file), "Project file doesn't exist" - comp_name = os.path.splitext(os.path.basename(input_file))[0] - results_path = input_file + "averesults" - solution_path = os.path.join(results_path, comp_name + ".asol") - # out = load_entire_aedt_file(solution) + if isinstance(input_file, str): + input_file = Path(input_file) + if not input_file.exists(): + raise FileNotFoundError(f"Project file '{input_file}' doesn't exist") + comp_name = Path(input_file).stem + results_path = input_file.parent / f"{comp_name}.siwaveresults" + solution_path = results_path / f"{comp_name}.asol" out = load_keyword_in_aedt_file(solution_path, "Solutions") if not solution: solution = list(out["Solutions"]["SYZSolutions"].keys())[0] - results_folder = os.path.join( - results_path, - out["Solutions"]["SYZSolutions"][solution]["DiskName"], - out["Solutions"]["SYZSolutions"][solution]["DiskName"] + ".syzinfo", + results_folder = ( + results_path + / out["Solutions"]["SYZSolutions"][solution]["DiskName"] + / f"{out['Solutions']['SYZSolutions'][solution]['DiskName']}.syzinfo" ) pin_names = [] diff --git a/src/ansys/aedt/core/modeler/geometry_operators.py b/src/ansys/aedt/core/modeler/geometry_operators.py index 710900f2573..c6a46272f14 100644 --- a/src/ansys/aedt/core/modeler/geometry_operators.py +++ b/src/ansys/aedt/core/modeler/geometry_operators.py @@ -1535,9 +1535,8 @@ def v_angle_sign(va, vb, vn, right_handed=True): cross = GeometryOperators.v_cross(va, vb) if GeometryOperators.v_norm(cross) < tol: return math.pi - assert GeometryOperators.is_collinear(cross, vn), ( - "vn must be the normal to the " "plane containing va and vb." - ) # pragma: no cover + if not GeometryOperators.is_collinear(cross, vn): + raise ValueError("vn must be the normal to the plane containing va and vb") # pragma: no cover vnn = GeometryOperators.normalize_vector(vn) if right_handed: @@ -1762,10 +1761,11 @@ def is_segment_intersecting_polygon(a, b, polygon): float ``True`` if the segment intersect the polygon. ``False`` otherwise. """ - assert len(a) == 2, "point must be a list in the form [x, y]" - assert len(b) == 2, "point must be a list in the form [x, y]" + if len(a) != 2 or len(b) != 2: + raise ValueError("Point must be a list in the form [x, y]") pl = len(polygon[0]) - assert len(polygon[1]) == pl, "Polygon x and y lists must be the same length" + if len(polygon[1]) != pl: + raise ValueError("The two sublists in polygon must have the same length") a_in = GeometryOperators.is_point_in_polygon(a, polygon) b_in = GeometryOperators.is_point_in_polygon(b, polygon) diff --git a/src/ansys/aedt/core/modeler/modeler_3d.py b/src/ansys/aedt/core/modeler/modeler_3d.py index 4945c344536..820e74ba00b 100644 --- a/src/ansys/aedt/core/modeler/modeler_3d.py +++ b/src/ansys/aedt/core/modeler/modeler_3d.py @@ -104,7 +104,7 @@ def create_3dcomponent( is_encrypted=False, allow_edit=False, security_message="", - password=None, + password=None, # nosec edit_password=None, password_type="UserSuppliedPassword", hide_contents=False, @@ -195,8 +195,10 @@ def create_3dcomponent( name = self._app.design_name dt_string = datetime.datetime.now().strftime("%H:%M:%S %p %b %d, %Y") if password_type not in ["UserSuppliedPassword", "InternalPassword"]: + self.logger.error("Password type must be 'UserSuppliedPassword' or 'InternalPassword'") return False if component_outline not in ["BoundingBox", "None"]: + self.logger.error("Component outline must be 'BoundingBox' or 'None'") return False if password is None: password = os.getenv("PYAEDT_ENCRYPTED_PASSWORD", "") @@ -262,19 +264,16 @@ def create_3dcomponent( for el in objs: if "CreateRegion:1" in self.oeditor.GetChildObject(el).GetChildNames(): objs.remove(el) - arg.append("IncludedParts:="), arg.append(objs) - arg.append("HiddenParts:=") - if not hide_contents_flag: - arg.append([]) - else: - arg.append(hide_contents) - if coordinate_systems: - allcs = coordinate_systems - else: - allcs = self.oeditor.GetCoordinateSystems() - arg.append("IncludedCS:="), arg.append(allcs) - arg.append("ReferenceCS:="), arg.append(reference_coordinate_system) - par_description = [] + arg += [ + "IncludedParts:=", + objs, + "HiddenParts:=", + hide_contents if hide_contents_flag else [], + "IncludedCS:=", + coordinate_systems if coordinate_systems else list(self.oeditor.GetCoordinateSystems()), + "ReferenceCS:=", + reference_coordinate_system, + ] variables = [] dependent_variables = [] if variables_to_include is not None and not variables_to_include == []: @@ -289,39 +288,34 @@ def create_3dcomponent( elif variables_to_include is None: variables = self._app._variable_manager.independent_variable_names dependent_variables = self._app._variable_manager.dependent_variable_names + arg += [ + "IncludedParameters:=", + variables, + "IncludedDependentParameters:=", + dependent_variables, + "ParameterDescription:=", + [item for el in variables for item in (el + ":=", "")], + "IsLicensed:=", + False, + "LicensingDllName:=", + "", + "VendorComponentIdentifier:=", + "", + "PublicKeyFile:=", + "", + ] - for el in variables: - par_description.append(el + ":=") - par_description.append("") - arg.append("IncludedParameters:="), arg.append(variables) - - arg.append("IncludedDependentParameters:="), arg.append(dependent_variables) - for el in variables: - par_description.append(el + ":=") - par_description.append("") - arg.append("ParameterDescription:="), arg.append(par_description) - arg.append("IsLicensed:="), arg.append(False) - arg.append("LicensingDllName:="), arg.append("") - arg.append("VendorComponentIdentifier:="), arg.append("") - arg.append("PublicKeyFile:="), arg.append("") arg2 = ["NAME:DesignData"] - if boundaries is not None: - boundaries = boundaries - else: + if not boundaries: boundaries = self.get_boundaries_name() - arg2.append("Boundaries:="), arg2.append(boundaries) + if boundaries: + arg2 += ["Boundaries:=", boundaries] if self._app.design_type == "Icepak": - meshregions = [mr.name for mr in self._app.mesh.meshregions] - try: - meshregions.remove("Global") - except Exception: - pass - if meshregions: - arg2.append("MeshRegions:="), arg2.append(meshregions) + mesh_regions = [mr.name for mr in self._app.mesh.meshregions if mr.name != "Global"] + if mesh_regions: + arg2 += ["MeshRegions:=", mesh_regions] else: - if excitations is not None: - excitations = excitations - else: + if excitations is None: excitations = self._app.excitations if self._app.design_type == "HFSS": exc = self._app.get_oo_name(self._app.odesign, "Excitations") @@ -329,7 +323,7 @@ def create_3dcomponent( excitations.extend(exc) excitations = list(set([i.split(":")[0] for i in excitations])) if excitations: - arg2.append("Excitations:="), arg2.append(excitations) + arg2 += ["Excitations:=", excitations] meshops = [el.name for el in self._app.mesh.meshoperations] if meshops: used_mesh_ops = [] @@ -342,9 +336,9 @@ def create_3dcomponent( mesh_comp.append(self.objects[item].name) if all(included_obj in objs for included_obj in mesh_comp): used_mesh_ops.append(self._app.mesh.meshoperations[mesh].name) - arg2.append("MeshOperations:="), arg2.append(used_mesh_ops) + arg2 += ["MeshOperations:=", used_mesh_ops] else: - arg2.append("MeshOperations:="), arg2.append(meshops) + arg2 += ["MeshOperations:=", meshops] arg3 = ["NAME:ImageFile", "ImageFile:=", ""] if export_auxiliary: if isinstance(export_auxiliary, bool): @@ -514,13 +508,16 @@ def replace_3dcomponent( for el in objs: if "CreateRegion:1" in self.oeditor.GetChildObject(el).GetChildNames(): objs.remove(el) - arg.append("IncludedParts:="), arg.append(objs) - arg.append("HiddenParts:="), arg.append([]) - if not coordinate_systems: - coordinate_systems = list(self.oeditor.GetCoordinateSystems()) - arg.append("IncludedCS:="), arg.append(coordinate_systems) - arg.append("ReferenceCS:="), arg.append(reference_coordinate_system) - par_description = [] + arg += [ + "IncludedParts:=", + objs, + "HiddenParts:=", + [], + "IncludedCS:=", + coordinate_systems if coordinate_systems else list(self.oeditor.GetCoordinateSystems()), + "ReferenceCS:=", + reference_coordinate_system, + ] variables = [] if variables_to_include: dependent_variables = [] @@ -535,34 +532,24 @@ def replace_3dcomponent( else: variables = self._app._variable_manager.independent_variable_names dependent_variables = self._app._variable_manager.dependent_variable_names - - for el in variables: - par_description.append(el + ":=") - par_description.append("") - arg.append("IncludedParameters:="), arg.append(variables) - - arg.append("IncludedDependentParameters:="), arg.append(dependent_variables) - - for el in variables: - par_description.append(el + ":=") - par_description.append("") - arg.append("ParameterDescription:="), arg.append(par_description) + arg += [ + "IncludedParameters:=", + variables, + "IncludedDependentParameters:=", + dependent_variables, + "ParameterDescription:=", + [item for el in variables for item in (el + ":=", "")], + ] arg2 = ["NAME:DesignData"] - if boundaries: - boundaries = boundaries - else: + if not boundaries: boundaries = self.get_boundaries_name() if boundaries: - arg2.append("Boundaries:="), arg2.append(boundaries) + arg2 += ["Boundaries:=", boundaries] if self._app.design_type == "Icepak": - meshregions = [mr.name for mr in self._app.mesh.meshregions] - try: - meshregions.remove("Global") - except Exception: - pass - if meshregions: - arg2.append("MeshRegions:="), arg2.append(meshregions) + mesh_regions = [mr.name for mr in self._app.mesh.meshregions if mr.name != "Global"] + if mesh_regions: + arg2 += ["MeshRegions:=", mesh_regions] else: if excitations: excitations = excitations @@ -574,7 +561,7 @@ def replace_3dcomponent( excitations.extend(exc) excitations = list(set([i.split(":")[0] for i in excitations])) if excitations: - arg2.append("Excitations:="), arg2.append(excitations) + arg2 += ["Excitations:=", excitations] meshops = [el.name for el in self._app.mesh.meshoperations] if meshops: used_mesh_ops = [] @@ -587,9 +574,9 @@ def replace_3dcomponent( mesh_comp.append(self.objects[item].name) if all(included_obj in objs for included_obj in mesh_comp): used_mesh_ops.append(self._app.mesh.meshoperations[mesh].name) - arg2.append("MeshOperations:="), arg2.append(used_mesh_ops) + arg2 += ["MeshOperations:=", used_mesh_ops] else: - arg2.append("MeshOperations:="), arg2.append(meshops) + arg2 += ["MeshOperations:=", meshops] arg3 = ["NAME:ImageFile", "ImageFile:=", ""] old_components = self.user_defined_component_names self.oeditor.ReplaceWith3DComponent(arg, arg2, arg3) diff --git a/src/ansys/aedt/core/modeler/modeler_pcb.py b/src/ansys/aedt/core/modeler/modeler_pcb.py index 4c4a5c62a4a..4243ff77c6e 100644 --- a/src/ansys/aedt/core/modeler/modeler_pcb.py +++ b/src/ansys/aedt/core/modeler/modeler_pcb.py @@ -176,7 +176,8 @@ def model_units(self): @model_units.setter def model_units(self, units): - assert units in AEDT_UNITS["Length"], f"Invalid units string {units}." + if not units in AEDT_UNITS["Length"]: + raise ValueError(f"Invalid units '{units}'") self.oeditor.SetActiveUnits(units) self._model_units = units @@ -357,7 +358,7 @@ def merge_design(self, merged_design=None, x="0.0", y="0.0", z="0.0", rotation=" comp_name = str(i) break except Exception: - continue + self.logger.debug(f"Couldn't get component name from component {i}") if not comp_name: return False comp = ComponentsSubCircuit3DLayout(self, comp_name) @@ -648,8 +649,6 @@ def convert_to_selections(self, assignment, return_list=False): objnames.append(el) elif "name" in dir(el): objnames.append(el.name) - else: - pass if return_list: return objnames else: diff --git a/src/ansys/aedt/core/modeler/pcb/object_3d_layout.py b/src/ansys/aedt/core/modeler/pcb/object_3d_layout.py index 7850f9abc72..61ff2a36901 100644 --- a/src/ansys/aedt/core/modeler/pcb/object_3d_layout.py +++ b/src/ansys/aedt/core/modeler/pcb/object_3d_layout.py @@ -1064,12 +1064,12 @@ def name(self): def name(self, value): try: del self._primitives._lines[self.name] - vMaterial = ["NAME:Name", "Value:=", value] - self.change_property(vMaterial) + args = ["NAME:Name", "Value:=", value] + self.change_property(args) self._name = value self._primitives._lines[self._name] = self except Exception: - pass + self.logger.debug(f"Couldn't update geometry name into '{value}'.") @property def is_closed(self): diff --git a/src/ansys/aedt/core/modeler/pcb/primitives_3d_layout.py b/src/ansys/aedt/core/modeler/pcb/primitives_3d_layout.py index 14e613e554a..d0b5868bea8 100644 --- a/src/ansys/aedt/core/modeler/pcb/primitives_3d_layout.py +++ b/src/ansys/aedt/core/modeler/pcb/primitives_3d_layout.py @@ -769,7 +769,7 @@ def padstacks(self): if p[0] == "NAME:psd": props = p except Exception: - pass + self.logger.debug("Couldn't access first property.") self._padstacks[name] = Padstack(name, self.opadstackmanager, self.model_units) for prop in props: @@ -819,9 +819,8 @@ def padstacks(self): self._padstacks[name].layers[lay_name].connectiony = lay[14] self._padstacks[name].layers[lay_name].connectiondir = lay[16] i += 1 - pass except Exception: - pass + self.logger.debug(f"Exception caught when updating padstack '{name}' properties.") return self._padstacks diff --git a/src/ansys/aedt/core/modules/material_workbench.py b/src/ansys/aedt/core/modules/material_workbench.py index 7bbaca5a01f..ddeba623502 100644 --- a/src/ansys/aedt/core/modules/material_workbench.py +++ b/src/ansys/aedt/core/modules/material_workbench.py @@ -31,12 +31,12 @@ from collections import defaultdict import copy import re -from xml.etree.ElementTree import ParseError from ansys.aedt.core.aedt_logger import pyaedt_logger as logger from ansys.aedt.core.generic.data_handlers import normalize_string_format from ansys.aedt.core.modules.material import MatProperties import defusedxml +from defusedxml.ElementTree import ParseError defusedxml.defuse_stdlib() diff --git a/src/ansys/aedt/core/modules/solve_setup.py b/src/ansys/aedt/core/modules/solve_setup.py index 61987fe02e0..665765ff531 100644 --- a/src/ansys/aedt/core/modules/solve_setup.py +++ b/src/ansys/aedt/core/modules/solve_setup.py @@ -1892,11 +1892,12 @@ def solver_type(self): try: return self.properties["Solver"] except Exception: - pass + self.p_app.logger.debug("Cannot retrieve solver type with key 'Solver'") try: return self.props["SolveSetupType"] except Exception: - return None + self.p_app.logger.debug("Cannot retrieve solver type with key 'SolveSetupType'") + return None @pyaedt_function_handler() def create(self): diff --git a/src/ansys/aedt/core/rpc/rpyc_services.py b/src/ansys/aedt/core/rpc/rpyc_services.py index c6e637d01db..ab58f12caa1 100644 --- a/src/ansys/aedt/core/rpc/rpyc_services.py +++ b/src/ansys/aedt/core/rpc/rpyc_services.py @@ -7,6 +7,7 @@ import signal import sys import time +from typing import List from ansys.aedt.core.generic.general_methods import generate_unique_name from ansys.aedt.core.generic.general_methods import env_path @@ -311,14 +312,12 @@ def exposed_run_script(self, script, aedt_version="2021.2", ansysem_path=None, n command.append(ng_feature) command = [exe_path, ng_feature, "-RunScriptAndExit", script_file] try: - p = subprocess.Popen(command) # nosec - p.wait() + subprocess.run(command, check=True) # nosec except subprocess.CalledProcessError as e: msg = f"Command failed with error: {e}" logger.error(msg) return msg return "Script Executed." - else: return "Ansys EM not found or wrong AEDT Version." @@ -891,7 +890,7 @@ def exposed_restore(self): sys.stdout = sys.__stdout__ @staticmethod - def aedt_grpc(port=None, beta_options=None, use_aedt_relative_path=False, non_graphical=True): + def aedt_grpc(port=None, beta_options: List[str]=None, use_aedt_relative_path=False, non_graphical=True, check_interval=2): """Start a new AEDT session on a specified gRPC port. Returns @@ -905,13 +904,13 @@ def aedt_grpc(port=None, beta_options=None, use_aedt_relative_path=False, non_gr import secrets secure_random = secrets.SystemRandom() port = check_port(secure_random.randint(18500, 20000)) - if port == 0: print("Error. No ports are available.") return False elif port in sessions: print(f"AEDT Session already opened on port {port}.") return True + ansysem_path = os.getenv("PYAEDT_SERVER_AEDT_PATH", "") if is_linux: executable = "ansysedt" @@ -919,45 +918,39 @@ def aedt_grpc(port=None, beta_options=None, use_aedt_relative_path=False, non_gr executable = "ansysedt.exe" if ansysem_path and not use_aedt_relative_path: aedt_exe = os.path.join(ansysem_path, executable) + if not is_safe_path(aedt_exe): + logger.warning("Ansys EM path not safe.") + return False else: aedt_exe = executable if non_graphical: ng_feature = "-features=SF6694_NON_GRAPHICAL_COMMAND_EXECUTION,SF159726_SCRIPTOBJECT" - if beta_options: - for option in range(beta_options.__len__()): - if beta_options[option] not in ng_feature: - ng_feature += "," + beta_options[option] - - command = [ - aedt_exe, - "-grpcsrv", - str(port), - ng_feature, - "-ng", - - ] else: ng_feature = "-features=SF159726_SCRIPTOBJECT" - if beta_options: - for option in range(beta_options.__len__()): - if beta_options[option] not in ng_feature: - ng_feature += "," + beta_options[option] - command = [aedt_exe, "-grpcsrv", str(port), ng_feature] - subprocess.Popen(command) + if beta_options: + for option in beta_options: + if option not in ng_feature: + ng_feature += f",{option}" + command = [aedt_exe, "-grpcsrv", str(port), ng_feature] + if non_graphical: + command.append("-ng") + + process = subprocess.Popen(command) # nosec timeout = 60 - s = socket.socket() - machine_name = "127.0.0.1" while timeout > 0: - try: - s.connect((machine_name, port)) - except socket.error: - timeout -= 2 - time.sleep(2) - else: - s.close() - timeout = 0 - print(f"Service has started on port {port}") - return port + with socket.socket() as s: + try: + s.connect(("127.0.0.1", port)) + logger.info(f"Service accessible on port {port}") + return port + except socket.error: + logger.debug(f"Service not available yet, new try in {check_interval}s.") + timeout -= 2 + time.sleep(check_interval) + + process.terminate() + logger.error(f"Service did not start within the timeout of {timeout} seconds.") + return False @property def aedt_port(self): @@ -1120,7 +1113,6 @@ def on_connect(self, connection): self.connection = connection self._processes = {} self._edb = [] - pass def on_disconnect(self, connection): """Finalize the service when the connection is closed.""" @@ -1148,20 +1140,21 @@ def start_service(self, port): """ try: port = check_port(port) - if os.getenv("PYAEDT_SERVER_AEDT_PATH",""): - ansysem_path = os.getenv("PYAEDT_SERVER_AEDT_PATH","") + ansysem_path = os.getenv("PYAEDT_SERVER_AEDT_PATH") + if ansysem_path and not os.path.exists(ansysem_path): + raise FileNotFoundError(f"The ANSYSEM path '{ansysem_path}' does not exist.") else: - aa = aedt_versions.list_installed_ansysem - if aa: - ansysem_path = os.environ[aa[0]] + version_list = aedt_versions.list_installed_ansysem + if version_list: + ansysem_path = os.environ[version_list[0]] else: - raise Exception("no ANSYSEM_ROOTXXX environment variable defined.") - name = os.path.normpath( + raise Exception("No ANSYSEM_ROOTXXX environment variable is defined.") + + script_path = os.path.normpath( os.path.join(os.path.abspath(os.path.dirname(__file__)), "local_server.py") ) - cmd_service = [sys.executable, name, ansysem_path, "1", str(port)] - print(cmd_service) - p = subprocess.Popen(cmd_service) + command = [sys.executable, script_path, ansysem_path, "1", str(port)] + p = subprocess.Popen(command) # nosec time.sleep(2) self._processes[port] = p return port diff --git a/src/ansys/aedt/core/visualization/advanced/rcs_visualization.py b/src/ansys/aedt/core/visualization/advanced/rcs_visualization.py index d4fa1d309c8..5b6cae3aef6 100644 --- a/src/ansys/aedt/core/visualization/advanced/rcs_visualization.py +++ b/src/ansys/aedt/core/visualization/advanced/rcs_visualization.py @@ -132,7 +132,9 @@ def __init__(self, input_file): self.__frequency_units = self.__metadata["frequency_units"] - self.__monostatic_file = self.output_dir / self.__metadata["monostatic_file"] + self.__monostatic_file = None + if self.__metadata["monostatic_file"]: + self.__monostatic_file = self.output_dir / self.__metadata["monostatic_file"] self.__data_conversion_function = "dB20" self.__window = "Flat" @@ -141,19 +143,23 @@ def __init__(self, input_file): self.__upsample_range = 512 self.__upsample_azimuth = 64 - if not self.__monostatic_file.is_file(): + if self.__monostatic_file and not self.__monostatic_file.is_file(): raise Exception("Monostatic file invalid.") self.rcs_column_names = ["data"] # Load farfield data - is_rcs_loaded = self.__init_rcs() + if self.__monostatic_file: + is_rcs_loaded = self.__init_rcs() + else: + is_rcs_loaded = True if not is_rcs_loaded: # pragma: no cover raise RuntimeError("RCS information can not be loaded.") - # Update active frequency if passed in the initialization - self.frequency = self.frequencies[0] + if self.__monostatic_file: + # Update active frequency if passed in the initialization + self.frequency = self.frequencies[0] @property def raw_data(self): @@ -611,9 +617,6 @@ class MonostaticRCSPlotter(object): def __init__(self, rcs_data): - # Public - self.modeler_window = None - # Private self.__rcs_data = rcs_data self.__logger = logger @@ -716,7 +719,7 @@ def plot_rcs( """ curves = [] all_secondary_sweep_value = secondary_sweep_value - if primary_sweep.lower() == "freq" or primary_sweep.lower() == "frequency": + if primary_sweep.casefold() == "freq" or primary_sweep.casefold() == "frequency": x_key = "Freq" x = self.rcs_data.frequencies if secondary_sweep == "IWaveTheta": @@ -732,7 +735,7 @@ def plot_rcs( y_key = "IWavePhi" else: data = self.rcs_data.rcs_active_frequency - if primary_sweep.lower() == "iwavephi": + if primary_sweep.casefold() == "iwavephi": x_key = "IWavePhi" y_key = "IWaveTheta" x = self.rcs_data.available_incident_wave_phi @@ -902,7 +905,9 @@ def plot_range_profile( return new @pyaedt_function_handler() - def plot_waterfall(self, title="Waterfall", output_file=None, show=True, is_polar=False, size=(1920, 1440)): + def plot_waterfall( + self, title="Waterfall", output_file=None, show=True, is_polar=False, size=(1920, 1440), figure=None + ): """Create a 2D contour plot of the waterfall. Parameters @@ -918,6 +923,10 @@ def plot_waterfall(self, title="Waterfall", output_file=None, show=True, is_pola Whether to display in polar coordinates. The default is ``True``. size : tuple, optional Image size in pixel (width, height). + figure : :class:`matplotlib.pyplot.Figure`, optional + An existing Matplotlib `Figure` to which the plot is added. + If not provided, a new `Figure` and `Axes` objects are created. + Default is ``None``. Returns ------- @@ -961,16 +970,11 @@ def plot_waterfall(self, title="Waterfall", output_file=None, show=True, is_pola } new.add_trace(plot_data, 2, props) - _ = new.plot_contour( - trace=0, - polar=is_polar, - snapshot_path=output_file, - show=show, - ) + _ = new.plot_contour(trace=0, polar=is_polar, snapshot_path=output_file, show=show, figure=figure) return new @pyaedt_function_handler() - def plot_isar_2d(self, title="ISAR", output_file=None, show=True, size=(1920, 1440)): + def plot_isar_2d(self, title="ISAR", output_file=None, show=True, size=(1920, 1440), figure=None): """Create a 2D contour plot of the ISAR. Parameters @@ -984,6 +988,10 @@ def plot_isar_2d(self, title="ISAR", output_file=None, show=True, size=(1920, 14 If ``False``, the Matplotlib instance of the plot is shown. size : tuple, optional Image size in pixel (width, height). + figure : :class:`matplotlib.pyplot.Figure`, optional + An existing Matplotlib `Figure` to which the plot is added. + If not provided, a new `Figure` and `Axes` objects are created. + Default is ``None``. Returns ------- @@ -1019,12 +1027,7 @@ def plot_isar_2d(self, title="ISAR", output_file=None, show=True, size=(1920, 14 } new.add_trace(plot_data, 2, props) - _ = new.plot_contour( - trace=0, - polar=False, - snapshot_path=output_file, - show=show, - ) + _ = new.plot_contour(trace=0, polar=False, snapshot_path=output_file, show=show, figure=figure) return new @pyaedt_function_handler() @@ -1617,6 +1620,92 @@ def add_waterfall( self.all_scene_actors["results"]["waterfall"][waterfall_name] = rcs_mesh + @pyaedt_function_handler() + def add_isar_2d( + self, + plot_type="plane", + color_bar="jet", + ): + """Add the ISAR 2D. + + Parameters + ---------- + plot_type : str, optional + The type of plot to create for the range profile. It can be ``"plane"``, ``"relief"``, and `"projection"``. + The default is ``"plane"``. + color_bar : str, optional + Color mapping to be applied to the RCS data. It can be a color (``"blue"``, + ``"green"``, ...) or a colormap (``"jet"``, ``"viridis"``, ...). The default is ``"jet"``. + """ + data_isar_2d = self.rcs_data.isar_2d + + down_range = data_isar_2d["Down-range"].unique() + cross_range = data_isar_2d["Cross-range"].unique() + + values_2d = data_isar_2d.pivot(index="Cross-range", columns="Down-range", values="Data").to_numpy() + + x, y = np.meshgrid(down_range, cross_range) + z = np.zeros_like(x) + + if plot_type.casefold() == "relief": + m = 2.0 + b = -1.0 + z = (values_2d - values_2d.min()) / (values_2d.max() - values_2d.min()) * m + b + + if plot_type.casefold() in ["relief", "plane"]: + actor = pv.StructuredGrid() + actor.points = np.c_[x.ravel(), y.ravel(), z.ravel()] + + actor.dimensions = (len(down_range), len(cross_range), 1) + + actor["values"] = values_2d.ravel() + + else: + scene_actors = self.all_scene_actors["model"] + if scene_actors is None: + return None + actor = pv.PolyData() + for model_actor in scene_actors.values(): + mesh = model_actor.custom_object.get_mesh() + xypoints = mesh.points + cpos = values_2d.flatten() + xpos_ypos = np.column_stack((x.flatten(), y.flatten(), cpos)) + all_indices = self.__find_nearest_neighbors(xpos_ypos, xypoints) + + mag_for_color = np.ndarray.flatten(cpos[all_indices]) + if mesh.__class__.__name__ != "PolyData": + mesh_triangulated = mesh.triangulate() + model_actor.custom_object.mesh = pv.PolyData(mesh_triangulated.points, mesh_triangulated.cells) + else: + model_actor.custom_object.mesh.clear_data() + model_actor.custom_object.mesh[self.rcs_data.data_conversion_function] = mag_for_color + actor += model_actor.custom_object.mesh + + all_results_actors = list(self.all_scene_actors["results"].keys()) + + if "isar_2d" not in all_results_actors: + self.all_scene_actors["results"]["isar_2d"] = {} + + index = 0 + while f"isar_2d_{index}" in self.all_scene_actors["results"]["isar_2d"]: + index += 1 + + isar_name = f"isar_2d_{index}" + + isar_object = SceneMeshObject() + isar_object.name = isar_name + + scalar_dict = dict(color="#000000", title="ISAR 2D") + isar_object.scalar_dict = scalar_dict + + isar_object.cmap = color_bar + + isar_object.mesh = actor + + rcs_mesh = MeshObjectPlot(isar_object, isar_object.get_mesh()) + + self.all_scene_actors["results"]["isar_2d"][isar_name] = rcs_mesh + @pyaedt_function_handler() def clear_scene(self, first_level=None, second_level=None, name=None): if not first_level: @@ -1651,7 +1740,7 @@ def __get_pyvista_range_profile_actor( if extents is None: extents = [0, 10, 0, 10, 0, 10] - plot_type_lower = plot_type.lower() + plot_type_lower = plot_type.casefold() actor = None if ( diff --git a/src/ansys/aedt/core/visualization/advanced/touchstone_parser.py b/src/ansys/aedt/core/visualization/advanced/touchstone_parser.py index ff44c50950f..5cf6556dfab 100644 --- a/src/ansys/aedt/core/visualization/advanced/touchstone_parser.py +++ b/src/ansys/aedt/core/visualization/advanced/touchstone_parser.py @@ -26,7 +26,7 @@ import itertools import os import re -import subprocess +import subprocess # nosec import warnings from ansys.aedt.core.aedt_logger import pyaedt_logger as logger @@ -502,12 +502,12 @@ def check_touchstone_files(input_dir="", passivity=True, causality=True): """ out = {} - sNpFiles = find_touchstone_files(input_dir) - if not sNpFiles: + snp_files = find_touchstone_files(input_dir) + if not snp_files: return out aedt_install_folder = list(aedt_versions.installed_versions.values())[0] - for snpf in sNpFiles: - out[snpf] = [] + for file_name, path in snp_files.items(): + out[file_name] = [] if os.name == "nt": genequiv_path = os.path.join(aedt_install_folder, "genequiv.exe") else: @@ -518,22 +518,20 @@ def check_touchstone_files(input_dir="", passivity=True, causality=True): if causality: cmd.append("-checkcausality") - cmd.append(sNpFiles[snpf]) + cmd.append(path) my_env = os.environ.copy() - p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=my_env) # nosec - output = p.communicate() - output_str = str(output[0]) - output_lst = output_str.split("\\r\\n") + result = subprocess.run( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=my_env, text=True, check=True + ) # nosec + output_lst = result.stdout.splitlines() - if len(output_lst) == 1: - output_lst = output_str.splitlines() for line in output_lst: if "Input data" in line and passivity: msg_log = line[17:] is_passive = True if "non-passive" in msg_log: is_passive = False - out[snpf].append(["passivity", is_passive, msg_log]) + out[file_name].append(["passivity", is_passive, msg_log]) if "Maximum causality" in line and causality: msg_log = line[17:] is_causal = True @@ -544,10 +542,10 @@ def check_touchstone_files(input_dir="", passivity=True, causality=True): except Exception: is_causal = False raise Exception("Failed evaluating causality value.") - out[snpf].append(["causality", is_causal, msg_log]) + out[file_name].append(["causality", is_causal, msg_log]) if "Causality check is inconclusive" in line and causality: is_causal = False - out[snpf].append(["causality", is_causal, line[17:]]) + out[file_name].append(["causality", is_causal, line[17:]]) return out diff --git a/src/ansys/aedt/core/visualization/plot/matplotlib.py b/src/ansys/aedt/core/visualization/plot/matplotlib.py index e9377d75de6..9b93320c858 100644 --- a/src/ansys/aedt/core/visualization/plot/matplotlib.py +++ b/src/ansys/aedt/core/visualization/plot/matplotlib.py @@ -1311,14 +1311,7 @@ def _plot_limit_lines(self, convert_to_radians=False): @pyaedt_function_handler() def plot_contour( - self, - trace=0, - polar=False, - levels=64, - max_theta=180, - color_bar=None, - snapshot_path=None, - show=True, + self, trace=0, polar=False, levels=64, max_theta=180, color_bar=None, snapshot_path=None, show=True, figure=None ): """Create a Matplotlib figure contour based on a list of data. @@ -1343,6 +1336,9 @@ def plot_contour( The default value is ``None``. show : bool, optional Whether to show the plot or return the matplotlib object. Default is ``True``. + figure : :class:`matplotlib.pyplot.Figure`, optional + An existing Matplotlib `Figure` to which the plot is added. + If not provided, a new `Figure` and `Axes` object are created. Returns ------- @@ -1355,7 +1351,14 @@ def plot_contour( else: tr = tr[0] projection = "polar" if polar else "rectilinear" - self.fig, self.ax = plt.subplots(subplot_kw={"projection": projection}) + + if not figure: + self.fig, self.ax = plt.subplots(subplot_kw={"projection": projection}) + self.ax = plt.gca() + else: + self.fig = figure + self.ax = figure.add_subplot(111, polar=polar) + self.ax.set_xlabel(tr.x_label) if polar: self.ax.set_rticks(np.linspace(0, max_theta, 3)) @@ -1366,7 +1369,7 @@ def plot_contour( ph = tr._spherical_data[2] th = tr._spherical_data[1] data_to_plot = tr._spherical_data[0] - plt.contourf( + contour = self.ax.contourf( ph, th, data_to_plot, @@ -1374,10 +1377,9 @@ def plot_contour( cmap="jet", ) if color_bar: - cbar = plt.colorbar() + cbar = self.fig.colorbar(contour, ax=self.ax) cbar.set_label(color_bar, rotation=270, labelpad=20) - self.ax = plt.gca() self.ax.yaxis.set_label_coords(-0.1, 0.5) self._plot(snapshot_path, show) return self.fig diff --git a/src/ansys/aedt/core/visualization/post/spisim.py b/src/ansys/aedt/core/visualization/post/spisim.py index 5dea72f34c1..a1a0987a826 100644 --- a/src/ansys/aedt/core/visualization/post/spisim.py +++ b/src/ansys/aedt/core/visualization/post/spisim.py @@ -73,54 +73,42 @@ def working_directory(self, val): self._working_directory = val @pyaedt_function_handler() - def _compute_spisim(self, parameter, out_file="", touchstone_file="", config_file=""): + def __compute_spisim(self, parameter, config_file, out_file=""): exec_name = "SPISimJNI_LX64.exe" if is_linux else "SPISimJNI_WIN64.exe" - spisimExe = os.path.join(self.desktop_install_dir, "spisim", "SPISim", "modules", "ext", exec_name) + spisim_exe = os.path.join(self.desktop_install_dir, "spisim", "SPISim", "modules", "ext", exec_name) + command = [spisim_exe, parameter] - cfgCmmd = "" - if touchstone_file != "": - cfgCmmd = cfgCmmd + '-i "%s"' % touchstone_file if config_file != "": - if is_linux: - cfgCmmd = "-v CFGFILE=%s" % config_file - else: - cfgCmmd = '-v CFGFILE="%s"' % config_file - if out_file: - cfgCmmd += ', -o "%s"' % out_file - command = [spisimExe, parameter, cfgCmmd] - # Debug('%s %s' % (cmdList[0], ' '.join(arguments))) - # try up to three times to be sure + command += ["-v", f"CFGFILE={config_file}"] if out_file: + command += [",", "-o", f"{out_file}"] out_processing = os.path.join(out_file, generate_unique_name("spsim_out") + ".txt") else: out_processing = os.path.join(generate_unique_folder_name(), generate_unique_name("spsim_out") + ".txt") my_env = os.environ.copy() my_env.update(settings.aedt_environment_variables) - if is_linux: # pragma: no cover if "ANSYSEM_ROOT_PATH" not in my_env: # pragma: no cover my_env["ANSYSEM_ROOT_PATH"] = self.desktop_install_dir if "SPISIM_OUTPUT_LOG" not in my_env: # pragma: no cover my_env["SPISIM_OUTPUT_LOG"] = os.path.join(out_file, generate_unique_name("spsim_out") + ".log") - with open_file(out_processing, "w") as outfile: - subprocess.Popen(command, env=my_env, stdout=outfile, stderr=outfile).wait() # nosec - else: - with open_file(out_processing, "w") as outfile: - subprocess.Popen(" ".join(command), env=my_env, stdout=outfile, stderr=outfile).wait() # nosec + + with open_file(out_processing, "w") as outfile: + subprocess.run(command, env=my_env, stdout=outfile, stderr=outfile, check=True) # nosec return out_processing @pyaedt_function_handler() - def _get_output_parameter_from_result(self, out_file, parameter_name): + def __get_output_parameter_from_result(self, out_file, parameter_name): if parameter_name == "ERL": try: with open_file(out_file, "r") as infile: lines = infile.read() - parmDat = lines.split("[ParmDat]:", 1)[1] - for keyValu in parmDat.split(","): - dataAry = keyValu.split("=") - if dataAry[0].strip().lower() == parameter_name.lower(): - return float(dataAry[1].strip().split()[0]) + parm_dat = lines.split("[ParmDat]:", 1)[1] + for key_value in parm_dat.split(","): + data_arr = key_value.split("=") + if data_arr[0].strip().lower() == parameter_name.lower(): + return float(data_arr[1].strip().split()[0]) self.logger.error( f"Failed to compute {parameter_name}. Check input parameters and retry" ) # pragma: no cover @@ -267,27 +255,22 @@ def compute_erl( cfg_dict["REFLRHO"] = permitted_reflection if permitted_reflection is not None else cfg_dict["REFLRHO"] cfg_dict["NCYCLES"] = reflections_length if reflections_length is not None else cfg_dict["NCYCLES"] - new_cfg_file = os.path.join(self.working_directory, "spisim_erl.cfg").replace("\\", "/") - with open_file(new_cfg_file, "w") as fp: + config_file = os.path.join(self.working_directory, "spisim_erl.cfg").replace("\\", "/") + with open_file(config_file, "w") as fp: for k, v in cfg_dict.items(): fp.write(f"# {k}: {k}\n") fp.write(f"{k} = {v}\n") retries = 3 if "PYTEST_CURRENT_TEST" in os.environ: retries = 10 - trynumb = 0 - while trynumb < retries: - out_processing = self._compute_spisim( - "CalcERL", - touchstone_file=self.touchstone_file, - config_file=new_cfg_file, - out_file=self.working_directory, - ) - results = self._get_output_parameter_from_result(out_processing, "ERL") + nb_retry = 0 + while nb_retry < retries: + out_processing = self.__compute_spisim("CalcERL", config_file, out_file=self.working_directory) + results = self.__get_output_parameter_from_result(out_processing, "ERL") if results: return results self.logger.warning("Failing to compute ERL, retrying...") - trynumb += 1 + nb_retry += 1 self.logger.error("Failed to compute ERL.") return False @@ -345,14 +328,14 @@ def compute_com( com_param.set_parameter("Port Order", "[1 3 2 4]" if port_order == "EvenOdd" else "[1 2 3 4]") com_param.set_parameter("RESULT_DIR", out_folder if out_folder else self.working_directory) - return self._compute_com(com_param) + return self.__compute_com(com_param) @pyaedt_function_handler - def _compute_com( + def __compute_com( self, com_parameter, ): - """Compute Channel Operating Margin. + """Compute Channel Operating Margin (COM). Parameters ---------- @@ -361,7 +344,7 @@ def _compute_com( Returns ------- - + float or list """ thru_snp = com_parameter.parameters["THRUSNP"].replace("\\", "/") fext_snp = com_parameter.parameters["FEXTARY"].replace("\\", "/") @@ -376,8 +359,8 @@ def _compute_com( cfg_file = os.path.join(com_parameter.parameters["RESULT_DIR"], "com_parameters.cfg") com_parameter.export_spisim_cfg(cfg_file) - out_processing = self._compute_spisim(parameter="COM", config_file=cfg_file) - return self._get_output_parameter_from_result(out_processing, "COM") + out_processing = self.__compute_spisim("COM", cfg_file) + return self.__get_output_parameter_from_result(out_processing, "COM") @pyaedt_function_handler def export_com_configure_file(self, file_path, standard=1): diff --git a/src/ansys/aedt/core/visualization/report/standard.py b/src/ansys/aedt/core/visualization/report/standard.py index 2b271ba3f8c..4eb0e650ed0 100644 --- a/src/ansys/aedt/core/visualization/report/standard.py +++ b/src/ansys/aedt/core/visualization/report/standard.py @@ -117,6 +117,36 @@ def pulse_rise_time(self): def pulse_rise_time(self, val): self._legacy_props["context"]["pulse_rise_time"] = val + @property + def maximum_time(self): + """Value of maximum time for TDR plot. + + Returns + ------- + float + Maximum time. + """ + return self._legacy_props["context"].get("maximum_time", 0) if self.domain == "Time" else 0 + + @maximum_time.setter + def maximum_time(self, val): + self._legacy_props["context"]["maximum_time"] = val + + @property + def step_time(self): + """Value of step time for TDR plot. + + Returns + ------- + float + step time. + """ + return self._legacy_props["context"].get("step_time", 0) if self.domain == "Time" else 0 + + @step_time.setter + def step_time(self, val): + self._legacy_props["context"]["step_time"] = val + @property def time_windowing(self): """Returns the TDR time windowing. @@ -227,8 +257,8 @@ def _context(self): 1, 1, "", - self.pulse_rise_time / 5, - self.pulse_rise_time * 100, + (self.pulse_rise_time / 5) if not self.step_time else self.step_time, + (self.pulse_rise_time * 100) if not self.maximum_time else self.maximum_time, "EnsDiffPairKey", False, "1", @@ -254,8 +284,8 @@ def _context(self): 1, 1, "", - self.pulse_rise_time / 5, - self.pulse_rise_time * 100, + (self.pulse_rise_time / 5) if not self.step_time else self.step_time, + (self.pulse_rise_time * 100) if not self.maximum_time else self.maximum_time, "EnsDiffPairKey", False, "0", @@ -281,8 +311,8 @@ def _context(self): 1, 1, "", - self.pulse_rise_time / 5, - self.pulse_rise_time * 100, + (self.pulse_rise_time / 5) if not self.step_time else self.step_time, + (self.pulse_rise_time * 100) if not self.maximum_time else self.maximum_time, ], ] if self.sub_design_id: diff --git a/tests/system/general/example_models/T49/rcs_metadata.json b/tests/system/general/example_models/T49/rcs_metadata.json deleted file mode 100644 index 5fee8041a9c..00000000000 --- a/tests/system/general/example_models/T49/rcs_metadata.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "solution": "Trihedral_RCS", - "monostatic_file": "rcs_data.h5", - "model_units": "mm", - "frequency_units": "GHz", - "model_info": { - "Polyline1": [ - "Polyline1.obj", - [ - 143, - 175, - 143 - ], - 1.0, - "mm" - ], - "Polyline1_1": [ - "Polyline1_1.obj", - [ - 143, - 175, - 143 - ], - 1.0, - "mm" - ], - "Polyline1_2": [ - "Polyline1_2.obj", - [ - 143, - 175, - 143 - ], - 1.0, - "mm" - ] - } -} \ No newline at end of file diff --git a/tests/system/general/example_models/T49/rcs_metadata_fake.json b/tests/system/general/example_models/T49/rcs_metadata_fake.json deleted file mode 100644 index 9dfa9d6b079..00000000000 --- a/tests/system/general/example_models/T49/rcs_metadata_fake.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "solution": "Trihedral_RCS", - "monostatic_file": "invented.h5", - "model_units": "mm", - "frequency_units": "GHz", - "model_info": { - "Polyline1": [ - "Polyline1.obj", - [ - 143, - 175, - 143 - ], - 1.0, - "mm" - ], - "Polyline1_1": [ - "Polyline1_1.obj", - [ - 143, - 175, - 143 - ], - 1.0, - "mm" - ], - "Polyline1_2": [ - "Polyline1_2.obj", - [ - 143, - 175, - 143 - ], - 1.0, - "mm" - ] - } -} \ No newline at end of file diff --git a/tests/system/general/test_12_PostProcessing.py b/tests/system/general/test_12_PostProcessing.py index 6289b68e246..a27b98cb51f 100644 --- a/tests/system/general/test_12_PostProcessing.py +++ b/tests/system/general/test_12_PostProcessing.py @@ -417,6 +417,8 @@ def test_18_diff_plot(self, diff_test): new_report2 = diff_test.post.reports_by_category.standard("TDRZ(1)") new_report2.differential_pairs = True new_report2.pulse_rise_time = 3e-12 + new_report2.maximum_time = 30e-12 + new_report2.step_time = 6e-13 new_report2.time_windowing = 3 new_report2.domain = "Time" diff --git a/tests/system/general/test_49_RCS_data_plotter.py b/tests/system/general/test_49_RCS_data_plotter.py index 2dfc9a3deb4..bd4a4a8c8b4 100644 --- a/tests/system/general/test_49_RCS_data_plotter.py +++ b/tests/system/general/test_49_RCS_data_plotter.py @@ -22,7 +22,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import os +import json +from pathlib import Path import shutil from ansys.aedt.core.visualization.advanced.rcs_visualization import MonostaticRCSData @@ -46,11 +47,57 @@ def desktop(): @pytest.fixture(scope="class") def setup_test_data(request, local_scratch): """Fixture to set up the test data directory and file before running the test class.""" - dir_original = os.path.join(TESTS_GENERAL_PATH, "example_models", test_subfolder) - data_dir = os.path.join(local_scratch.path, "rcs_files") + dir_original = Path(TESTS_GENERAL_PATH) / "example_models" / test_subfolder + data_dir = Path(local_scratch.path) / "rcs_files" shutil.copytree(dir_original, data_dir) - request.cls.metadata_file = os.path.join(data_dir, "rcs_metadata.json") - request.cls.metadata_file_fake = os.path.join(data_dir, "rcs_metadata_fake.json") + + metadata = { + "solution": "Trihedral_RCS", + "monostatic_file": "rcs_data.h5", + "model_units": "mm", + "frequency_units": "GHz", + "model_info": { + "Polyline1": ["Polyline1.obj", [143, 175, 143], 1.0, "mm"], + "Polyline1_1": ["Polyline1_1.obj", [143, 175, 143], 1.0, "mm"], + "Polyline1_2": ["Polyline1_2.obj", [143, 175, 143], 1.0, "mm"], + }, + } + metadata_file = data_dir / "rcs_metadata.json" + with open(metadata_file, "w") as f: + json.dump(metadata, f) + request.cls.metadata_file = metadata_file + + metadata_fake = { + "solution": "Trihedral_RCS", + "monostatic_file": "invented.h5", + "model_units": "mm", + "frequency_units": "GHz", + "model_info": { + "Polyline1": ["Polyline1.obj", [143, 175, 143], 1.0, "mm"], + "Polyline1_1": ["Polyline1_1.obj", [143, 175, 143], 1.0, "mm"], + "Polyline1_2": ["Polyline1_2.obj", [143, 175, 143], 1.0, "mm"], + }, + } + metadata_file_fake = data_dir / "rcs_metadata_fake.json" + with open(metadata_file_fake, "w") as f: + json.dump(metadata_fake, f) + request.cls.metadata_file_fake = metadata_file_fake + + metadata_no_data = { + "solution": "Trihedral_RCS", + "monostatic_file": None, + "model_units": "mm", + "frequency_units": None, + "model_info": { + "Polyline1": ["Polyline1.obj", [143, 175, 143], 1.0, "mm"], + "Polyline1_1": ["Polyline1_1.obj", [143, 175, 143], 1.0, "mm"], + "Polyline1_2": ["Polyline1_2.obj", [143, 175, 143], 1.0, "mm"], + }, + } + metadata_file_no_data = data_dir / "rcs_metadata_no_data.json" + with open(metadata_file_no_data, "w") as f: + json.dump(metadata_no_data, f) + request.cls.metadata_file_no_data = metadata_file_no_data yield @@ -61,16 +108,16 @@ def test_01_rcs_data(self): MonostaticRCSData(input_file="invented") with pytest.raises(Exception, match="Monostatic file invalid."): - MonostaticRCSData(input_file=self.metadata_file_fake) + MonostaticRCSData(input_file=str(self.metadata_file_fake)) - rcs_data = MonostaticRCSData(input_file=self.metadata_file) + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) assert isinstance(rcs_data.raw_data, pd.DataFrame) assert isinstance(rcs_data.metadata, dict) assert rcs_data.name == "HH" assert rcs_data.solution == "Trihedral_RCS" - assert os.path.isfile(rcs_data.input_file) + assert Path(rcs_data.input_file).is_file() assert rcs_data.frequency_units == "GHz" assert len(rcs_data.frequencies) == 3 @@ -140,7 +187,7 @@ def test_01_rcs_data(self): assert isinstance(rcs_data.isar_2d, pd.DataFrame) def test_02_rcs_plotter_properties(self): - rcs_data = MonostaticRCSData(input_file=self.metadata_file) + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) assert rcs_plotter.rcs_data @@ -153,7 +200,7 @@ def test_02_rcs_plotter_properties(self): assert isinstance(rcs_plotter.radius, float) def test_03_rcs_plotter_rcs(self): - rcs_data = MonostaticRCSData(input_file=self.metadata_file) + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) rcs_plotter1 = rcs_plotter.plot_rcs( @@ -180,14 +227,14 @@ def test_03_rcs_plotter_rcs(self): assert isinstance(rcs_plotter7, ReportPlotter) def test_04_rcs_plotter_range_profile(self): - rcs_data = MonostaticRCSData(input_file=self.metadata_file) + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) rcs_plotter = rcs_plotter.plot_range_profile(show=False) assert isinstance(rcs_plotter, ReportPlotter) def test_05_rcs_plotter_waterfall(self): - rcs_data = MonostaticRCSData(input_file=self.metadata_file) + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) rcs_plotter1 = rcs_plotter.plot_waterfall(show=False) @@ -197,14 +244,14 @@ def test_05_rcs_plotter_waterfall(self): assert isinstance(rcs_plotter2, ReportPlotter) def test_06_rcs_plotter_2d_isar(self): - rcs_data = MonostaticRCSData(input_file=self.metadata_file) + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) rcs_plotter1 = rcs_plotter.plot_isar_2d(show=False) assert isinstance(rcs_plotter1, ReportPlotter) def test_07_rcs_plotter_add_rcs(self): - rcs_data = MonostaticRCSData(input_file=self.metadata_file) + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) rcs_plotter.add_rcs() @@ -216,7 +263,7 @@ def test_07_rcs_plotter_add_rcs(self): assert rcs_plotter.clear_scene() def test_08_rcs_plotter_add_profile_settings(self): - rcs_data = MonostaticRCSData(input_file=self.metadata_file) + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) rcs_plotter.add_range_profile_settings() @@ -225,7 +272,7 @@ def test_08_rcs_plotter_add_profile_settings(self): assert rcs_plotter.clear_scene() def test_09_rcs_plotter_add_waterfall_settings(self): - rcs_data = MonostaticRCSData(input_file=self.metadata_file) + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) rcs_plotter.add_waterfall_settings() @@ -234,7 +281,7 @@ def test_09_rcs_plotter_add_waterfall_settings(self): assert rcs_plotter.clear_scene() def test_10_rcs_plotter_add_isar_2d_settings(self): - rcs_data = MonostaticRCSData(input_file=self.metadata_file) + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) rcs_plotter.add_isar_2d_settings() @@ -243,7 +290,7 @@ def test_10_rcs_plotter_add_isar_2d_settings(self): assert rcs_plotter.clear_scene() def test_11_rcs_plotter_add_range_profile(self): - rcs_data = MonostaticRCSData(input_file=self.metadata_file) + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) rcs_plotter.add_range_profile() @@ -252,10 +299,35 @@ def test_11_rcs_plotter_add_range_profile(self): assert rcs_plotter.clear_scene() def test_12_rcs_plotter_add_waterfall(self): - rcs_data = MonostaticRCSData(input_file=self.metadata_file) + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) rcs_plotter.add_waterfall() plot = rcs_plotter.plot_scene(show=False) assert isinstance(plot, Plotter) assert rcs_plotter.clear_scene() + + def test_13_rcs_plotter_add_isar_2d(self): + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file)) + rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) + rcs_plotter.show_geometry = False + + rcs_plotter.add_isar_2d() + plot = rcs_plotter.plot_scene(show=False) + assert isinstance(plot, Plotter) + assert rcs_plotter.clear_scene() + + rcs_plotter.add_isar_2d("Relief") + plot = rcs_plotter.plot_scene(show=False) + assert isinstance(plot, Plotter) + assert rcs_plotter.clear_scene() + + def test_14_rcs_plotter_no_data(self): + rcs_data = MonostaticRCSData(input_file=str(self.metadata_file_no_data)) + rcs_plotter = MonostaticRCSPlotter(rcs_data=rcs_data) + rcs_plotter.show_geometry = False + + rcs_plotter.add_range_profile_settings() + plot = rcs_plotter.plot_scene(show=False) + assert isinstance(plot, Plotter) + assert rcs_plotter.clear_scene() diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index b47473860d7..3f4fd0841a1 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -25,15 +25,16 @@ from unittest.mock import MagicMock from unittest.mock import patch +from ansys.aedt.core.generic.settings import settings +from ansys.aedt.core.maxwell import Maxwell3d import pytest +settings.enable_error_handler = False + @pytest.fixture def maxwell_3d_setup(): - """Fixture used to setup a Maxwell3d instance.""" - with patch("ansys.aedt.core.maxwell.FieldAnalysis3D") as mock_fiel_analysis_3d, patch( - "ansys.aedt.core.maxwell.Maxwell" - ) as mock_maxwell: - mock_fiel_analysis_3d.return_value = MagicMock() - mock_maxwell.return_value = MagicMock() - yield {"FieldAnalysis3D": mock_fiel_analysis_3d, "Maxwell": mock_maxwell} + """Fixture used to mock the creation of a Maxwell instance.""" + with patch("ansys.aedt.core.maxwell.Maxwell3d.__init__", lambda x: None): + mock_instance = MagicMock(spec=Maxwell3d) + yield mock_instance diff --git a/tests/unit/test_primitives_nexxim.py b/tests/unit/test_primitives_nexxim.py new file mode 100644 index 00000000000..830c53ed349 --- /dev/null +++ b/tests/unit/test_primitives_nexxim.py @@ -0,0 +1,51 @@ +# -*- 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. + +from pathlib import Path +from unittest.mock import MagicMock +from unittest.mock import patch +import warnings + +from ansys.aedt.core.modeler.circuits.primitives_nexxim import NexximComponents +import pytest + + +def test_add_siwave_dynamic_link_failure_with_file_not_found(): + file = Path("dummy") + modeler = MagicMock() + nexxim = NexximComponents(modeler) + + with pytest.raises(FileNotFoundError): + nexxim.add_siwave_dynamic_link(file) + + +@patch.object(warnings, "warn") +def test_add_subcircuit_link_warning_call(mock_warn): + file = Path("dummy") + modeler = MagicMock() + nexxim = NexximComponents(modeler) + + nexxim._add_subcircuit_link("dummy_comp", ["dummy_pin"], file, "dummy_source_design", image_subcircuit_path=file) + + mock_warn.assert_any_call("Image extension is not valid. Use default image instead.") diff --git a/tests/unit/test_warnings.py b/tests/unit/test_warnings.py index 8711af96542..ee2b487bf19 100644 --- a/tests/unit/test_warnings.py +++ b/tests/unit/test_warnings.py @@ -62,7 +62,7 @@ def test_deprecation_warning_with_valid_python_version(mock_warn, monkeypatch): def test_alias_deprecation_warning(): - """Test that pyaedt alias warning is triggered.""" + """Test that pyaedt alias warning is triggered.""" import importlib import pyaedt