diff --git a/.github/labeler.yml b/.github/labeler.yml index 954fa3db203..f0ccc72fd79 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -13,3 +13,10 @@ testing: - _unittest/conftest.py - _unittest_ironpython/run_unittests.py - _unittest_ironpython/run_unittests_batchmode.cmd +# TODO : Remove once EDB is extracted from PyAEDT +edb: +- examples/00-EDB/** +- examples/01-HFSS3DLayout/EDB_in_3DLayout.py +- examples/05-Q3D/Q3D_from_EDB.py +- pyaedt/edb_core/** +- pyaedt/edb.py diff --git a/.github/workflows/full_documentation.yml b/.github/workflows/full_documentation.yml index 6fe075780fd..9803b95be47 100644 --- a/.github/workflows/full_documentation.yml +++ b/.github/workflows/full_documentation.yml @@ -34,7 +34,7 @@ jobs: # The type of runner that the job will run on name: full_documentation runs-on: [windows-latest, pyaedt] - timeout-minutes: 600 + timeout-minutes: 480 strategy: matrix: python-version: ['3.10'] @@ -73,10 +73,10 @@ jobs: testenv\Scripts\Activate.ps1 sphinx-build -j auto --color -b html -a doc/source doc/_build/html -# - name: Create PDF Documentations -# run: | -# testenv\Scripts\Activate.ps1 -# .\doc\make.bat pdf + - name: Create PDF Documentations + run: | + testenv\Scripts\Activate.ps1 + .\doc\make.bat pdf - name: Upload HTML documentation artifact uses: actions/upload-artifact@v3 @@ -92,20 +92,20 @@ jobs: path: doc/_build/html/EDBAPI retention-days: 7 -# - name: Upload PDF documentation artifact -# uses: actions/upload-artifact@v3 -# with: -# name: documentation-pdf -# path: doc/_build/pdf -# retention-days: 7 - -# - name: Release -# uses: softprops/action-gh-release@v1 -# if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') -# with: -# generate_release_notes: true -# files: | -# doc/_build/pdf + - name: Upload PDF documentation artifact + uses: actions/upload-artifact@v3 + with: + name: documentation-pdf + path: doc/_build/pdf + retention-days: 7 + + - name: Release + uses: softprops/action-gh-release@v1 + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + with: + generate_release_notes: true + files: | + doc/_build/pdf doc-deploy-stable: name: Deploy stable documentation @@ -169,4 +169,4 @@ jobs: host-url: ${{ vars.MEILISEARCH_HOST_URL }} api-key: ${{ env.MEILISEARCH_API_KEY }} doc-artifact-name: documentation-html-edb # Add only EDB API as page in this index. - pymeilisearchopts: --port 8001 #serve in another port \ No newline at end of file + pymeilisearchopts: --port 8001 #serve in another port diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index a853245d5f8..17f0b6580c6 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -66,11 +66,15 @@ jobs: # uses: pyansys/pydpf-actions/check-licenses@v2.0 - name: 'Unit testing' - timeout-minutes: 40 - run: | - testenv_s\Scripts\Activate.ps1 - Set-Item -Path env:PYTHONMALLOC -Value "malloc" - pytest --durations=50 -v --cov=pyaedt --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest_solvers + uses: nick-fields/retry@v2 + with: + max_attempts: 3 + retry_on: error + timeout_minutes: 40 + command: | + testenv_s\Scripts\Activate.ps1 + Set-Item -Path env:PYTHONMALLOC -Value "malloc" + pytest --durations=50 -v --cov=pyaedt --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest_solvers - uses: codecov/codecov-action@v3 env: @@ -125,11 +129,15 @@ jobs: # uses: pyansys/pydpf-actions/check-licenses@v2.0 - name: 'Unit testing' - timeout-minutes: 40 - run: | - testenv\Scripts\Activate.ps1 - Set-Item -Path env:PYTHONMALLOC -Value "malloc" - pytest -n 6 --dist loadfile --durations=50 -v --cov=pyaedt --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest + uses: nick-fields/retry@v2 + with: + max_attempts: 3 + retry_on: error + timeout_minutes: 40 + command: | + testenv\Scripts\Activate.ps1 + Set-Item -Path env:PYTHONMALLOC -Value "malloc" + pytest -n 6 --dist loadfile --durations=50 -v --cov=pyaedt --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest - uses: codecov/codecov-action@v3 env: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 148db396a9a..36e15bcaf00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,14 +38,14 @@ repos: - --max-line-length=120 - repo: https://github.com/codespell-project/codespell - rev: v2.2.5 + rev: v2.2.6 hooks: - id: codespell additional_dependencies: - tomli - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: requirements-txt-fixer - id: debug-statements @@ -53,7 +53,7 @@ repos: # validate GitHub workflow files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.26.3 + rev: 0.27.0 hooks: - id: check-github-workflows @@ -63,6 +63,12 @@ repos: - id: blacken-docs additional_dependencies: [black==23.9.1] + +# - repo: https://github.com/numpy/numpydoc +# rev: v1.6.0 +# hooks: +# - id: numpydoc-validation + # - repo: https://github.com/pycqa/pydocstyle # rev: 6.1.1 # hooks: diff --git a/_unittest/example_models/TEDB/stackup.json b/_unittest/example_models/TEDB/stackup.json new file mode 100644 index 00000000000..0fc405a8373 --- /dev/null +++ b/_unittest/example_models/TEDB/stackup.json @@ -0,0 +1,92 @@ +{ + "materials": { + "copper": { + "name": "copper", + "conductivity": 58000000.0 + }, + "fr4_epoxy": { + "name": "fr4_epoxy", + "loss_tangent": 0.02, + "permittivity": 4.4 + }, + "solder_mask": { + "name": "solder_mask", + "loss_tangent": 0.035, + "permittivity": 3.1 + } + }, + "layers": { + "TOP": { + "name": "TOP", + "type": "signal", + "material": "copper", + "dielectric_fill": "copper", + "thickness": 5.000000000000004e-05 + }, + "D1": { + "name": "D1", + "type": "dielectric", + "material": "fr4_epoxy", + "thickness": 0.0001 + }, + "L2": { + "name": "L2", + "type": "signal", + "material": "copper", + "dielectric_fill": "copper", + "thickness": 3.5000000000000004e-05 + }, + "D2": { + "name": "D2", + "type": "dielectric", + "material": "fr4_epoxy", + "thickness": 0.0001 + }, + "L3": { + "name": "L3", + "type": "signal", + "material": "copper", + "dielectric_fill": "copper", + "thickness": 3.5000000000000004e-05 + }, + "D3": { + "name": "D3", + "type": "dielectric", + "material": "fr4_epoxy", + "thickness": 0.0001 + }, + "L4": { + "name": "L4", + "type": "signal", + "material": "copper", + "dielectric_fill": "copper", + "thickness": 3.5000000000000004e-05 + }, + "D4": { + "name": "D4", + "type": "dielectric", + "material": "fr4_epoxy", + "thickness": 0.0001 + }, + "L5": { + "name": "L5", + "type": "signal", + "material": "copper", + "dielectric_fill": "copper", + "thickness": 3.5000000000000004e-05 + }, + "D5": { + "name": "D5", + "type": "dielectric", + "material": "fr4_epoxy", + "thickness": 0.0001 + }, + "BOT": { + "name": "BOT", + "type": "signal", + "material": "copper", + "dielectric_fill": "copper", + "thickness": 5.000000000000004e-05 + } + } +} \ No newline at end of file diff --git a/_unittest/test_00_EDB.py b/_unittest/test_00_EDB.py index f7c687af0b6..66fd114c5ad 100644 --- a/_unittest/test_00_EDB.py +++ b/_unittest/test_00_EDB.py @@ -1,3 +1,4 @@ +import json import os # Setup paths for module imports @@ -117,6 +118,7 @@ def test_003_create_coax_port_on_component(self): assert self.edbapp.components["U6"].pins["R3"].id assert self.edbapp.terminals assert self.edbapp.ports + assert self.edbapp.components["U6"].pins["R3"].get_connected_objects() def test_004_get_properties(self): assert len(self.edbapp.components.components) > 0 @@ -884,6 +886,11 @@ def test_069_create_path(self): assert trace assert isinstance(trace.get_center_line(), list) assert isinstance(trace.get_center_line(True), list) + self.edbapp["delta_x"] = "1mm" + assert trace.add_point("delta_x", "1mm", True) + assert trace.get_center_line(True)[-1][0] == "(delta_x)+(0.025)" + assert trace.add_point(0.001, 0.002) + assert trace.get_center_line()[-1] == [0.001, 0.002] def test_070_create_outline(self): edbapp = Edb( @@ -1840,6 +1847,18 @@ def test_125c_layer(self): assert layer.material == "copper" edbapp.close() + def test_125d_stackup(self): + fpath = os.path.join(local_path, "example_models", test_subfolder, "stackup.json") + stackup_json = json.load(open(fpath, "r")) + + edbapp = Edb(edbversion=desktop_version) + edbapp.stackup.load(fpath) + edbapp.close() + + edbapp = Edb(edbversion=desktop_version) + edbapp.stackup.load(stackup_json) + edbapp.close() + def test_126_comp_def(self): source_path = os.path.join(local_path, "example_models", test_subfolder, "ANSYS-HSD_V1.aedb") target_path = os.path.join(self.local_scratch.path, "test_0126.aedb") @@ -2461,6 +2480,11 @@ def test_130_create_padstack_instance(self): assert pad_instance3.dcir_equipotential_region pad_instance3.dcir_equipotential_region = False assert not pad_instance3.dcir_equipotential_region + + trace = edb.modeler.create_trace([[0, 0], [0, 10e-3]], "1_Top", "0.1mm", "trace_with_via_fence") + edb.padstacks.create_padstack("via_0") + trace.create_via_fence("1mm", "1mm", "via_0") + edb.close() def test_131_assign_hfss_extent_non_multiple_with_simconfig(self): @@ -2841,6 +2865,7 @@ def test_145_arc_data(self): assert self.edbapp.nets["1.2V_DVDDL"].primitives[0].arcs[0].height def test_145_via_volume(self): + # vias = [ via for via in list(self.edbapp.padstacks.padstack_instances.values()) @@ -2879,10 +2904,29 @@ def test_147_find_dc_shorts(self): target_path = os.path.join(self.local_scratch.path, "test_dc_shorts", "ANSYS-HSD_V1_dc_shorts.aedb") self.local_scratch.copyfolder(source_path, target_path) edbapp = Edb(target_path, edbversion=desktop_version) - dc_shorts = edbapp.nets.find_dc_shorts() + dc_shorts = edbapp.layout_validation.dc_shorts() assert dc_shorts + edbapp.nets.nets["DDR4_A0"].name = "DDR4$A0" + edbapp.layout_validation.illegal_net_names(True) + edbapp.layout_validation.illegal_rlc_values(True) + # assert len(dc_shorts) == 20 assert ["LVDS_CH09_N", "GND"] in dc_shorts assert ["LVDS_CH09_N", "DDR4_DM3"] in dc_shorts assert ["DDR4_DM3", "LVDS_CH07_N"] in dc_shorts + assert len(edbapp.nets["DDR4_DM3"].find_dc_short()) > 0 + edbapp.nets["DDR4_DM3"].find_dc_short(True) + assert len(edbapp.nets["DDR4_DM3"].find_dc_short()) == 0 edbapp.close() + + def test_148_load_amat(self): + assert "Rogers RO3003 (tm)" in self.edbapp.materials.materials_in_aedt + material_file = os.path.join(self.edbapp.materials.syslib, "Materials.amat") + assert self.edbapp.materials.add_material_from_aedt("Arnold_Magnetics_N28AH_-40C") + assert "Arnold_Magnetics_N28AH_-40C" in self.edbapp.materials.materials.keys() + assert self.edbapp.materials.load_amat(material_file) + material_list = list(self.edbapp.materials.materials.keys()) + assert material_list + assert len(material_list) > 0 + assert self.edbapp.materials.materials["Rogers RO3003 (tm)"].loss_tangent == 0.0013 + assert self.edbapp.materials.materials["Rogers RO3003 (tm)"].permittivity == 3.0 diff --git a/_unittest/test_01_downloads.py b/_unittest/test_01_downloads.py index 21fb893f42a..f8d1b71a447 100644 --- a/_unittest/test_01_downloads.py +++ b/_unittest/test_01_downloads.py @@ -88,6 +88,7 @@ def test_13_download_specific_folder(self): def test_14_download_icepak_3d_component(self): assert self.examples.download_icepak_3d_component() + @pytest.mark.skipif(is_linux, reason="Failing download files") def test_15_download_fss_file(self): example_folder = self.examples.download_FSS_3dcomponent() assert os.path.exists(example_folder) diff --git a/_unittest/test_03_Materials.py b/_unittest/test_03_Materials.py index 2adcae5d68a..7fc62a651e3 100644 --- a/_unittest/test_03_Materials.py +++ b/_unittest/test_03_Materials.py @@ -69,8 +69,17 @@ def test_02_create_material(self): assert self.aedtapp.change_validation_settings() assert self.aedtapp.change_validation_settings(ignore_unclassified=True, skip_intersections=True) - assert mat1.set_magnetic_coercitivity(1, 2, 3, 4) - assert mat1.get_magnetic_coercitivity() == ("1A_per_meter", "2", "3", "4") + assert mat1.set_magnetic_coercivity(1, 2, 3, 4) + assert mat1.get_magnetic_coercivity() == ("1A_per_meter", "2", "3", "4") + mat1.coordinate_system = "Cylindrical" + assert mat1.coordinate_system == "Cylindrical" + mat1.magnetic_coercivity = [2, 1, 0, 1] + + assert mat1.get_magnetic_coercivity() == ("2A_per_meter", "1", "0", "1") + mat1.magnetic_coercivity.value = ["1", "2", "3", "4"] + assert mat1.get_magnetic_coercivity() == ("1A_per_meter", "2", "3", "4") + assert mat1.magnetic_coercivity.evaluated_value == [1.0, 2.0, 3.0, 4.0] + assert mat1.set_electrical_steel_coreloss(1, 2, 3, 4, 0.002) assert mat1.get_curve_coreloss_type() == "Electrical Steel" assert mat1.get_curve_coreloss_values()["core_loss_equiv_cut_depth"] == "0.002meter" diff --git a/_unittest/test_08_Primitives3D.py b/_unittest/test_08_Primitives3D.py index d7302f2e8f5..d23a7fe61ec 100644 --- a/_unittest/test_08_Primitives3D.py +++ b/_unittest/test_08_Primitives3D.py @@ -170,17 +170,10 @@ def test_02_create_box(self): assert o.material_name == "copper" assert "MyCreatedBox_11" in self.aedtapp.modeler.solid_names assert len(self.aedtapp.modeler.object_names) == len(self.aedtapp.modeler.objects) + assert not self.aedtapp.modeler.create_box([0, 0], [10, 10, 10], "MyCreatedBox_12", "Copper") + assert not self.aedtapp.modeler.create_box([0, 0, 0], [10, 10], "MyCreatedBox_12", "Copper") - def test_03_create_box_assertions(self): - try: - invalid_entry = "Frank" - self.aedtapp.modeler.create_box([0, 0, 0], invalid_entry, "MyCreatedBox", "Copper") - except ValueError: - pass - else: - assert False - - def test_04_create_polyhedron(self): + def test_03_create_polyhedron(self): o1 = self.aedtapp.modeler.create_polyhedron() assert o1.id > 0 assert o1.name.startswith("New") @@ -207,6 +200,37 @@ def test_04_create_polyhedron(self): assert o1.name in self.aedtapp.modeler.solid_names assert o2.name in self.aedtapp.modeler.solid_names assert len(self.aedtapp.modeler.object_names) == len(self.aedtapp.modeler.objects) + + assert not self.aedtapp.modeler.create_polyhedron( + cs_axis=AXIS.Z, + center_position=[0, 0], + start_position=[0, 1, 0], + height=2.0, + num_sides=5, + name="MyPolyhedron", + matname="Aluminum", + ) + + assert not self.aedtapp.modeler.create_polyhedron( + cs_axis=AXIS.Z, + center_position=[0, 0, 0], + start_position=[0, 1], + height=2.0, + num_sides=5, + name="MyPolyhedron", + matname="Aluminum", + ) + + assert not self.aedtapp.modeler.create_polyhedron( + cs_axis=AXIS.Z, + center_position=[0, 0, 0], + start_position=[0, 0, 0], + height=2.0, + num_sides=5, + name="MyPolyhedron", + matname="Aluminum", + ) + pass def test_05_center_and_centroid(self): @@ -276,6 +300,8 @@ def test_14_create_sphere(self): assert o.name.startswith("MySphere") assert o.object_type == "Solid" assert o.is3d is True + assert not self.aedtapp.modeler.create_sphere([10, 10], radius, "MySphere", "Copper") + assert not self.aedtapp.modeler.create_sphere(udp, -5, "MySphere", "Copper") def test_15_create_cylinder(self): udp = self.aedtapp.modeler.Position(20, 20, 0) @@ -287,6 +313,8 @@ def test_15_create_cylinder(self): assert o.name.startswith("MyCyl") assert o.object_type == "Solid" assert o.is3d is True + assert not self.aedtapp.modeler.create_cylinder(axis, [2, 2], radius, height, 8, "MyCyl", "Copper") + assert not self.aedtapp.modeler.create_cylinder(axis, udp, -0.1, height, 8, "MyCyl", "Copper") pass def test_16_create_ellipse(self): @@ -414,6 +442,7 @@ def test_23_create_rectangle(self): assert o.name.startswith("MyRectangle") assert o.object_type == "Sheet" assert o.is3d is False + assert not self.aedtapp.modeler.create_rectangle(plane, udp, [4, 5, 10], name="MyRectangle", matname="Copper") def test_24_create_cone(self): udp = self.aedtapp.modeler.Position(5, 3, 8) @@ -423,6 +452,11 @@ def test_24_create_cone(self): assert o.name.startswith("MyCone") assert o.object_type == "Solid" assert o.is3d is True + assert not self.aedtapp.modeler.create_cone(axis, [1, 1], 20, 10, 5, name="MyCone", matname="Copper") + assert not self.aedtapp.modeler.create_cone(axis, udp, 20, 20, 5, name="MyCone", matname="Copper") + assert not self.aedtapp.modeler.create_cone(axis, udp, -20, 20, 5, name="MyCone", matname="Copper") + assert not self.aedtapp.modeler.create_cone(axis, udp, 20, -20, 5, name="MyCone", matname="Copper") + assert not self.aedtapp.modeler.create_cone(axis, udp, 20, 20, -5, name="MyCone", matname="Copper") def test_25_get_object_id(self): udp = self.aedtapp.modeler.Position(5, 3, 8) @@ -647,6 +681,8 @@ def test_43_fillet_and_undo(self): assert o.edges[0].fillet() self.aedtapp._odesign.Undo() assert o.edges[0].fillet() + r = self.create_rectangle(name="MyRect") + assert not r.edges[0].fillet() def test_44_create_polyline_basic_segments(self): prim3D = self.aedtapp.modeler @@ -1071,6 +1107,12 @@ def test_55_create_bond_wires(self): name="low", ) assert b6 + assert not self.aedtapp.modeler.create_bondwire( + [0, 0], [10, 10, 2], h1=0.15, h2=0, diameter=0.034, facets=8, matname="copper", name="jedec51" + ) + assert not self.aedtapp.modeler.create_bondwire( + [0, 0, 0], [10, 10], h1=0.15, h2=0, diameter=0.034, facets=8, matname="copper", name="jedec51" + ) def test_56_create_group(self): assert self.aedtapp.modeler.create_group(["jedec51", "jedec41"], "mygroup") @@ -1269,11 +1311,12 @@ def test_69_create_torus(self): assert torus.is3d is True def test_70_create_torus_exceptions(self): - with pytest.raises(ValueError) as excinfo: - self.aedtapp.modeler.create_torus( - [30, 30], major_radius=-0.3, minor_radius=0.5, axis="Z", name="torus", material_name="Copper" - ) - assert "Center argument must be a valid 3 element sequence." in str(excinfo.value) + assert self.aedtapp.modeler.create_torus( + [30, 30, 0], major_radius=1.3, minor_radius=0.5, axis="Z", name="torus", material_name="Copper" + ) + assert not self.aedtapp.modeler.create_torus( + [30, 30], major_radius=1.3, minor_radius=0.5, axis="Z", name="torus", material_name="Copper" + ) def test_71_create_point(self): name = "mypoint" @@ -1517,20 +1560,22 @@ def test_77_create_helix(self): right_hand=False, ) - # Test that an exception is raised if the name of the polyline is not provided. - # We can't use with.pytest.raises pattern below because IronPython does not support pytest. - try: - self.aedtapp.modeler.create_helix( - polyline_name="", - position=[0, 0, 0], - x_start_dir=1.0, - y_start_dir=1.0, - z_start_dir=1.0, - ) - except ValueError as exc_info: - assert "The name of the polyline cannot be an empty string." in str(exc_info.args[0]) - else: - assert False + assert not self.aedtapp.modeler.create_helix( + polyline_name="", + position=[0, 0, 0], + x_start_dir=1.0, + y_start_dir=1.0, + z_start_dir=1.0, + ) + + assert not self.aedtapp.modeler.create_helix( + polyline_name=polyline_left.name, + position=[0, 0], + x_start_dir=1.0, + y_start_dir=1.0, + z_start_dir=1.0, + right_hand=False, + ) def test_78_get_touching_objects(self): box1 = self.aedtapp.modeler.create_box([-20, -20, -20], [1, 1, 1], matname="copper") @@ -1746,3 +1791,43 @@ def test_85_insert_layoutcomponent(self): assert comp2.layout_component.display_mode == 1 comp2.layout_component.layers["Trace"] = [True, True, 90] assert comp2.layout_component.update_visibility() + + def test_87_set_mesh_fusion_settings(self): + self.aedtapp.insert_design("MeshFusionSettings") + box1 = self.aedtapp.modeler.create_box([0, 0, 0], [10, 20, 30]) + obj_3dcomp = self.aedtapp.modeler.replace_3dcomponent( + object_list=[box1.name], + ) + box2 = self.aedtapp.modeler.create_box([0, 0, 0], [100, 20, 30]) + obj2_3dcomp = self.aedtapp.modeler.replace_3dcomponent( + object_list=[box2.name], + ) + assert self.aedtapp.set_mesh_fusion_settings(component=obj2_3dcomp.name, volume_padding=None, priority=None) + + assert self.aedtapp.set_mesh_fusion_settings( + component=[obj_3dcomp.name, obj2_3dcomp.name, "Dummy"], volume_padding=None, priority=None + ) + + assert self.aedtapp.set_mesh_fusion_settings( + component=[obj_3dcomp.name, obj2_3dcomp.name], + volume_padding=[[0, 5, 0, 0, 0, 1], [0, 0, 0, 2, 0, 0]], + priority=None, + ) + assert not self.aedtapp.set_mesh_fusion_settings( + component=[obj_3dcomp.name, obj2_3dcomp.name], volume_padding=[[0, 0, 0, 2, 0, 0]], priority=None + ) + + assert self.aedtapp.set_mesh_fusion_settings( + component=[obj_3dcomp.name, obj2_3dcomp.name], volume_padding=None, priority=[obj2_3dcomp.name, "Dummy"] + ) + + assert self.aedtapp.set_mesh_fusion_settings( + component=[obj_3dcomp.name, obj2_3dcomp.name], + volume_padding=[[0, 5, 0, 0, 0, 1], [10, 0, 0, 2, 0, 0]], + priority=[obj_3dcomp.name], + ) + assert self.aedtapp.set_mesh_fusion_settings( + component=None, + volume_padding=None, + priority=None, + ) diff --git a/_unittest/test_12_1_PostProcessing.py b/_unittest/test_12_1_PostProcessing.py index 8815f2e26f2..120b5e46be3 100644 --- a/_unittest/test_12_1_PostProcessing.py +++ b/_unittest/test_12_1_PostProcessing.py @@ -199,7 +199,15 @@ def test_05_export_report_to_jpg(self): assert os.path.exists(os.path.join(self.local_scratch.path, "MyTestScattering.jpg")) def test_06_export_report_to_csv(self): - self.aedtapp.post.export_report_to_csv(self.local_scratch.path, "MyTestScattering") + self.aedtapp.post.export_report_to_csv( + self.local_scratch.path, + "MyTestScattering", + start="3GHz", + end="6GHz", + step="0.12GHz", + uniform=True, + use_trace_number_format=False, + ) assert os.path.exists(os.path.join(self.local_scratch.path, "MyTestScattering.csv")) def test_06_export_report_to_rdat(self): diff --git a/_unittest/test_14_AedtLogger.py b/_unittest/test_14_AedtLogger.py index 48c9c636afa..a9c8436f3aa 100644 --- a/_unittest/test_14_AedtLogger.py +++ b/_unittest/test_14_AedtLogger.py @@ -73,7 +73,7 @@ def test_02_output_file_with_app_filter(self): # file handler on every logger has been released properly. # Otherwise, we can't read the content of the log file. - # delete the global file handler but not the log hadler because + # delete the global file handler but not the log handler because # it is used to write some info messages when closing AEDT. logger.disable_log_on_file() diff --git a/_unittest/test_41_3dlayout_modeler.py b/_unittest/test_41_3dlayout_modeler.py index eb26e281f63..20947315811 100644 --- a/_unittest/test_41_3dlayout_modeler.py +++ b/_unittest/test_41_3dlayout_modeler.py @@ -1,4 +1,5 @@ import os +import tempfile import time from _unittest.conftest import config @@ -678,12 +679,14 @@ def test_42_post_processing_3d_layout(self, add_app): test = add_app( project_name="test_post_3d_layout_solved_23R2", application=Hfss3dLayout, subfolder=test_subfolder ) - assert test.post.create_fieldplot_layers_nets( + pl1 = test.post.create_fieldplot_layers_nets( [["TOP", "GND", "V3P3_S5"], ["PWR", "V3P3_S5"]], - "Mag_Volume_Force_Density", - intrinsics={"Time": "1ms"}, + "Mag_E", + intrinsics={"Freq": "1GHz"}, plot_name="Test_Layers", ) + assert pl1 + assert pl1.export_image_from_aedtplt(tempfile.gettempdir()) self.aedtapp.close_project(test.project_name) @pytest.mark.skipif(is_linux, reason="Bug on linux") diff --git a/doc/source/Getting_started/Installation.rst b/doc/source/Getting_started/Installation.rst index 6371f08d164..e0569085fb0 100644 --- a/doc/source/Getting_started/Installation.rst +++ b/doc/source/Getting_started/Installation.rst @@ -45,8 +45,8 @@ Starting from 2023R2, a Ribbon button is available in Automation Tab as in the e Build Toolkits with PyAEDT ~~~~~~~~~~~~~~~~~~~~~~~~~~ You can create and install external toolkits. -The template provides a framework to create your own toolkits using PyAEDT. -The template can be found at `Template `_. +The Antenna Wizard toolkit provides an example for modeling antennas using Ansys Electronics Desktop (AEDT). +The Antenna Wizard can be found at `Antenna Wizard `_. .. image:: ../Resources/template_ribbon.png :width: 800 @@ -97,67 +97,6 @@ For example, on Windows with Python 3.7, install PyAEDT and all its dependencies pip install --no-cache-dir --no-index --find-links=file:////PyAEDT-v-wheelhouse-Windows-3.7 pyaedt -Install from a batch file -~~~~~~~~~~~~~~~~~~~~~~~~~ -If you are running on Windows, you can download -:download:`PyAEDT Environment with IDE bat file <../Resources/pyaedt_with_IDE.bat>` -and run this batch file on your local machine. Using this approach -provides you with a complete integrated development environment (IDE) -for writing PyAEDT scripts in Windows with a simple batch file. - -This batch file executes these steps: - -1. Creates a Python virtual environment in your ``%APPDATA%`` folder. To accomplish - this, it uses CPython in the selected version of AEDT available on your machine. -2. Installs PyAEDT. -3. Optionally installs `Spyder `_ with -s flag. -4. Installs `Jupyter Lab `_. -5. Creates a symbolic link from your PyAEDT installation to AEDT ``PersonalLib`` so - that scripts can also be run within AEDT. -6. Updates PyAEDT. -7. Install PyAEDT toolkit in AEDT to enable PyAEDT Console and PyAEDT Run Script. -8. Runs the tool that you choose (Spyder, Jupyter Lab, or a simple console). - -.. image:: ../Resources/toolkits.png - :width: 800 - :alt: PyAEDT toolkit installed after batch run - -Steps 1 through 5 are executed only the first time that you run the batch file or when ``-f`` is used: - -.. code:: - - pyaedt_with_IDE.bat --force-install - - pyaedt_with_IDE.bat -f - -Step 6 is executed only when running the command with the ``-update`` option: - -.. code:: - - pyaedt_with_IDE.bat --update - - pyaedt_with_IDE.bat -u - -Optionally, you can decide to pass a Python path. This path is then used to create a virtual environment: - -.. code:: - - pyaedt_with_IDE.bat -f -p - - -In addition, it is possible to install the PyAEDT package and all its dependencies provided in the wheelhouse by -executing the batch file mentioned earlier. You must use the Wheelhouse 3.7 package if no Python path is provided. -Otherwise, you must download and use the correct wheelhouse: - -.. code:: - - pyaedt_with_IDE.bat-w PyAEDT-v-wheelhouse-Windows-3.7 - - pyaedt_with_IDE.bat -p -w PyAEDT-v-wheelhouse-Windows-3.8 - pyaedt_with_IDE.bat -p -w PyAEDT-v-wheelhouse-Windows-3.7 - pyaedt_with_IDE.bat -p -w PyAEDT-v-wheelhouse-Windows-3.9 - - Use IronPython in AEDT ~~~~~~~~~~~~~~~~~~~~~~ PyAEDT is designed to work in CPython 3.7+ and supports many advanced processing packages like diff --git a/doc/source/Getting_started/Quickcode.rst b/doc/source/Getting_started/Quickcode.rst index 529eb42963d..7c484435191 100644 --- a/doc/source/Getting_started/Quickcode.rst +++ b/doc/source/Getting_started/Quickcode.rst @@ -31,7 +31,7 @@ page on the Ansys Developer portal, you can post questions, share ideas, and get To reach the project support team, email `pyansys.core@ansys.com `_. -Example Workflow +Example workflow ---------------- Here’s a brief example of how PyAEDT works: diff --git a/doc/source/Resources/pyaedt_with_IDE.bat b/doc/source/Resources/pyaedt_with_IDE.bat deleted file mode 100644 index 15841b86f4f..00000000000 --- a/doc/source/Resources/pyaedt_with_IDE.bat +++ /dev/null @@ -1,152 +0,0 @@ -@echo off -set current_dir=%~dp0 -setlocal enabledelayedexpansion -set argCount=0 -for %%x in (%*) do ( - set /a argCount+=1 - set "argVec[!argCount!]=%%~x" -) - -set args=%1 %2 %3 %4 %5 %6 -set update_pyaedt=n -set install_pyaedt=n -set install_spyder=n - -for /L %%i in (1,1,%argCount%) do ( - if [!argVec[%%i]!]==[-f] set install_pyaedt=y - if [!argVec[%%i]!]==[--force-install] set install_pyaedt=y - if [!argVec[%%i]!]==[-u] set update_pyaedt=y - if [!argVec[%%i]!]==[--update] set update_pyaedt=y - if [!argVec[%%i]!]==[-p] ( - set /A usepython=%%i+1 - ) - if [!argVec[%%i]!]==[-w] ( - set /A usewheel=%%i+1 - ) - if [!argVec[%%i]!]==[-s] set install_spyder=y - -) -if NOT [%usepython%]==[] ( - set pythonpyaedt="!argVec[%usepython%]!" - echo Python Path has been specified. -) -if NOT [%usewheel%]==[] ( - set wheelpyaedt="!argVec[%usewheel%]!" - if [%usepython%]==[] ( - echo ---------------------------------------------------------------------- - echo WheelHouse has been specified. Make sure you are using version 3_7. - echo ---------------------------------------------------------------------- - - ) ELSE ( - echo ---------------------------------------------------------------------------------------------- - echo WheelHouse has been spefified. Make sure you are using the same version of Python interpreter. - echo ---------------------------------------------------------------------------------------------- - - ) -) - - -set env_vars=ANSYSEM_ROOT232 ANSYSEM_ROOT231 ANSYSEM_ROOT222 ANSYSEM_ROOT221 ANSYSEM_ROOT212 -set /A choice_index=1 -for %%c in (%env_vars%) do ( - set env_var_name=%%c - if defined !env_var_name! ( - set root_var[!choice_index!]=!env_var_name! - set version=!env_var_name:ANSYSEM_ROOT=! - set versions[!choice_index!]=!version! - set version_pretty=20!version:~0,2! R!version:~2,1! - echo [!choice_index!] !version_pretty! - set /A choice_index=!choice_index!+1 - ) -) -REM If choice_index wasn't incremented then it means none of the variables are installed -if [%choice_index%]==1 ( - echo AEDT 2021 R2 or later must be installed. - pause - EXIT /B -) - -set /p chosen_index="Select Version to Install PyAEDT for (number in bracket): " -if [%chosen_index%] == [] set chosen_index=1 - -set chosen_root=!root_var[%chosen_index%]! -set version=!versions[%chosen_index%]! -echo Selected %version% at !%chosen_root%!. - -set aedt_path=potato -if [%specified_python%]==[y] ( - aedt_path=!argVec[%python_path_index%]! -) else ( - set aedt_path=!%chosen_root%!\commonfiles\CPython\3_7\winx64\Release\python - echo Built-in Python is !aedt_path!. -) -set aedt_path=!aedt_path:"=! - -echo %aedt_path% - - - -set pyaedt_install_dir=%APPDATA%\pyaedt_env_ide\v%version% -echo %pyaedt_install_dir% -if NOT exist "%pyaedt_install_dir%" ( - set install_pyaedt=y -) -setlocal enableDelayedExpansion - -if [%install_pyaedt%]==[y] ( - if exist "%pyaedt_install_dir%" ( - echo Removing existing PyAEDT environment. - @RD /S /Q "%pyaedt_install_dir%" - ) - echo Installing PyAEDT environment in "%pyaedt_install_dir%". - - cd "%APPDATA%" - - if [%pythonpyaedt%] == [] ( - "%aedt_path%\python.exe" -m venv "%pyaedt_install_dir%" --system-site-packages - ) ELSE ( - "%pythonpyaedt%\python.exe" -m venv "%pyaedt_install_dir%" - ) - call "%pyaedt_install_dir%\Scripts\activate.bat" - if NOT [%wheelpyaedt%]==[] ( - echo Installing PyAEDT from local wheels %arg1%. - pip install --no-cache-dir --no-index --find-links=%wheelpyaedt% pyaedt - ) ELSE ( - IF EXIST %current_dir%.git ( - echo Installing PyAEDT from local clone "%current_dir%". - ) ELSE ( - echo Installing PyAEDT from pip. - ) - - python -m pip install --upgrade pip - pip --default-timeout=1000 install wheel - - IF EXIST %current_dir%.git ( - pushd %current_dir% - pip install . - popd - ) ELSE ( - pip --default-timeout=1000 install pyaedt - ) - - pip --default-timeout=1000 install jupyterlab -I - if [%install_spyder%]==[y] pip --default-timeout=1000 install spyder - pip --default-timeout=1000 install ipython -U - pip --default-timeout=1000 install ipyvtklink - ) - if [%pythonpyaedt%]==[] ( - if %version% geq 231 pip uninstall -y pywin32 - ) - - call python "%pyaedt_install_dir%\Lib\site-packages\pyaedt\misc\aedtlib_personalib_install.py" --version=%version% -) -if [%update_pyaedt%]==[y] ( - echo Updating PyAEDT. - "%pyaedt_install_dir%\Scripts\pip" install pythonnet -U - "%pyaedt_install_dir%\Scripts\pip" install pyaedt --no-deps -U - call "%pyaedt_install_dir%\Scripts\python" "%pyaedt_install_dir%\Lib\site-packages\pyaedt\misc\aedtlib_personalib_install.py" --version=%version% - -) - - -cmd /k "%pyaedt_install_dir%\Scripts\activate.bat" diff --git a/doc/source/conf.py b/doc/source/conf.py index dfa5cf51105..f4e03a5fb1f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -158,7 +158,7 @@ def setup(app): "GL10", # reST directives {directives} must be followed by two colons # Return "RT04", # Return value description should start with a capital letter" - "RT05", # Return value description should finish with "."' + "RT05", # Return value description should finish with "." # Summary "SS01", # No summary found "SS02", # Summary does not start with a capital letter @@ -166,7 +166,7 @@ def setup(app): "SS04", # Summary contains heading whitespaces "SS05", # Summary must start with infinitive verb, not third person # Parameters - "PR10", # Parameter "{param_name}" requires a space before the colon ' + "PR10", # Parameter "{param_name}" requires a space before the colon # separating the parameter name and type", } diff --git a/doc/styles/Vocab/ANSYS/accept.txt b/doc/styles/Vocab/ANSYS/accept.txt index 9110f363fe4..16e7708252b 100644 --- a/doc/styles/Vocab/ANSYS/accept.txt +++ b/doc/styles/Vocab/ANSYS/accept.txt @@ -81,5 +81,6 @@ EDT pyansys Slurm Python.NET - +Toolkits +toolkits diff --git a/examples/02-HFSS/Waveguide_Filter.py b/examples/02-HFSS/Waveguide_Filter.py index 42ed095620e..edae3bcad21 100644 --- a/examples/02-HFSS/Waveguide_Filter.py +++ b/examples/02-HFSS/Waveguide_Filter.py @@ -199,8 +199,7 @@ def place_iris(zpos, dz, n): ############################################################################### # Generate S-Parameter Plots -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -################################################################################# +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ # The following commands fetch solution data from HFSS for plotting directly # from the Python interpreter. # Caution: The syntax for expressions must be identical to that used @@ -209,7 +208,27 @@ def place_iris(zpos, dz, n): traces_to_plot = hfss.get_traces_for_plot(second_element_filter="P1*") report = hfss.post.create_report(traces_to_plot) # Creates a report in HFSS solution = report.get_solution_data() + plt = solution.plot(solution.expressions) # Matplotlib axes object. +############################################################################### +# Generate E field plot +# ~~~~~~~~~~~~~~~~~~~~~ +# The following command generates a field plot in HFSS and uses PyVista +# to plot the field in Jupyter. + +plot=hfss.post.plot_field(quantity="Mag_E", + objects_list=["Global:XZ"], + plot_type="CutPlane", + setup_name=hfss.nominal_adaptive, + intrinsics={"Freq":"9.8GHz", "Phase":"0deg"}, + export_path=hfss.working_directory, + show=False) + +############################################################################### +# Save and close the desktop +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ +# The following command saves the project to a file and closes the desktop. + hfss.save_project() hfss.release_desktop() diff --git a/examples/03-Maxwell/Maxwell2D_NissanLeaf.py b/examples/03-Maxwell/Maxwell2D_NissanLeaf.py index 1dda2ff6558..60bccf51e97 100644 --- a/examples/03-Maxwell/Maxwell2D_NissanLeaf.py +++ b/examples/03-Maxwell/Maxwell2D_NissanLeaf.py @@ -183,7 +183,7 @@ mat_PM = M2D.materials.add_material("Arnold_Magnetics_N30UH_80C_new") mat_PM.update() mat_PM.conductivity = "555555.5556" -mat_PM.set_magnetic_coercitivity(value=-800146.66287534, x=1, y=0, z=0) +mat_PM.set_magnetic_coercivity(value=-800146.66287534, x=1, y=0, z=0) mat_PM.mass_density = "7500" BH_List_PM = [] with open(filename_PM) as f: diff --git a/examples/06-Multiphysics/MRI.py b/examples/06-Multiphysics/MRI.py index 01790e3641b..f943c1b749c 100644 --- a/examples/06-Multiphysics/MRI.py +++ b/examples/06-Multiphysics/MRI.py @@ -99,11 +99,20 @@ # Draw Point1 at origin of the implant coordinate system hfss.sar_setup(-1, Average_SAR_method=1, TissueMass=1, MaterialDensity=1, ) -hfss.post.create_fieldplot_cutplane("implant:YZ", "Average_SAR", filter_objects=["implant_box"]) +hfss.post.create_fieldplot_cutplane(objlist="implant:YZ", + quantityName="Average_SAR", + filter_objects=["implant_box"]) hfss.modeler.set_working_coordinate_system("implant") hfss.modeler.create_point([0, 0, 0], name="Point1") +hfss.post.plot_field(quantity="Average_SAR", + object_list="implant:YZ", + plot_type="CutPlane", + show_legend=False, + filter_objects=["implant_box"], + ) + ############################################################################### # Adjust Input Power to MRI Coil # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -113,7 +122,9 @@ # input_scale = 1/AverageSAR at Point1 -sol_data = hfss.post.get_solution_data("Average_SAR", primary_sweep_variable="Freq", context="Point1", +sol_data = hfss.post.get_solution_data(expressions="Average_SAR", + primary_sweep_variable="Freq", + context="Point1", report_category="Fields") sol_data.data_real() @@ -204,6 +215,15 @@ report_category="Fields") data.plot() +mech.post.plot_animated_field(quantity="Temperature", + object_list="implant:YZ", + plot_type="CutPlane", + intrinsics={"Time": "10s"}, + variation_variable="Time", + variation_list=["10s", "20s", "30s", "40s", "50s", "60s"], + filter_objects=["implant_box"], + ) + ############################################################################### # Thermal Simulation # ~~~~~~~~~~~~~~~~~~ @@ -277,7 +297,7 @@ # Plot Temperature on cut plane. # Plot Temperature on monitor point. -ipk.analyze(num_cores=6) +ipk.analyze(num_cores=4,num_tasks=4) ipk.post.create_fieldplot_cutplane("implant:YZ", "Temperature", filter_objects=["implant_box"], intrinsincDict={"Time": "0s"}) ipk.save_project() diff --git a/examples/07-Circuit/Reports.py b/examples/07-Circuit/Reports.py index 5c3a5d2fc6a..e09ee381453 100644 --- a/examples/07-Circuit/Reports.py +++ b/examples/07-Circuit/Reports.py @@ -9,9 +9,8 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~ # Perform required imports and set the local path to the path for PyAEDT. -# sphinx_gallery_thumbnail_path = 'Resources/spectrum_plot.png' import os - +from IPython.display import Image import pyaedt # Set local path to path for PyAEDT @@ -33,7 +32,7 @@ # The Boolean parameter ``new_thread`` defines whether to create a new instance # of AEDT or try to connect to an existing instance of it. -non_graphical = False +non_graphical = True NewThread = True ############################################################################### @@ -47,6 +46,7 @@ specified_version=desktopVersion, new_desktop_session=True ) +cir.analyze() ############################################################################### # Create spectrum report @@ -58,10 +58,17 @@ # in non-graphical mode in AEDT 2023 R2 and later. report1 = cir.post.create_report_from_configuration(os.path.join(project_path,'Spectrum_CISPR_Basic.json')) +out = cir.post.export_report_to_jpg(cir.working_directory, report1.plot_name) +Image(out) -if not non_graphical: - report1_full = cir.post.create_report_from_configuration(os.path.join(project_path,'Spectrum_CISPR_Custom.json')) +############################################################################### +# Create spectrum report +# ~~~~~~~~~~~~~~~~~~~~~~ +# Every aspect of the report can be customized. +report1_full = cir.post.create_report_from_configuration(os.path.join(project_path,'Spectrum_CISPR_Custom.json')) +out = cir.post.export_report_to_jpg(cir.working_directory, report1_full.plot_name) +Image(out) ############################################################################### # Create transient report # ~~~~~~~~~~~~~~~~~~~~~~~ @@ -70,31 +77,45 @@ # before generating the report. You can create custom reports in non-graphical # mode in AEDT 2023 R2 and later. -if non_graphical: - props = pyaedt.data_handler.json_to_dict(os.path.join(project_path, 'Transient_CISPR_Basic.json')) -else: - props = pyaedt.data_handler.json_to_dict(os.path.join(project_path, 'Transient_CISPR_Custom.json')) + +props = pyaedt.data_handler.json_to_dict(os.path.join(project_path, 'Transient_CISPR_Custom.json')) report2 = cir.post.create_report_from_configuration(input_dict=props, solution_name="NexximTransient") +out = cir.post.export_report_to_jpg(cir.working_directory, report2.plot_name) +Image(out) + +############################################################################### +# Create transient report +# ~~~~~~~~~~~~~~~~~~~~~~~ +# Property dictionary can be customized in any aspect and new report can be created easily. +# In this example the curve name is customized. + props["expressions"] = {"V(Battery)": {}, "V(U1_VDD)": {}} props["plot_name"] = "Battery Voltage" report3 = cir.post.create_report_from_configuration(input_dict=props, solution_name="NexximTransient") +out = cir.post.export_report_to_jpg(cir.working_directory, report3.plot_name) +Image(out) ############################################################################### # Create eye diagram # ~~~~~~~~~~~~~~~~~~ # Create an eye diagram. If the JSON file contains an eye mask, you can create -# an eye diagram and fully customize it. You can create custom reports in -# non-graphical mode in AEDT 2023 R2 and later. +# an eye diagram and fully customize it. report4 = cir.post.create_report_from_configuration(os.path.join(project_path, 'EyeDiagram_CISPR_Basic.json')) +out = cir.post.export_report_to_jpg(cir.working_directory, report4.plot_name) +Image(out) -if not non_graphical: - report4_full = cir.post.create_report_from_configuration(os.path.join(project_path, 'EyeDiagram_CISPR_Custom.json')) +############################################################################### +# Create eye diagram +# ~~~~~~~~~~~~~~~~~~ +# You can create custom reports in +# non-graphical mode in AEDT 2023 R2 and later. -if not non_graphical: - cir.post.export_report_to_jpg(cir.working_directory, report4.plot_name) +report4_full = cir.post.create_report_from_configuration(os.path.join(project_path, 'EyeDiagram_CISPR_Custom.json')) +out = cir.post.export_report_to_jpg(cir.working_directory, report4_full.plot_name) +Image(out) ################################################ # This is how the spectrum looks like # .. image:: Resources/spectrum_plot.png diff --git a/examples/07-EMIT/EMIT_HFSS_Example.py b/examples/07-EMIT/EMIT_HFSS_Example.py new file mode 100644 index 00000000000..840d5b96f56 --- /dev/null +++ b/examples/07-EMIT/EMIT_HFSS_Example.py @@ -0,0 +1,139 @@ +""" +EMIT: HFSS to EMIT coupling +--------------------------- +This example shows how you can use PyAEDT to open an AEDT project with +an HFSS design, create an EMIT design in the project, and link the HFSS design +as a coupling link in the EMIT design. +""" +############################################################################### +# Perform required imports +# ~~~~~~~~~~~~~~~~~~~~~~~~ +# Perform required imports. +# +# sphinx_gallery_thumbnail_path = "Resources/emit_hfss.png" + +import os + +# Import required modules +import pyaedt +from pyaedt.generic.filesystem import Scratch +from pyaedt.emit_core.emit_constants import TxRxMode, ResultType + +############################################################################### +## Set non-graphical mode +# ~~~~~~~~~~~~~~~~~~~~~~~ +# Set non-graphical mode. +# You can set ``non_graphical`` either to ``True`` or ``False``. +# The Boolean parameter ``new_thread`` defines whether to create a new instance +# of AEDT or try to connect to an existing instance of it. +# +# The following code uses AEDT 2023 R2. + +non_graphical = False +NewThread = True +desktop_version = "2023.2" +scratch_path = pyaedt.generate_unique_folder_name() + +############################################################################### +# Launch AEDT with EMIT +# ~~~~~~~~~~~~~~~~~~~~~ +# Launch AEDT with EMIT. The ``Desktop`` class initializes AEDT and starts it +# on the specified version and in the specified graphical mode. + +d = pyaedt.launch_desktop(desktop_version, non_graphical, NewThread) + +temp_folder = os.path.join(scratch_path, ("EmitHFSSExample")) +if not os.path.exists(temp_folder): + os.mkdir(temp_folder) + +example_name = "Cell Phone RFI Desense" +example_aedt = example_name + ".aedt" +example_results = example_name + ".aedtresults" +example_lock = example_aedt + ".lock" +example_pdf_file = example_name + " Example.pdf" + +example_dir = os.path.join(d.install_path, "Examples\\EMIT") +example_project = os.path.join(example_dir, example_aedt) +example_results_folder = os.path.join(example_dir, example_results) +example_pdf = os.path.join(example_dir, example_pdf_file) + +######################################################################################################## +# If the ``Cell Phone RFT Defense`` example is not +# in the installation directory, exit from this example. + +if not os.path.exists(example_project): + msg = """ + Cell phone RFT Desense example file is not in the + Examples/EMIT directory under the EDT installation. You cannot run this example. + """ + print(msg) + d.release_desktop(True, True) + exit() + +my_project = os.path.join(temp_folder, example_aedt) +my_results_folder = os.path.join(temp_folder, example_results) +my_project_lock = os.path.join(temp_folder, example_lock) +my_project_pdf = os.path.join(temp_folder, example_pdf_file) + +if os.path.exists(my_project): + os.remove(my_project) + +if os.path.exists(my_project_lock): + os.remove(my_project_lock) + +with Scratch(scratch_path) as local_scratch: + local_scratch.copyfile(example_project, my_project) + local_scratch.copyfolder(example_results_folder, my_results_folder) + if os.path.exists(example_pdf): + local_scratch.copyfile(example_pdf, my_project_pdf) + +aedtapp = pyaedt.Emit(my_project) + +############################################################################### +# Create and connect EMIT components +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Create two radios with antennas connected to each one. + +rad1, ant1 = aedtapp.modeler.components.create_radio_antenna("Bluetooth Low Energy (LE)") +rad2, ant2 = aedtapp.modeler.components.create_radio_antenna("Bluetooth Low Energy (LE)") + +############################################################################### +# Define coupling among RF systems +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Define coupling among the RF systems. + +for link in aedtapp.couplings.linkable_design_names: + aedtapp.couplings.add_link(link) + +for link in aedtapp.couplings.coupling_names: + aedtapp.couplings.update_link(link) + +############################################################################### +# Run EMIT simulation +# ~~~~~~~~~~~~~~~~~~~ +# Run the EMIT simulation. This portion of the EMIT API is not yet implemented. +# +# This part of the example requires Ansys AEDT 2023 R2. + +if desktop_version > "2023.1": + rev = aedtapp.results.analyze() + rx_bands = rev.get_band_names(rad1.name, TxRxMode.RX) + tx_bands = rev.get_band_names(rad2.name, TxRxMode.TX) + domain = aedtapp.results.interaction_domain() + domain.set_receiver(rad1.name, rx_bands[0], -1) + domain.set_interferer(rad2.name,tx_bands[0]) + interaction = rev.run(domain) + worst = interaction.get_worst_instance(ResultType.EMI) + if worst.has_valid_values(): + emi = worst.get_value(ResultType.EMI) + print("Worst case interference is: {} dB".format(emi)) + +############################################################################### +# Save project and close AEDT +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# After the simulation completes, you can close AEDT or release it using the +# :func:`pyaedt.Desktop.force_close_desktop` method. +# All methods provide for saving the project before closing. + +aedtapp.save_project() +aedtapp.release_desktop(close_projects=True, close_desktop=True) diff --git a/examples/07-EMIT/interference_gui.py b/examples/07-EMIT/interference_gui.py index 6dfce44cb1b..7a2c2a78904 100644 --- a/examples/07-EMIT/interference_gui.py +++ b/examples/07-EMIT/interference_gui.py @@ -57,7 +57,7 @@ def install(package): # Add emitapi to system path emit_path = os.path.join(desktop.install_path, "Delcross") -sys.path.append(emit_path) +sys.path.insert(0,emit_path) import EmitApiPython api = EmitApiPython.EmitApi() @@ -244,8 +244,20 @@ def open_file_dialog(self): # Close previous project and open specified one if self.emitapp is not None: self.emitapp.close_project() + self.emitapp = None desktop_proj = desktop.load_project(self.file_path_box.text()) + # check for an empty project (i.e. no designs) + if isinstance(desktop_proj, bool): + self.file_path_box.setText("") + msg = QtWidgets.QMessageBox() + msg.setWindowTitle("Error: Project missing designs.") + msg.setText( + "The selected project has no designs. Projects must have at least " + "one EMIT design. See AEDT log for more information.") + x = msg.exec() + return + # Check if project is already open if desktop_proj.lock_file == None: msg = QtWidgets.QMessageBox() @@ -317,6 +329,9 @@ def design_dropdown_changed(self): self.warning_label.setText("Warning: The selected design must contain at least two radios.") self.warning_label.setHidden(False) + # clear the table if the design is changed + self.clear_table() + ############################################################################### # Enable radio specific protection levels # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -556,7 +571,26 @@ def populate_table(self): button.setEnabled(True) img_btn.setEnabled(True) - + + def clear_table(self): + # get the table/buttons based on current tab + if self.tab_widget.currentIndex() == 0: + table = self.protection_matrix + button = self.protection_export_btn + img_btn = self.protection_save_img_btn + else: + table = self.interference_matrix + button = self.interference_export_btn + img_btn = self.interference_save_img_btn + + # disable export options + button.setEnabled(False) + img_btn.setEnabled(False) + + # clear the table + table.setColumnCount(0) + table.setRowCount(0) + ############################################################################### # GUI closing event # ~~~~~~~~~~~~~~~~~ @@ -564,10 +598,13 @@ def populate_table(self): def closeEvent(self, event): msg = QtWidgets.QMessageBox() msg.setWindowTitle("Closing GUI") - msg.setText("Closing AEDT, please wait for GUI to close on its own.") + msg.setText("Closing AEDT. Wait for the GUI to close on its own.") x = msg.exec() - self.emitapp.close_project() - self.emitapp.close_desktop() + if self.emitapp: + self.emitapp.close_project() + self.emitapp.close_desktop() + else: + desktop.release_desktop(True, True) ############################################################################### # Run GUI diff --git a/ignore_words.txt b/ignore_words.txt index 7e7db77bd34..c8fa792f47c 100644 --- a/ignore_words.txt +++ b/ignore_words.txt @@ -31,3 +31,4 @@ ro aline COM gRPC +Toolkits diff --git a/pyaedt/__init__.py b/pyaedt/__init__.py index 99d4411193d..99ceb167ade 100644 --- a/pyaedt/__init__.py +++ b/pyaedt/__init__.py @@ -6,7 +6,7 @@ pyaedt_path = os.path.dirname(__file__) -__version__ = "0.7.0" +__version__ = "0.7.1" version = __version__ import pyaedt.downloads as downloads diff --git a/pyaedt/aedt_logger.py b/pyaedt/aedt_logger.py index f1c635f56a3..a3a307eb1f8 100644 --- a/pyaedt/aedt_logger.py +++ b/pyaedt/aedt_logger.py @@ -320,7 +320,7 @@ def get_messages(self, project_name=None, design_name=None, level=0, aedt_messag """ project_name = project_name or self._project_name design_name = design_name or self._design_name - if self._log_on_desktop or aedt_messages: + if aedt_messages and self._desktop.GetVersion() > "2022.2": global_message_data = self._desktop.GetMessages("", "", level) # if a 3d component is open, GetMessages without the project name argument returns messages with # "(3D Component)" appended to project name diff --git a/pyaedt/application/AEDT_File_Management.py b/pyaedt/application/AEDT_File_Management.py index 2b20cd25013..6d641ba45a4 100644 --- a/pyaedt/application/AEDT_File_Management.py +++ b/pyaedt/application/AEDT_File_Management.py @@ -79,7 +79,7 @@ def create_output_folder(ProjectDir): # set pathname for the files ResultsPath = os.path.join(npath, os.path.basename(npath), "Results") - # Add foldes for outputs + # Add folders for outputs if not os.path.exists(OutputPath): os.mkdir(OutputPath) if not os.path.exists(PicturePath): diff --git a/pyaedt/application/Analysis.py b/pyaedt/application/Analysis.py index 12bf51149aa..23d37b3ebf0 100644 --- a/pyaedt/application/Analysis.py +++ b/pyaedt/application/Analysis.py @@ -1724,27 +1724,36 @@ def analyze_setup( skip_files = True if not skip_files: if num_cores: - skip_files = update_hpc_option(target_name, "NumCores", num_cores, False) + succeeded = update_hpc_option(target_name, "NumCores", num_cores, False) + skip_files = True if not succeeded else skip_files if num_gpu: - skip_files = update_hpc_option(target_name, "NumGPUs", num_gpu, False) + succeeded = update_hpc_option(target_name, "NumGPUs", num_gpu, False) + skip_files = True if not succeeded else skip_files if num_tasks: - skip_files = update_hpc_option(target_name, "NumEngines", num_tasks, False) - skip_files = update_hpc_option(target_name, "ConfigName", config_name, True) - skip_files = update_hpc_option(target_name, "DesignType", self.design_type, True) + succeeded = update_hpc_option(target_name, "NumEngines", num_tasks, False) + skip_files = True if not succeeded else skip_files + succeeded = update_hpc_option(target_name, "ConfigName", config_name, True) + skip_files = True if not succeeded else skip_files + succeeded = update_hpc_option(target_name, "DesignType", self.design_type, True) + skip_files = True if not succeeded else skip_files if self.design_type == "Icepak": use_auto_settings = False - skip_files = update_hpc_option(target_name, "UseAutoSettings", use_auto_settings, False) + succeeded = update_hpc_option(target_name, "UseAutoSettings", use_auto_settings, False) + skip_files = True if not succeeded else skip_files if num_variations_to_distribute: - skip_files = update_hpc_option( + succeeded = update_hpc_option( target_name, "NumVariationsToDistribute", num_variations_to_distribute, False ) + skip_files = True if not succeeded else skip_files if isinstance(allowed_distribution_types, list): num_adt = len(allowed_distribution_types) adt_string = "', '".join(allowed_distribution_types) adt_string = "[{}: '{}']".format(num_adt, adt_string) - skip_files = update_hpc_option( + + succeeded = update_hpc_option( target_name, "AllowedDistributionTypes", adt_string, False, separator="" ) + skip_files = True if not succeeded else skip_files if settings.remote_rpc_session: remote_name = ( diff --git a/pyaedt/application/Design.py b/pyaedt/application/Design.py index 7f57ec975c2..be6978c1e23 100644 --- a/pyaedt/application/Design.py +++ b/pyaedt/application/Design.py @@ -364,7 +364,7 @@ def boundaries_by_type(self): @property def excitations_by_type(self): - """Design excitations by tupe. + """Design excitations by type. Returns ------- diff --git a/pyaedt/application/JobManager.py b/pyaedt/application/JobManager.py index 76ca8823bed..1277424e1f5 100644 --- a/pyaedt/application/JobManager.py +++ b/pyaedt/application/JobManager.py @@ -185,7 +185,7 @@ def update_cluster_cores(file_name, param_name, param_val): def update_hpc_template(file_name, param_name, param_val): - """Update a paramerter in the HPC template file. + """Update a parameter in the HPC template file. Parameters ---------- diff --git a/pyaedt/application/Variables.py b/pyaedt/application/Variables.py index db9694a3947..06d91b48ab2 100644 --- a/pyaedt/application/Variables.py +++ b/pyaedt/application/Variables.py @@ -263,7 +263,7 @@ def decompose_variable_value(variable_value, full_variables={}): Returns ------- tuples - tuples made of the float value of the variable and the units exposed as a string. + Tuples made of the float value of the variable and the units exposed as a string. """ # set default return values - then check for valid units float_value = variable_value @@ -323,7 +323,7 @@ def generate_validation_errors(property_names, expected_settings, actual_setting List of property names. expected_settings : List[str] List of the expected settings. - actual_settings: List[str] + actual_settings : List[str] List of actual settings. Returns @@ -1761,7 +1761,7 @@ def __mul__(self, other): """Multiply the variable with a number or another variable and return a new object. Parameters - --------- + ---------- other : numbers.Number or variable Object to be multiplied. @@ -1820,7 +1820,7 @@ def __add__(self, other): """Add the variable to another variable to return a new object. Parameters - --------- + ---------- other : class:`pyaedt.application.Variables.Variable` Object to be multiplied. @@ -1861,7 +1861,7 @@ def __sub__(self, other): """Subtract another variable from the variable to return a new object. Parameters - --------- + ---------- other : class:`pyaedt.application.Variables.Variable` Object to be subtracted. @@ -1904,7 +1904,7 @@ def __truediv__(self, other): """Divide the variable by a number or another variable to return a new object. Parameters - --------- + ---------- other : numbers.Number or variable Object by which to divide. @@ -1948,7 +1948,7 @@ def __rtruediv__(self, other): """Divide another object by this object. Parameters - --------- + ---------- other : numbers.Number or variable Object to divide by. diff --git a/pyaedt/application/aedt_objects.py b/pyaedt/application/aedt_objects.py index c88ef3508aa..eb76dd488c4 100644 --- a/pyaedt/application/aedt_objects.py +++ b/pyaedt/application/aedt_objects.py @@ -65,7 +65,7 @@ def get_module(self, module_name): @property def o_symbol_manager(self): - """Aedt Simbol Manager. + """Aedt Symbol Manager. References ---------- diff --git a/pyaedt/circuit.py b/pyaedt/circuit.py index 4a99c6552c9..f641716cba9 100644 --- a/pyaedt/circuit.py +++ b/pyaedt/circuit.py @@ -1587,7 +1587,7 @@ def connect_circuit_models_from_multi_zone_cutout( Returns ------- bool - ``True`` when succeessful, ``False`` when failed. + ``True`` when successful, ``False`` when failed. Examples -------- diff --git a/pyaedt/desktop.py b/pyaedt/desktop.py index b33bd4f293a..93b9bd14c82 100644 --- a/pyaedt/desktop.py +++ b/pyaedt/desktop.py @@ -66,6 +66,8 @@ def launch_desktop_on_port(): command = [full_path, "-grpcsrv", str(port)] if non_graphical: command.append("-ng") + if settings.wait_for_license: + command.append("-waitforlicense") my_env = os.environ.copy() for env, val in settings.aedt_environment_variables.items(): my_env[env] = val @@ -131,6 +133,8 @@ def launch_aedt_in_lsf(non_graphical, port): # pragma: no cover ] if non_graphical: command.append("-ng") + if settings.wait_for_license: + command.append("-waitforlicense") print(command) p = subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) timeout = settings.lsf_timeout diff --git a/pyaedt/edb.py b/pyaedt/edb.py index ec2cc3ddd01..ee9a5b13c4b 100644 --- a/pyaedt/edb.py +++ b/pyaedt/edb.py @@ -37,11 +37,14 @@ from pyaedt.edb_core.edb_data.terminals import PadstackInstanceTerminal from pyaedt.edb_core.edb_data.terminals import Terminal from pyaedt.edb_core.edb_data.variables import Variable +from pyaedt.edb_core.general import LayoutObjType +from pyaedt.edb_core.general import Primitives from pyaedt.edb_core.general import TerminalType from pyaedt.edb_core.general import convert_py_list_to_net_list from pyaedt.edb_core.hfss import EdbHfss from pyaedt.edb_core.ipc2581.ipc2581 import Ipc2581 from pyaedt.edb_core.layout import EdbLayout +from pyaedt.edb_core.layout_validation import LayoutValidation from pyaedt.edb_core.materials import Materials from pyaedt.edb_core.net_class import EdbDifferentialPairs from pyaedt.edb_core.net_class import EdbExtendedNets @@ -328,6 +331,11 @@ def project_variables(self): p_var[i] = Variable(self, i) return p_var + @property + def layout_validation(self): + """:class:`pyaedt.edb_core.edb_data.layout_validation.LayoutValidation`.""" + return LayoutValidation(self) + @property def variables(self): """Get all Edb variables. @@ -1032,9 +1040,53 @@ def layout_instance(self): """Edb Layout Instance.""" return self.layout.layout_instance + @pyaedt_function_handler + def get_connected_objects(self, layout_object_instance): + """Get connected objects. + + Returns + ------- + list + """ + temp = [] + for i in list( + [ + loi.GetLayoutObj() + for loi in self.layout_instance.GetConnectedObjects(layout_object_instance._edb_object).Items + ] + ): + obj_type = i.GetObjType().ToString() + if obj_type == LayoutObjType.PadstackInstance.name: + from pyaedt.edb_core.edb_data.padstacks_data import EDBPadstackInstance + + temp.append(EDBPadstackInstance(i, self)) + elif obj_type == LayoutObjType.Primitive.name: + prim_type = i.GetPrimitiveType().ToString() + if prim_type == Primitives.Path.name: + from pyaedt.edb_core.edb_data.primitives_data import EdbPath + + temp.append(EdbPath(i, self)) + elif prim_type == Primitives.Rectangle.name: + from pyaedt.edb_core.edb_data.primitives_data import EdbRectangle + + temp.append(EdbRectangle(i, self)) + elif prim_type == Primitives.Circle.name: + from pyaedt.edb_core.edb_data.primitives_data import EdbCircle + + temp.append(EdbCircle(i, self)) + elif prim_type == Primitives.Polygon.name: + from pyaedt.edb_core.edb_data.primitives_data import EdbPolygon + + temp.append(EdbPolygon(i, self)) + else: + continue + else: + continue + return temp + @property def pins(self): - """EDBPadstackInstance of Component. + """EDB padstack instance of the component. .. deprecated:: 0.6.62 Use new method :func:`edb.padstacks.pins` instead. diff --git a/pyaedt/edb_core/components.py b/pyaedt/edb_core/components.py index 20afa025359..d5d21b2f7ef 100644 --- a/pyaedt/edb_core/components.py +++ b/pyaedt/edb_core/components.py @@ -1240,7 +1240,7 @@ def _create_pin_group_terminal(self, pingroup, isref=False): @pyaedt_function_handler() def _is_top_component(self, cmp): - """Test the component placment layer. + """Test the component placement layer. Parameters ---------- diff --git a/pyaedt/edb_core/dotnet/database.py b/pyaedt/edb_core/dotnet/database.py index f0527335342..35de57a6bc5 100644 --- a/pyaedt/edb_core/dotnet/database.py +++ b/pyaedt/edb_core/dotnet/database.py @@ -80,6 +80,43 @@ def arcs(self): # pragma: no cover """List of Edb.Geometry.ArcData.""" return list(self.edb_api.GetArcData()) + def get_points(self): + """Get all points in polygon. + + Returns + ------- + list[list[edb_value]] + """ + + return [[self._pedb.edb_value(i.X), self._pedb.edb_value(i.Y)] for i in list(self.edb_api.Points)] + + def add_point(self, x, y, incremental=False): + """Add a point at the end of the point list of the polygon. + + Parameters + ---------- + x: str, int, float + X coordinate. + y: str, in, float + Y coordinate. + incremental: bool + Whether to add the point incrementally. The default value is ``False``. When + ``True``, the coordinates of the added point are incremental to the last point. + + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if incremental: + x = self._pedb.edb_value(x) + y = self._pedb.edb_value(y) + last_point = self.get_points()[-1] + x = "({})+({})".format(x, last_point[0].ToString()) + y = "({})+({})".format(y, last_point[1].ToString()) + return self.edb_api.AddPoint(GeometryDotNet(self._pedb).point_data(x, y)) + def get_bbox_of_boxes(self, points): """Get the EDB .NET API ``Edb.Geometry.GetBBoxOfBoxes`` database. diff --git a/pyaedt/edb_core/edb_data/connectable.py b/pyaedt/edb_core/edb_data/connectable.py index 3eda3bd5dbb..bc55363f7d7 100644 --- a/pyaedt/edb_core/edb_data/connectable.py +++ b/pyaedt/edb_core/edb_data/connectable.py @@ -1,7 +1,16 @@ from pyaedt import pyaedt_function_handler +from pyaedt.edb_core.edb_data.obj_base import ObjBase -class LayoutObj(object): +class LayoutObjInstance: + """Manages EDB functionalities for the layout object instance.""" + + def __init__(self, pedb, edb_object): + self._pedb = pedb + self._edb_object = edb_object + + +class LayoutObj(ObjBase): """Manages EDB functionalities for the layout object.""" def __getattr__(self, key): # pragma: no cover @@ -14,8 +23,7 @@ def __getattr__(self, key): # pragma: no cover raise AttributeError("Attribute not present") def __init__(self, pedb, edb_object): - self._pedb = pedb - self._edb_object = edb_object + super().__init__(pedb, edb_object) @property def _edb(self): @@ -28,9 +36,10 @@ def _edb(self): return self._pedb.edb_api @property - def _layout(self): - """Return Ansys.Ansoft.Edb.Cell.Layout object.""" - return self._pedb.active_layout + def _layout_obj_instance(self): + """Returns :class:`pyaedt.edb_core.edb_data.connectable.LayoutObjInstance`.""" + obj = self._pedb.layout_instance.GetLayoutObjInstance(self._edb_object, None) + return LayoutObjInstance(self._pedb, obj) @property def _edb_properties(self): @@ -42,9 +51,9 @@ def _edb_properties(self, value): self._edb_object.SetProductSolverOption(self._edb.edb_api.ProductId.Designer, "HFSS", value) @property - def is_null(self): - """Determine if this object is null.""" - return self._edb_object.IsNull() + def _obj_type(self): + """Returns LayoutObjType.""" + return self._edb_object.GetObjType().ToString() @property def id(self): @@ -81,6 +90,12 @@ def net(self): return EDBNetsData(self._edb_object.GetNet(), self._pedb) + @net.setter + def net(self, value): + """Set net.""" + net = self._pedb.nets[value] + self._edb_object.SetNet(net.net_object) + @property def component(self): """Component connected to this object. diff --git a/pyaedt/edb_core/edb_data/hfss_simulation_setup_data.py b/pyaedt/edb_core/edb_data/hfss_simulation_setup_data.py index 7c9e0e395ef..76e9cc8e6da 100644 --- a/pyaedt/edb_core/edb_data/hfss_simulation_setup_data.py +++ b/pyaedt/edb_core/edb_data/hfss_simulation_setup_data.py @@ -1031,7 +1031,7 @@ def basic(self): Returns ------- - ``True`` if bais adaptive is used, ``False`` otherwise. + ``True`` if basic adaptive is used, ``False`` otherwise. """ return self.adaptive_settings.Basic @@ -1042,7 +1042,7 @@ def basic(self, value): @property def do_adaptive(self): - """Whether if adpative mesh is on. + """Whether if adaptive mesh is on. Returns ------- diff --git a/pyaedt/edb_core/edb_data/nets_data.py b/pyaedt/edb_core/edb_data/nets_data.py index b16c572c4fd..0574e17b7f8 100644 --- a/pyaedt/edb_core/edb_data/nets_data.py +++ b/pyaedt/edb_core/edb_data/nets_data.py @@ -74,6 +74,22 @@ def components(self): comps[comp.refdes] = comp return comps + @pyaedt_function_handler + def find_dc_short(self, fix=False): + """Find DC-shorted nets. + + Parameters + ---------- + fix : bool, optional + If `True`, rename all the nets. (default) + If `False`, only report dc shorts. + Returns + ------- + List[List[str, str]] + [[net name, net name]]. + """ + return self._app.layout_validation.dc_shorts(self.name, fix) + @pyaedt_function_handler() def plot( self, diff --git a/pyaedt/edb_core/edb_data/obj_base.py b/pyaedt/edb_core/edb_data/obj_base.py new file mode 100644 index 00000000000..4d43360e47a --- /dev/null +++ b/pyaedt/edb_core/edb_data/obj_base.py @@ -0,0 +1,16 @@ +class ObjBase(object): + """Manages EDB functionalities for a base object.""" + + def __init__(self, pedb, model): + self._pedb = pedb + self._edb_object = model + + @property + def is_null(self): + """Flag indicating if this object is null.""" + return self._edb_object.IsNull() + + @property + def type(self): + """Get type.""" + return self._edb_object.GetType() diff --git a/pyaedt/edb_core/edb_data/padstacks_data.py b/pyaedt/edb_core/edb_data/padstacks_data.py index 2c381ab0e3d..a37d2b2987a 100644 --- a/pyaedt/edb_core/edb_data/padstacks_data.py +++ b/pyaedt/edb_core/edb_data/padstacks_data.py @@ -1869,12 +1869,6 @@ def get_connected_object_id_set(self): layoutObjInst = self.object_instance return [loi.GetLayoutObj().GetId() for loi in layoutInst.GetConnectedObjects(layoutObjInst).Items] - @pyaedt_function_handler() - def _get_connected_object_obj_set(self): - layoutInst = self._edb_padstackinstance.GetLayout().GetLayoutInstance() - layoutObjInst = self.object_instance - return list([loi.GetLayoutObj() for loi in layoutInst.GetConnectedObjects(layoutObjInst).Items]) - @pyaedt_function_handler() def get_reference_pins(self, reference_net="GND", search_radius=5e-3, max_limit=0, component_only=True): """Search for reference pins using given criteria. diff --git a/pyaedt/edb_core/edb_data/primitives_data.py b/pyaedt/edb_core/edb_data/primitives_data.py index 514b0d279ae..4b29f1c4dbc 100644 --- a/pyaedt/edb_core/edb_data/primitives_data.py +++ b/pyaedt/edb_core/edb_data/primitives_data.py @@ -3,6 +3,7 @@ from pyaedt.edb_core.dotnet.primitive import BondwireDotNet from pyaedt.edb_core.dotnet.primitive import CircleDotNet from pyaedt.edb_core.dotnet.primitive import PathDotNet +from pyaedt.edb_core.dotnet.primitive import PolygonDataDotNet from pyaedt.edb_core.dotnet.primitive import PolygonDotNet from pyaedt.edb_core.dotnet.primitive import RectangleDotNet from pyaedt.edb_core.dotnet.primitive import TextDotNet @@ -81,11 +82,7 @@ def type(self): ------- str """ - types = ["Circle", "Path", "Polygon", "Rectangle", "Bondwire"] - str_type = self.primitive_type.ToString().split(".") - if str_type[-1] in types: - return str_type[-1] - return None + return self._edb_object.GetPrimitiveType().ToString() @property def net_name(self): @@ -149,6 +146,15 @@ def is_void(self): """ return self._edb_object.IsVoid() + def get_connected_objects(self): + """Get connected objects. + + Returns + ------- + list + """ + return self._pedb.get_connected_objects(self._layout_obj_instance) + class EDBPrimitives(EDBPrimitivesMain): """Manages EDB functionalities for a primitives. @@ -349,12 +355,6 @@ def get_connected_object_id_set(self): layoutObjInst = layoutInst.GetLayoutObjInstance(self.primitive_object, None) # 2nd arg was [] return [loi.GetLayoutObj().GetId() for loi in layoutInst.GetConnectedObjects(layoutObjInst).Items] - @pyaedt_function_handler() - def _get_connected_object_obj_set(self): - layoutInst = self.primitive_object.GetLayout().GetLayoutInstance() - layoutObjInst = layoutInst.GetLayoutObjInstance(self.primitive_object, None) - return list([loi.GetLayoutObj() for loi in layoutInst.GetConnectedObjects(layoutObjInst).Items]) - @pyaedt_function_handler() def convert_to_polygon(self): """Convert path to polygon. @@ -716,6 +716,27 @@ def length(self): length += GeometryOperators.points_distance(center_line[pt_ind], center_line[pt_ind + 1]) return length + @pyaedt_function_handler + def add_point(self, x, y, incremental=False): + """Add a point at the end of the path. + + Parameters + ---------- + x: str, int, float + X coordinate. + y: str, in, float + Y coordinate. + incremental: bool + Add point incrementally. If True, coordinates of the added point is incremental to the last point. + The default value is ``False``. + Returns + ------- + bool + """ + center_line = PolygonDataDotNet(self._pedb, self._edb_object.GetCenterLine()) + center_line.add_point(x, y, incremental) + return self._edb_object.SetCenterLine(center_line.edb_api) + @pyaedt_function_handler def get_center_line(self, to_string=False): """Get the center line of the trace. @@ -814,6 +835,113 @@ def create_edge_port( else: return self._app.hfss.create_edge_port_vertical(self.id, pos, name, 50, reference_layer) + pyaedt_function_handler() + + def create_via_fence(self, distance, gap, padstack_name): + """Create via fences on both sides of the trace. + + Parameters + ---------- + distance: str, float + Distance between via fence and trace center line. + gap: str, float + Gap between vias. + padstack_name: str + Name of the via padstack. + + Returns + ------- + + """ + + def getAngle(v1, v2): # pragma: no cover + v1_mag = math.sqrt(v1[0] ** 2 + v1[1] ** 2) + v2_mag = math.sqrt(v2[0] ** 2 + v2[1] ** 2) + dotsum = v1[0] * v2[0] + v1[1] * v2[1] + if v1[0] * v2[1] - v1[1] * v2[0] > 0: + scale = 1 + else: + scale = -1 + dtheta = scale * math.acos(dotsum / (v1_mag * v2_mag)) + + return dtheta + + def getLocations(line, gap): # pragma: no cover + location = [line[0]] + residual = 0 + + for n in range(len(line) - 1): + x0, y0 = line[n] + x1, y1 = line[n + 1] + length = math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) + dx, dy = (x1 - x0) / length, (y1 - y0) / length + x = x0 - dx * residual + y = y0 - dy * residual + length = length + residual + while length >= gap: + x += gap * dx + y += gap * dy + location.append((x, y)) + length -= gap + + residual = length + return location + + def getParalletLines(pts, distance): # pragma: no cover + leftline = [] + rightline = [] + + x0, y0 = pts[0] + x1, y1 = pts[1] + vector = (x1 - x0, y1 - y0) + orientation1 = getAngle((1, 0), vector) + + leftturn = orientation1 + math.pi / 2 + righrturn = orientation1 - math.pi / 2 + leftPt = (x0 + distance * math.cos(leftturn), y0 + distance * math.sin(leftturn)) + leftline.append(leftPt) + rightPt = (x0 + distance * math.cos(righrturn), y0 + distance * math.sin(righrturn)) + rightline.append(rightPt) + + for n in range(1, len(pts) - 1): + x0, y0 = pts[n - 1] + x1, y1 = pts[n] + x2, y2 = pts[n + 1] + + v1 = (x1 - x0, y1 - y0) + v2 = (x2 - x1, y2 - y1) + dtheta = getAngle(v1, v2) + orientation1 = getAngle((1, 0), v1) + + leftturn = orientation1 + dtheta / 2 + math.pi / 2 + righrturn = orientation1 + dtheta / 2 - math.pi / 2 + + distance2 = distance / math.sin((math.pi - dtheta) / 2) + leftPt = (x1 + distance2 * math.cos(leftturn), y1 + distance2 * math.sin(leftturn)) + leftline.append(leftPt) + rightPt = (x1 + distance2 * math.cos(righrturn), y1 + distance2 * math.sin(righrturn)) + rightline.append(rightPt) + + x0, y0 = pts[-2] + x1, y1 = pts[-1] + + vector = (x1 - x0, y1 - y0) + orientation1 = getAngle((1, 0), vector) + leftturn = orientation1 + math.pi / 2 + righrturn = orientation1 - math.pi / 2 + leftPt = (x1 + distance * math.cos(leftturn), y1 + distance * math.sin(leftturn)) + leftline.append(leftPt) + rightPt = (x1 + distance * math.cos(righrturn), y1 + distance * math.sin(righrturn)) + rightline.append(rightPt) + return leftline, rightline + + distance = self._pedb.edb_value(distance).ToDouble() + gap = self._pedb.edb_value(gap).ToDouble() + center_line = self.get_center_line() + leftline, rightline = getParalletLines(center_line, distance) + for x, y in getLocations(rightline, gap) + getLocations(leftline, gap): + self._pedb.padstacks.place([x, y], padstack_name) + class EdbRectangle(EDBPrimitives, RectangleDotNet): def __init__(self, raw_primitive, core_app): diff --git a/pyaedt/edb_core/edb_data/terminals.py b/pyaedt/edb_core/edb_data/terminals.py index 48e0f349476..30e551e241e 100644 --- a/pyaedt/edb_core/edb_data/terminals.py +++ b/pyaedt/edb_core/edb_data/terminals.py @@ -442,7 +442,12 @@ def create(self, padstack_instance, name=None, layer=None, is_ref=False): layer_obj = self._pedb.stackup.signal_layers[layer] terminal = self._edb.cell.terminal.PadstackInstanceTerminal.Create( - self._layout, self.net.net_object, name, padstack_instance._edb_object, layer_obj._edb_layer, isRef=is_ref + self._pedb.active_layout, + self.net.net_object, + name, + padstack_instance._edb_object, + layer_obj._edb_layer, + isRef=is_ref, ) terminal = PadstackInstanceTerminal(self._pedb, terminal) diff --git a/pyaedt/edb_core/general.py b/pyaedt/edb_core/general.py index 951c252d1f1..6be4a4b417d 100644 --- a/pyaedt/edb_core/general.py +++ b/pyaedt/edb_core/general.py @@ -172,3 +172,36 @@ class TerminalType(Enum): PadstackInstanceTerminal = 3 BundleTerminal = 4 PinGroupTerminal = 5 + + +class Primitives(Enum): + Rectangle = 0 + Circle = 1 + Polygon = 2 + Path = 3 + Bondwire = 4 + PrimitivePlugin = 5 + Text = 6 + Path3D = 7 + BoardBendDef = 8 + InValidType = 9 + + +class LayoutObjType(Enum): + InvalidLayoutObj = -1 + Primitive = 0 + PadstackInstance = 1 + Terminal = 2 + TerminalInstance = 3 + CellInstance = 4 + Layer = 5 + Net = 6 + Padstack = 7 + Group = 8 + NetClass = 9 + Cell = 10 + DifferentialPair = 11 + PinGroup = 12 + VoltageRegulator = 13 + ExtendedNet = 14 + LayoutObjTypeCount = 15 diff --git a/pyaedt/edb_core/layout_validation.py b/pyaedt/edb_core/layout_validation.py new file mode 100644 index 00000000000..1f5446ab8d8 --- /dev/null +++ b/pyaedt/edb_core/layout_validation.py @@ -0,0 +1,250 @@ +import re + +from pyaedt.edb_core.edb_data.padstacks_data import EDBPadstackInstance +from pyaedt.edb_core.edb_data.primitives_data import EDBPrimitives +from pyaedt.generic.general_methods import generate_unique_name +from pyaedt.generic.general_methods import pyaedt_function_handler + + +class LayoutValidation: + """Manages all layout validation capabilities""" + + def __init__(self, pedb): + self._pedb = pedb + + @pyaedt_function_handler() + def dc_shorts(self, net_list=None, fix=False): + """Find DC shorts on layout. + + Parameters + ---------- + net_list : str or list[str], optional + List of nets. + fix : bool, optional + If `True`, rename all the nets. (default) + If `False`, only report dc shorts. + + Returns + ------- + List[List[str, str]] + [[net name, net name]]. + + Examples + -------- + + >>> edb = Edb("edb_file") + >>> dc_shorts = edb.layout_validation.dc_shorts() + + """ + if not net_list: + net_list = list(self._pedb.nets.nets.keys()) + elif isinstance(net_list, str): + net_list = [net_list] + _objects_list = {} + _padstacks_list = {} + for prim in self._pedb.modeler.primitives: + n_name = prim.net_name + if n_name in _objects_list: + _objects_list[n_name].append(prim) + else: + _objects_list[n_name] = [prim] + for pad in list(self._pedb.padstacks.instances.values()): + n_name = pad.net_name + if n_name in _padstacks_list: + _padstacks_list[n_name].append(pad) + else: + _padstacks_list[n_name] = [pad] + dc_shorts = [] + for net in net_list: + objs = [] + for i in _objects_list.get(net, []): + objs.append(i) + for i in _padstacks_list.get(net, []): + objs.append(i) + if not len(objs): + self._pedb.nets[net].delete() + continue + + connected_objs = objs[0].get_connected_objects() + connected_objs.append(objs[0]) + net_dc_shorts = [obj for obj in connected_objs] + if net_dc_shorts: + dc_nets = list(set([obj.net.name for obj in net_dc_shorts if not obj.net.name == net])) + for dc in dc_nets: + if dc: + dc_shorts.append([net, dc]) + if fix: + temp = [] + for i in net_dc_shorts: + temp.append(i.net.name) + temp_key = set(temp) + temp_count = {temp.count(i): i for i in temp_key} + temp_count = dict(sorted(temp_count.items())) + while True: + temp_name = list(temp_count.values()).pop() + if not temp_name.lower().startswith("unnamed"): + break + elif temp_name.lower(): + break + elif len(temp) == 0: + break + for i in net_dc_shorts: + if not i.net.name == temp_name: + i.net = temp_name + return dc_shorts + + @pyaedt_function_handler() + def disjoint_nets( + self, net_list=None, keep_only_main_net=False, clean_disjoints_less_than=0.0, order_by_area=False + ): + """Find and fix disjoint nets from a given netlist. + + Parameters + ---------- + net_list : str, list, optional + List of nets on which check disjoints. If `None` is provided then the algorithm will loop on all nets. + keep_only_main_net : bool, optional + Remove all secondary nets other than principal one (the one with more objects in it). Default is `False`. + clean_disjoints_less_than : bool, optional + Clean all disjoint nets with area less than specified area in square meters. Default is `0.0` to disable it. + order_by_area : bool, optional + Whether if the naming order has to be by number of objects (fastest) or area (slowest but more accurate). + Default is ``False``. + Returns + ------- + List + New nets created. + + Examples + -------- + + >>> renamed_nets = edb.layout_validation.disjoint_nets(["GND","Net2"]) + """ + timer_start = self._pedb._logger.reset_timer() + + if not net_list: + net_list = list(self._pedb.nets.keys()) + elif isinstance(net_list, str): + net_list = [net_list] + _objects_list = {} + _padstacks_list = {} + for prim in self._pedb.modeler.primitives: + n_name = prim.net_name + if n_name in _objects_list: + _objects_list[n_name].append(prim) + else: + _objects_list[n_name] = [prim] + for pad in list(self._pedb.padstacks.instances.values()): + n_name = pad.net_name + if n_name in _padstacks_list: + _padstacks_list[n_name].append(pad) + else: + _padstacks_list[n_name] = [pad] + new_nets = [] + disjoints_objects = [] + self._pedb._logger.reset_timer() + for net in net_list: + net_groups = [] + obj_dict = {} + for i in _objects_list.get(net, []): + obj_dict[i.id] = i + for i in _padstacks_list.get(net, []): + obj_dict[i.id] = i + objs = list(obj_dict.values()) + l = len(objs) + while l > 0: + l1 = objs[0].get_connected_object_id_set() + l1.append(objs[0].id) + repetition = False + for net_list in net_groups: + if set(l1).intersection(net_list): + net_groups.append([i for i in l1 if i not in net_list]) + repetition = True + if not repetition: + net_groups.append(l1) + objs = [i for i in objs if i.id not in l1] + l = len(objs) + if len(net_groups) > 1: + + def area_calc(elem): + sum = 0 + for el in elem: + try: + if isinstance(obj_dict[el], EDBPrimitives): + if not obj_dict[el].is_void: + sum += obj_dict[el].area() + except: + pass + return sum + + if order_by_area: + areas = [area_calc(i) for i in net_groups] + sorted_list = [x for _, x in sorted(zip(areas, net_groups), reverse=True)] + else: + sorted_list = sorted(net_groups, key=len, reverse=True) + for disjoints in sorted_list[1:]: + if keep_only_main_net: + for geo in disjoints: + try: + obj_dict[geo].delete() + except KeyError: + pass + elif len(disjoints) == 1 and ( + isinstance(obj_dict[disjoints[0]], EDBPadstackInstance) + or clean_disjoints_less_than + and obj_dict[disjoints[0]].area() < clean_disjoints_less_than + ): + try: + obj_dict[disjoints[0]].delete() + except KeyError: + pass + else: + new_net_name = generate_unique_name(net, n=6) + net_obj = self._pedb.nets.find_or_create_net(new_net_name) + if net_obj: + new_nets.append(net_obj.GetName()) + for geo in disjoints: + try: + obj_dict[geo].net_name = net_obj + except KeyError: + pass + disjoints_objects.extend(disjoints) + self._pedb._logger.info("Found {} objects in {} new nets.".format(len(disjoints_objects), len(new_nets))) + self._pedb._logger.info_timer("Disjoint Cleanup Completed.", timer_start) + + return new_nets + + def illegal_net_names(self, fix=False): + """Find and fix illegal net names.""" + pattern = r"[\(\)\\\/:;*?<>\'\"|`~$]" + + nets = self._pedb.nets.nets + + renamed_nets = [] + for net, val in nets.items(): + if re.findall(pattern, net): + renamed_nets.append(net) + if fix: + new_name = re.sub(pattern, "_", net) + val.name = new_name + + self._pedb._logger.info("Found {} illegal net names.".format(len(renamed_nets))) + return + + def illegal_rlc_values(self, fix=False): + """Find and fix rlc illegal values.""" + inductors = self._pedb.components.inductors + + temp = [] + for k, v in inductors.items(): + componentProperty = v.edbcomponent.GetComponentProperty() + model = componentProperty.GetModel().Clone() + pinpairs = model.PinPairs + + if not len(list(pinpairs)): # pragma: no cover + temp.append(k) + if fix: + v.rlc_values = [0, 1, 0] + + self._pedb._logger.info("Found {} inductors have no value.".format(len(temp))) + return diff --git a/pyaedt/edb_core/materials.py b/pyaedt/edb_core/materials.py index 9f1fdd4c4da..154ef5d5d4b 100644 --- a/pyaedt/edb_core/materials.py +++ b/pyaedt/edb_core/materials.py @@ -1,7 +1,9 @@ from __future__ import absolute_import # noreorder import difflib +import fnmatch import logging +import os import warnings from pyaedt import is_ironpython @@ -297,40 +299,59 @@ def _json_format(self): @pyaedt_function_handler() def _load(self, input_dict): + default_material = { + "name": "default", + "conductivity": 0, + "loss_tangent": 0, + "magnetic_loss_tangent": 0, + "mass_density": 0, + "permittivity": 1, + "permeability": 1, + "poisson_ratio": 0, + "specific_heat": 0, + "thermal_conductivity": 0, + "youngs_modulus": 0, + "thermal_expansion_coefficient": 0, + "dielectric_model_frequency": None, + "dc_permittivity": None, + } if input_dict: - self.conductivity = input_dict["conductivity"] - self.loss_tangent = input_dict["loss_tangent"] - self.magnetic_loss_tangent = input_dict["magnetic_loss_tangent"] - self.mass_density = input_dict["mass_density"] - self.permittivity = input_dict["permittivity"] - self.permeability = input_dict["permeability"] - self.poisson_ratio = input_dict["poisson_ratio"] - self.specific_heat = input_dict["specific_heat"] - self.thermal_conductivity = input_dict["thermal_conductivity"] - self.youngs_modulus = input_dict["youngs_modulus"] - self.thermal_expansion_coefficient = input_dict["thermal_expansion_coefficient"] - if input_dict["dielectric_model_frequency"] is not None: + for k, v in input_dict.items(): + default_material[k] = v + + self.conductivity = default_material["conductivity"] + self.loss_tangent = default_material["loss_tangent"] + self.magnetic_loss_tangent = default_material["magnetic_loss_tangent"] + self.mass_density = default_material["mass_density"] + self.permittivity = default_material["permittivity"] + self.permeability = default_material["permeability"] + self.poisson_ratio = default_material["poisson_ratio"] + self.specific_heat = default_material["specific_heat"] + self.thermal_conductivity = default_material["thermal_conductivity"] + self.youngs_modulus = default_material["youngs_modulus"] + self.thermal_expansion_coefficient = default_material["thermal_expansion_coefficient"] + if default_material["dielectric_model_frequency"] is not None: # pragma: no cover if self._edb_material_def.GetDielectricMaterialModel(): model = self._edb_material_def.GetDielectricMaterialModel() - self.dielectric_model_frequency = input_dict["dielectric_model_frequency"] - self.loss_tangent_at_frequency = input_dict["loss_tangent_at_frequency"] - self.permittivity_at_frequency = input_dict["permittivity_at_frequency"] - if input_dict["dc_permittivity"] is not None: + self.dielectric_model_frequency = default_material["dielectric_model_frequency"] + self.loss_tangent_at_frequency = default_material["loss_tangent_at_frequency"] + self.permittivity_at_frequency = default_material["permittivity_at_frequency"] + if default_material["dc_permittivity"] is not None: model.SetUseDCRelativePermitivity(True) - self.dc_permittivity = input_dict["dc_permittivity"] - self.dc_conductivity = input_dict["dc_conductivity"] + self.dc_permittivity = default_material["dc_permittivity"] + self.dc_conductivity = default_material["dc_conductivity"] else: if not self._pclass.add_djordjevicsarkar_material( - input_dict["name"], - input_dict["permittivity_at_frequency"], - input_dict["loss_tangent_at_frequency"], - input_dict["dielectric_model_frequency"], - input_dict["dc_permittivity"], - input_dict["dc_conductivity"], + default_material["name"], + default_material["permittivity_at_frequency"], + default_material["loss_tangent_at_frequency"], + default_material["dielectric_model_frequency"], + default_material["dc_permittivity"], + default_material["dc_conductivity"], ): self._pclass._pedb.logger.warning( 'Cannot set DS model for material "{}". Check for realistic ' - "values that define DS Model".format(input_dict["name"]) + "values that define DS Model".format(default_material["name"]) ) else: # To unset DS model if already assigned to the material in database @@ -346,12 +367,38 @@ def __getitem__(self, item): def __init__(self, pedb): self._pedb = pedb + self._syslib = os.path.join(self._pedb.base_path, "syslib") + self._personal_lib = None + self._materials_in_aedt = None if not self.materials: self.add_material("air") self.add_material("copper", 1, 0.999991, 5.8e7, 0, 0) self.add_material("fr4_epoxy", 4.4, 1, 0, 0.02, 0) self.add_material("solder_mask", 3.1, 1, 0, 0.035, 0) + @property + def materials_in_aedt(self): + """Retrieve the dictionary of materials available in AEDT syslib.""" + if self._materials_in_aedt: + return self._materials_in_aedt + self._materials_in_aedt = self._read_materials() + return self._materials_in_aedt + + @property + def syslib(self): + """Retrieve the project sys library.""" + return self._syslib + + @property + def personallib(self): + """Get or Set the user personallib.""" + return self._personal_lib + + @personallib.setter + def personallib(self, value): + self._personal_lib = value + self._materials_in_aedt = self._read_materials() + @pyaedt_function_handler() def _edb_value(self, value): return self._pedb.edb_value(value) @@ -788,3 +835,137 @@ def get_property_by_material_name(self, property_name, material_name): else: return property_box.ToDouble() return False + + @pyaedt_function_handler() + def add_material_from_aedt(self, material_name): + """Add a material read from ``syslib amat`` library. + + Parameters + ---------- + material_name : str + Material name. + + Returns + ------- + bool + "True`` when successful, ``False`` when failed. + """ + if material_name in self.materials_in_aedt: + if material_name in list(self.materials.keys()): + self._pedb.logger.warning("Material {} already exists. Skipping it.".format(material_name)) + return False + new_material = self.add_material(name=material_name) + material = self.materials_in_aedt[material_name] + try: + new_material.permittivity = float(material["permittivity"]) + except (KeyError, TypeError): + pass + try: + new_material.conductivity = float(material["conductivity"]) + except (KeyError, TypeError): + pass + try: + new_material.mass_density = float(material["mass_density"]) + except (KeyError, TypeError): + pass + try: + new_material.permeability = float(material["permeability"]) + except (KeyError, TypeError): + pass + try: + new_material.loss_tangent = float(material["dielectric_loss_tangent"]) + except (KeyError, TypeError): + pass + try: + new_material.specific_heat = float(material["specific_heat"]) + except (KeyError, TypeError): + pass + try: + new_material.thermal_expansion_coefficient = float(material["thermal_expansion_coeffcient"]) + except (KeyError, TypeError): + pass + return True + + @pyaedt_function_handler() + def load_amat(self, amat_file): + """Load material from an amat file and add materials to Edb. + + Parameters + ---------- + amat_file : str + Full path to the amat file to read and add to the Edb. + """ + material_dict = self._read_materials(amat_file) + for material_name, material in material_dict.items(): + if not material_name in list(self.materials.keys()): + new_material = self.add_material(name=material_name) + try: + new_material.permittivity = float(material["permittivity"]) + except (KeyError, TypeError): + pass + try: + new_material.conductivity = float(material["conductivity"]) + except (KeyError, TypeError): + pass + try: + new_material.mass_density = float(material["mass_density"]) + except (KeyError, TypeError): + pass + try: + new_material.permeability = float(material["permeability"]) + except (KeyError, TypeError): + pass + try: + new_material.loss_tangent = float(material["dielectric_loss_tangent"]) + except (KeyError, TypeError): + pass + try: + new_material.specific_heat = float(material["specific_heat"]) + except (KeyError, TypeError): + pass + try: + new_material.thermal_expansion_coefficient = float(material["thermal_expansion_coeffcient"]) + except (KeyError, TypeError): + pass + return True + + @pyaedt_function_handler() + def _read_materials(self, mat_file=None): + def get_mat_list(file_name, mats): + from pyaedt.generic.LoadAEDTFile import load_entire_aedt_file + + mread = load_entire_aedt_file(file_name) + for mat, mdict in mread.items(): + if mat != "$base_index$": + try: + mats[mat] = mdict["MaterialDef"][mat] + except KeyError: + mats[mat] = mdict + + if mat_file and os.path.exists(mat_file): + materials = {} + get_mat_list(mat_file, materials) + return materials + + amat_sys = [ + os.path.join(dirpath, filename) + for dirpath, _, filenames in os.walk(self.syslib) + for filename in filenames + if fnmatch.fnmatch(filename, "*.amat") + ] + amat_personal = [] + if self.personallib: + amat_personal = [ + os.path.join(dirpath, filename) + for dirpath, _, filenames in os.walk(self.personallib) + for filename in filenames + if fnmatch.fnmatch(filename, "*.amat") + ] + materials = {} + for amat in amat_sys: + get_mat_list(amat, materials) + + if amat_personal: + for amat in amat_personal: + get_mat_list(amat, materials) + return materials diff --git a/pyaedt/edb_core/nets.py b/pyaedt/edb_core/nets.py index 30ae3591510..35747b59830 100644 --- a/pyaedt/edb_core/nets.py +++ b/pyaedt/edb_core/nets.py @@ -6,8 +6,6 @@ import warnings from pyaedt.edb_core.edb_data.nets_data import EDBNetsData -from pyaedt.edb_core.edb_data.padstacks_data import EDBPadstackInstance -from pyaedt.edb_core.edb_data.primitives_data import EDBPrimitives from pyaedt.edb_core.general import convert_py_list_to_net_list from pyaedt.generic.constants import CSS4_COLORS from pyaedt.generic.general_methods import generate_unique_name @@ -46,7 +44,6 @@ def __getitem__(self, name): def __init__(self, p_edb): self._pedb = p_edb - self._nets = {} self._nets_by_comp_dict = {} self._comps_by_nets_dict = {} @@ -89,10 +86,10 @@ def nets(self): dict[str, :class:`pyaedt.edb_core.edb_data.nets_data.EDBNetsData`] Dictionary of nets. """ - + temp = {} for net in self._layout.nets: - self._nets[net.name] = EDBNetsData(net.api_object, self._pedb) - return self._nets + temp[net.name] = EDBNetsData(net.api_object, self._pedb) + return temp @property def netlist(self): @@ -1149,6 +1146,9 @@ def find_and_fix_disjoint_nets( ): """Find and fix disjoint nets from a given netlist. + .. deprecated:: + Use new property :func:`edb.layout_validation.disjoint_nets` instead. + Parameters ---------- net_list : str, list, optional @@ -1170,158 +1170,10 @@ def find_and_fix_disjoint_nets( >>> renamed_nets = edb_core.nets.find_and_fix_disjoint_nets(["GND","Net2"]) """ - timer_start = self._logger.reset_timer() - - if not net_list: - net_list = list(self.nets.keys()) - elif isinstance(net_list, str): - net_list = [net_list] - _objects_list = {} - _padstacks_list = {} - for prim in self._pedb.modeler.primitives: - n_name = prim.net_name - if n_name in _objects_list: - _objects_list[n_name].append(prim) - else: - _objects_list[n_name] = [prim] - for pad in list(self._pedb.padstacks.instances.values()): - n_name = pad.net_name - if n_name in _padstacks_list: - _padstacks_list[n_name].append(pad) - else: - _padstacks_list[n_name] = [pad] - new_nets = [] - disjoints_objects = [] - self._logger.reset_timer() - for net in net_list: - net_groups = [] - obj_dict = {} - for i in _objects_list.get(net, []): - obj_dict[i.id] = i - for i in _padstacks_list.get(net, []): - obj_dict[i.id] = i - objs = list(obj_dict.values()) - l = len(objs) - while l > 0: - l1 = objs[0].get_connected_object_id_set() - l1.append(objs[0].id) - repetition = False - for net_list in net_groups: - if set(l1).intersection(net_list): - net_groups.append([i for i in l1 if i not in net_list]) - repetition = True - if not repetition: - net_groups.append(l1) - objs = [i for i in objs if i.id not in l1] - l = len(objs) - if len(net_groups) > 1: - - def area_calc(elem): - sum = 0 - for el in elem: - try: - if isinstance(obj_dict[el], EDBPrimitives): - if not obj_dict[el].is_void: - sum += obj_dict[el].area() - except: - pass - return sum - - if order_by_area: - areas = [area_calc(i) for i in net_groups] - sorted_list = [x for _, x in sorted(zip(areas, net_groups), reverse=True)] - else: - sorted_list = sorted(net_groups, key=len, reverse=True) - for disjoints in sorted_list[1:]: - if keep_only_main_net: - for geo in disjoints: - try: - obj_dict[geo].delete() - except KeyError: - pass - elif len(disjoints) == 1 and ( - isinstance(obj_dict[disjoints[0]], EDBPadstackInstance) - or clean_disjoints_less_than - and obj_dict[disjoints[0]].area() < clean_disjoints_less_than - ): - try: - obj_dict[disjoints[0]].delete() - except KeyError: - pass - else: - new_net_name = generate_unique_name(net, n=6) - net_obj = self.find_or_create_net(new_net_name) - if net_obj: - new_nets.append(net_obj.GetName()) - for geo in disjoints: - try: - obj_dict[geo].net_name = net_obj - except KeyError: - pass - disjoints_objects.extend(disjoints) - self._logger.info("Found {} objects in {} new nets.".format(len(disjoints_objects), len(new_nets))) - self._logger.info_timer("Disjoint Cleanup Completed.", timer_start) - - return new_nets - - @pyaedt_function_handler() - def find_dc_shorts(self, net_list=None): - """Find DC shorts on layout. - - Parameters - ---------- - net_list : str or list[str] - Optional - - Returns - ------- - List[List[str, str]] - [[net name, net name]]. - - Examples - -------- - - >>> edb = Edb("edb_file") - >>> dc_shorts = edb.nets.find_dc_shorts() - - """ - if not net_list: - net_list = list(self.nets.keys()) - elif isinstance(net_list, str): - net_list = [net_list] - _objects_list = {} - _padstacks_list = {} - for prim in self._pedb.modeler.primitives: - n_name = prim.net_name - if n_name in _objects_list: - _objects_list[n_name].append(prim) - else: - _objects_list[n_name] = [prim] - for pad in list(self._pedb.padstacks.instances.values()): - n_name = pad.net_name - if n_name in _padstacks_list: - _padstacks_list[n_name].append(pad) - else: - _padstacks_list[n_name] = [pad] - dc_shorts = [] - for net in net_list: - objs = [] - for i in _objects_list.get(net, []): - objs.append(i) - for i in _padstacks_list.get(net, []): - objs.append(i) - try: - connected_objs = objs[0]._get_connected_object_obj_set() - connected_objs.append(objs[0].api_object) - net_dc_shorts = [obj for obj in connected_objs if not obj.GetNet().GetName() == net] - if net_dc_shorts: - dc_nets = list(set([obj.GetNet().GetName() for obj in net_dc_shorts])) - for dc in dc_nets: - if dc: - dc_shorts.append([net, dc]) - except: - pass - return dc_shorts + warnings.warn("Use new function :func:`edb.layout_validation.disjoint_nets` instead.", DeprecationWarning) + return self._pedb.layout_validation.disjoint_nets( + net_list, keep_only_main_net, clean_disjoints_less_than, order_by_area + ) @pyaedt_function_handler() def merge_nets_polygons(self, net_list): diff --git a/pyaedt/edb_core/stackup.py b/pyaedt/edb_core/stackup.py index 898f5be9432..09043aab508 100644 --- a/pyaedt/edb_core/stackup.py +++ b/pyaedt/edb_core/stackup.py @@ -666,7 +666,10 @@ def add_layer( fillMaterial = "fr4_epoxy" if material.lower() not in materials_lower: - logger.error(material + " does not exist in material library") + if material in self._pedb.materials.materials_in_aedt: + self._pedb.materials.add_material_from_aedt("material") + else: + logger.error(material + " does not exist in material library") else: material = materials_lower[material.lower()] @@ -1636,54 +1639,87 @@ def residual_copper_area_per_layer(self): temp_data = {name: area / outline_area * 100 for name, area in temp_data.items()} return temp_data + @pyaedt_function_handler + def _import_dict(self, json_dict): + """Import stackup from a dictionary.""" + for k, v in json_dict.items(): + if k == "materials": + for material in v.values(): + self._pedb.materials._load_materials(material) + if k == "layers": + if len(list(v.values())) == len(list(self.stackup_layers.values())): + imported_layers_list = [l_dict["name"] for l_dict in list(v.values())] + layout_layer_list = list(self.stackup_layers.keys()) + for layer_name in imported_layers_list: + layer_index = imported_layers_list.index(layer_name) + if layout_layer_list[layer_index] != layer_name: + self.stackup_layers[layout_layer_list[layer_index]].name = layer_name + prev_layer = None + for layer_name, layer in v.items(): + if layer["name"] not in self.stackup_layers: + default_layer = { + "name": "default", + "type": "signal", + "material": "copper", + "dielectric_fill": "fr4_epoxy", + "thickness": 3.5000000000000004e-05, + "etch_factor": 0.0, + "roughness_enabled": False, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0, + "color": [242, 140, 102], + } + + if not layer["type"] == "signal": + default_layer["color"] = [27, 110, 76] + + for k, v in layer.items(): + default_layer[k] = v + + layer = default_layer + + if not prev_layer: + self.add_layer( + layer_name, + method="add_on_top", + layer_type=layer["type"], + material=layer["material"], + fillMaterial=layer["dielectric_fill"], + thickness=layer["thickness"], + ) + prev_layer = layer_name + else: + self.add_layer( + layer_name, + base_layer=layer_name, + method="insert_below", + layer_type=layer["type"], + material=layer["material"], + fillMaterial=layer["dielectric_fill"], + thickness=layer["thickness"], + ) + prev_layer = layer_name + if layer_name in self.stackup_layers: + self.stackup_layers[layer["name"]]._load_layer(layer) + return True + @pyaedt_function_handler def _import_json(self, file_path): + """Import stackup from a json file.""" if file_path: f = open(file_path) json_dict = json.load(f) # pragma: no cover - for k, v in json_dict.items(): - if k == "materials": - for material in v.values(): - self._pedb.materials._load_materials(material) - if k == "layers": - if len(list(v.values())) == len(list(self.stackup_layers.values())): - imported_layers_list = [l_dict["name"] for l_dict in list(v.values())] - layout_layer_list = list(self.stackup_layers.keys()) - for layer_name in imported_layers_list: - layer_index = imported_layers_list.index(layer_name) - if layout_layer_list[layer_index] != layer_name: - self.stackup_layers[layout_layer_list[layer_index]].name = layer_name - prev_layer = None - for layer_name, layer in v.items(): - if layer["name"] not in self.stackup_layers: - if not prev_layer: - self.add_layer( - layer_name, - method="add_on_top", - layer_type=layer["type"], - material=layer["material"], - fillMaterial=layer["dielectric_fill"], - thickness=layer["thickness"], - ) - prev_layer = layer_name - else: - self.add_layer( - layer_name, - base_layer=layer_name, - method="insert_below", - layer_type=layer["type"], - material=layer["material"], - fillMaterial=layer["dielectric_fill"], - thickness=layer["thickness"], - ) - prev_layer = layer_name - if layer_name in self.stackup_layers: - self.stackup_layers[layer["name"]]._load_layer(layer) - return True + return self._import_dict(json_dict) @pyaedt_function_handler def _import_csv(self, file_path): - """Import stackup defnition from a CSV file. + """Import stackup definition from a CSV file. Parameters ---------- @@ -2092,7 +2128,9 @@ def load(self, file_path): >>> edb.stackup.load("stackup.xml") """ - if file_path.endswith(".csv"): + if isinstance(file_path, dict): + return self._import_dict(file_path) + elif file_path.endswith(".csv"): return self._import_csv(file_path) elif file_path.endswith(".json"): return self._import_json(file_path) diff --git a/pyaedt/emit_core/results/revision.py b/pyaedt/emit_core/results/revision.py index 6519a34c0da..f4fa48f7ddd 100644 --- a/pyaedt/emit_core/results/revision.py +++ b/pyaedt/emit_core/results/revision.py @@ -79,7 +79,7 @@ def _load_revision(self): Load this revision. Examples - ---------- + -------- >>> aedtapp.results.revision.load_revision() """ if self.revision_loaded: @@ -105,7 +105,7 @@ def result_mode_error(): @pyaedt_function_handler() def get_interaction(self, domain): """ - Creates a new interaction for a domain. + Create a new interaction for a domain. Parameters ---------- @@ -118,7 +118,7 @@ def get_interaction(self, domain): Interaction object. Examples - ---------- + -------- >>> domain = aedtapp.results.interaction_domain() >>> rev.get_interaction(domain) @@ -146,7 +146,7 @@ def run(self, domain): Interaction object. Examples - ---------- + -------- >>> domain = aedtapp.results.interaction_domain() >>> rev.run(domain) @@ -180,7 +180,7 @@ def is_domain_valid(self, domain): ``InteractionDomain`` object for constraining the analysis parameters. Examples - ---------- + -------- >>> domain = aedtapp.interaction_domain() >>> aedtapp.results.current_revision.is_domain_valid(domain) True @@ -200,12 +200,12 @@ def get_instance_count(self, domain): ``InteractionDomain`` object for constraining the analysis parameters. Returns - -------- + ------- count : int Number of instances in the domain for the current revision. Examples - ---------- + -------- >>> domain = aedtapp.interaction_domain() >>> num_instances = aedtapp.results.current_revision.get_instance_count(domain) """ @@ -228,7 +228,7 @@ def get_receiver_names(self): List of receiver names. Examples - ---------- + -------- >>> rxs = aedtapp.results.current_revision.get_reciver_names() """ if self.revision_loaded: @@ -262,7 +262,7 @@ def get_interferer_names(self, interferer_type=None): List of interfering systems' names. Examples - ---------- + -------- >>> transmitters = aedtapp.results.current_revision.get_interferer_names(InterfererType.TRANSMITTERS) >>> emitters = aedtapp.results.current_revision.get_interferer_names(InterfererType.EMITTERS) >>> both = aedtapp.results.current_revision.get_interferer_names(InterfererType.TRANSMITTERS_AND_EMITTERS) @@ -301,7 +301,7 @@ def get_band_names(self, radio_name, tx_rx_mode=None): List of ``tx`` or ``rx`` band/waveform names. Examples - ---------- + -------- >>> bands = aedtapp.results.current_revision.get_band_names('Bluetooth', TxRxMode.RX) >>> waveforms = aedtapp.results.current_revision.get_band_names('USB_3.x', TxRxMode.TX) """ @@ -340,7 +340,7 @@ def get_active_frequencies(self, radio_name, band_name, tx_rx_mode, units=""): List of ``tx`` or ``rx`` radio/emitter frequencies. Examples - ---------- + -------- >>> freqs = aedtapp.results.current_revision.get_active_frequencies( 'Bluetooth', 'Rx - Base Data Rate', TxRxMode.RX) """ @@ -361,7 +361,7 @@ def notes(self): Add notes to the revision. Examples - ---------- + -------- >>> aedtapp.results.current_revision.notes = "Added a filter to the WiFi Radio." >>> aedtapp.results.current_revision.notes 'Added a filter to the WiFi Radio.' @@ -383,7 +383,7 @@ def n_to_1_limit(self): - A value of ``-1`` allows unlimited N to 1. (N is set to the maximum.) Examples - ---------- + -------- >>> aedtapp.results.current_revision.n_to_1_limit = 2**20 >>> aedtapp.results.current_revision.n_to_1_limit 1048576 @@ -477,12 +477,30 @@ def interference_type_classification(self, domain, use_filter=False, filter_list domain.set_receiver(rx_radio, rx_band) domain.set_interferer(tx_radio, tx_band) interaction = self.run(domain) + # check for valid interaction, this would catch any disabled radio pairs + if not interaction.is_valid(): + continue + domain.set_receiver(rx_radio, rx_band, rx_freq) tx_freqs = self.get_active_frequencies(tx_radio, tx_band, modeTx) for tx_freq in tx_freqs: domain.set_interferer(tx_radio, tx_band, tx_freq) instance = interaction.get_instance(domain) - tx_prob = instance.get_largest_problem_type(ResultType.EMI).replace(" ", "").split(":")[1] + if not instance.has_valid_values(): + # check for saturation somewhere in the chain + # set power so its flagged as strong interference + if instance.get_result_warning() == "An amplifier was saturated.": + max_power = 200 + else: + # other warnings (e.g. no path from Tx to Rx, + # no power received, error in configuration, etc) + # should just be skipped + continue + else: + tx_prob = ( + instance.get_largest_problem_type(ResultType.EMI).replace(" ", "").split(":")[1] + ) + power = instance.get_value(ResultType.EMI) if ( rx_start_freq - rx_channel_bandwidth / 2 <= tx_freq @@ -500,14 +518,10 @@ def interference_type_classification(self, domain, use_filter=False, filter_list in_filters = True # Save the worst case interference values - if ( - instance.has_valid_values() - and instance.get_value(ResultType.EMI) > max_power - and in_filters - ): - prob = instance.get_largest_problem_type(ResultType.EMI) - max_power = instance.get_value(ResultType.EMI) + if power > max_power and in_filters: + max_power = power largest_rx_prob = rx_prob + prob = instance.get_largest_problem_type(ResultType.EMI) largest_tx_prob = prob.replace(" ", "").split(":") if max_power > -200: @@ -622,6 +636,9 @@ def protection_level_classification( domain.set_receiver(rx_radio, rx_band) domain.set_interferer(tx_radio, tx_band) interaction = self.run(domain) + # check for valid interaction, this would catch any disabled radio pairs + if not interaction.is_valid(): + continue domain.set_receiver(rx_radio, rx_band, rx_freq) tx_freqs = self.get_active_frequencies(tx_radio, tx_band, modeTx) @@ -630,7 +647,18 @@ def protection_level_classification( for tx_freq in tx_freqs: domain.set_interferer(tx_radio, tx_band, tx_freq) instance = interaction.get_instance(domain) - power = instance.get_value(mode_power) + if not instance.has_valid_values(): + # check for saturation somewhere in the chain + # set power so its flagged as "damage threshold" + if instance.get_result_warning() == "An amplifier was saturated.": + max_power = 200 + else: + # other warnings (e.g. no path from Tx to Rx, + # no power received, error in configuration, etc) + # should just be skipped + continue + else: + power = instance.get_value(mode_power) if power > damage_threshold: classification = "damage" @@ -648,8 +676,8 @@ def protection_level_classification( else: filtering = True - if instance.get_value(mode_power) > max_power and filtering: - max_power = instance.get_value(mode_power) + if power > max_power and filtering: + max_power = power # If the worst case for the band-pair is below the power thresholds, then # there are no interference issues and no offset is required. diff --git a/pyaedt/generic/general_methods.py b/pyaedt/generic/general_methods.py index fe0bc64b8be..5d325359349 100644 --- a/pyaedt/generic/general_methods.py +++ b/pyaedt/generic/general_methods.py @@ -1084,7 +1084,7 @@ def _create_json_file(json_dict, full_json_path): # ---------- # version : str, optional # Version to check. The default is ``None``, in which case all versions are checked. -# When specififying a version, you can use a three-digit format like ``"222"`` or a +# When specifying a version, you can use a three-digit format like ``"222"`` or a # five-digit format like ``"2022.2"``. # student_version : bool, optional # Whether to check for student version sessions. The default is ``False``. @@ -1275,7 +1275,7 @@ def grpc_active_sessions(version=None, student_version=False, non_graphical=Fals ---------- version : str, optional Version to check. The default is ``None``, in which case all versions are checked. - When specififying a version, you can use a three-digit format like ``"222"`` or a + When specifying a version, you can use a three-digit format like ``"222"`` or a five-digit format like ``"2022.2"``. student_version : bool, optional Whether to check for student version sessions. The default is ``False``. diff --git a/pyaedt/generic/plot.py b/pyaedt/generic/plot.py index 180e39a5772..0d68f27f88e 100644 --- a/pyaedt/generic/plot.py +++ b/pyaedt/generic/plot.py @@ -1516,6 +1516,7 @@ def plot(self, export_image_path=None): bool """ self.pv = pv.Plotter(notebook=self.is_notebook, off_screen=self.off_screen, window_size=self.windows_size) + self.pv.enable_ssao() self.meshes = None if self.background_image: self.pv.add_background_image(self.background_image) @@ -1582,8 +1583,8 @@ def plot(self, export_image_path=None): if self.show_axes: self.pv.show_axes() - if not self.is_notebook: - self.pv.show_grid(color=tuple(axes_color), grid=self.show_grid) + if not self.is_notebook and self.show_grid: + self.pv.show_grid(color=tuple(axes_color), grid=self.show_grid, fmt="%.2e") if self.bounding_box: self.pv.add_bounding_box(color=tuple(axes_color)) self.pv.set_focus(self.pv.mesh.center) diff --git a/pyaedt/generic/settings.py b/pyaedt/generic/settings.py index f970d20601d..4cf34adcb04 100644 --- a/pyaedt/generic/settings.py +++ b/pyaedt/generic/settings.py @@ -65,6 +65,22 @@ def __init__(self): self._desktop_launch_timeout = 90 self._number_of_grpc_api_retries = 6 self._retry_n_times_time_interval = 0.1 + self._wait_for_license = False + + @property + def wait_for_license(self): + """Whether if Electronics Desktop has to be launched with ``-waitforlicense`` flag enabled or not. + Default is ``False``. + + Returns + ------- + bool + """ + return self._wait_for_license + + @wait_for_license.setter + def wait_for_license(self, value): + self._wait_for_license = value @property def retry_n_times_time_interval(self): diff --git a/pyaedt/hfss.py b/pyaedt/hfss.py index f3c7ceeffc7..4eca4251975 100644 --- a/pyaedt/hfss.py +++ b/pyaedt/hfss.py @@ -321,7 +321,7 @@ def _create_boundary(self, name, props, boundary_type): """Create a boundary. Parameters - --------- + ---------- name : str Name of the boundary. props : list @@ -6467,3 +6467,85 @@ def set_radiated_power_calc_method(self, method="Auto"): """ self.oradfield.EditRadiatedPowerCalculationMethod(method) return True + + @pyaedt_function_handler() + def set_mesh_fusion_settings(self, component=None, volume_padding=None, priority=None): + # type: (list|str, list, list) -> bool + + """Set mesh fusion settings in Hfss. + + component : list, optional + List of active 3D Components. + The default is ``None``, in which case components are disabled. + volume_padding : list, optional + List of mesh envelope padding, the format is ``[+x, -x, +y, -y, +z, -z]``. + The default is ``None``, in which case all zeros are applied. + priority : list, optional + List of components with the priority flag enabled. The default is ``None``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + References + ---------- + + >>> oDesign.SetDoMeshAssembly + + Examples + -------- + + >>> import pyaedt + >>> app = pyaedt.Hfss() + >>> app.set_mesh_fusion_settings(component=["Comp1", "Comp2"], + ... volume_padding=[[0,0,0,0,0,0], [0,0,5,0,0,0]], + ... priority=["Comp1"]) + """ + arg = ["NAME:AllSettings"] + arg2 = ["NAME:MeshAssembly"] + arg3 = ["NAME:Priority Components"] + + if component and not isinstance(component, list): + component = [component] + + if not volume_padding and component: + for comp in component: + if comp in self.modeler.user_defined_component_names: + mesh_assembly_arg = ["NAME:" + comp] + mesh_assembly_arg.append("MeshAssemblyBoundingVolumePadding:=") + mesh_assembly_arg.append(["0", "0", "0", "0", "0", "0"]) + arg2.append(mesh_assembly_arg) + else: + self.logger.warning(comp + " does not exist.") + + elif component and isinstance(volume_padding, list) and len(volume_padding) == len(component): + count = 0 + for comp in component: + padding = [str(pad) for pad in volume_padding[count]] + if comp in self.modeler.user_defined_component_names: + mesh_assembly_arg = ["NAME:" + comp] + mesh_assembly_arg.append("MeshAssemblyBoundingVolumePadding:=") + mesh_assembly_arg.append(padding) + arg2.append(mesh_assembly_arg) + else: + self.logger.warning("{0} does not exist".format(str(comp))) + count += 1 + elif component and isinstance(volume_padding, list) and len(volume_padding) != len(component): + self.logger.error("Volume padding length is different than component list length.") + return False + + if priority and not isinstance(priority, list): + priority = [priority] + + if component and priority: + for p in priority: + if p in self.modeler.user_defined_component_names: + arg3.append(p) + else: + self.logger.warning("{0} does not exist".format(str(p))) + + arg.append(arg2) + arg.append(arg3) + self.odesign.SetDoMeshAssembly(arg) + return True diff --git a/pyaedt/icepak.py b/pyaedt/icepak.py index 8fa6dcdba92..c7ccc225e07 100644 --- a/pyaedt/icepak.py +++ b/pyaedt/icepak.py @@ -4066,7 +4066,7 @@ def assign_solid_block( set to ``"Transient"``, acceptable values are `"Exponential"``, `"Linear"``, ``"Piecewise Linear"``, ``"Power Law"``, ``"Sinusoidal"``, and ``"SquareWave"``. - For the ``"Values"`` key, a list of strings contain the parameters required by - the ``"Function"`` key selection. For example, whn``"Linear"`` is set as the + 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 @@ -4199,7 +4199,7 @@ def assign_hollow_block( When the ``"Type"`` key is set to ``"Transient"``, acceptable values are `"Exponential"``, `"Linear"``, ``"Piecewise Linear"``, ``"Power Law"``, ``"Sinusoidal"``, and ``"Square Wave"``. - For the ``"Values"`` key, a list of strings contain the parameters required by the ``"Function"`` - key selection. For example, whn``"Linear"`` is set as the ``"Function"`` key, two parameters are required: + 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. diff --git a/pyaedt/maxwell.py b/pyaedt/maxwell.py index 114d786f4a7..67acc669ebe 100644 --- a/pyaedt/maxwell.py +++ b/pyaedt/maxwell.py @@ -10,7 +10,6 @@ from pyaedt.application.Analysis3D import FieldAnalysis3D from pyaedt.application.Variables import decompose_variable_value -from pyaedt.generic.DataHandlers import float_units from pyaedt.generic.constants import SOLUTIONS from pyaedt.generic.general_methods import generate_unique_name from pyaedt.generic.general_methods import open_file @@ -1231,6 +1230,25 @@ def assign_force(self, input_object, reference_cs="Global", is_virtual=True, for ---------- >>> oModule.AssignForce + + Examples + -------- + + Assign virtual force to a magnetic object: + + >>> iron_object = m3d.modeler.create_box([0, 0, 0], [2, 10, 10], name="iron") + >>> magnet_object = m3d.modeler.create_box([10, 0, 0], [2, 10, 10], name="magnet") + >>> m3d.assign_material(iron_object, "iron") + >>> m3d.assign_material(magnet_object, "NdFe30") + >>> m3d.assign_force("iron", force_name="force_iron", is_virtual=True) + + Assign Lorentz force to a conductor: + + >>> conductor1 = m3d.modeler.create_box([0, 0, 0], [1, 1, 10], name="conductor1") + >>> conductor2 = m3d.modeler.create_box([10, 0, 0], [1, 1, 10], name="conductor2") + >>> m3d.assign_material(conductor1, "copper") + >>> m3d.assign_material(conductor2, "copper") + >>> m3d.assign_force("conductor1", force_name="force_copper", is_virtual=False) # conductor, use Lorentz force """ if self.solution_type not in ["ACConduction", "DCConduction"]: input_object = self.modeler.convert_to_selections(input_object, True) @@ -2242,7 +2260,7 @@ def _create_boundary(self, name, props, boundary_type): """Create a boundary. Parameters - --------- + ---------- name : str Name of the boundary. props : list @@ -2810,23 +2828,19 @@ def xy_plane(self, value=True): @property def model_depth(self): """Model depth.""" - - if "ModelDepth" in self.design_properties: - value_str = self.design_properties["ModelDepth"] - a = None - try: - a = float_units(value_str) - except: - a = self.variable_manager[value_str].value - finally: - return a + design_settings = self.design_settings() + if "ModelDepth" in design_settings: + value_str = design_settings["ModelDepth"] + return value_str else: return None @model_depth.setter def model_depth(self, value): """Set model depth.""" - return self.change_design_settings({"ModelDepth": self.modeler._arg_with_dim(value, self.modeler.model_units)}) + if isinstance(value, float) or isinstance(value, int): + value = self.modeler._arg_with_dim(value, self.modeler.model_units) + self.change_design_settings({"ModelDepth": value}) @pyaedt_function_handler() def generate_design_data(self, linefilter=None, objectfilter=None): diff --git a/pyaedt/misc/install_extra_toolkits.py b/pyaedt/misc/install_extra_toolkits.py index 8d97f8c5425..3748895fe1e 100644 --- a/pyaedt/misc/install_extra_toolkits.py +++ b/pyaedt/misc/install_extra_toolkits.py @@ -22,13 +22,6 @@ "installation_path": "Project", "package_name": "ansys.aedt.toolkits.choke", }, - "TemplateToolkit": { - "pip": "git+https://github.com/ansys/pyaedt-toolkit-template.git", - "image": "pyansys.png", - "toolkit_script": "ansys/aedt/toolkits/template/run_toolkit.py", - "installation_path": "Project", - "package_name": "ansys.aedt.toolkits.template", - }, } diff --git a/pyaedt/misc/pyaedt_local_config.acf b/pyaedt/misc/pyaedt_local_config.acf index cb73887a0d1..4a496f85e08 100644 --- a/pyaedt/misc/pyaedt_local_config.acf +++ b/pyaedt/misc/pyaedt_local_config.acf @@ -1,7 +1,7 @@ $begin 'Configs' $begin 'Configs' $begin 'DSOConfig' - ConfigName='PyAEDT' + ConfigName='pyaedt_config' DesignType='HFSS' $begin 'DSOMachineList' $begin 'DSOMachineInfo' diff --git a/pyaedt/modeler/advanced_cad/stackup_3d.py b/pyaedt/modeler/advanced_cad/stackup_3d.py index a634d9e623d..1c043f58c51 100644 --- a/pyaedt/modeler/advanced_cad/stackup_3d.py +++ b/pyaedt/modeler/advanced_cad/stackup_3d.py @@ -1333,7 +1333,7 @@ def signals(self): @property def objects(self): - """List of obects created. + """List of objects created. Returns ------- diff --git a/pyaedt/modeler/cad/Modeler.py b/pyaedt/modeler/cad/Modeler.py index e10a55c80e6..ab4bfaa1511 100644 --- a/pyaedt/modeler/cad/Modeler.py +++ b/pyaedt/modeler/cad/Modeler.py @@ -482,7 +482,7 @@ def create( axis_position : int, FacePrimitive, EdgePrimitive, VertexPrimitive Specify where the X or Y axis is pointing. The position must belong to the face where the coordinate system is defined. - Select which axis is considered with the option ``axix``. + Select which axis is considered with the option ``axis``. If a face is specified, the position is placed on the face center. It must be the same as ``face``. If an edge is specified, the position is placed on the edce midpoint. If a vertex is specified, the position is placed on the vertex. @@ -2476,7 +2476,7 @@ def create_face_coordinate_system( axis_position : int, FacePrimitive, EdgePrimitive, VertexPrimitive Specify where the X or Y axis is pointing. The position must belong to the face where the coordinate system is defined. - Select which axis is considered with the option ``axix``. + Select which axis is considered with the option ``axis``. If a face is specified, the position is placed on the face center. It must be the same as ``face``. If an edge is specified, the position is placed on the edce midpoint. If a vertex is specified, the position is placed on the vertex. @@ -4480,6 +4480,9 @@ def unite(self, unite_list, purge=False, keep_originals=False): szSelections = self.convert_to_selections(objs) vArg1 = ["NAME:Selections", "Selections:=", szSelections] vArg2 = ["NAME:UniteParameters", "KeepOriginals:=", keep_originals] + if settings.aedt_version > "2022.2": + vArg2.append("TurnOnNBodyBoolean:=") + vArg2.append(True) self.oeditor.Unite(vArg1, vArg2) if szSelections.split(",")[0] in self.unclassified_names: self.logger.error("Error in uniting objects.") @@ -5296,7 +5299,7 @@ def explicitly_subtract(self, diellist, metallist): @pyaedt_function_handler() def find_port_faces(self, port_sheets): - """Find the vaccums given a list of input sheets. + """Find the vacuums given a list of input sheets. Starting from a list of input sheets, this method creates a list of output sheets that represent the blank parts (vacuums) and the tool parts of all the intersections diff --git a/pyaedt/modeler/cad/Primitives3D.py b/pyaedt/modeler/cad/Primitives3D.py index e00948b94ae..45c352abd12 100644 --- a/pyaedt/modeler/cad/Primitives3D.py +++ b/pyaedt/modeler/cad/Primitives3D.py @@ -72,8 +72,8 @@ def create_box(self, position, dimensions_list, name=None, matname=None): Returns ------- - :class:`pyaedt.modeler.cad.object3d.Object3d` - 3D object. + bool, :class:`pyaedt.modeler.cad.object3d.Object3d` + 3D object or ``False`` if it fails. References ---------- @@ -82,19 +82,32 @@ def create_box(self, position, dimensions_list, name=None, matname=None): Examples -------- + This example shows how to create a box in HFSS. + The required parameters are ``position`` that provides the origin of the + box and ``dimensions_list`` that provide the box sizes. + The optional parameter ``matname`` allows you to set the material name of the box. + The optional parameter ``name`` allows you to assign a name to the box. + + This method applies to all 3D applications: HFSS, Q3D, Icepak, Maxwell 3D, and + Mechanical. >>> from pyaedt import hfss >>> hfss = Hfss() >>> origin = [0,0,0] >>> dimensions = [10,5,20] - >>> #Material and name are not mandatory fields - >>> box_object = hfss.modeler.primivites.create_box(origin, dimensions, name="mybox", matname="copper") + >>> box_object = hfss.modeler.primivites.create_box(position=origin, + ... dimensions_list=dimensions, + ... name="mybox", + ... matname="copper") """ if len(position) != 3: - raise ValueError("Position argument must be a valid three-element list.") + self.logger.error("The ``position`` argument must be a valid three-element list.") + return False if len(dimensions_list) != 3: - raise ValueError("Dimension argument must be a valid 3 element List") + self.logger.error("The ``dimension_list`` argument must be a valid three-element list.") + return False + XPosition, YPosition, ZPosition = self._pos_with_arg(position) XSize, YSize, ZSize = self._pos_with_arg(dimensions_list) vArg1 = ["NAME:BoxParameters"] @@ -130,13 +143,13 @@ def create_cylinder(self, cs_axis, position, radius, height, numSides=0, name=No Name of the cylinder. The default is ``None``, in which case the default name is assigned. matname : str, optional - Name of the material. The default is ''None``, in which case the + Name of the material. The default is ``None``, in which case the default material is assigned. Returns ------- - :class:`pyaedt.modeler.cad.object3d.Object3d` - 3D object. + bool, :class:`pyaedt.modeler.cad.object3d.Object3d` + 3D object or ``False`` if it fails. References ---------- @@ -145,15 +158,31 @@ def create_cylinder(self, cs_axis, position, radius, height, numSides=0, name=No Examples -------- + This example shows how to create a cylinder in HFSS. + The required parameters are ``cs_axis``, ``position``, ``radius``, and ``height``. The + ``cs_axis`` parameter provides the direction axis of the cylinder. The ``position`` + parameter provides the origin of the cylinder. The other two parameters provide + the radius and height of the cylinder. + + The optional parameter ``matname`` allows you to set the material name of the cylinder. + The optional parameter ``name`` allows to assign a name to the cylinder. + + This method applies to all 3D applications: HFSS, Q3D, Icepak, Maxwell 3D, and + Mechanical. + >>> from pyaedt import Hfss >>> aedtapp = Hfss() - >>> cylinder_object = aedtapp.modeler..create_cylinder(cs_axis='Z', position=[0,0,0], + >>> cylinder_object = aedtapp.modeler.create_cylinder(cs_axis='Z', position=[0,0,0], ... radius=2, height=3, name="mycyl", ... matname="vacuum") """ if isinstance(radius, (int, float)) and radius < 0: - raise ValueError("Radius must be greater than 0.") + self.logger.error("The ``radius`` argument must be greater than 0.") + return False + if len(position) != 3: + self.logger.error("The ``position`` argument must be a valid three-element list.") + return False szAxis = GeometryOperators.cs_axis_str(cs_axis) XCenter, YCenter, ZCenter = self._pos_with_arg(position) @@ -177,8 +206,8 @@ def create_cylinder(self, cs_axis, position, radius, height, numSides=0, name=No def create_polyhedron( self, cs_axis=None, - center_position=(0.0, 0.0, 0.0), - start_position=(0.0, 1.0, 0.0), + center_position=[0.0, 0.0, 0.0], + start_position=[0.0, 1.0, 0.0], height=1.0, num_sides=12, name=None, @@ -210,8 +239,8 @@ def create_polyhedron( Returns ------- - :class:`pyaedt.modeler.cad.object3d.Object3d` - 3D object. + bool, :class:`pyaedt.modeler.cad.object3d.Object3d` + 3D object or ``False`` if it fails. References ---------- @@ -220,15 +249,32 @@ def create_polyhedron( Examples -------- + The following examples shows how to create a regular polyhedron in HFSS. + The required parameters are cs_axis that provides the direction axis of the polyhedron, + center_position that provides the center of the polyhedron, start_position of the polyhedron, + height of the polyhedron and num_sides to determine the number of sides. + The parameter matname is optional and allows to set the material name of the polyhedron. + The parameter name is optional and allows to give a name to the polyhedron. + This method applies to all 3D applications: HFSS, Q3D, Icepak, Maxwell 3D, Mechanical. + >>> from pyaedt import Hfss >>> aedtapp = Hfss() >>> ret_obj = aedtapp.modeler.create_polyhedron(cs_axis='X', center_position=[0, 0, 0], ... start_position=[0,5,0], height=0.5, ... num_sides=8, name="mybox", matname="copper") - """ test = cs_axis cs_axis = GeometryOperators.cs_axis_str(cs_axis) + if len(center_position) != 3: + self.logger.error("The ``center_position`` argument must be a valid three-element list.") + return False + if len(start_position) != 3: + self.logger.error("The ``start_position`` argument must be a valid three-element list.") + return False + if center_position == start_position: + self.logger.error("The ``center_position`` and ``start_position`` arguments must be different.") + return False + x_center, y_center, z_center = self._pos_with_arg(center_position) x_start, y_start, z_start = self._pos_with_arg(start_position) @@ -257,7 +303,7 @@ def create_cone(self, cs_axis, position, bottom_radius, top_radius, height, name cs_axis : str Axis of rotation of the starting point around the center point. The default is ``None``, in which case the Z axis is used. - center_position : list, optional + position : list, optional List of ``[x, y, z]`` coordinates for the center position of the bottom of the cone. bottom_radius : float @@ -275,8 +321,8 @@ def create_cone(self, cs_axis, position, bottom_radius, top_radius, height, name Returns ------- - :class:`pyaedt.modeler.cad.object3d.Object3d` - 3D object. + bool, :class:`pyaedt.modeler.cad.object3d.Object3d` + 3D object or ``False`` if it fails. References ---------- @@ -285,6 +331,19 @@ def create_cone(self, cs_axis, position, bottom_radius, top_radius, height, name Examples -------- + This example shows how to create a cone in HFSS. + The required parameters are ``cs_axis``, ``position``, ``bottom_radius``, and + ``top_radius``. The ``cs_axis`` parameter provides the direction axis of + the cone. The ``position`` parameter provides the starting point of the + cone. The ``bottom_radius`` and ``top_radius`` parameters provide the + radius and `eight of the cone. + + The optional parameter ``matname`` allows you to set the material name of the cone. + The optional parameter ``name`` allows you to assign a name to the cone. + + This method applies to all 3D applications: HFSS, Q3D, Icepak, Maxwell 3D, and + Mechanical. + >>> from pyaedt import Hfss >>> aedtapp = Hfss() >>> cone_object = aedtapp.modeler.create_cone(cs_axis='Z', position=[0, 0, 0], @@ -293,13 +352,20 @@ def create_cone(self, cs_axis, position, bottom_radius, top_radius, height, name """ if bottom_radius == top_radius: - raise ValueError("Bottom radius and top radius must have different values.") + self.logger.error("the ``bottom_radius`` and ``top_radius`` arguments must have different values.") + return False if isinstance(bottom_radius, (int, float)) and bottom_radius < 0: - raise ValueError("Bottom radius must be greater than 0.") + self.logger.error("The ``bottom_radius`` argument must be greater than 0.") + return False if isinstance(top_radius, (int, float)) and top_radius < 0: - raise ValueError("Top radius must be greater than 0.") + self.logger.error("The ``top_radius`` argument must be greater than 0.") + return False if isinstance(height, (int, float)) and height <= 0: - raise ValueError("Height must be greater than 0.") + self.logger.error("The ``height`` argument must be greater than 0.") + return False + if len(position) != 3: + self.logger.error("The ``position`` argument must be a valid three-element list.") + return False XCenter, YCenter, ZCenter = self._pos_with_arg(position) szAxis = GeometryOperators.cs_axis_str(cs_axis) @@ -339,8 +405,8 @@ def create_sphere(self, position, radius, name=None, matname=None): Returns ------- - :class:`pyaedt.modeler.cad.object3d.Object3d` - 3D object. + bool, :class:`pyaedt.modeler.cad.object3d.Object3d` + 3D object or ``False`` if it fails. References ---------- @@ -349,16 +415,26 @@ def create_sphere(self, position, radius, name=None, matname=None): Examples -------- + This example shows how to create a sphere in HFSS. + The required parameters are ``position``, which provides the center of the sphere, and + ``radius``, which is the radius of the sphere. The optional parameter ``matname`` + allows you to set the material name of the sphere. The optional parameter + ``name`` allows to assign a name to the sphere. + + This method applies to all 3D applications: HFSS, Q3D, Icepak, Maxwell 3D, and + Mechanical. + >>> from pyaedt import Hfss >>> aedtapp = Hfss() >>> ret_object = aedtapp.modeler.create_sphere(position=[0,0,0], radius=2, ... name="mysphere", matname="copper") - """ if len(position) != 3: - raise ValueError("Position argument must be a valid 3 elements List.") + self.logger.error("The ``position`` argument must be a valid three-element list.") + return False if isinstance(radius, (int, float)) and radius < 0: - raise ValueError("Radius must be greater than 0.") + self.logger.error("The ``radius`` argument must be greater than 0.") + return False XCenter, YCenter, ZCenter = self._pos_with_arg(position) @@ -398,8 +474,8 @@ def create_torus(self, center, major_radius, minor_radius, axis=None, name=None, Returns ------- - :class:`pyaedt.modeler.cad.object3d.Object3d` - 3D object. + bool, :class:`pyaedt.modeler.cad.object3d.Object3d` + 3D object or ``False`` if it fails. References ---------- @@ -409,17 +485,23 @@ def create_torus(self, center, major_radius, minor_radius, axis=None, name=None, Examples -------- Create a torus named ``"mytorus"`` about the Z axis with a major - radius of 1, minor radius of 0.5, and a material of ``"copper"``. + radius of 1 , minor radius of 0.5, and a material of ``"copper"``. + The optional parameter ``matname`` allows you to set the material name of the sphere. + The optional parameter ``name`` allows you to give a name to the sphere. + + This method applies to all 3D applications: HFSS, Q3D, Icepak, Maxwell 3D, and + Mechanical. + >>> from pyaedt import Hfss >>> hfss = Hfss() >>> origin = [0, 0, 0] - >>> torus = hfss.modeler.create_torus(origin, major_radius=1, + >>> torus = hfss.modeler.create_torus(center=origin, major_radius=1, ... minor_radius=0.5, axis="Z", ... name="mytorus", material_name="copper") - """ if len(center) != 3: - raise ValueError("Center argument must be a valid 3 element sequence.") + self.logger.error("The ``center`` argument must be a valid three-element list.") + return False # if major_radius <= 0 or minor_radius <= 0: # raise ValueError("Both major and minor radius must be greater than 0.") # if minor_radius >= major_radius: @@ -525,12 +607,15 @@ def create_bondwire( >>> object_id = hfss.modeler.create_bondwire(origin, endpos,h1=0.5, h2=0.1, alpha=75, beta=4, ... bond_type=0, name="mybox", matname="copper") """ + if len(start_position) != 3: + self.logger.error("The ``start_position`` argument must be a valid three-Element List") + return False x_position, y_position, z_position = self._pos_with_arg(start_position) + if len(end_position) != 3: + self.logger.error("The ``end_position`` argument must be a valid three-Element List") + return False x_position_end, y_position_end, z_position_end = self._pos_with_arg(end_position) - if x_position is None or y_position is None or z_position is None: - raise AttributeError("Position Argument must be a valid 3 Element List") - cont = 0 x_length = None y_length = None @@ -549,8 +634,6 @@ def create_bondwire( z_length = "(" + str(n) + ") - (" + str(m) + ")" cont += 1 - if x_length is None or y_length is None or z_length is None: - raise AttributeError("Dimension Argument must be a valid 3 Element List") if bond_type == 0: bondwire = "JEDEC_5Points" elif bond_type == 1: @@ -623,15 +706,17 @@ def create_rectangle(self, csPlane, position, dimension_list, name=None, matname Returns ------- - :class:`pyaedt.modeler.cad.object3d.Object3d` - 3D object. + bool, :class:`pyaedt.modeler.cad.object3d.Object3d` + 3D object or ``False`` if it fails. References ---------- >>> oEditor.CreateRectangle - """ + if len(dimension_list) != 2: + self.logger.error("The ``dimension_list`` argument must be a valid two-element list.") + return False szAxis = GeometryOperators.cs_plane_to_axis_str(csPlane) XStart, YStart, ZStart = self._pos_with_arg(position) @@ -686,6 +771,27 @@ def create_circle( >>> oEditor.CreateCircle + Examples + -------- + The following example shows how to create a circle in HFSS. + The required parameters are ``cs_plane``, ``position``, ``radius``, + and ``num_sides``. The ``cs_plane`` parameter provides the plane + that the circle is designed on. The ``position`` parameter provides + the origin of the circle. The ``radius`` and ``num_sides`` parameters + provide the radius and number of discrete sides of the circle, + + The optional parameter ``matname`` allows you to set the material name + of the circle. The optional parameter ``name`` allows you to assign a name + to the circle. + + This method applies to all 3D applications: HFSS, Q3D, Icepak, Maxwell 3D, + and Mechanical. + + >>> from pyaedt import Hfss + >>> aedtapp = Hfss() + >>> circle_object = aedtapp.modeler.create_circle(cs_plane='Z', position=[0,0,0], + ... radius=2, num_sides=8, name="mycyl", + ... matname="vacuum") """ non_model_flag = "" if non_model: @@ -741,6 +847,31 @@ def create_ellipse(self, cs_plane, position, major_radius, ratio, is_covered=Tru >>> oEditor.CreateEllipse + Examples + -------- + The following example shows how to create an ellipse in HFSS. + The required parameters are ``cs_plane``, ``position``, ``major_radius``, + ``ratio``, and ``is_covered``. The ``cs_plane`` parameter provides + the plane that the ellipse is designed on. The ``position`` parameter + provides the origin of the ellipse. The ``major_radius`` parameter provides + the radius of the ellipse. The ``ratio`` parameter is a ratio between the + major radius and minor radius of the ellipse. The ``is_covered`` parameter + is a flag indicating if the ellipse is covered. + + The optional parameter ``matname`` allows you to set the material name + of the ellipse. The optional parameter ``name`` allows you to assign a name + to the ellipse. + + This method applies to all 3D applications: HFSS, Q3D, Icepak, Maxwell 3D, + and Mechanical. + + >>> from pyaedt import Hfss + >>> aedtapp = Hfss() + >>> ellipse = aedtapp.modeler.create_ellipse(cs_plane='Z', position=[0,0,0], + ... major_radius=2, ratio=2, is_covered=True, name="myell", + ... matname="vacuum") + + """ szAxis = GeometryOperators.cs_plane_to_axis_str(cs_plane) XStart, YStart, ZStart = self._pos_with_arg(position) @@ -833,6 +964,33 @@ def create_equationbased_curve( >>> oEditor.CreateEquationCurve + Examples + -------- + The following example shows how to create an equation- based curve in HFSS. + The required parameters are ``cs_plane``, ``position``, ``major_radius``, + ``ratio``, and ``is_covered``. The ``cs_plane`` parameter provides + the plane that the ellipse is designed on. The ``position`` parameter + provides the origin of the ellipse. The ``major_radius`` parameter provides + the radius of the ellipse. The ``ratio`` parameter is a ratio between the + major radius and minor radius of the ellipse. The ``is_covered`` parameter + is a flag indicating if the ellipse is covered. + + The optional parameter ``matname`` allows you to set the material name + of the ellipse. The optional parameter ``name`` allows you to assign a name + to the ellipse. + + This method applies to all 3D applications: HFSS, Q3D, Icepak, Maxwell 3D, + and Mechanical. + + >>> from pyaedt import Hfss + >>> aedtapp = Hfss() + >>> eq_xsection = self.aedtapp.modeler.create_equationbased_curve(x_t="_t", + ... y_t="_t*2", + ... num_points=200, + ... z_t=0, + ... t_start=0.2, + ... t_end=1.2, + ... xsection_type="Circle") """ x_section = self._crosssection_arguments( type=xsection_type, @@ -905,18 +1063,49 @@ def create_helix( Returns ------- - :class:`pyaedt.modeler.cad.object3d.Object3d` - 3D object. + bool, :class:`pyaedt.modeler.cad.object3d.Object3d` + 3D object or ``False`` if it fails. References ---------- >>> oEditor.CreateHelix + Examples + -------- + The following example shows how to create a polyline and then create an helix from the polyline. + This method applies to all 3D applications: HFSS, Q3D, Icepak, Maxwell 3D, and + Mechanical. + + >>> from pyaedt import Hfss + >>> aedtapp = Hfss() + >>> udp1 = [0, 0, 0] + >>> udp2 = [5, 0, 0] + >>> udp3 = [10, 5, 0] + >>> udp4 = [15, 3, 0] + >>> polyline = aedtapp.modeler.create_polyline( + ... [udp1, udp2, udp3, udp4], cover_surface=False, name="helix_polyline" + ... ) + + >>> helix_right_turn = aedtapp.modeler.create_helix( + ... polyline_name=polyline.name, + ... position=[0, 0, 0], + ... x_start_dir=0, + ... y_start_dir=1.0, + ... z_start_dir=1.0, + ... num_thread=1, + ... right_hand=True, + ... radius_increment=0.0, + ... thread=1.0, + ... ) """ if not polyline_name or polyline_name == "": - raise ValueError("The name of the polyline cannot be an empty string.") + self.logger.error("The name of the polyline cannot be an empty string.") + return False + if len(position) != 3: + self.logger.error("The ``position`` argument must be a valid three-element list.") + return False x_center, y_center, z_center = self._pos_with_arg(position) vArg1 = ["NAME:Selections"] @@ -958,19 +1147,28 @@ def convert_segments_to_line(self, object_name): Parameters ---------- - object_name : int, str, or Object3d + object_name : int, str, or :class:`pyaedt.modeler.cad.object3d.Object3d` Specified for the object. Returns ------- - :class:`pyaedt.modeler.cad.object3d.Object3d` - 3D object. + bool + ``True`` if successful, ``False`` if it fails. References ---------- >>> oEditor.ChangeProperty + Examples + -------- + + >>> from pyaedt import Hfss + >>> aedtapp = Hfss() + >>> edge_object = aedtapp.modeler.create_object_from_edge("my_edge") + >>> aedtapp.modeler.generate_object_history(edge_object) + >>> aedtapp.modeler.convert_segments_to_line(edge_object.name) + """ this_object = self._resolve_object(object_name) edges = this_object.edges @@ -1010,8 +1208,8 @@ def create_udm( Returns ------- - :class:`pyaedt.modeler.components_3d.UserDefinedComponent` - User-defined component object. + bool, :class:`pyaedt.modeler.components_3d.UserDefinedComponent` + User-defined component object or ``False`` if it fails. References ---------- @@ -1117,11 +1315,15 @@ def create_spiral( Returns ------- - :class:`pyaedt.modeler.Object3d.Polyline` - Polyline object. + bool, :class:`pyaedt.modeler.Object3d.Polyline` + Polyline object or ``False`` if it fails. """ - assert internal_radius > 0, "Internal Radius must be greater than 0." - assert faces > 0, "Faces must be greater than 0." + if internal_radius < 0: + self.logger.error("The ``internal_radius`` argument must be greater than 0.") + return False + if faces < 0: + self.logger.error("The ``faces`` argument must be greater than 0.") + return False dtheta = 2 * pi / faces theta = pi / 2 pts = [(internal_radius, 0, elevation), (internal_radius, internal_radius * tan(dtheta / 2), elevation)] diff --git a/pyaedt/modeler/cad/elements3d.py b/pyaedt/modeler/cad/elements3d.py index b7f82b8f46a..4b3f549022e 100644 --- a/pyaedt/modeler/cad/elements3d.py +++ b/pyaedt/modeler/cad/elements3d.py @@ -69,7 +69,7 @@ class EdgeTypePrimitive(object): @pyaedt_function_handler() def fillet(self, radius=0.1, setback=0.0): - """Add a fillet to the selected edge. + """Add a fillet to the selected edges in 3D/vertices in 2D. Parameters ---------- @@ -98,7 +98,7 @@ def fillet(self, radius=0.1, setback=0.0): if self._object3d.is3d: edge_id_list = [self.id] else: - self._object3d.logger.error("Filet is possible only on a vertex in 2D designs.") + self._object3d.logger.error("Fillet is possible only on a vertex in 2D designs.") return False vArg1 = ["NAME:Selections", "Selections:=", self._object3d.name, "NewPartsModelFlag:=", "Model"] @@ -116,7 +116,7 @@ def fillet(self, radius=0.1, setback=0.0): @pyaedt_function_handler() def chamfer(self, left_distance=1, right_distance=None, angle=45, chamfer_type=0): - """Add a chamfer to the selected edge. + """Add a chamfer to the selected edges in 3D/vertices in 2D. Parameters ---------- diff --git a/pyaedt/modeler/modeler3d.py b/pyaedt/modeler/modeler3d.py index c691e9892ba..48ed3f6288b 100644 --- a/pyaedt/modeler/modeler3d.py +++ b/pyaedt/modeler/modeler3d.py @@ -1390,7 +1390,7 @@ def change_region_padding(self, padding_data, padding_type, direction=None, regi ``True`` if successful, else ``None``. Examples - ---------- + -------- >>> import pyaedt >>> app = pyaedt.Icepak() >>> app.modeler.change_region_padding("10mm", padding_type="Absolute Offset", direction="-X") @@ -1474,7 +1474,7 @@ def change_region_coordinate_system(self, region_cs="Global", region_name="Regio ``True`` if successful, else ``None``. Examples - ---------- + -------- >>> import pyaedt >>> app = pyaedt.Icepak() >>> app.modeler.create_coordinate_system(origin=[1, 1, 1], name="NewCS") diff --git a/pyaedt/modeler/pcb/object3dlayout.py b/pyaedt/modeler/pcb/object3dlayout.py index d1d2cb7e759..9ccf0fdae8f 100644 --- a/pyaedt/modeler/pcb/object3dlayout.py +++ b/pyaedt/modeler/pcb/object3dlayout.py @@ -1879,7 +1879,7 @@ class PDSHole(object): The default is ``"0mm"``. ypos : str, optional The default is ``"0mm"``. - rot : str, otpional + rot : str, optional Rotation in degrees. The default is ``"0deg"``. """ diff --git a/pyaedt/modules/AdvancedPostProcessing.py b/pyaedt/modules/AdvancedPostProcessing.py index 96e6853e576..b5373a42e15 100644 --- a/pyaedt/modules/AdvancedPostProcessing.py +++ b/pyaedt/modules/AdvancedPostProcessing.py @@ -199,7 +199,7 @@ def get_model_plotter_geometries( assert self._app._aedt_version >= "2021.2", self.logger.error("Object is supported from AEDT 2021 R2.") files = [] - if get_objects_from_aedt: + if get_objects_from_aedt and self._app.solution_type not in ["HFSS3DLayout", "HFSS 3D Layout Design"]: files = self.export_model_obj( obj_list=objects, export_as_single_objects=plot_as_separate_objects, @@ -321,6 +321,7 @@ def plot_field_from_fieldplot( dark_mode=False, show_grid=False, show_bounding=False, + show_legend=True, ): """Export a field plot to an image file (JPG or PNG) using Python PyVista. @@ -364,21 +365,25 @@ def plot_field_from_fieldplot( Whether to display the axes grid or not. The default is ``False``. show_bounding : bool, optional Whether to display the axes bounding box or not. The default is ``False``. + show_legend : bool, optional + Whether to display the legend or not. The default is ``True``. Returns ------- :class:`pyaedt.generic.plot.ModelPlotter` Model Object. """ + is_pcb = False + if self._app.solution_type in ["HFSS3DLayout", "HFSS 3D Layout Design"]: + is_pcb = True if not plot_folder: self.ofieldsreporter.UpdateAllFieldsPlots() else: self.ofieldsreporter.UpdateQuantityFieldsPlots(plot_folder) file_to_add = self.export_field_plot(plotname, self._app.working_directory) - model = self.get_model_plotter_geometries(generate_mesh=False, get_objects_from_aedt=plot_cad_objs) - + model.show_legend = show_legend model.off_screen = not show if dark_mode: model.background_color = [40, 40, 40] @@ -395,6 +400,8 @@ def plot_field_from_fieldplot( model.camera_position = view elif view != "isometric": self.logger.warning("Wrong view setup. It has to be one of xy, xz, yz, isometric.") + if is_pcb: + model.z_scale = 5 if scale_min and scale_max: model.range_min = scale_min @@ -427,6 +434,8 @@ def plot_field( dark_mode=False, show_bounding=False, show_grid=False, + show_legend=True, + filter_objects=[], ): """Create a field plot using Python PyVista and export to an image file (JPG or PNG). @@ -477,12 +486,18 @@ def plot_field( Whether to display the axes grid or not. The default is ``False``. show_bounding : bool, optional Whether to display the axes bounding box or not. The default is ``False``. + show_legend : bool, optional + Whether to display the legend or not. The default is ``True``. + filter_objects : list, optional + Objects list for filtering the ``CutPlane`` plots. Returns ------- :class:`pyaedt.generic.plot.ModelPlotter` Model Object. """ + if os.getenv("PYAEDT_DOC_GENERATION", "False").lower() in ("true", "1", "t"): # pragma: no cover + show = False if not setup_name: setup_name = self._app.existing_analysis_sweeps[0] if not intrinsics: @@ -496,7 +511,9 @@ def plot_field( elif plot_type == "Volume": plotf = self.create_fieldplot_volume(object_list, quantity, setup_name, intrinsics) else: - plotf = self.create_fieldplot_cutplane(object_list, quantity, setup_name, intrinsics) + plotf = self.create_fieldplot_cutplane( + object_list, quantity, setup_name, intrinsics, filter_objects=filter_objects + ) # if plotf: # file_to_add = self.export_field_plot(plotf.name, self._app.working_directory, plotf.name) @@ -516,6 +533,7 @@ def plot_field( dark_mode=dark_mode, show_grid=show_grid, show_bounding=show_bounding, + show_legend=show_legend, ) if not keep_plot_after_generation: plotf.delete() @@ -545,6 +563,8 @@ def plot_animated_field( dark_mode=False, show_grid=False, show_bounding=False, + show_legend=True, + filter_objects=[], ): """Create an animated field plot using Python PyVista and export to a gif file. @@ -596,12 +616,18 @@ def plot_animated_field( Whether to display the axes grid or not. The default is ``False``. show_bounding : bool, optional Whether to display the axes bounding box or not. The default is ``False``. + show_legend : bool, optional + Whether to display the legend or not. The default is ``True``. + filter_objects : list, optional + Objects list for filtering the ``CutPlane`` plots. Returns ------- :class:`pyaedt.generic.plot.ModelPlotter` Model Object. """ + if os.getenv("PYAEDT_DOC_GENERATION", "False").lower() in ("true", "1", "t"): # pragma: no cover + show = False if intrinsics is None: intrinsics = {} if not export_path: @@ -622,7 +648,9 @@ def plot_animated_field( elif plot_type == "Volume": plotf = self.create_fieldplot_volume(object_list, quantity, setup_name, intrinsics) else: - plotf = self.create_fieldplot_cutplane(object_list, quantity, setup_name, intrinsics) + plotf = self.create_fieldplot_cutplane( + object_list, quantity, setup_name, intrinsics, filter_objects=filter_objects + ) if plotf: file_to_add = self.export_field_plot(plotf.name, export_path, plotf.name + str(v)) if file_to_add: @@ -637,6 +665,7 @@ def plot_animated_field( model.background_color = [40, 40, 40] model.bounding_box = show_bounding model.show_grid = show_grid + model.show_legend = show_legend if fields_to_add: model.add_frames_from_file(fields_to_add, log_scale=log_scale) if export_gif: diff --git a/pyaedt/modules/Boundary.py b/pyaedt/modules/Boundary.py index 48572ecbfcc..630fd3937cf 100644 --- a/pyaedt/modules/Boundary.py +++ b/pyaedt/modules/Boundary.py @@ -225,7 +225,7 @@ def targetcs(self): Returns ------- str - Native Component Coordinate System + Native Component Coordinate System. """ if "TargetCS" in list(self.props.keys()): return self.props["TargetCS"] @@ -3690,7 +3690,7 @@ def auto_update(self, b): Parameters ---------- - b: bool + b : bool Whether to enable auto-update. """ @@ -3848,7 +3848,7 @@ def name(self, new_network_name): Parameters ---------- - new_network_name: str + new_network_name : str New name of the network. """ bound_names = [b.name for b in self._app.boundaries] @@ -3929,11 +3929,11 @@ def add_boundary_node(self, name, assignment_type, value): Parameters ---------- - name: str + name : str Name of the node. - assignment_type: str + assignment_type : str Type assignment. Options are ``"Power"`` and ``"Temperature"``. - value: str or float or dict + value : str or float or dict String, float, or dictionary containing the value of the assignment. If a float is passed the ``"W"`` or ``"cel"`` unit is used, depending on the selection for the ``assignment_type`` parameter. If ``"Power"` @@ -3943,7 +3943,7 @@ def add_boundary_node(self, name, assignment_type, value): Returns ------- bool - True if successful. + ``True`` if successful. Examples -------- @@ -4082,7 +4082,7 @@ def add_nodes_from_dictionaries(self, nodes_dict): Add nodes to the network from dictionary. Parameters - ------- + ---------- nodes_dict : list or dict A dictionary or list of dictionaries containing nodes to add to the network. Different node types require different key and value pairs: @@ -4431,8 +4431,8 @@ def props(self, props): Set properties of the node. Parameters - ------- - props: dict + ---------- + props : dict Node properties. """ self._props = props diff --git a/pyaedt/modules/Material.py b/pyaedt/modules/Material.py index 485888cd68f..e07e596693c 100644 --- a/pyaedt/modules/Material.py +++ b/pyaedt/modules/Material.py @@ -14,6 +14,7 @@ """ from collections import OrderedDict import copy +import warnings from pyaedt.generic.DataHandlers import _dict2arg from pyaedt.generic.constants import CSS4_COLORS @@ -33,6 +34,7 @@ class MatProperties(object): "conductivity", "dielectric_loss_tangent", "magnetic_loss_tangent", + "magnetic_coercivity", "thermal_conductivity", "mass_density", "specific_heat", @@ -43,13 +45,41 @@ class MatProperties(object): "molecular_mass", "viscosity", ] - defaultvalue = [1.0, 1.0, 0, 0, 0, 0.01, 0, 0, 0, 0, 0, 0.8, 0, 0, 0, 0, 0, 0] + defaultvalue = [ + 1.0, + 1.0, + 0, + 0, + 0, + OrderedDict( + { + "Magnitude": 0, + "DirComp1": 1, + "DirComp2": 0, + "DirComp3": 0, + } + ), + 0.01, + 0, + 0, + 0, + 0, + 0, + 0.8, + 0, + 0, + 0, + 0, + 0, + 0, + ] defaultunit = [ None, None, "[siemens m^-1]", None, None, + None, "[W m^-1 C^-1]", "[Kg m^-3]", "[J Kg^-1 C^-1]", @@ -167,7 +197,7 @@ def get_defaultvalue(cls, aedtname=None): class ClosedFormTM(object): - """Manges closed-form thermal modifiers.""" + """Manages closed-form thermal modifiers.""" Tref = "22cel" C1 = 0 @@ -235,10 +265,10 @@ def __init__(self, material, name, val=None, thermalmodifier=None, spatialmodifi if val is not None and isinstance(val, (str, float, int)): self.value = val - elif val is not None and val["property_type"] == "AnisoProperty": + elif val is not None and "property_type" in val.keys() and val["property_type"] == "AnisoProperty": self.type = "anisotropic" self.value = [val["component1"], val["component2"], val["component3"]] - elif val is not None and val["property_type"] == "nonlinear": + elif val is not None and "property_type" in val.keys() and val["property_type"] == "nonlinear": self.type = "nonlinear" for e, v in val.items(): if e == "BTypeForSingleCurve": @@ -254,6 +284,16 @@ def __init__(self, material, name, val=None, thermalmodifier=None, spatialmodifi self._unit = v["DimUnits"] elif e == "Temperatures": self.temperatures = v + elif val is not None and isinstance(val, OrderedDict) and "Magnitude" in val.keys(): + self.type = "vector" + magnitude = val["Magnitude"] + units = None + if isinstance(magnitude, str): + units = "".join(filter(lambda c: c.isalpha() or c == "_", val["Magnitude"])) + magnitude = "".join(filter(str.isdigit, val["Magnitude"])) + if units: + self.unit = units + self.value = [str(magnitude), str(val["DirComp1"]), str(val["DirComp2"]), str(val["DirComp3"])] if not isinstance(thermalmodifier, list): thermalmodifier = [thermalmodifier] for tm in thermalmodifier: @@ -281,8 +321,8 @@ def type(self): Parameters ---------- type : str - Type of properties. Options are ``simple"``, - ``"anisotropic",`` ``"tensor"``, and ``"nonlinear",`` + Type of properties. Options are ``"simple"``, + ``"anisotropic"``, ``"tensor"``, ``"vector"``, and ``"nonlinear"`` """ return self._type @@ -307,6 +347,11 @@ def type(self, type): @property def evaluated_value(self): """Evaluated value.""" + evaluated_expression = [] + if isinstance(self.value, list): + for value in self.value: + evaluated_expression.append(self._material._materials._app.evaluate_expression(value)) + return evaluated_expression return self._material._materials._app.evaluate_expression(self.value) @property @@ -322,7 +367,7 @@ def value(self, val): if isinstance(val, list) and isinstance(val[0], list): self._property_value[0].value = val self.set_non_linear() - elif isinstance(val, list): + elif isinstance(val, list) and self.type != "vector": if len(val) == 3: self.type = "anisotropic" elif len(val) == 9: @@ -337,6 +382,12 @@ def value(self, val): i += 1 if self._material._material_update: self._material._update_props(self.name, val) + + elif isinstance(val, list) and self.type == "vector": + if len(val) == 4: + self._property_value[0].value = val + if self._material._material_update: + self._material._update_props(self.name, val) else: self.type = "simple" self._property_value[0].value = val @@ -1044,7 +1095,7 @@ def __init__(self, materials, name, props=None): self._oproject = self._materials._oproject self.logger = self._materials.logger self.name = name - self.coordinate_system = "" + self._coordinate_system = "" self.is_sweep_material = False if props: self._props = props.copy() @@ -1069,13 +1120,24 @@ def __init__(self, materials, name, props=None): self.mod_since_lib = self._props["ModSinceLib"] del self._props["ModSinceLib"] + @property + def coordinate_system(self): + """Material coordinate system.""" + return self._coordinate_system + + @coordinate_system.setter + def coordinate_system(self, value): + if value in ["Cartesian", "Cylindrical", "Spherical"]: + self._coordinate_system = value + self._update_props("CoordinateSystemType", value) + @pyaedt_function_handler() def _get_args(self, props=None): """Retrieve the arguments for a property. Parameters ---------- - prop : str, optoinal + prop : str, optional Name of the property. The default is ``None``. """ if not props: @@ -1158,6 +1220,9 @@ def _update_props(self, propname, provpavlue, update_aedt=True): self._props[propname] = OrderedDict({"property_type": "nonlinear", pr_name: bh}) if update_aedt: return self.update() + elif isinstance(provpavlue, list) and material_props_type and material_props_type == "vector": + if propname == "magnetic_coercivity": + return self.set_magnetic_coercivity(provpavlue[0], provpavlue[1], provpavlue[2], provpavlue[3]) return False @@ -1551,6 +1616,28 @@ def diffusivity(self): def diffusivity(self, value): self._diffusivity.value = value + @property + def magnetic_coercivity(self): + """Magnetic coercivity. + + Returns + ------- + :class:`pyaedt.modules.Material.MatProperty` + Magnetic coercivity of the material. + + References + ---------- + + >>> oDefinitionManager.EditMaterial + """ + return self._magnetic_coercivity + + @magnetic_coercivity.setter + def magnetic_coercivity(self, value): + if isinstance(value, list) and len(value) == 4: + self.set_magnetic_coercivity(value[0], value[1], value[2], value[3]) + self._magnetic_coercivity.value = value + @property def molecular_mass(self): """Molecular mass. @@ -1839,8 +1926,24 @@ def stacking_direction(self, value): self._update_props("stacking_direction", OrderedDict({"property_type": "ChoiceProperty", "Choice": value})) @pyaedt_function_handler() - def set_magnetic_coercitivity(self, value=0, x=1, y=0, z=0): - """Set Magnetic Coercitivity for material. + def set_magnetic_coercitivity(self, value=0, x=1, y=0, z=0): # pragma: no cover + """Set magnetic coercivity for material. + + .. deprecated:: 0.7.0 + + Returns + ------- + bool + + """ + warnings.warn( + "`set_magnetic_coercitivity` is deprecated. Use `set_magnetic_coercivity` instead.", DeprecationWarning + ) + return self.set_magnetic_coercivity(value, x, y, z) + + @pyaedt_function_handler() + def set_magnetic_coercivity(self, value=0, x=1, y=0, z=0): + """Set magnetic coercivity for material. Parameters ---------- @@ -2081,8 +2184,8 @@ def get_curve_coreloss_values(self): return out @pyaedt_function_handler() - def get_magnetic_coercitivity(self): - """Get the magnetic coercitivity values. + def get_magnetic_coercivity(self): + """Get the magnetic coercivity values. Returns ------- @@ -2098,6 +2201,22 @@ def get_magnetic_coercitivity(self): ) return False + @pyaedt_function_handler() + def get_magnetic_coercitivity(self): # pragma: no cover + """Get the magnetic coercivity values. + + .. deprecated:: 0.7.0 + + Returns + ------- + bool + + """ + warnings.warn( + "`get_magnetic_coercitivity` is deprecated. Use `get_magnetic_coercivity` instead.", DeprecationWarning + ) + return self.get_magnetic_coercivity() + @pyaedt_function_handler() def is_conductor(self, threshold=100000): """Check if the material is a conductor. diff --git a/pyaedt/modules/MeshIcepak.py b/pyaedt/modules/MeshIcepak.py index b3df7d30034..9773f3bbed6 100644 --- a/pyaedt/modules/MeshIcepak.py +++ b/pyaedt/modules/MeshIcepak.py @@ -187,7 +187,7 @@ def autosettings(self): if self.SubModels: arg.append("SubModels:=") arg.append(self.SubModels) - else: + if self.Objects: arg.append("Objects:=") arg.append(self.Objects) arg.extend(self._new_versions_fields) @@ -242,7 +242,7 @@ def manualsettings(self): if self.SubModels: arg.append("SubModels:=") arg.append(self.SubModels) - else: + if self.Objects: arg.append("Objects:=") arg.append(self.Objects) arg.extend(self._new_versions_fields) @@ -669,12 +669,23 @@ def assign_mesh_region(self, objectlist=[], level=5, is_submodel=False, name=Non except Exception: # pragma : no cover created = False if created: - objectlist2 = self.modeler.object_names - added_obj = [i for i in objectlist2 if i not in all_objs] - if not added_obj: - added_obj = [i for i in objectlist2 if i not in all_objs or i in objectlist] - meshregion.Objects = added_obj - meshregion.SubModels = None + if virtual_region and self._app.check_beta_option_enabled( + "S544753_ICEPAK_VIRTUALMESHREGION_PARADIGM" + ): # pragma : no cover + if is_submodel: + meshregion.Objects = [i for i in objectlist if i in all_objs] + meshregion.SubModels = [i for i in objectlist if i not in all_objs] + else: + meshregion.Objects = objectlist + meshregion.SubModels = None + else: + objectlist2 = self.modeler.object_names + added_obj = [i for i in objectlist2 if i not in all_objs] + if not added_obj: + added_obj = [i for i in objectlist2 if i not in all_objs or i in objectlist] + meshregion.Objects = added_obj + meshregion.SubModels = None + meshregion.update() return meshregion else: diff --git a/pyaedt/modules/PostProcessor.py b/pyaedt/modules/PostProcessor.py index fbdf473ded6..286207b85f1 100644 --- a/pyaedt/modules/PostProcessor.py +++ b/pyaedt/modules/PostProcessor.py @@ -751,7 +751,7 @@ def available_quantities_categories( Report Display Type. Default is `None` which will take first default type which is in most of the case "Rectangular Plot". solution : str, optional - Report Setup. Default is `None` which will take first nominal_adpative solution. + Report Setup. Default is `None` which will take first nominal_adaptive solution. context : str, optional Report Category. Default is `""` which will take first default context. is_siwave_dc : bool, optional @@ -816,7 +816,7 @@ def available_report_quantities( Report Display Type. Default is ``None`` which will take first default type which is in most of the case "Rectangular Plot". solution : str, optional - Report Setup. Default is `None` which will take first nominal_adpative solution. + Report Setup. Default is `None` which will take first nominal_adaptive solution. quantities_category : str, optional The category to which quantities belong. It has to be one of ``available_quantities_categories`` method. Default is ``None`` which will take first default quantity.". @@ -1145,7 +1145,18 @@ def steal_focus_oneditor(self): return True @pyaedt_function_handler() - def export_report_to_file(self, output_dir, plot_name, extension, unique_file=False): + def export_report_to_file( + self, + output_dir, + plot_name, + extension, + unique_file=False, + uniform=False, + start=None, + end=None, + step=None, + use_trace_number_format=False, + ): """Export a 2D Plot data to a file. This method leaves the data in the plot (as data) as a reference @@ -1167,6 +1178,20 @@ def export_report_to_file(self, output_dir, plot_name, extension, unique_file=Fa * (Ansoft Report Data Files) .rdat unique_file : bool If set to True, generates unique file in output_dit + uniform : bool, optional + Whether the export uniform points to the file. The + default is ``False``. + start : str, optional + Start range with units for the sweep if the ``uniform`` parameter + is set to ``True``. + end : str, optional + End range with units for the sweep if the ``uniform`` parameter + is set to ``True``. + step : str, optional + Step range with units for the sweep if the ``uniform`` parameter is + set to ``True``. + use_trace_number_format : bool, optional + Whether to use trace number formats. The default is ``False``. Returns ------- @@ -1177,6 +1202,7 @@ def export_report_to_file(self, output_dir, plot_name, extension, unique_file=Fa ---------- >>> oModule.ExportReportDataToFile + >>> oModule.ExportUniformPointsToFile >>> oModule.ExportToFile Examples @@ -1204,13 +1230,18 @@ def export_report_to_file(self, output_dir, plot_name, extension, unique_file=Fa if extension == ".rdat": self.oreportsetup.ExportReportDataToFile(plot_name, file_path) + elif uniform: + self.oreportsetup.ExportUniformPointsToFile(plot_name, file_path, start, end, step, use_trace_number_format) + else: self.oreportsetup.ExportToFile(plot_name, file_path) return file_path @pyaedt_function_handler() - def export_report_to_csv(self, project_dir, plot_name): + def export_report_to_csv( + self, project_dir, plot_name, uniform=False, start=None, end=None, step=None, use_trace_number_format=False + ): """Export the 2D Plot data to a CSV file. This method leaves the data in the plot (as data) as a reference @@ -1222,6 +1253,20 @@ def export_report_to_csv(self, project_dir, plot_name): Path to the project directory. The csv file will be plot_name.csv. plot_name : str Name of the plot to export. + uniform : bool, optional + Whether the export uniform points to the file. The + default is ``False``. + start : str, optional + Start range with units for the sweep if the ``uniform`` parameter + is set to ``True``. + end : str, optional + End range with units for the sweep if the ``uniform`` parameter + is set to ``True``. + step : str, optional + Step range with units for the sweep if the ``uniform`` parameter is + set to ``True``. + use_trace_number_format : bool, optional + Whether to use trace number formats. The default is ``False``. Returns ------- @@ -1233,8 +1278,18 @@ def export_report_to_csv(self, project_dir, plot_name): >>> oModule.ExportReportDataToFile >>> oModule.ExportToFile + >>> oModule.ExportUniformPointsToFile """ - return self.export_report_to_file(project_dir, plot_name, extension=".csv") + return self.export_report_to_file( + project_dir, + plot_name, + extension=".csv", + uniform=uniform, + start=start, + end=end, + step=step, + use_trace_number_format=use_trace_number_format, + ) @pyaedt_function_handler() def export_report_to_jpg(self, project_dir, plot_name, width=0, height=0): @@ -1247,9 +1302,9 @@ def export_report_to_jpg(self, project_dir, plot_name, width=0, height=0): plot_name : str Name of the plot to export. width : int, optional - Image width. Default is ``0`` which takes Desktop size or 500 pixel in case of non-graphical mode. + Image width. Default is ``0`` which takes Desktop size or 1980 pixel in case of non-graphical mode. height : int, optional - Image height. Default is ``0`` which takes Desktop size or 500 pixel in case of non-graphical mode. + Image height. Default is ``0`` which takes Desktop size or 1020 pixel in case of non-graphical mode. Returns ------- @@ -1264,11 +1319,11 @@ def export_report_to_jpg(self, project_dir, plot_name, width=0, height=0): # path npath = project_dir file_name = os.path.join(npath, plot_name + ".jpg") # name of the image file - if self._app.desktop_class.non_graphical: + if self._app.desktop_class.non_graphical: # pragma: no cover if width == 0: - width = 500 + width = 1980 if height == 0: - height = 500 + height = 1020 self.oreportsetup.ExportImageToFile(plot_name, file_name, width, height) return True @@ -3450,7 +3505,7 @@ def export_model_obj(self, obj_list=None, export_path=None, export_as_single_obj if not self._app.modeler[el].display_wireframe: transp = 0.6 t = self._app.modeler[el].transparency - if t: + if t is not None: transp = t files_exported.append([fname, self._app.modeler[el].color, 1 - transp]) else: diff --git a/pyproject.toml b/pyproject.toml index 768973cda8d..b771bc9f609 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,9 +25,10 @@ classifiers = [ ] dependencies = [ - "cffi == 1.15.1;platform_system=='Linux'", + "cffi == 1.15.1;platform_system=='Linux' and python_version == '3.7'", + "cffi == 1.16.0;platform_system=='Linux' and python_version > '3.7'", "pywin32 >= 303;platform_system=='Windows'", - "pythonnet == 3.0.2", + "ansys-pythonnet>=3.1.0rc2", "rpyc==5.3.1", "psutil", "dotnetcore2 ==3.1.23;platform_system=='Linux'", @@ -36,8 +37,8 @@ dependencies = [ [project.optional-dependencies] tests = [ "ipython==8.13.0; python_version < '3.9'", - "ipython==8.15.0; python_version >= '3.9'", - "imageio==2.31.4", + "ipython==8.16.1; python_version >= '3.9'", + "imageio==2.31.5", "joblib==1.3.2", "matplotlib==3.5.3; python_version == '3.7'", "matplotlib==3.7.3; python_version == '3.8'", @@ -61,11 +62,11 @@ tests = [ "scikit-rf", ] doc = [ - "ansys-sphinx-theme==0.12.0", - "imageio==2.31.4", + "ansys-sphinx-theme==0.12.2", + "imageio==2.31.5", "imageio-ffmpeg==0.4.9", "ipython==8.13.0; python_version < '3.9'", - "ipython==8.15.0; python_version >= '3.9'", + "ipython==8.16.1; python_version >= '3.9'", "ipywidgets==8.1.1", "joblib==1.3.2", "jupyterlab==4.0.6", @@ -73,7 +74,8 @@ doc = [ "matplotlib==3.7.3; python_version == '3.8'", "matplotlib==3.8.0; python_version > '3.8'", "nbsphinx==0.9.3", - "numpydoc==1.6.0", + "numpydoc==1.5.0; python_version == '3.7'", + "numpydoc==1.6.0; python_version > '3.7'", "osmnx", "pypandoc==1.11", "pytest-sphinx==0.5.0", @@ -178,4 +180,29 @@ testpaths = [ "_unittest", ] - +[tool.numpydoc_validation] +checks = [ + "GL06", # Found unknown section + "GL07", # Sections are in the wrong order. + "GL08", # The object does not have a docstring + "GL09", # Deprecation warning should precede extended summary + "GL10", # reST directives {directives} must be followed by two colons + # Return + "RT04", # Return value description should start with a capital letter" + "RT05", # Return value description should finish with "." + # Summary + "SS01", # No summary found + "SS02", # Summary does not start with a capital letter + "SS03", # Summary does not end with a period + "SS04", # Summary contains heading whitespaces + "SS05", # Summary must start with infinitive verb, not third person + # Parameters + "PR10", # Parameter "{param_name}" requires a space before the colon + # separating the parameter name and type", +] +exclude = [ + '\.AEDTMessageManager.add_message$', # bad SS05 + '\.Modeler3D\.create_choke$', # bad RT05 + '\._unittest\', # missing docstring for tests + 'HistoryProps.', # bad RT05 because of the base class named OrderedDict +]