diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b7ba2393c4..e49a24303f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ exclude: | repos: - repo: https://github.com/psf/black - rev: 23.10.1 # IF VERSION CHANGES --> MODIFY "blacken-docs" MANUALLY AS WELL!! + rev: 23.11.0 # IF VERSION CHANGES --> MODIFY "blacken-docs" MANUALLY AS WELL!! hooks: - id: black args: @@ -61,7 +61,7 @@ repos: rev: 1.16.0 hooks: - id: blacken-docs - additional_dependencies: [black==23.10.1] + additional_dependencies: [black==23.11.0] # - repo: https://github.com/numpy/numpydoc diff --git a/_unittest/conftest.py b/_unittest/conftest.py index 2e481dcfa91..a7202f811fb 100644 --- a/_unittest/conftest.py +++ b/_unittest/conftest.py @@ -138,6 +138,7 @@ def desktop(): for key in keys: del _desktop_sessions[key] d = Desktop(desktop_version, NONGRAPHICAL, new_thread) + d.odesktop.SetTempDirectory(tempfile.gettempdir()) d.disable_autosave() d.odesktop.SetDesktopConfiguration("All") d.odesktop.SetSchematicEnvironment(0) diff --git a/_unittest/test_01_Design.py b/_unittest/test_01_Design.py index 00e37c48092..b2ee32b2b48 100644 --- a/_unittest/test_01_Design.py +++ b/_unittest/test_01_Design.py @@ -1,4 +1,5 @@ import os +import tempfile from _unittest.conftest import config from _unittest.conftest import desktop_version @@ -90,6 +91,7 @@ def test_06_libs(self): def test_06a_set_temp_dir(self): assert os.path.exists(self.aedtapp.set_temporary_directory(os.path.join(self.local_scratch.path, "temp_dir"))) assert self.aedtapp.set_temporary_directory(os.path.join(self.local_scratch.path, "temp_dir")) + self.aedtapp.set_temporary_directory(tempfile.gettempdir()) def test_08_objects(self): print(self.aedtapp.oboundary) diff --git a/_unittest/test_03_Materials.py b/_unittest/test_03_Materials.py index 53b23a0bbf1..560ee092fd9 100644 --- a/_unittest/test_03_Materials.py +++ b/_unittest/test_03_Materials.py @@ -232,8 +232,31 @@ def test_09_non_linear_materials(self, add_app): assert app.materials.material_keys["mymat2"].is_used def test_10_add_material_sweep(self): - assert self.aedtapp.materials.add_material_sweep(["copper", "aluminum"], "sweep_copper") - assert "sweep_copper" in list(self.aedtapp.materials.material_keys.keys()) + material_name = "sweep_material" + assert self.aedtapp.materials.add_material_sweep(["copper", "aluminum", "FR4_epoxy"], material_name) + assert material_name in list(self.aedtapp.materials.material_keys.keys()) + properties_to_check = [ + "permittivity", + "permeability", + "conductivity", + "dielectric_loss_tangent", + "thermal_conductivity", + "mass_density", + "specific_heat", + "thermal_expansion_coefficient", + "youngs_modulus", + "poissons_ratio", + ] + # check if the variables are correctly created + assert "$ID" + material_name in self.aedtapp.variable_manager.variable_names + for prop in properties_to_check: + var_name = "$" + material_name + "_" + prop + assert var_name in self.aedtapp.variable_manager.variable_names + # check if the material properties are correct + for prop in properties_to_check: + var_name = "$" + material_name + "_" + prop + mat_prop = getattr(self.aedtapp.materials[material_name], prop).value + assert mat_prop == var_name + "[$ID" + material_name + "]" def test_11_material_case(self): assert self.aedtapp.materials["Aluminum"] == self.aedtapp.materials["aluminum"] @@ -245,3 +268,9 @@ def test_12_material_model(self): self.aedtapp["$dk"] = 3 self.aedtapp["$df"] = 0.01 assert mat.set_djordjevic_sarkar_model(dk="$dk", df="$df") + + def test_13_get_materials_in_project(self): + used_materials = self.aedtapp.materials.get_used_project_material_names() + assert isinstance(used_materials, list) + for m in [mat for mat in self.aedtapp.materials if mat.is_used]: + assert m.name in used_materials diff --git a/_unittest/test_09_VariableManager.py b/_unittest/test_09_VariableManager.py index 1fd01c51075..3eac9011b0d 100644 --- a/_unittest/test_09_VariableManager.py +++ b/_unittest/test_09_VariableManager.py @@ -461,14 +461,12 @@ def test_16_maxwell_circuit_variables(self): assert mc["var3"] == "10deg" assert mc["var4"] == "10rad" - def test_17_project_sweep_variable(self): + 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 - self.aedtapp.materials.add_material_sweep(["copper", "aluminum"], "sweep_alu") - assert "$sweep_alupermittivity" in self.aedtapp.variable_manager.dependent_variables def test_18_test_optimization_properties(self): var = "v1" @@ -668,6 +666,8 @@ def test_32_delete_unused_variables(self): self.aedtapp.modeler.create_rectangle(0, ["used_var", "used_var", "used_var"], [10, 20]) mat1 = self.aedtapp.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) diff --git a/_unittest/test_98_Icepak.py b/_unittest/test_98_Icepak.py index b9d858fb6ea..b9e5ab38d95 100644 --- a/_unittest/test_98_Icepak.py +++ b/_unittest/test_98_Icepak.py @@ -1278,3 +1278,87 @@ def test_68_mesh_priority_3d_comp(self, add_app): assert app.mesh.add_priority(entity_type=2, comp_name="all_3d_objects1", priority=2) app.close_project(name="3d_comp_mesh_prio_test", save_project=False) + + def test_69_recirculation_boundary(self): + box = self.aedtapp.modeler.create_box([5, 5, 5], [1, 2, 3], "BlockBoxEmpty", "copper") + box.solve_inside = False + assert not self.aedtapp.assign_recirculation_opening( + [box.top_face_x, box.bottom_face_x, box.bottom_face_y], box.top_face_x, flow_assignment="10kg_per_s_m2" + ) + assert self.aedtapp.assign_recirculation_opening( + [box.top_face_x, box.bottom_face_x], box.top_face_x, conductance_external_temperature="25cel" + ) + assert self.aedtapp.assign_recirculation_opening( + [box.top_face_x, box.bottom_face_x], box.top_face_x, start_time="0s" + ) + self.aedtapp.solution_type = "Transient" + assert self.aedtapp.assign_recirculation_opening([box.top_face_x, box.bottom_face_x], box.top_face_x) + assert self.aedtapp.assign_recirculation_opening([box.top_face_x.id, box.bottom_face_x.id], box.top_face_x.id) + assert not self.aedtapp.assign_recirculation_opening( + [box.top_face_x.id, box.bottom_face_x.id], + box.top_face_x.id, + thermal_specification="Conductance", + flow_direction=[1], + ) + temp_dict = {"Function": "Square Wave", "Values": ["1cel", "0s", "1s", "0.5s", "0cel"]} + flow_dict = {"Function": "Sinusoidal", "Values": ["0kg_per_s_m2", 1, 1, "1s"]} + recirc = self.aedtapp.assign_recirculation_opening( + [box.top_face_x.id, box.bottom_face_x.id], + box.top_face_x.id, + thermal_specification="Temperature", + assignment_value=temp_dict, + flow_assignment=flow_dict, + ) + assert recirc + assert recirc.update() + self.aedtapp.solution_type = "SteadyState" + assert not self.aedtapp.assign_recirculation_opening( + [box.top_face_x.id, box.bottom_face_x.id], + box.top_face_x.id, + thermal_specification="Temperature", + assignment_value=temp_dict, + flow_assignment=flow_dict, + ) + assert not self.aedtapp.assign_recirculation_opening( + [box.top_face_x.id, box.bottom_face_x.id], + box.top_face_x.id, + thermal_specification="Temperature", + flow_direction="Side", + ) + assert self.aedtapp.assign_recirculation_opening( + [box.top_face_x.id, box.bottom_face_x.id], + box.top_face_x.id, + thermal_specification="Temperature", + flow_direction=[0, 1, 0], + ) + assert not self.aedtapp.assign_recirculation_opening( + [box.top_face_x.id, box.bottom_face_x.id], + box.top_face_x.id, + thermal_specification="Temperature", + flow_assignment=flow_dict, + ) + + def test_70_blower_boundary(self): + cylinder = self.aedtapp.modeler.create_cylinder(cs_axis="X", position=[0, 0, 0], radius=10, height=1) + curved_face = [f for f in cylinder.faces if not f.is_planar] + planar_faces = [f for f in cylinder.faces if f.is_planar] + assert not self.aedtapp.assign_blower_type1(curved_face + planar_faces, planar_faces, [10, 5, 0], [0, 1, 2, 4]) + blower = self.aedtapp.assign_blower_type1( + [f.id for f in curved_face + planar_faces], [f.id for f in planar_faces], [10, 5, 0], [0, 2, 4] + ) + assert blower + assert blower.update() + box = self.aedtapp.modeler.create_box([5, 5, 5], [1, 2, 3], "BlockBoxEmpty", "copper") + assert self.aedtapp.assign_blower_type2([box.faces[0], box.faces[1]], [box.faces[0]], [10, 5, 0], [0, 2, 4]) + + def test_71_assign_adiabatic_plate(self): + box = self.aedtapp.modeler.create_box([5, 5, 5], [1, 2, 3], "Box", "copper") + rectangle = self.aedtapp.modeler.create_rectangle(0, [0, 0, 0], [1, 2]) + assert self.aedtapp.assign_adiabatic_plate( + box.top_face_x, {"RadiateTo": "AllObjects"}, {"RadiateTo": "AllObjects"} + ) + assert self.aedtapp.assign_adiabatic_plate(box.top_face_x.id) + assert self.aedtapp.assign_adiabatic_plate(rectangle) + ad_plate = self.aedtapp.assign_adiabatic_plate(rectangle.name) + assert ad_plate + assert ad_plate.update() diff --git a/_unittest_solvers/conftest.py b/_unittest_solvers/conftest.py index 62566a67495..88608481ed0 100644 --- a/_unittest_solvers/conftest.py +++ b/_unittest_solvers/conftest.py @@ -131,6 +131,7 @@ def local_scratch(init_scratch): @pytest.fixture(scope="module", autouse=True) def desktop(): d = Desktop(desktop_version, NONGRAPHICAL, new_thread) + d.odesktop.SetTempDirectory(tempfile.gettempdir()) d.disable_autosave() yield d diff --git a/pyaedt/application/Variables.py b/pyaedt/application/Variables.py index 48257aaadcd..e9cd41646af 100644 --- a/pyaedt/application/Variables.py +++ b/pyaedt/application/Variables.py @@ -19,6 +19,7 @@ import os import re import types +import warnings from pyaedt import pyaedt_function_handler from pyaedt.generic.constants import AEDT_UNITS @@ -1237,7 +1238,7 @@ def delete_variable(self, var_name): return False @pyaedt_function_handler() - def is_used_variable(self, var_name): + def is_used(self, var_name): """Find if a variable is used. Parameters @@ -1268,6 +1269,27 @@ def is_used_variable(self, var_name): return used return used + @pyaedt_function_handler() + def is_used_variable(self, var_name): + """Find if a variable is used. + + .. deprecated:: 0.7.4 + Use :func:`is_used` method instead. + + Parameters + ---------- + var_name : str + Name of the variable. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + warnings.warn("`is_used_variable` is deprecated. Use `is_used` method instead.", DeprecationWarning) + return self.is_used(var_name) + def _find_used_variable_history(self, history, var_name): """Find if a variable is used. @@ -1307,7 +1329,7 @@ def delete_unused_variables(self): var_list = self.variable_names for var in var_list[:]: - if not self.is_used_variable(var): + if not self.is_used(var): self.delete_variable(var) return True diff --git a/pyaedt/edb_core/edb_data/nets_data.py b/pyaedt/edb_core/edb_data/nets_data.py index 0574e17b7f8..6b8ef68b62e 100644 --- a/pyaedt/edb_core/edb_data/nets_data.py +++ b/pyaedt/edb_core/edb_data/nets_data.py @@ -239,7 +239,7 @@ def serial_rlc(self): { i: v for i, v in self._app._nets[net].components.items() - if list(set(v.nets).intersection(nets)) != [net] + if list(set(v.nets).intersection(nets)) != [net] and v.type in ["Resistor", "Inductor", "Capacitor"] } ) return comps_common diff --git a/pyaedt/edb_core/edb_data/siwave_simulation_setup_data.py b/pyaedt/edb_core/edb_data/siwave_simulation_setup_data.py index 90abe4e3f2e..99002a940da 100644 --- a/pyaedt/edb_core/edb_data/siwave_simulation_setup_data.py +++ b/pyaedt/edb_core/edb_data/siwave_simulation_setup_data.py @@ -1,3 +1,5 @@ +import warnings + from pyaedt.edb_core.edb_data.simulation_setup import BaseSimulationSetup from pyaedt.edb_core.general import convert_netdict_to_pydict from pyaedt.edb_core.general import convert_pydict_to_netdict @@ -1005,11 +1007,13 @@ def set_pi_slider(self, value): - ``0``: Optimal speed - ``1``: Balanced - ``2``: Optimal accuracy + + .. deprecated:: 0.7.5 + Use :property:`pi_slider_position` property instead. + """ - self.use_si_settings = False - self.use_custom_settings = False + warnings.warn("`set_pi_slider` is deprecated. Use `pi_slider_position` property instead.", DeprecationWarning) self.pi_slider_position = value - self.advanced_settings.set_pi_slider(value) @pyaedt_function_handler def set_si_slider(self, value): @@ -1020,11 +1024,14 @@ def set_si_slider(self, value): - ``0``: Optimal speed - ``1``: Balanced - ``2``: Optimal accuracy``` + + .. deprecated:: 0.7.5 + Use :property:`si_slider_position` property instead. + """ - self.use_si_settings = True - self.use_custom_settings = False + warnings.warn("`set_si_slider` is deprecated. Use `si_slider_position` property instead.", DeprecationWarning) + self.si_slider_position = value - self.advanced_settings.set_si_slider(value) @property def pi_slider_position(self): @@ -1038,9 +1045,13 @@ def pi_slider_position(self, value): self._edb_object = self._set_edb_setup_info(edb_setup_info) self._update_setup() + self.use_si_settings = False + self.use_custom_settings = False + self.advanced_settings.set_pi_slider(value) + @property def si_slider_position(self): - """SI solider position. Values are from ``1`` to ``3``.""" + """SI slider position. Values are from ``1`` to ``3``.""" return self.get_sim_setup_info.SimulationSettings.SISliderPos @si_slider_position.setter @@ -1050,6 +1061,10 @@ def si_slider_position(self, value): self._edb_object = self._set_edb_setup_info(edb_setup_info) self._update_setup() + self.use_si_settings = True + self.use_custom_settings = False + self.advanced_settings.set_si_slider(value) + @property def use_custom_settings(self): """Custom settings to use. diff --git a/pyaedt/generic/configurations.py b/pyaedt/generic/configurations.py index fb32675cab7..91403b5f4c3 100644 --- a/pyaedt/generic/configurations.py +++ b/pyaedt/generic/configurations.py @@ -1060,8 +1060,8 @@ def import_config(self, config_file, *args): self._app.logger.warning("Material %s already exists. Renaming to %s", el, newname) else: newname = el - newmat = Material(self._app, el, val) - if newmat.update(): + newmat = Material(self._app, el, val, material_update=True) + if newmat: self._app.materials.material_keys[newname] = newmat else: # pragma: no cover self.results.import_materials = False diff --git a/pyaedt/generic/touchstone_parser.py b/pyaedt/generic/touchstone_parser.py index 3650790a729..75a506b89fc 100644 --- a/pyaedt/generic/touchstone_parser.py +++ b/pyaedt/generic/touchstone_parser.py @@ -460,7 +460,7 @@ def get_worst_curve(self, freq_min=None, freq_max=None, worst_is_higher=True, cu @pyaedt_function_handler() def read_touchstone(file_path): - """Load the contents of a Touchstone file into an NPort + """Load the contents of a Touchstone file into an NPort. Parameters ---------- diff --git a/pyaedt/hfss3dlayout.py b/pyaedt/hfss3dlayout.py index 96b9860b64a..edbb836c5af 100644 --- a/pyaedt/hfss3dlayout.py +++ b/pyaedt/hfss3dlayout.py @@ -777,7 +777,15 @@ def create_scattering( @pyaedt_function_handler() def export_touchstone( - self, setup_name=None, sweep_name=None, file_name=None, variations=None, variations_value=None + self, + setup_name=None, + sweep_name=None, + file_name=None, + variations=None, + variations_value=None, + renormalization=False, + impedance=None, + gamma_impedance_comments=False, ): """Export a Touchstone file. @@ -797,6 +805,15 @@ def export_touchstone( variations_value : list, optional List of all parameter variation values. For example, ``["22cel", "100"]``. The default is ``None``. + renormalization : bool, optional + Perform renormalization before export. + The default is ``False``. + impedance : float, optional + Real impedance value in ohm, for renormalization, if not specified considered 50 ohm. + The default is ``None``. + gamma_impedance_comments : bool, optional + Include Gamma and Impedance values in comments. + The default is ``False``. Returns ------- @@ -814,6 +831,9 @@ def export_touchstone( file_name=file_name, variations=variations, variations_value=variations_value, + renormalization=renormalization, + impedance=impedance, + comments=gamma_impedance_comments, ) @pyaedt_function_handler() diff --git a/pyaedt/icepak.py b/pyaedt/icepak.py index 474ba02677e..c20bee56ea5 100644 --- a/pyaedt/icepak.py +++ b/pyaedt/icepak.py @@ -11,6 +11,7 @@ from pyaedt import is_ironpython from pyaedt import is_linux from pyaedt.generic.general_methods import GrpcApiError +from pyaedt.modeler.cad.elements3d import FacePrimitive from pyaedt.modules.SetupTemplates import SetupKeys if is_linux and is_ironpython: @@ -2952,7 +2953,7 @@ def assign_surface_material(self, obj, mat): if oo: from pyaedt.modules.Material import SurfaceMaterial - sm = SurfaceMaterial(self.materials, mat) + sm = SurfaceMaterial(self.materials, mat, material_update=False) sm.coordinate_system = oo.GetPropEvaluatedValue("Coordinate System Type") props = oo.GetPropNames() if "Surface Emissivity" in props: @@ -2965,6 +2966,8 @@ def assign_surface_material(self, obj, mat): sm.surface_diffuse_absorptance = oo.GetPropEvaluatedValue("Solar Diffuse Absorptance") if "Solar Normal Absorptance" in props: sm.surface_incident_absorptance = oo.GetPropEvaluatedValue("Solar Normal Absorptance") + sm.update() + sm._material_update = True self.materials.surface_material_keys[mat.lower()] = sm return True @@ -4869,3 +4872,420 @@ def assign_symmetry_wall( raise SystemExit except (GrpcApiError, SystemExit): return None + + @pyaedt_function_handler() + def assign_adiabatic_plate(self, assignment, high_radiation_dict=None, low_radiation_dict=None, boundary_name=None): + """ + Assign adiabatic plate boundary condition. + + Parameters + ---------- + assignment : list + List of strings containing object names, or list of integers + containing face ids or list of faces or objects. + high_radiation_dict : dictionary, optional + Dictionary containing the radiation assignment for the high side. + The two keys that are always required are ``"RadiateTo"`` and + ``"Surface Material"``. If the value of ``"RadiateTo"`` is + ``"RefTemperature"``, then the others required keys are + ``"Ref. Temperature"`` and ``"View Factor"``. The other possible + value of ``"RadiateTo"`` is ``"AllObjects"``. Default is ``None`` + in which case the radiation on the high side is set to off. + low_radiation_dict : dictionary, optional + Dictionary containing the radiation assignment for the low side. + The dictionary structure is the same of ``high_radiation_dict``. + Default is ``None``, in which case the radiation on the low side + is set to off. + + Returns + ------- + :class:`pyaedt.modules.Boundary.BoundaryObject` + Boundary object when successful or ``None`` when failed. + + References + ---------- + + >>> oModule.AssignAdiabaticPlateBoundary + + Examples + -------- + >>> from pyaedt import Icepak + >>> ipk = Icepak() + >>> box = ipk.modeler.create_box([5, 5, 5], [1, 2, 3], "Box", "copper") + >>> ad_plate = ipk.assign_adiabatic_plate(box.top_face_x, None, {"RadiateTo": "AllObjects"}) + + """ + if not isinstance(assignment, list): + assignment = [assignment] + if isinstance(assignment[0], str): + key = "Objects" + elif isinstance(assignment[0], int): + key = "Faces" + elif isinstance(assignment[0], FacePrimitive): + key = "Faces" + assignment = [f.id for f in assignment] + else: + key = "Objects" + assignment = [o.name for o in assignment] + props = {key: assignment} + for rad_dict, side in zip([high_radiation_dict, low_radiation_dict], ["HighSide", "LowSide"]): + props[side] = {"Radiate": bool(rad_dict)} + if rad_dict is not None: + for k, v in rad_dict.items(): + if side == "HighSide": + if k == "RadiateTo": + v += " - High" + k += " - High" + props[side][k] = v + + if not boundary_name: + boundary_name = generate_unique_name("AdiabaticPlate") + + bound = BoundaryObject(self, boundary_name, props, "Adiabatic Plate") + try: + if bound.create(): + self._boundaries[bound.name] = bound + return bound + else: # pragma: no cover + raise SystemExit + except (GrpcApiError, SystemExit): # pragma: no cover + return None + + @pyaedt_function_handler() + def assign_recirculation_opening(self, face_list, extract_face, thermal_specification="Temperature", + assignment_value="0cel", conductance_external_temperature=None, + flow_specification="Mass Flow", flow_assignment="0kg_per_s_m2", + flow_direction=None, start_time=None, end_time=None, boundary_name=None): + """Assign recirculation faces. + + Parameters + ---------- + face_list : list + List of face primitive objects or a list of integers + containing faces IDs. + extract_face : modeler.cad.elements3d.FacePrimitive, int + ID of the face on the extract side. + thermal_specification : str, optional + Type of the thermal assignment across the two recirculation + faces. The default is ``"Temperature"``. Options are + ``"Conductance"``, ``"Heat Input"``, and ``"Temperature"``. + assignment_value : str or dict, optional + String with value and units of the thermal assignment. For a + transient assignment, a dictionary can be used. The dictionary + should contain two keys: ``"Function"`` and ``"Values"``. + - For the ``"Function"`` key, options are + ``"Exponential"``, ``"Linear"``, ``"Piecewise Linear"``, + ``"Power Law"``, ``"Sinusoidal"``, and ``"Square Wave"``. + - For the ``"Values"`` key, provide a list of strings containing the + parameters required by the ``"Function"`` key selection. For + example, when ``"Linear"`` is set as the ``"Function"`` key, two + parameters are required: the value of the variable at t=0 and the + slope of the line. For the parameters required by each ``"Function"`` + key selection, see the Icepak documentation. + The parameters must contain the units where needed. + The default value is ``"0cel"``. + conductance_external_temperature : str, optional + External temperature value, which is needed if + ``thermal_specification`` is set to ``"Conductance"``. + The default is ``None``. + flow_specification : str, optional + Flow specification for the recirculation zone. The default is + ``"Mass Flow"``. Options are: ``"Mass Flow"``, ``"Mass Flux"``, + and ``"Volume Flow"``. + flow_assignment : str or dict, optional + String with the value and units of the flow assignment. For a + transient assignment, a dictionary can be used. The dictionary + should contain two keys: ``"Function"`` and ``"Values"``. + - For the ``"Function"`` key, options are + ``"Exponential"``, ``"Linear"``, ``"Piecewise Linear"``, + ``"Power Law"``, ``"Sinusoidal"``, and ``"Square Wave"``. + - For the ``"Values"`` key, provide a list of strings containing the + parameters required by the ``"Function"`` key selection. For + example, when``"Linear"`` is set as the ``"Function"`` key, two + parameters are required: the value of the variable at t=0 and the + slope of the line. For the parameters required by each + ``"Function"`` key selection, see the Icepak documentation. + The parameters must contain the units where needed. + The default value is ``"0kg_per_s_m2"``. + flow_direction : list, optional + Flow direction enforced at the recirculation zone. The default value + is ``None``, in which case the normal direction is used. + start_time : str, optional + Start of the time interval. This parameter is relevant only if the + simulation is transient. The default value is ``"0s"``. + end_time : str, optional + End of the time interval. This parameter is relevant only if the + simulation is transient. The default value is ``"0s"``. + boundary_name : str, optional + Name of the recirculation boundary. The default is ``None``, in + which case the boundary is automatically generated. + + Returns + ------- + :class:`pyaedt.modules.Boundary.BoundaryObject` + Boundary object when successful or ``None`` when failed. + + References + ---------- + + >>> oModule.AssignRecircBoundary + + Examples + -------- + >>> from pyaedt import Icepak + >>> ipk = Icepak() + >>> ipk.solution_type = "Transient" + >>> box = ipk.modeler.create_box([5, 5, 5], [1, 2, 3], "BlockBoxEmpty", "copper") + >>> box.solve_inside = False + >>> recirc = ipk.assign_recirculation_opening([box.top_face_x, box.bottom_face_x], box.top_face_x, + >>> flow_assignment="10kg_per_s_m2") + + """ + if not len(face_list) == 2: + self.logger.error("Recirculation boundary condition must be assigned to two faces.") + return False + if conductance_external_temperature is not None and thermal_specification is not "Conductance": + self.logger.warning( + '``conductance_external_temperature`` does not have any effect unless the ``thermal_specification`` ' + 'is ``"Conductance"``.') + if conductance_external_temperature is not None and thermal_specification is not "Conductance": + self.logger.warning( + '``conductance_external_temperature`` must be specified when ``thermal_specification`` ' + 'is ``"Conductance"``. Setting ``conductance_external_temperature`` to ``"AmbientTemp"``.') + if (start_time is not None or end_time is not None) and not self.solution_type == "Transient": + self.logger.warning( + '``start_time`` and ``end_time`` only effect steady-state simulations.') + elif self.solution_type == "Transient" and not (start_time and end_time): + self.logger.warning( + '``start_time`` and ``end_time`` should be declared for transient simulations. Setting them to "0s".') + start_time = "0s" + end_time = "0s" + assignment_dict = { + "Conductance": "Conductance", + "Heat Input": "Heat Flow", + "Temperature": "Temperature Change" + } + props = {} + if not isinstance(face_list[0], int): + face_list = [f.id for f in face_list] + props["Faces"] = face_list + if isinstance(extract_face, int): + extract_face = [extract_face] + else: + extract_face = [extract_face.id] + props["ExtractFace"] = extract_face + props["Thermal Condition"] = thermal_specification + if isinstance(assignment_value, dict): + if not self.solution_type == "Transient": + self.logger.error("Transient assignment is supported only in transient designs.") + return None + assignment = self._parse_variation_data( + assignment_dict[thermal_specification], + "Transient", + variation_value=assignment_value["Values"], + function=assignment_value["Function"], + ) + props.update(assignment) + else: + props[assignment_dict[thermal_specification]] = assignment_value + if thermal_specification == "Conductance": + props["External Temp"] = conductance_external_temperature + if isinstance(flow_assignment, dict): + if not self.solution_type == "Transient": + self.logger.error("Transient assignment is supported only in transient designs.") + return None + assignment = self._parse_variation_data( + flow_specification + " Rate", + "Transient", + variation_value=flow_assignment["Values"], + function=flow_assignment["Function"], + ) + props.update(assignment) + else: + props[flow_specification + " Rate"] = flow_assignment + if flow_direction is None: + props["Supply Flow Direction"] = "Normal" + else: + props["Supply Flow Direction"] = "Specified" + if not (isinstance(flow_direction, list)): + self.logger.error("``flow_direction`` can be only ``None`` or a list of strings or floats.") + return False + elif len(flow_direction) != 3: + self.logger.error("``flow_direction`` must have only three components.") + return False + for direction, val in zip(["X", "Y", "Z"], flow_direction): + props[direction] = str(val) + if self.solution_type == "Transient": + props["Start"] = start_time + props["End"] = end_time + if not boundary_name: + boundary_name = generate_unique_name("Recirculating") + + bound = BoundaryObject(self, boundary_name, props, "Recirculating") + try: + if bound.create(): + self._boundaries[bound.name] = bound + return bound + else: # pragma: no cover + raise SystemExit + except (GrpcApiError, SystemExit): # pragma : no cover + return None + + @pyaedt_function_handler() + def assign_blower_type1(self, faces, inlet_face, fan_curve_pressure, fan_curve_flow, blower_power="0W", blade_rpm=0, + blade_angle="0rad", fan_curve_pressure_unit="n_per_meter_sq", + fan_curve_flow_unit="m3_per_s", boundary_name=None): + """Assign blower type 1. + + Parameters + ---------- + faces : list + List of modeler.cad.elements3d.FacePrimitive or of integers + containing faces ids. + inlet_face : modeler.cad.elements3d.FacePrimitive, int or list + Inlet faces. + fan_curve_pressure : list + List of the fan curve pressure values. Only floats should + be included in the list as their unit can be modified with + fan_curve_pressure_unit argument. + fan_curve_flow : list + List of the fan curve flow value. Only floats should be + included in the list as their unit can be modified with + fan_curve_flow_unit argument. + blower_power : str, optional + blower power expressed as a string containing the value and unit. + Default is "0W". + blade_rpm : float, optional + Blade RPM value. Default is 0. + blade_angle : str, optional + Blade angle expressed as a string containing value and the unit. + Default is "0rad". + fan_curve_pressure_unit : str, optional + Fan curve pressure unit. Default is "n_per_meter_sq". + fan_curve_flow_unit : str, optional + Fan curve flow unit. Default is "m3_per_s". + boundary_name : str, optional + Name of the recirculation boundary. The default is ``None``, in + which case the boundary is automatically generated. + + + Returns + ------- + :class:`pyaedt.modules.Boundary.BoundaryObject` + Boundary object when successful or ``None`` when failed. + + References + ---------- + + >>> oModule.AssignBlowerBoundary + + Examples + -------- + >>> from pyaedt import Icepak + >>> ipk = Icepak() + >>> cylinder = self.aedtapp.modeler.create_cylinder(cs_axis="X", position=[0,0,0], radius=10, height=1) + >>> curved_face = [f for f in cylinder.faces if not f.is_planar] + >>> planar_faces = [f for f in cylinder.faces if f.is_planar] + >>> cylinder.solve_inside=False + >>> blower = self.aedtapp.assign_blower_type1([f.id for f in curved_face+planar_faces], + >>> [f.id for f in planar_faces], [10, 5, 0], [0, 2, 4]) + + """ + props = {} + props["Blade RPM"] = blade_rpm + props["Fan Blade Angle"] = blade_angle + props["Blower Type"] = "Type 1" + return self._assign_blower(props, faces, inlet_face, fan_curve_flow_unit, fan_curve_pressure_unit, + fan_curve_flow, fan_curve_pressure, blower_power, boundary_name) + + @pyaedt_function_handler() + def assign_blower_type2(self, faces, inlet_face, fan_curve_pressure, fan_curve_flow, blower_power="0W", + exhaust_angle="0rad", fan_curve_pressure_unit="n_per_meter_sq", + fan_curve_flow_unit="m3_per_s", boundary_name=None): + """Assign blower type 2. + + Parameters + ---------- + faces : list + List of modeler.cad.elements3d.FacePrimitive or of integers + containing faces ids. + inlet_face : modeler.cad.elements3d.FacePrimitive, int or list + Inlet faces. + fan_curve_pressure : list + List of the fan curve pressure values. Only floats should + be included in the list as their unit can be modified with + fan_curve_pressure_unit argument. + fan_curve_flow : list + List of the fan curve flow value. Only floats should be + included in the list as their unit can be modified with + fan_curve_flow_unit argument. + blower_power : str, optional + blower power expressed as a string containing the value and unit. + Default is "0W". + exhaust_angle : float, optional + Exhaust angle expressed as a string containing value and the unit. + Default is "0rad". + fan_curve_pressure_unit : str, optional + Fan curve pressure unit. Default is "n_per_meter_sq". + fan_curve_flow_unit : str, optional + Fan curve flow unit. Default is "m3_per_s". + boundary_name : str, optional + Name of the recirculation boundary. The default is ``None``, in + which case the boundary is automatically generated. + + + Returns + ------- + :class:`pyaedt.modules.Boundary.BoundaryObject` + Boundary object when successful or ``None`` when failed. + + References + ---------- + + >>> oModule.AssignBlowerBoundary + + Examples + -------- + >>> from pyaedt import Icepak + >>> ipk = Icepak() + >>> box = ipk.modeler.create_box([5, 5, 5], [1, 2, 3], "BlockBoxEmpty", "copper") + >>> box.solve_inside=False + >>> blower = self.aedtapp.assign_blower_type2([box.faces[0], box.faces[1]], + >>> [box.faces[0]], [10, 5, 0], [0, 2, 4]) + + """ + props = {} + props["Exhaust Exit Angle"] = exhaust_angle + props["Blower Type"] = "Type 2" + return self._assign_blower(props, faces, inlet_face, fan_curve_flow_unit, fan_curve_pressure_unit, + fan_curve_flow, fan_curve_pressure, blower_power, boundary_name) + + @pyaedt_function_handler() + def _assign_blower(self, props, faces, inlet_face, fan_curve_flow_unit, fan_curve_pressure_unit, fan_curve_flow, + fan_curve_pressure, blower_power, boundary_name): + if isinstance(faces[0], int): + props["Faces"] = faces + else: + props["Faces"] = [f.id for f in faces] + if not isinstance(inlet_face, list): + inlet_face = [inlet_face] + if not isinstance(inlet_face[0], int): + props["InletFace"] = [f.id for f in inlet_face] + props["Blower Power"] = blower_power + props["DimUnits"] = [fan_curve_flow_unit, fan_curve_pressure_unit] + if len(fan_curve_flow) != len(fan_curve_pressure): + self.logger.error("``fan_curve_flow`` and ``fan_curve_pressure`` must have the same length.") + return False + props["X"] = [str(pt) for pt in fan_curve_flow] + props["Y"] = [str(pt) for pt in fan_curve_pressure] + if not boundary_name: + boundary_name = generate_unique_name("Blower") + bound = BoundaryObject(self, boundary_name, props, "Blower") + try: + if bound.create(): + self._boundaries[bound.name] = bound + return bound + else: # pragma : no cover + raise SystemExit + except (GrpcApiError, SystemExit): # pragma: no cover + return None diff --git a/pyaedt/maxwell.py b/pyaedt/maxwell.py index 499cae7b0bc..fb63f86563f 100644 --- a/pyaedt/maxwell.py +++ b/pyaedt/maxwell.py @@ -800,8 +800,8 @@ def assign_translate_motion( Returns ------- - :class:`pyaedt.modules.Boundary.BoundaryObject` - Boundary object. + :class:`pyaedt.modules.Boundary.BoundaryObject` or ``False`` + Boundary object or bool if not successful. References ---------- diff --git a/pyaedt/modeler/advanced_cad/oms.py b/pyaedt/modeler/advanced_cad/oms.py index 48674e53004..9f230692036 100644 --- a/pyaedt/modeler/advanced_cad/oms.py +++ b/pyaedt/modeler/advanced_cad/oms.py @@ -191,9 +191,9 @@ def generate_buildings(self, center_lat_lon, terrain_mesh, max_radius=500): # create closed and filled polygon from outline of building roof = self.create_building_roof(points) - if np.isnan(float(h)) == False: + if np.isnan(float(h)) is False: extrude_h = float(h) * 2 - elif np.isnan(float(l)) == False: + elif np.isnan(float(l)) is False: extrude_h = float(l) * 10 else: extrude_h = 15.0 diff --git a/pyaedt/modeler/cad/Modeler.py b/pyaedt/modeler/cad/Modeler.py index 8714124a50b..b8feba49549 100644 --- a/pyaedt/modeler/cad/Modeler.py +++ b/pyaedt/modeler/cad/Modeler.py @@ -431,7 +431,7 @@ def props(self): @property def _part_name(self): - """Internally get the part name which the face belongs to""" + """Internally get the part name which the face belongs to.""" if not self.face_id: # face_id has not been defined yet return None @@ -596,7 +596,7 @@ def create( @pyaedt_function_handler() def _get_type_from_id(self, obj_id): - """Get the entity type from the id""" + """Get the entity type from the id.""" for obj in self._modeler.objects.values(): if obj.id == obj_id: return "3dObject" @@ -613,7 +613,7 @@ def _get_type_from_id(self, obj_id): @pyaedt_function_handler() def _get_type_from_object(self, obj): - """Get the entity type from the object""" + """Get the entity type from the object.""" if type(obj) is FacePrimitive: return "Face" elif type(obj) is EdgePrimitive: @@ -1640,7 +1640,7 @@ def update(self): @pyaedt_function_handler() def _get_type_from_id(self, obj_id): - """Get the entity type from the id""" + """Get the entity type from the id.""" for obj in self._modeler.objects.values(): if obj.id == obj_id: return "3dObject" diff --git a/pyaedt/modules/Boundary.py b/pyaedt/modules/Boundary.py index 630fd3937cf..92ea792ce45 100644 --- a/pyaedt/modules/Boundary.py +++ b/pyaedt/modules/Boundary.py @@ -556,6 +556,8 @@ def create(self): self._app.oboundary.AssignStationaryWallBoundary(self._get_args()) elif bound_type == "Symmetry Wall": self._app.oboundary.AssignSymmetryWallBoundary(self._get_args()) + elif bound_type == "Recirculating": + self._app.oboundary.AssignRecircBoundary(self._get_args()) elif bound_type == "Resistance": self._app.oboundary.AssignResistanceBoundary(self._get_args()) elif bound_type == "Conducting Plate": @@ -568,6 +570,8 @@ def create(self): self._app.oboundary.AssignGrilleBoundary(self._get_args()) elif bound_type == "Block": self._app.oboundary.AssignBlockBoundary(self._get_args()) + elif bound_type == "Blower": + self._app.oboundary.AssignBlowerBoundary(self._get_args()) elif bound_type == "SourceIcepak": self._app.oboundary.AssignSourceBoundary(self._get_args()) elif bound_type == "Opening": @@ -728,6 +732,8 @@ def update(self): self._app.oboundary.EditStationaryWallBoundary(self._boundary_name, self._get_args()) # pragma: no cover elif bound_type == "Symmetry Wall": self._app.oboundary.EditSymmetryWallBoundary(self._boundary_name, self._get_args()) # pragma: no cover + elif bound_type == "Recirculating": + self._app.oboundary.EditRecircBoundary(self._boundary_name, self._get_args()) elif bound_type == "Resistance": self._app.oboundary.EditResistanceBoundary(self._boundary_name, self._get_args()) # pragma: no cover elif bound_type == "Conducting Plate": @@ -744,6 +750,8 @@ def update(self): self._app.oboundary.EditEMLoss(self._boundary_name, self._get_args()) # pragma: no cover elif bound_type == "Block": self._app.oboundary.EditBlockBoundary(self._boundary_name, self._get_args()) + elif bound_type == "Blower": + self._app.oboundary.EditBlowerBoundary(self._boundary_name, self._get_args()) elif bound_type == "SourceIcepak": self._app.oboundary.EditSourceBoundary(self._boundary_name, self._get_args()) elif bound_type == "HeatFlux": diff --git a/pyaedt/modules/LayerStackup.py b/pyaedt/modules/LayerStackup.py index fed46ca06fc..05e010c8536 100644 --- a/pyaedt/modules/LayerStackup.py +++ b/pyaedt/modules/LayerStackup.py @@ -160,13 +160,13 @@ def __init__(self, app, layertype="signal", negative=False): # Rough option self._user = False self._RMdl = "Huray" - self._NR = 0.5 + self._NR = 0.0005 self._HRatio = 2.9 self._BRMdl = "Huray" - self._BNR = 0.5 + self._BNR = 0.0005 self._BHRatio = 2.9 self._SRMdl = "Huray" - self._SNR = 0.5 + self._SNR = 0.0005 self._SHRatio = 2.9 # Solver option self._usp = False diff --git a/pyaedt/modules/MaterialLib.py b/pyaedt/modules/MaterialLib.py index dcbfecbc873..782e21c07d8 100644 --- a/pyaedt/modules/MaterialLib.py +++ b/pyaedt/modules/MaterialLib.py @@ -49,7 +49,6 @@ def __init__(self, app): self._desktop = self._app.odesktop self._oproject = self._app.oproject self.logger = self._app.logger - # self.material_keys = self._get_materials() self.material_keys = {} self._surface_material_keys = {} self._load_from_project() @@ -199,19 +198,6 @@ def _get_aedt_case_name(self, material_name): return self._mat_names_aedt[self.mat_names_aedt_lower.index(material_name.lower())] return False - @pyaedt_function_handler() - def _get_materials(self): - """Get materials.""" - mats = {} - try: - for ds in self._app.project_properties["AnsoftProject"]["Definitions"]["Materials"]: - mats[ds.lower()] = Material( - self, ds, self._app.project_properties["AnsoftProject"]["Definitions"]["Materials"][ds] - ) - except: - pass - return mats - @pyaedt_function_handler() def _get_surface_materials(self): mats = {} @@ -221,6 +207,7 @@ def _get_surface_materials(self): self, ds, self._app.project_properties["AnsoftProject"]["Definitions"]["SurfaceMaterials"][ds], + material_update=False, ) except: pass @@ -330,8 +317,8 @@ def add_material(self, materialname, props=None): elif self._get_aedt_case_name(materialname): return self._aedmattolibrary(self._get_aedt_case_name(materialname)) else: - material = Material(self, materialname, props) - if material.update(): + material = Material(self, materialname, props, material_update=True) + if material: self.logger.info("Material has been added. Edit it to update in Desktop.") self.material_keys[materialname.lower()] = material self._mats.append(materialname) @@ -375,10 +362,11 @@ def add_surface_material(self, material_name, emissivity=None): self.logger.warning("Warning. The material is already in the database. Change the name or edit it.") return self.surface_material_keys[material_name.lower()] else: - material = SurfaceMaterial(self._app, material_name) + material = SurfaceMaterial(self._app, material_name, material_update=False) if emissivity: material.emissivity = emissivity - material.update() + material.update() + material._material_update = True self.logger.info("Material has been added. Edit it to update in Desktop.") self.surface_material_keys[material_name.lower()] = material return self.surface_material_keys[material_name.lower()] @@ -404,22 +392,15 @@ def _create_mat_project_vars(self, matlist): return matprop @pyaedt_function_handler() - def add_material_sweep(self, swargs, matname): + def add_material_sweep(self, materials_list, material_name): """Create a sweep material made of an array of materials. - If a material needs to have a dataset (thermal modifier), then a - dataset is created. Material properties are loaded from the XML file - database ``amat.xml``. - Parameters ---------- - swargs : list + materials_list : list List of materials to merge into a single sweep material. - matname : str + material_name : str Name of the sweep material. - enableTM : bool, optional - Unavailable currently. Whether to enable the thermal modifier. - The default is ``True``. Returns ------- @@ -441,25 +422,26 @@ def add_material_sweep(self, swargs, matname): >>> hfss.materials.add_material_sweep(["MyMaterial", "MyMaterial2"], "Sweep_copper") """ matsweep = [] - for args in swargs: - matobj = self.checkifmaterialexists(args) + for mat in materials_list: + matobj = self.checkifmaterialexists(mat) if matobj: matsweep.append(matobj) mat_dict = self._create_mat_project_vars(matsweep) - newmat = Material(self, matname) - index = "$ID" + matname + newmat = Material(self, material_name, material_update=False) + index = "$ID" + material_name newmat.is_sweep_material = True self._app[index] = 0 for el in mat_dict: if el in list(mat_dict.keys()): - self._app["$" + matname + el] = mat_dict[el] - newmat.__dict__["_" + el].value = "$" + matname + el + "[" + index + "]" - newmat._update_props(el, "$" + matname + el + "[" + index + "]", False) + array_var_name = "$" + material_name + "_" + el + self._app[array_var_name] = mat_dict[el] + newmat.__dict__["_" + el].value = array_var_name + "[" + index + "]" + newmat._update_props(el, array_var_name + "[" + index + "]", False) newmat.update() - self.material_keys[matname.lower()] = newmat + self.material_keys[material_name.lower()] = newmat return index @pyaedt_function_handler() @@ -526,8 +508,7 @@ def duplicate_material(self, material_name, new_name=None, props=None): if not new_name: new_name = material_name + "_clone" - new_material = Material(self, new_name, material._props) - new_material.update() + new_material = Material(self, new_name, material._props, material_update=False) # Parameterize material properties if these were passed. if props: @@ -543,8 +524,8 @@ def duplicate_material(self, material_name, new_name=None, props=None): setattr(new_material, p, var_name) except TypeError: print("p = {}".format(p)) - - # new_material.update() # Assign parameter to material property. + new_material.update() + new_material._material_update = True self._mats.append(new_name) self.material_keys[new_name.lower()] = new_material return new_material @@ -580,8 +561,9 @@ def duplicate_surface_material(self, material, new_name): if not material.lower() in list(self.surface_material_keys.keys()): self.logger.error("Material {} is not present".format(material)) return False - newmat = SurfaceMaterial(self, new_name.lower(), self.surface_material_keys[material.lower()]._props) - newmat.update() + newmat = SurfaceMaterial( + self, new_name.lower(), self.surface_material_keys[material.lower()]._props, material_update=True + ) self.surface_material_keys[new_name.lower()] = newmat return newmat @@ -811,8 +793,8 @@ def import_materials_from_file(self, full_json_path): self.logger.warning("Material %s already exists. Renaming to %s", el, newname) else: newname = el - newmat = Material(self, newname, val) - newmat.update() + newmat = Material(self, newname, val, material_update=True) + # newmat.update() self.material_keys[newname] = newmat materials_added.append(newmat) return materials_added @@ -864,9 +846,25 @@ def import_materials_from_excel(self, material_file): and not (isinstance(val[keys.index(prop)], float) and math.isnan(val[keys.index(prop)])) ): props[prop] = float(val[keys.index(prop)]) - new_material = Material(self, newname, props) - new_material.update() + new_material = Material(self, newname, props, material_update=True) + # new_material.update() self.material_keys[newname] = new_material materials_added.append(new_material) return materials_added + + @pyaedt_function_handler + def get_used_project_material_names(self): + """Get list of material names in current project. + + Returns + ------- + List of str + List of material names used in the current project. + + References + ---------- + + >>> oDefinitionManager.GetInUseProjectMaterialNames + """ + return self.odefinition_manager.GetInUseProjectMaterialNames() diff --git a/pyproject.toml b/pyproject.toml index 6bd31d0a1f7..d0b8e7090c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ tests = [ "pytest==7.4.2", "pytest-cov==4.1.0", "pytest-xdist==3.3.1", + "vtk==9.2.6", "pyvista==0.38.0; python_version <= '3.7'", "pyvista==0.42.2; python_version > '3.7'", "scikit-learn==1.3.0; python_version == '3.7'", @@ -80,6 +81,7 @@ doc = [ "osmnx", "pypandoc==1.11", "pytest-sphinx==0.5.0", + "vtk==9.2.6", "pyvista==0.42.2; python_version > '3.7'", "pyvista==0.38.0; python_version <= '3.7'", "recommonmark==0.7.1", @@ -112,6 +114,7 @@ full = [ "pandas==2.0.3; python_version == '3.8'", "pandas==2.1.1; python_version > '3.9'", "osmnx", + "vtk==9.2.6", "pyvista==0.42.2; python_version > '3.7'", "pyvista==0.38.0; python_version <= '3.7'", "SRTM.py", @@ -131,6 +134,7 @@ all = [ "pandas==2.0.3; python_version == '3.8'", "pandas==2.1.1; python_version > '3.9'", "osmnx", + "vtk==9.2.6", "pyvista==0.42.2; python_version > '3.7'", "pyvista==0.38.0; python_version <= '3.7'", "SRTM.py", @@ -204,6 +208,6 @@ checks = [ exclude = [ '\.AEDTMessageManager.add_message$', # bad SS05 '\.Modeler3D\.create_choke$', # bad RT05 - '\._unittest\', # missing docstring for tests + '\._unittest', # missing docstring for tests 'HistoryProps.', # bad RT05 because of the base class named OrderedDict -] +] \ No newline at end of file