diff --git a/.github/labeler.yml b/.github/labeler.yml index f0ccc72fd79..ffb1e7ed0f9 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,22 +1,12 @@ documentation: -- doc/source/**/* -- README.md -- README_CN.md +- any-glob-to-any-file: ["doc/source/**/*", "README.md", "README_CN.md"] maintenance: -- .github/**/* -- .flake8 -- setup.py -- setup-distutils.py +- any-glob-to-any-file: [".github/**/*", ".flake8", "pyproject.toml"] dependencies: -- requirements/**/* +- any-glob-to-any-file: [requirements/**/*] testing: -- _unittest/conftest.py -- _unittest_ironpython/run_unittests.py -- _unittest_ironpython/run_unittests_batchmode.cmd +- any-glob-to-any-file: ["_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 +- any-glob-to-any-file: ["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/build_documentation.yml b/.github/workflows/build_documentation.yml index a35b6f62d80..98a71cf956c 100644 --- a/.github/workflows/build_documentation.yml +++ b/.github/workflows/build_documentation.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Check documentation style" - uses: ansys/actions/doc-style@v4 + uses: ansys/actions/doc-style@v5 with: token: ${{ secrets.GITHUB_TOKEN }} vale-config: "doc/.vale.ini" @@ -36,7 +36,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 diff --git a/.github/workflows/cpython_linux.yml b/.github/workflows/cpython_linux.yml index 4d10c19eff2..4275fa0134b 100644 --- a/.github/workflows/cpython_linux.yml +++ b/.github/workflows/cpython_linux.yml @@ -38,7 +38,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} architecture: 'x86' @@ -84,7 +84,7 @@ jobs: pytest --tx 2*popen --durations=50 --dist loadfile -v _unittest_solvers - name: Upload pytest test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: pytest-results path: junit/test-results.xml diff --git a/.github/workflows/full_documentation.yml b/.github/workflows/full_documentation.yml index 4f80360f4f5..7c8331ab2e8 100644 --- a/.github/workflows/full_documentation.yml +++ b/.github/workflows/full_documentation.yml @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -79,21 +79,21 @@ jobs: # .\doc\make.bat pdf - name: Upload HTML documentation artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: documentation-html path: doc/_build/html retention-days: 7 - name: Upload HTML documentation artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: documentation-html-edb path: doc/_build/html/EDBAPI retention-days: 7 # - name: Upload PDF documentation artifact -# uses: actions/upload-artifact@v3 +# uses: actions/upload-artifact@v4 # with: # name: documentation-pdf # path: doc/_build/pdf @@ -114,7 +114,7 @@ jobs: if: github.event_name == 'push' && contains(github.ref, 'refs/tags') steps: - name: Deploy the stable documentation - uses: ansys/actions/doc-deploy-stable@v4 + uses: ansys/actions/doc-deploy-stable@v5 with: cname: ${{ env.DOCUMENTATION_CNAME }} token: ${{ secrets.GITHUB_TOKEN }} @@ -129,13 +129,13 @@ jobs: steps: - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ env.MAIN_PYTHON_VERSION }} - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 - name: Display structure of downloaded files run: ls -R @@ -153,7 +153,7 @@ jobs: echo "VERSION_MEILI=$VERSION_MEILI" >> $GITHUB_ENV - name: "Deploy the stable documentation index for PyAEDT API" - uses: ansys/actions/doc-deploy-index@v4 + uses: ansys/actions/doc-deploy-index@v5 with: cname: ${{ env.DOCUMENTATION_CNAME }}/version/${{ env.VERSION }} index-name: pyaedt-v${{ env.VERSION_MEILI }} @@ -162,7 +162,7 @@ jobs: pymeilisearchopts: --stop_urls \"EDBAPI\" # Add EDB API as another index. - name: "Deploy the stable documentation index for EDB API" - uses: ansys/actions/doc-deploy-index@v4 + uses: ansys/actions/doc-deploy-index@v5 with: cname: ${{ env.DOCUMENTATION_CNAME }}/version/${{ env.VERSION }}/EDBAPI/ index-name: pyedb-v${{ env.VERSION_MEILI }} diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index c801a34dd44..9027021d052 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -32,10 +32,9 @@ jobs: # Label based on modified files - name: Label based on changed files - uses: actions/labeler@v4 + uses: actions/labeler@v5 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" - sync-labels: '' # Label based on branch name - uses: actions-ecosystem/action-add-labels@v1 diff --git a/.github/workflows/nightly-docs.yml b/.github/workflows/nightly-docs.yml index 2aa10e4283c..b70bbb0bf28 100644 --- a/.github/workflows/nightly-docs.yml +++ b/.github/workflows/nightly-docs.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 @@ -39,14 +39,14 @@ jobs: make -C doc phtml - name: Upload documentation HTML artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: documentation-html path: doc/_build/html retention-days: 7 - name: Upload HTML documentation artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: documentation-html-edb path: doc/_build/html/EDBAPI @@ -58,7 +58,7 @@ jobs: steps: - name: Deploy development documentation - uses: ansys/actions/doc-deploy-dev@v4 + uses: ansys/actions/doc-deploy-dev@v5 with: cname: ${{ env.DOCUMENTATION_CNAME }} token: ${{ secrets.GITHUB_TOKEN }} @@ -70,13 +70,13 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 - name: Display structure of downloaded files run: ls -R - name: "Deploy the dev documentation index for PyAEDT API" - uses: ansys/actions/doc-deploy-index@v4 + uses: ansys/actions/doc-deploy-index@v5 with: cname: ${{ env.DOCUMENTATION_CNAME }}/version/dev index-name: pyaedt-vdev @@ -85,7 +85,7 @@ jobs: pymeilisearchopts: --stop_urls \"EDBAPI\" # Add EDB API as another index to show it in dropdown button - name: "Deploy the dev documentation index for EDB API" - uses: ansys/actions/doc-deploy-index@v4 + uses: ansys/actions/doc-deploy-index@v5 with: cname: ${{ env.DOCUMENTATION_CNAME }}/version/dev/EDBAPI/ index-name: pyedb-vdev diff --git a/.github/workflows/unit_test_prerelease.yml b/.github/workflows/unit_test_prerelease.yml index 1c79c395a48..272a6ec491c 100644 --- a/.github/workflows/unit_test_prerelease.yml +++ b/.github/workflows/unit_test_prerelease.yml @@ -40,7 +40,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -80,7 +80,7 @@ jobs: name: 'Upload coverage to Codecov' - name: Upload pytest test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: pytest-results path: junit/test-results.xml diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 8242a8d8749..cdefc1e64d6 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -38,7 +38,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -83,9 +83,9 @@ jobs: name: 'Upload coverage to Codecov' - name: Upload pytest test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: pytest-results + name: pytest-solver-results path: junit/test-results.xml # Use always() to always run this step to publish test results when there are test failures if: ${{ always() }} @@ -101,7 +101,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -146,7 +146,7 @@ jobs: name: 'Upload coverage to Codecov' - name: Upload pytest test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: pytest-results path: junit/test-results.xml diff --git a/.github/workflows/wheelhouse.yml b/.github/workflows/wheelhouse.yml index 2f8a07db1ff..4f8e7747e86 100644 --- a/.github/workflows/wheelhouse.yml +++ b/.github/workflows/wheelhouse.yml @@ -36,7 +36,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -75,7 +75,7 @@ jobs: dest: ${{ env.PACKAGE_NAME }}-v${{ steps.version.outputs.PYAEDT_VERSION }}-${{ runner.os }}-${{ matrix.python-version }}.zip - name: Upload Wheelhouse - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.PACKAGE_NAME }}-v${{ steps.version.outputs.PYAEDT_VERSION }}-${{ runner.os }}-${{ matrix.python-version }} path: '*.zip' diff --git a/.github/workflows/wheelhouse_linux.yml b/.github/workflows/wheelhouse_linux.yml index af755cdb9b8..7bb53664da2 100644 --- a/.github/workflows/wheelhouse_linux.yml +++ b/.github/workflows/wheelhouse_linux.yml @@ -36,7 +36,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -74,7 +74,7 @@ jobs: dest: ${{ env.PACKAGE_NAME }}-v${{ steps.version.outputs.PYAEDT_VERSION }}-wheelhouse-${{ runner.os }}-${{ matrix.python-version }}.zip - name: Upload Wheelhouse - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.PACKAGE_NAME }}-v${{ steps.version.outputs.PYAEDT_VERSION }}-wheelhouse-${{ runner.os }}-${{ matrix.python-version }} path: '*.zip' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f8497aa770..6c123dbddd0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,14 +17,14 @@ exclude: | repos: - repo: https://github.com/psf/black - rev: 23.11.0 # IF VERSION CHANGES --> MODIFY "blacken-docs" MANUALLY AS WELL!! + rev: 23.12.0 # IF VERSION CHANGES --> MODIFY "blacken-docs" MANUALLY AS WELL!! hooks: - id: black args: - --line-length=120 - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort name: isort (python) @@ -53,7 +53,7 @@ repos: # validate GitHub workflow files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.27.2 + rev: 0.27.3 hooks: - id: check-github-workflows @@ -61,7 +61,7 @@ repos: rev: 1.16.0 hooks: - id: blacken-docs - additional_dependencies: [black==23.11.0] + additional_dependencies: [black==23.12.0] # - repo: https://github.com/numpy/numpydoc diff --git a/_unittest/example_models/T12/coax_setup_solved_231.aedtz b/_unittest/example_models/T12/coax_setup_solved_231.aedtz index 72ab5f470d8..7470fa2d89f 100644 Binary files a/_unittest/example_models/T12/coax_setup_solved_231.aedtz and b/_unittest/example_models/T12/coax_setup_solved_231.aedtz differ diff --git a/_unittest/example_models/TEDB/test_merge_polygon.aedb/edb.def b/_unittest/example_models/TEDB/test_merge_polygon.aedb/edb.def new file mode 100644 index 00000000000..ae22e84345e Binary files /dev/null and b/_unittest/example_models/TEDB/test_merge_polygon.aedb/edb.def differ diff --git a/_unittest/test_00_EDB.py b/_unittest/test_00_EDB.py index 4a45846c28f..d0637e311fb 100644 --- a/_unittest/test_00_EDB.py +++ b/_unittest/test_00_EDB.py @@ -2383,6 +2383,11 @@ def test_134_create_port_between_pin_and_layer(self): reference_designator="U7", net_name="GND", group_name="U7_GND" ) U7.pins["F7"].create_port(reference=pin_group) + padstack_instance_terminals = [ + term for term in list(edbapp.terminals.values()) if "PadstackInstanceTerminal" in str(term.type) + ] + for term in padstack_instance_terminals: + assert term.position edbapp.close() def test_134_siwave_source_setter(self): @@ -2892,13 +2897,13 @@ def test_146_export_ipc(self): assert os.path.isfile(xml_file) ipc_edb = Edb(xml_file, edbversion=desktop_version) ipc_stats = ipc_edb.get_statistics() - assert ipc_stats.layout_size == (0.1492, 0.0837) + assert ipc_stats.layout_size == (0.15, 0.0845) assert ipc_stats.num_capacitors == 380 assert ipc_stats.num_discrete_components == 31 assert ipc_stats.num_inductors == 10 assert ipc_stats.num_layers == 15 assert ipc_stats.num_nets == 348 - assert ipc_stats.num_polygons == 138 + assert ipc_stats.num_polygons == 139 assert ipc_stats.num_resistors == 82 assert ipc_stats.num_traces == 1565 assert ipc_stats.num_traces == 1565 @@ -3011,3 +3016,85 @@ def test_153_update_padstacks_after_layer_name_changed(self): for padstack_inst in list(edbapp.padstacks.instances.values()): assert not [lay for lay in padstack_inst.layer_range_names if lay in old_layers] edbapp.close_edb() + + def test_154_create_pec_boundary_ports(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_custom_sball_height", "ANSYS-HSD_V1.aedb") + self.local_scratch.copyfolder(source_path, target_path) + edbapp = Edb(target_path, edbversion=desktop_version) + edbapp.components.create_port_on_pins(refdes="U1", pins="AU38", reference_pins="AU37", pec_boundary=True) + assert edbapp.terminals["Port_GND_U1-AU38"].boundary_type == "PecBoundary" + assert edbapp.terminals["Port_GND_U1-AU38_ref"].boundary_type == "PecBoundary" + edbapp.components.deactivate_rlc_component(component="C5", create_circuit_port=True, pec_boundary=True) + edbapp.components.add_port_on_rlc_component(component="C65", circuit_ports=False, pec_boundary=True) + assert edbapp.terminals["C5"].boundary_type == "PecBoundary" + assert edbapp.terminals["C65"].boundary_type == "PecBoundary" + + def test_154_merge_polygon(self): + source_path = os.path.join(local_path, "example_models", test_subfolder, "test_merge_polygon.aedb") + target_path = os.path.join(self.local_scratch.path, "test_merge_polygon", "test.aedb") + self.local_scratch.copyfolder(source_path, target_path) + edbapp = Edb(target_path, desktop_version) + assert edbapp.nets.merge_nets_polygons(["net1", "net2"]) + edbapp.close_edb() + + def test_155_layout_auto_parametrization(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_auto_parameters", "test.aedb") + self.local_scratch.copyfolder(source_path, target_path) + edbapp = Edb(target_path, desktop_version) + edbapp.auto_parametrize_design( + layers=True, + layer_filter="1_Top", + materials=False, + via_holes=False, + pads=False, + antipads=False, + traces=False, + ) + assert "$1_Top_thick" in edbapp.variables + edbapp.auto_parametrize_design( + layers=True, materials=False, via_holes=False, pads=False, antipads=False, traces=False + ) + assert len(list(edbapp.variables.keys())) == len(list(edbapp.stackup.stackup_layers.keys())) + edbapp.auto_parametrize_design( + layers=False, + materials=True, + via_holes=False, + pads=False, + antipads=False, + traces=False, + material_filter=["copper"], + ) + assert "$sigma_copper" in edbapp.variables + edbapp.auto_parametrize_design( + layers=False, materials=True, via_holes=False, pads=False, antipads=False, traces=False + ) + assert len(list(edbapp.variables.values())) == 26 + edbapp.auto_parametrize_design( + layers=False, materials=False, via_holes=True, pads=False, antipads=False, traces=False + ) + assert len(list(edbapp.variables.values())) == 65 + edbapp.auto_parametrize_design( + layers=False, materials=False, via_holes=False, pads=True, antipads=False, traces=False + ) + assert len(list(edbapp.variables.values())) == 469 + edbapp.auto_parametrize_design( + layers=False, materials=False, via_holes=False, pads=False, antipads=True, traces=False + ) + assert len(list(edbapp.variables.values())) == 469 + edbapp.auto_parametrize_design( + layers=False, + materials=False, + via_holes=False, + pads=False, + antipads=False, + traces=True, + trace_net_filter=["SFPA_Tx_Fault", "SFPA_Tx_Disable", "SFPA_SDA", "SFPA_SCL", "SFPA_Rx_LOS"], + ) + assert len(list(edbapp.variables.keys())) == 474 + edbapp.auto_parametrize_design( + layers=False, materials=False, via_holes=False, pads=False, antipads=False, traces=True + ) + assert len(list(edbapp.variables.values())) == 2308 + edbapp.close_edb() diff --git a/_unittest/test_01_3dlayout_edb.py b/_unittest/test_01_3dlayout_edb.py index 921598dc2da..ddfe5002090 100644 --- a/_unittest/test_01_3dlayout_edb.py +++ b/_unittest/test_01_3dlayout_edb.py @@ -343,6 +343,8 @@ def test_19_dcir(self): import pandas as pd self.dcir_example_project.analyze() + setup = self.dcir_example_project.get_setup("SIwaveDCIR1") + assert setup.is_solved assert self.dcir_example_project.get_dcir_solution_data("SIwaveDCIR1", "RL", "Path Resistance") assert self.dcir_example_project.get_dcir_solution_data("SIwaveDCIR1", "Vias", "Current") solution_data = self.dcir_example_project.get_dcir_solution_data("SIwaveDCIR1", "Sources", "Voltage") diff --git a/_unittest/test_01_GeometryOperators.py b/_unittest/test_01_GeometryOperators.py index 20f66325f10..7e05f1c41a5 100644 --- a/_unittest/test_01_GeometryOperators.py +++ b/_unittest/test_01_GeometryOperators.py @@ -369,16 +369,12 @@ def test_orient_polygon(self): xo2, yo2 = go.orient_polygon(x2, y2, clockwise=True) assert x2 == xo2 assert y2 == yo2 - try: + with pytest.raises(ValueError) as excinfo: go.orient_polygon([1], [2], clockwise=True) - assert False - except ValueError as e: - assert str(e) == "'x' length must be >= 2" - try: + assert str(excinfo) == "'x' length must be >= 2" + with pytest.raises(ValueError) as excinfo: go.orient_polygon([1, 2, 3], [1, 2], clockwise=True) - assert False - except ValueError as e: - assert str(e) == "'y' must be same length as 'x'" + assert str(excinfo) == "'y' must be same length as 'x'" def test_is_collinear(self): assert go.is_collinear([1, 0, 0], [1, 0, 0]) diff --git a/_unittest/test_02_3D_modeler.py b/_unittest/test_02_3D_modeler.py index 8dc36cfa7ac..05fd26db2a9 100644 --- a/_unittest/test_02_3D_modeler.py +++ b/_unittest/test_02_3D_modeler.py @@ -382,11 +382,8 @@ def test_40_create_coordinate_system(self): assert cs.change_cs_mode(1) assert cs.change_cs_mode(2) - try: + with pytest.raises(ValueError): cs.change_cs_mode(3) - assert False - except ValueError: - assert True assert cs.change_cs_mode(0) assert cs.delete() assert len(self.aedtapp.modeler.coordinate_systems) == 1 diff --git a/_unittest/test_03_Materials.py b/_unittest/test_03_Materials.py index 16e15f4df48..535a8ae4e84 100644 --- a/_unittest/test_03_Materials.py +++ b/_unittest/test_03_Materials.py @@ -1,5 +1,6 @@ import os +from _unittest.conftest import config from _unittest.conftest import local_path import pytest @@ -95,21 +96,12 @@ def test_02_create_material(self): assert mat1.material_appearance == [11, 22, 0] mat1.material_appearance = ["11", "22", "10"] assert mat1.material_appearance == [11, 22, 10] - try: + with pytest.raises(ValueError): mat1.material_appearance = [11, 22, 300] - assert False - except ValueError: - assert True - try: + with pytest.raises(ValueError): mat1.material_appearance = [11, -22, 0] - assert False - except ValueError: - assert True - try: + with pytest.raises(ValueError): mat1.material_appearance = [11, 22] - assert False - except ValueError: - assert True def test_03_create_modifiers(self): assert self.aedtapp.materials["new_copper2"].mass_density.add_thermal_modifier_free_form( @@ -269,6 +261,7 @@ def test_12_material_model(self): self.aedtapp["$df"] = 0.01 assert mat.set_djordjevic_sarkar_model(dk="$dk", df="$df") + @pytest.mark.skipif(not config["use_grpc"], reason="Not running in COM mode") def test_13_get_materials_in_project(self): used_materials = self.aedtapp.materials.get_used_project_material_names() assert isinstance(used_materials, list) @@ -296,13 +289,10 @@ def test_14_get_coreloss_coefficients(self): assert isinstance(coeff, list) assert len(coeff) == 3 assert all(isinstance(c, float) for c in coeff) - try: + with pytest.raises(TypeError): self.aedtapp.materials["mat_test"].get_core_loss_coefficients( points_list_at_freq=[[0, 0], [1, 3.5], [2, 7.4]] ) - assert False - except TypeError: - assert True coeff = self.aedtapp.materials["mat_test"].get_core_loss_coefficients( points_list_at_freq={ 60: [[0, 0], [1, 3.5], [2, 7.4]], @@ -320,20 +310,14 @@ def test_14_get_coreloss_coefficients(self): assert isinstance(coeff, list) assert len(coeff) == 3 assert all(isinstance(c, float) for c in coeff) - try: - coeff = self.aedtapp.materials["mat_test"].get_core_loss_coefficients( + with pytest.raises(TypeError): + self.aedtapp.materials["mat_test"].get_core_loss_coefficients( points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]}, thickness="invalid" ) - assert False - except TypeError: - assert True - try: - coeff = self.aedtapp.materials["mat_test"].get_core_loss_coefficients( + with pytest.raises(TypeError): + self.aedtapp.materials["mat_test"].get_core_loss_coefficients( points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]}, thickness=50 ) - assert False - except TypeError: - assert True def test_14_set_core_loss(self): mat = self.aedtapp.materials["mat_test"] @@ -347,13 +331,10 @@ def test_14_set_core_loss(self): assert self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( points_list_at_freq={"0.06kHz": [[0, 0], [1, 3.5], [2, 7.4]]} ) - try: + with pytest.raises(TypeError): self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( points_list_at_freq=[[0, 0], [1, 3.5], [2, 7.4]] ) - assert False - except TypeError: - assert True assert self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( points_list_at_freq={ 60: [[0, 0], [1, 3.5], [2, 7.4]], @@ -361,21 +342,27 @@ def test_14_set_core_loss(self): 150: [[0, 0], [1, 10], [2, 19]], } ) + assert self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( + points_list_at_freq={ + 60: [[0, 0], [1, 3.5], [2, 7.4]], + 100: [[0, 0], [1, 8], [2, 9]], + 150: [[0, 0], [1, 10], [2, 19]], + }, + core_loss_model_type="Power Ferrite", + ) + with pytest.raises(ValueError): + self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( + points_list_at_freq={80: [[0, 0], [1, 3.5], [2, 7.4]]}, core_loss_model_type="Power Ferrite" + ) # Test thickness assert self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]}, thickness="0.6mm" ) - try: - coeff = self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( + with pytest.raises(TypeError): + self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]}, thickness="invalid" ) - assert False - except TypeError: - assert True - try: - coeff = self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( + with pytest.raises(TypeError): + self.aedtapp.materials["mat_test"].set_coreloss_at_frequency( points_list_at_freq={60: [[0, 0], [1, 3.5], [2, 7.4]]}, thickness=50 ) - assert False - except TypeError: - assert True diff --git a/_unittest/test_05_Mesh.py b/_unittest/test_05_Mesh.py index 2be3d8160fc..5bb44abb903 100644 --- a/_unittest/test_05_Mesh.py +++ b/_unittest/test_05_Mesh.py @@ -1,3 +1,4 @@ +from _unittest.conftest import config from _unittest.conftest import desktop_version import pytest @@ -102,6 +103,7 @@ def test_04_assign_surface_priority(self): == "High" ) + @pytest.mark.skipif(not config["use_grpc"], reason="Not running in COM mode") def test_05_delete_mesh_ops(self): assert self.aedtapp.mesh.delete_mesh_operations("surface") assert len(self.aedtapp.mesh.meshoperation_names) == 2 diff --git a/_unittest/test_08_Primitives3D.py b/_unittest/test_08_Primitives3D.py index 4aa553bf993..84eca8e1095 100644 --- a/_unittest/test_08_Primitives3D.py +++ b/_unittest/test_08_Primitives3D.py @@ -701,12 +701,12 @@ def test_44_create_polyline_basic_segments(self): assert prim3D.create_polyline( position_list=test_points, segment_type=PolylineSegment("Spline", num_points=3), name="PL03_spline_3pt" ) - try: + with pytest.raises(ValueError) as execinfo: prim3D.create_polyline(position_list=test_points[0:3], segment_type="Spline", name="PL03_spline_str_3pt") - except ValueError as e: - assert str(e) == "The position_list argument must contain at least 4 points for segment of type Spline." - else: - assert False + assert ( + str(execinfo) + == "The 'position_list' argument must contain at least four points for segment of type 'Spline'." + ) assert prim3D.create_polyline( position_list=[[100, 100, 0]], segment_type=PolylineSegment("AngularArc", arc_center=[0, 0, 0], arc_angle="30deg"), @@ -1023,12 +1023,9 @@ def test_54a_create_spiral_and_add_segments(self): # test unclassified p11 = polyline_points[11] position_lst = [[-142, 130, 0], [-126, 63, 0], p11] - try: + with pytest.raises(ValueError) as execinfo: ind.insert_segment(position_lst, "Arc") - except ValueError as e: - assert str(e) == "Adding the segment result in an unclassified object. Undoing operation." - else: - assert False + assert str(execinfo) == "Adding the segment result in an unclassified object. Undoing operation." assert len(ind.points) == 64 assert len(ind.segment_types) == 55 diff --git a/_unittest/test_09_VariableManager.py b/_unittest/test_09_VariableManager.py index 3eac9011b0d..166f644eeca 100644 --- a/_unittest/test_09_VariableManager.py +++ b/_unittest/test_09_VariableManager.py @@ -253,18 +253,11 @@ def test_07_addition(self): v2 = Variable(3) v3 = Variable("3mA") v4 = Variable("10A") - - try: + with pytest.raises(AssertionError): v1 + v2 - assert False - except AssertionError: - pass - try: + with pytest.raises(AssertionError): v2 + v1 - assert False - except AssertionError: - pass result_1 = v2 + v2 result_2 = v3 + v4 result_3 = v3 + v3 @@ -286,17 +279,11 @@ def test_08_subtraction(self): v3 = Variable("3mA") v4 = Variable("10A") - try: + with pytest.raises(AssertionError): v1 - v2 - assert False - except AssertionError: - pass - try: + with pytest.raises(AssertionError): v2 - v1 - assert False - except AssertionError: - pass result_1 = v2 - v2 result_2 = v3 - v4 diff --git a/_unittest/test_11_Setup.py b/_unittest/test_11_Setup.py index 9d767b52602..a80faa8e912 100644 --- a/_unittest/test_11_Setup.py +++ b/_unittest/test_11_Setup.py @@ -131,6 +131,9 @@ def test_25a_create_parametrics(self): assert setup1.add_variation("w2", "0.1mm", 10, 11) assert setup1.add_variation("w2", start_point="0.2mm", variation_type="SingleValue") assert setup1.add_variation("w1", start_point="0.3mm", end_point=5, step=0.2, variation_type="LinearStep") + assert setup1.add_variation("w1", start_point="0.3mm", end_point=5, step=1, variation_type="DecadeCount") + assert setup1.add_variation("w1", start_point="0.3mm", end_point=5, step=1, variation_type="OctaveCount") + assert setup1.add_variation("w1", start_point="0.3mm", end_point=5, step=1, variation_type="ExponentialCount") assert setup1.add_calculation( calculation="dB(S(1,1))", ranges={"Freq": "3.5GHz"}, solution="My_HFSS_Setup : LastAdaptive" ) diff --git a/_unittest/test_12_1_PostProcessing.py b/_unittest/test_12_1_PostProcessing.py index 2349af9db82..7ec9f86a4b2 100644 --- a/_unittest/test_12_1_PostProcessing.py +++ b/_unittest/test_12_1_PostProcessing.py @@ -88,6 +88,7 @@ def test_01B_Field_Plot(self): ) assert len(self.aedtapp.setups[0].sweeps[0].frequencies) > 0 assert isinstance(self.aedtapp.setups[0].sweeps[0].basis_frequencies, list) + assert len(self.aedtapp.setups[0].sweeps[1].basis_frequencies) == 2 @pytest.mark.skipif(is_linux or sys.version_info < (3, 8), reason="Not running in ironpython") def test_01_Animate_plt(self): diff --git a/_unittest/test_16_3d_stackup.py b/_unittest/test_16_3d_stackup.py index a4e624dad62..a4655f14ae5 100644 --- a/_unittest/test_16_3d_stackup.py +++ b/_unittest/test_16_3d_stackup.py @@ -68,10 +68,8 @@ def test_02_line(self): def test_03_padstackline(self): p1 = self.st.add_padstack("Massimo", material="aluminum") p1.plating_ratio = 0.7 - try: + with pytest.raises(ValueError): p1.set_start_layer("non_existing_layer") - except ValueError: - assert True assert p1.set_start_layer("lay1") assert p1.set_stop_layer("top") p1.set_all_pad_value(1) diff --git a/_unittest/test_20_HFSS.py b/_unittest/test_20_HFSS.py index 1b59cd77f5b..16d2cdcf651 100644 --- a/_unittest/test_20_HFSS.py +++ b/_unittest/test_20_HFSS.py @@ -253,8 +253,8 @@ def test_06a_create_linear_count_sweep(self): assert sweep.props["Type"] == "Discrete" # Create a linear count sweep with the incorrect sweep type. - try: - sweep = self.aedtapp.create_linear_count_sweep( + with pytest.raises(AttributeError) as execinfo: + self.aedtapp.create_linear_count_sweep( setupname="MySetup", sweepname="IncorrectStep", unit="MHz", @@ -263,12 +263,10 @@ def test_06a_create_linear_count_sweep(self): num_of_freq_points=1234, sweep_type="Incorrect", ) - except AttributeError as e: - exception_raised = True assert ( - e.args[0] == "Invalid value for `sweep_type`. The value must be 'Discrete', 'Interpolating', or 'Fast'." + execinfo.args[0] + == "Invalid value for `sweep_type`. The value must be 'Discrete', 'Interpolating', or 'Fast'." ) - assert exception_raised self.aedtapp["der_var"] = "1mm" self.aedtapp["der_var2"] = "2mm" setup2 = self.aedtapp.create_setup("MySetup_2", setuptype=0) @@ -327,8 +325,8 @@ def test_06c_create_linear_step_sweep(self): assert sweep.props["Type"] == "Fast" # Create a linear step sweep with the incorrect sweep type. - try: - sweep = self.aedtapp.create_linear_step_sweep( + with pytest.raises(AttributeError) as execinfo: + self.aedtapp.create_linear_step_sweep( setupname="MySetup", sweepname="StepFast", unit=units, @@ -337,12 +335,10 @@ def test_06c_create_linear_step_sweep(self): step_size=step_size, sweep_type="Incorrect", ) - except AttributeError as e: - exception_raised = True assert ( - e.args[0] == "Invalid value for `sweep_type`. The value must be 'Discrete', 'Interpolating', or 'Fast'." + execinfo.args[0] + == "Invalid value for 'sweep_type'. The value must be 'Discrete', 'Interpolating', or 'Fast'." ) - assert exception_raised def test_06d_create_single_point_sweep(self): assert self.aedtapp.create_single_point_sweep( @@ -1157,37 +1153,28 @@ def test_45B_terminal_port(self): assert n_boundaries == 12 # Use two boxes with different dimensions. - try: + with pytest.raises(AttributeError) as execinfo: self.aedtapp.create_spiral_lumped_port(box1, box3) - except AttributeError as e: - assert e.args[0] == "The closest faces of the two objects must be identical in shape." - else: - assert False + assert execinfo.args[0] == "The closest faces of the two objects must be identical in shape." # Rotate box3 so that, box3 and box4 are not collinear anymore. # Spiral lumped port can only be created based on 2 collinear objects. box3.rotate(cs_axis="X", angle=90) - try: + with pytest.raises(AttributeError) as execinfo: self.aedtapp.create_spiral_lumped_port(box3, box4) - except AttributeError as e: - assert e.args[0] == "The two objects must have parallel adjacent faces." - else: - assert False + assert execinfo.args[0] == "The two objects must have parallel adjacent faces." # Rotate back box3 # rotate them slightly so that they are still parallel, but not aligned anymore with main planes. box3.rotate(cs_axis="X", angle=-90) box3.rotate(cs_axis="Y", angle=5) box4.rotate(cs_axis="Y", angle=5) - try: + with pytest.raises(AttributeError) as execinfo: self.aedtapp.create_spiral_lumped_port(box3, box4) - except AttributeError as e: assert ( - e.args[0] + execinfo.args[0] == "The closest faces of the two objects must be aligned with the main planes of the reference system." ) - else: - assert False self.aedtapp.delete_design("Design_Terminal_2", self.fall_back_name) def test_46_mesh_settings(self): @@ -1208,12 +1195,9 @@ def test_49_port_creation_exception(self): self.aedtapp.solution_type = "Modal" # Spiral lumped port can only be created in a 'Terminal' solution. - try: + with pytest.raises(Exception) as execinfo: self.aedtapp.create_spiral_lumped_port(box1, box2) - except Exception as e: - exception_raised = True - assert e.args[0] == "This method can be used only in Terminal solutions." - assert exception_raised + assert execinfo.args[0] == "This method can be used only in 'Terminal' solutions." self.aedtapp.solution_type = "Terminal" # Try to modify SBR+ TX RX antenna settings in a solution that is different from SBR+ diff --git a/_unittest/test_28_Maxwell3D.py b/_unittest/test_28_Maxwell3D.py index adb7f3186d9..26d5dc51f26 100644 --- a/_unittest/test_28_Maxwell3D.py +++ b/_unittest/test_28_Maxwell3D.py @@ -867,6 +867,7 @@ def test_52_assign_flux_tangential(self): assert self.aedtapp.assign_flux_tangential(box.faces[0], "FluxExample") assert self.aedtapp.assign_flux_tangential(box.faces[0].id, "FluxExample") + @pytest.mark.skipif(not config["use_grpc"], reason="Not running in COM mode") @pytest.mark.skipif(desktop_version < "2023.2", reason="Method available in beta from 2023.2") def test_53_assign_layout_force(self, layout_comp): nets_layers = { diff --git a/_unittest/test_41_3dlayout_modeler.py b/_unittest/test_41_3dlayout_modeler.py index 20947315811..8157cde93d4 100644 --- a/_unittest/test_41_3dlayout_modeler.py +++ b/_unittest/test_41_3dlayout_modeler.py @@ -436,9 +436,8 @@ def test_18b_create_linear_step_sweep(self): assert sweep4.props["Sweeps"]["Data"] == "LIN 1GHz 10GHz 0.12GHz" # Create a linear step sweep with the incorrect sweep type. - exception_raised = False - try: - sweep_raising_error = self.aedtapp.create_linear_step_sweep( + with pytest.raises(AttributeError) as execinfo: + self.aedtapp.create_linear_step_sweep( setupname=setup_name, unit="GHz", freqstart=1, @@ -448,12 +447,10 @@ def test_18b_create_linear_step_sweep(self): sweep_type="Incorrect", save_fields=True, ) - except AttributeError as e: - exception_raised = True assert ( - e.args[0] == "Invalid value for `sweep_type`. The value must be 'Discrete', 'Interpolating', or 'Fast'." + execinfo.args[0] == "Invalid value for 'sweep_type'. The value must be 'Discrete', " + "'Interpolating', or 'Fast'." ) - assert exception_raised def test_18c_create_single_point_sweep(self): setup_name = "RF_create_single_point" @@ -475,19 +472,15 @@ def test_18c_create_single_point_sweep(self): ) assert sweep6.props["Sweeps"]["Data"] == "1GHz 2GHz 3GHz 4GHz" - exception_raised = False - try: - sweep7 = self.aedtapp.create_single_point_sweep( + with pytest.raises(AttributeError) as execinfo: + self.aedtapp.create_single_point_sweep( setupname=setup_name, unit="GHz", freq=[], sweepname="RFBoardSingle", save_fields=False, ) - except AttributeError as e: - exception_raised = True - assert e.args[0] == "Frequency list is empty. Specify at least one frequency point." - assert exception_raised + assert execinfo.args[0] == "Frequency list is empty. Specify at least one frequency point." def test_18d_delete_setup(self): setup_name = "SetupToDelete" @@ -656,6 +649,7 @@ def test_41_test_create_polygon(self): assert p2.name == "poly_test_41_void" assert not self.aedtapp.modeler.create_polygon_void("Top", points2, "another_object", name="poly_43_void") + @pytest.mark.skipif(not config["use_grpc"], reason="Not running in COM mode") @pytest.mark.skipif(config["desktopVersion"] < "2023.2", reason="Working only from 2023 R2") def test_42_post_processing(self, add_app): test_post1 = add_app(project_name=test_post, application=Maxwell3d, subfolder=test_subfolder) diff --git a/_unittest/test_98_Icepak.py b/_unittest/test_98_Icepak.py index 6a6ee580a2f..57e2dd552ff 100644 --- a/_unittest/test_98_Icepak.py +++ b/_unittest/test_98_Icepak.py @@ -625,6 +625,7 @@ def test_49_delete_monitors(self): assert self.aedtapp.monitor.all_monitors == {} assert not self.aedtapp.monitor.delete_monitor("Test") + @pytest.mark.skipif(not config["use_grpc"], reason="Not running in COM mode") def test_50_advanced3dcomp_export(self): self.aedtapp.insert_design("advanced3dcompTest") surf1 = self.aedtapp.modeler.create_rectangle(self.aedtapp.PLANE.XY, [0, 0, 0], [10, 20], name="surf1") @@ -713,6 +714,7 @@ def test_50_advanced3dcomp_export(self): surf1.delete() fan_obj_3d.delete() + @pytest.mark.skipif(not config["use_grpc"], reason="Not running in COM mode") def test_51_advanced3dcomp_import(self): self.aedtapp.insert_design("test_3d_comp") surf1 = self.aedtapp.modeler.create_rectangle(self.aedtapp.PLANE.XY, [0, 0, 0], [10, 20], name="surf1") @@ -814,6 +816,7 @@ def test_51_advanced3dcomp_import(self): comp_file=os.path.join(file_path, file_name), targetCS="Global", auxiliary_dict=False, name="test" ) + @pytest.mark.skipif(not config["use_grpc"], reason="Not running in COM mode") def test_52_flatten_3d_components(self): self.aedtapp.insert_design("test_52") cs2 = self.aedtapp.modeler.create_coordinate_system(name="CS2") @@ -1424,3 +1427,28 @@ def test_72_assign_resistance(self): power_law_constant=1.5, power_law_exponent="3", ) + + def test_73_conducting_plate(self): + box = self.aedtapp.modeler.create_box([5, 5, 5], [1, 2, 3], "ResistanceBox", "copper") + box_face = box.top_face_x + assert self.aedtapp.assign_conducting_plate_with_thickness( + box_face.id, total_power=1, high_side_rad_material="Steel-oxidised-surface" + ) + assert self.aedtapp.assign_conducting_plate_with_resistance( + box_face.id, low_side_rad_material="Steel-oxidised-surface" + ) + self.aedtapp.modeler.create_rectangle(self.aedtapp.PLANE.XY, [0, 0, 0], [10, 20], name="surfPlateTest") + assert self.aedtapp.assign_conducting_plate_with_impedance("surfPlateTest") + x = [1, 2, 3] + y = [3, 4, 5] + self.aedtapp.create_dataset1d_design("Test_DataSet_Plate", x, y) + assert self.aedtapp.assign_conducting_plate_with_conductance( + "surfPlateTest", + total_power={ + "Type": "Temp Dep", + "Function": "Piecewise Linear", + "Values": "Test_DataSet_Plate", + }, + ) + with pytest.raises(AttributeError): + self.aedtapp.assign_conducting_plate_with_conductance([box_face.id, "surfPlateTest"]) diff --git a/_unittest_solvers/test_26_emit.py b/_unittest_solvers/test_26_emit.py index 9bc42eb0226..79596592837 100644 --- a/_unittest_solvers/test_26_emit.py +++ b/_unittest_solvers/test_26_emit.py @@ -1,7 +1,7 @@ # Import required modules import os import sys -import time +import tempfile from _unittest_solvers.conftest import config import pytest @@ -1182,81 +1182,81 @@ def test_license_session(self, add_app): results = self.aedtapp.results revision = self.aedtapp.results.analyze() - receivers = revision.get_receiver_names() - modeRx = TxRxMode.RX - modeTx = TxRxMode.TX - modeEmi = ResultType.EMI self.aedtapp.set_units("Frequency", "GHz") - def get_best_rx_channel(results, receiver): + def do_run(): domain = results.interaction_domain() rev = results.current_revision - # get interferers - interferers = rev.get_interferer_names() - best_emi = 300.0 - best_band = None - best_freq = None - - combinations_run = 0 - - domain.set_receiver(receiver) interaction = rev.run(domain) - for interferer in interferers: - for tx_band in rev.get_band_names(interferer, modeTx): - for tx_freq in rev.get_active_frequencies(interferer, tx_band, modeTx): - domain.set_interferer(interferer, tx_band, tx_freq) - rx_band = rev.get_band_names(receiver, modeRx)[0] - for rx_freq in rev.get_active_frequencies(receiver, rx_band, modeRx): - - domain.set_receiver(receiver, rx_band, rx_freq) - - if best_band == None: - best_band = rx_band - if best_freq == None: - best_freq = rx_freq - - try: - instance = interaction.get_instance(domain) - if instance.has_valid_values(): - emi = instance.get_value(modeEmi) - if emi < best_emi: - best_emi = emi - best_band = rx_band - best_freq = rx_freq - else: - assert(f'No valid values found') - except: - assert("No results between " + interferer + ": " + tx_band + - ": " + str(tx_freq) + " and " + receiver + ": " + - rx_band + ": " + str(rx_freq)) - - combinations_run += 1 - - if combinations_run >= 20: - return [best_band, best_freq] - return [best_band, best_freq] + number_of_runs = 5 + + # Do a run to ensure the license log exists + do_run() + + # Find the license log for this process + appdata_local_path = tempfile.gettempdir() + pid = os.getpid() + dot_ansys_directory = os.path.join(appdata_local_path, '.ansys') - # Warmup in case something isn't solved - best_case_rx_ch = {} - for rx in [receivers[0]]: - best_case_rx_ch[rx] = get_best_rx_channel(results, rx) + license_file_path = '' + with os.scandir(dot_ansys_directory) as dir: + for file in dir: + filename_pieces = file.name.split('.') + # Since machine names can contain periods, there may be over five splits here + # We only care about the first split and last three splits + if len(filename_pieces) >= 5: + if (filename_pieces[0] == 'ansyscl' and + filename_pieces[-3] == str(pid) and + filename_pieces[-2].isnumeric() and + filename_pieces[-1] == 'log'): + license_file_path = os.path.join(dot_ansys_directory, file.name) + break - # Get the time without the license session - start = time.perf_counter() - best_case_rx_ch = {} - for rx in [receivers[0]]: - best_case_rx_ch[rx] = get_best_rx_channel(results, rx) - end = time.perf_counter() - noLicenseSessionTime = end - start + assert license_file_path != '' - # Get the time using the license session - start = time.perf_counter() - best_case_rx_ch = {} + def count_license_actions(license_file_path): + # Count checkout/checkins in most recent license connection + checkouts = 0 + checkins = 0 + with open(license_file_path, 'r') as license_file: + lines = license_file.read().strip().split('\n') + for line in lines: + if 'NEW_CONNECTION' in line: + checkouts = 0 + checkins = 0 + elif 'CHECKOUT' in line or "SPLIT_CHECKOUT" in line: + checkouts += 1 + elif 'CHECKIN' in line: + checkins += 1 + return (checkouts, checkins) + + # Figure out how many checkouts and checkins per run we expect + # This could change depending on the user's EMIT HPC settings + pre_first_run_checkouts, pre_first_run_checkins = count_license_actions(license_file_path) + do_run() + post_first_run_checkouts, post_first_run_checkins = count_license_actions(license_file_path) + + checkouts_per_run = post_first_run_checkouts - pre_first_run_checkouts + checkins_per_run = post_first_run_checkins - pre_first_run_checkins + + start_checkouts, start_checkins = count_license_actions(license_file_path) + + # Run without license session + for i in range(number_of_runs): + do_run() + + # Run with license session with revision.get_license_session(): - for rx in [receivers[0]]: - best_case_rx_ch[rx] = get_best_rx_channel(results, rx) - end = time.perf_counter() - licenseSessionTime = end - start + for i in range(number_of_runs): + do_run() + + end_checkouts, end_checkins = count_license_actions(license_file_path) + + checkouts = end_checkouts - start_checkouts + checkins = end_checkins - start_checkins + + expected_checkouts = checkouts_per_run * (number_of_runs + 1) + expected_checkins = checkins_per_run * (number_of_runs + 1) - assert (licenseSessionTime*2) < noLicenseSessionTime + assert checkouts == expected_checkouts and checkins == expected_checkins diff --git a/doc/source/_static/connector_example.png b/doc/source/_static/connector_example.png new file mode 100644 index 00000000000..a1083574c27 Binary files /dev/null and b/doc/source/_static/connector_example.png differ diff --git a/doc/source/_static/parametrized_design.png b/doc/source/_static/parametrized_design.png new file mode 100644 index 00000000000..80bcb84b1af Binary files /dev/null and b/doc/source/_static/parametrized_design.png differ diff --git a/doc/source/conf.py b/doc/source/conf.py index 864471ad0ff..e69b01843aa 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -94,6 +94,7 @@ def setup(app): sys.path.append(os.path.join(root_path)) from pyaedt import __version__ +from pyaedt import is_windows project = "PyAEDT" copyright = f"(c) {datetime.datetime.now().year} ANSYS, Inc. All rights reserved" @@ -254,7 +255,7 @@ def setup(app): os.makedirs(pyvista.FIGURE_PATH) # gallery build requires AEDT install -if os.name != "posix" and "PYAEDT_CI_NO_EXAMPLES" not in os.environ: +if is_windows and "PYAEDT_CI_NO_EXAMPLES" not in os.environ: # suppress annoying matplotlib bug warnings.filterwarnings( diff --git a/examples/00-EDB/13_edb_create_component.py b/examples/00-EDB/13_edb_create_component.py new file mode 100644 index 00000000000..27a3f3b8610 --- /dev/null +++ b/examples/00-EDB/13_edb_create_component.py @@ -0,0 +1,166 @@ +""" +EDB: geometry creation +---------------------- +This example shows how to +1, Create a layout layer stackup. +2, Create Padstack definition. +3, Place padstack instances at given location. +4, Create primitives, polygon and trace. +5, Create component from pins. +6, Create HFSS simulation setup and excitation ports. +""" +###################################################################### +# +# Final expected project +# ~~~~~~~~~~~~~~~~~~~~~~ +# +# .. image:: ../../_static/connector_example.png +# :width: 600 +# :alt: Connector from Vias. +###################################################################### + +###################################################################### +# Create connector component from pad-stack +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Initialize an empty EDB layout object on version 2023 R2. +###################################################################### + +import os +import pyaedt +from pyaedt import Edb + +aedb_path = os.path.join(pyaedt.generate_unique_folder_name(), + pyaedt.generate_unique_name("component_example") + ".aedb") + + +edb = Edb(edbpath=aedb_path, edbversion="2023.2") +print("EDB is located at {}".format(aedb_path)) + +###################### +# Initialize variables +# ~~~~~~~~~~~~~~~~~~~~ + +layout_count = 12 +diel_material_name = "FR4_epoxy" +diel_thickness = "0.15mm" +cond_thickness_outer = "0.05mm" +cond_thickness_inner = "0.017mm" +soldermask_thickness = "0.05mm" +trace_in_layer = "TOP" +trace_out_layer = "L10" +trace_width = "200um" +connector_size = 2e-3 +conectors_position = [[0, 0], [10e-3, 0]] + +################ +# Create stackup +# ~~~~~~~~~~~~~~ +edb.stackup.create_symmetric_stackup(layer_count=layout_count, inner_layer_thickness=cond_thickness_inner, + outer_layer_thickness=cond_thickness_outer, + soldermask_thickness=soldermask_thickness, dielectric_thickness=diel_thickness, + dielectric_material=diel_material_name) + +###################### +# Create ground planes +# ~~~~~~~~~~~~~~~~~~~~ +# + +ground_layers = [layer_name for layer_name in edb.stackup.signal_layers.keys() if layer_name not in + [trace_in_layer, trace_out_layer]] +plane_shape = edb.modeler.Shape("rectangle", pointA=["-3mm", "-3mm"], pointB=["13mm", "3mm"]) +for i in ground_layers: + edb.modeler.create_polygon(plane_shape, i, net_name="VSS") + +###################### +# Add design variables +# ~~~~~~~~~~~~~~~~~~~~ + +edb.add_design_variable("$via_hole_size", "0.3mm") +edb.add_design_variable("$antipaddiam", "0.7mm") +edb.add_design_variable("$paddiam", "0.5mm") +edb.add_design_variable("trace_in_width", "0.2mm", is_parameter=True) +edb.add_design_variable("trace_out_width", "0.1mm", is_parameter=True) + +############################ +# Create padstack definition +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ + +edb.padstacks.create_padstack(padstackname="Via", holediam="$via_hole_size", antipaddiam="$antipaddiam", + paddiam="$paddiam") + +#################### +# Create connector 1 +# ~~~~~~~~~~~~~~~~~~ + +component1_pins = [edb.padstacks.place_padstack(conectors_position[0], "Via", net_name="VDD", fromlayer=trace_in_layer, + tolayer=trace_out_layer), + edb.padstacks.place_padstack([conectors_position[0][0] - connector_size / 2, + conectors_position[0][1] - connector_size / 2], + "Via", net_name="VSS"), + edb.padstacks.place_padstack([conectors_position[0][0] + connector_size / 2, + conectors_position[0][1] - connector_size / 2], + "Via", net_name="VSS"), + edb.padstacks.place_padstack([conectors_position[0][0] + connector_size / 2, + conectors_position[0][1] + connector_size / 2], + "Via", net_name="VSS"), + edb.padstacks.place_padstack([conectors_position[0][0] - connector_size / 2, + conectors_position[0][1] + connector_size / 2], + "Via", net_name="VSS")] + +#################### +# Create connector 2 +# ~~~~~~~~~~~~~~~~~~ + +component2_pins = [ + edb.padstacks.place_padstack(conectors_position[-1], "Via", net_name="VDD", fromlayer=trace_in_layer, + tolayer=trace_out_layer), + edb.padstacks.place_padstack([conectors_position[1][0] - connector_size / 2, + conectors_position[1][1] - connector_size / 2], + "Via", net_name="VSS"), + edb.padstacks.place_padstack([conectors_position[1][0] + connector_size / 2, + conectors_position[1][1] - connector_size / 2], + "Via", net_name="VSS"), + edb.padstacks.place_padstack([conectors_position[1][0] + connector_size / 2, + conectors_position[1][1] + connector_size / 2], + "Via", net_name="VSS"), + edb.padstacks.place_padstack([conectors_position[1][0] - connector_size / 2, + conectors_position[1][1] + connector_size / 2], + "Via", net_name="VSS")] + +#################### +# Create layout pins +# ~~~~~~~~~~~~~~~~~~ + +for padstack_instance in list(edb.padstacks.instances.values()): + padstack_instance.is_pin = True + +############################ +# create component from pins +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ + +edb.components.create(component1_pins, 'connector_1') +edb.components.create(component2_pins, 'connector_2') + +################################################################################ +# Creating ports and adding simulation setup using SimulationConfiguration class +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +sim_setup = edb.new_simulation_configuration() +sim_setup.solver_type = sim_setup.SOLVER_TYPE.Hfss3dLayout +sim_setup.batch_solve_settings.cutout_subdesign_expansion = 0.01 +sim_setup.batch_solve_settings.do_cutout_subdesign = False +sim_setup.batch_solve_settings.signal_nets = ["VDD"] +sim_setup.batch_solve_settings.components = ["connector_1", "connector_2"] +sim_setup.batch_solve_settings.power_nets = ["VSS"] +sim_setup.ac_settings.start_freq = "0GHz" +sim_setup.ac_settings.stop_freq = "5GHz" +sim_setup.ac_settings.step_freq = "1GHz" +edb.build_simulation_project(sim_setup) + +########################### +# Save EDB and open in AEDT +# ~~~~~~~~~~~~~~~~~~~~~~~~~ +edb.save_edb() +edb.close_edb() +h3d = pyaedt.Hfss3dLayout(specified_version="2023.2", projectname=aedb_path, non_graphical=False) +h3d.release_desktop(False, False) diff --git a/examples/00-EDB/14_edb_create_parametrized_design.py b/examples/00-EDB/14_edb_create_parametrized_design.py new file mode 100644 index 00000000000..97077594374 --- /dev/null +++ b/examples/00-EDB/14_edb_create_parametrized_design.py @@ -0,0 +1,69 @@ +""" +EDB: paramterized design +------------------------ +This example shows how to +1, Create an HFSS simulation project using SimulationConfiguration class. +2, Create automatically parametrized design. +""" +###################################################################### +# +# Final expected project +# ~~~~~~~~~~~~~~~~~~~~~~ +# +# .. image:: ../../_static/parametrized_design.png +# :width: 600 +# :alt: Fully automated parametrization. +###################################################################### + +###################################################################### +# Create HFSS simulatio project +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Load an existing EDB folder. +###################################################################### + +import os +import pyaedt + +project_path = pyaedt.generate_unique_folder_name() +target_aedb = pyaedt.downloads.download_file('edb/ANSYS-HSD_V1.aedb', destination=project_path) +print("Project folder will be", target_aedb) + +aedt_version = "2024.1" +edb = pyaedt.Edb(edbpath=target_aedb, edbversion=aedt_version) +print("EDB is located at {}".format(target_aedb)) + +######################################################################## +# Create SimulationConfiguration object and define simulation parameters +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +simulation_configuration = edb.new_simulation_configuration() +simulation_configuration.signal_nets = ["PCIe_Gen4_RX0_P", "PCIe_Gen4_RX0_N", + "PCIe_Gen4_RX1_P", "PCIe_Gen4_RX1_N"] +simulation_configuration.power_nets = ["GND"] +simulation_configuration.components = ["X1", "U1"] +simulation_configuration.do_cutout_subdesign = True +simulation_configuration.start_freq = "OGHz" +simulation_configuration.stop_freq = "20GHz" +simulation_configuration.step_freq = "10MHz" + +########################## +# Build simulation project +# ~~~~~~~~~~~~~~~~~~~~~~~~ + +edb.build_simulation_project(simulation_configuration) + +############################# +# Generated design parameters +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# + +edb.auto_parametrize_design(layers=True, materials=True, via_holes=True, pads=True, antipads=True, traces=True) +edb.save_edb() +edb.close_edb() + +###################### +# Open project in AEDT +# ~~~~~~~~~~~~~~~~~~~~ + +hfss = pyaedt.Hfss3dLayout(projectname=target_aedb, specified_version=aedt_version) +hfss.release_desktop(False, False) diff --git a/examples/01-Modeling-Setup/HFSS_CoordinateSystem.py b/examples/01-Modeling-Setup/HFSS_CoordinateSystem.py index 6f38864eb4b..29bb7e6abaf 100644 --- a/examples/01-Modeling-Setup/HFSS_CoordinateSystem.py +++ b/examples/01-Modeling-Setup/HFSS_CoordinateSystem.py @@ -196,6 +196,48 @@ name="CS_FCS", origin=[0, 0, 0], reference_cs=fcs6.name, mode="view", view="iso" ) +############################################################################### +# Create object coordinate system +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Create object coordinate system with origin on face + +obj_cs = hfss.modeler.create_object_coordinate_system( + obj=box, origin=box.faces[0], x_axis=box.edges[0], y_axis=[0, 0, 0], name="box_obj_cs" + ) +obj_cs.rename("new_obj_cs") + +############################################################################### +# Create object coordinate system +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Create object coordinate system with origin on edge + +obj_cs_1 = hfss.modeler.create_object_coordinate_system( + obj=box.name, origin=box.edges[0], x_axis=[1, 0, 0], y_axis=[0, 1, 0], name="obj_cs_1" + ) +obj_cs_1.set_as_working_cs() + +############################################################################### +# Create object coordinate system +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Create object coordinate system with origin specified on point + +obj_cs_2 = hfss.modeler.create_object_coordinate_system( + obj=box.name, origin=[0, 0.8, 0], x_axis=[1, 0, 0], y_axis=[0, 1, 0], name="obj_cs_2" + ) +new_obj_cs_2 = hfss.modeler.duplicate_coordinate_system_to_global(obj_cs_2) +obj_cs_2.delete() + +############################################################################### +# Create object coordinate system +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Create object coordinate system with origin on vertex + +obj_cs_3 = hfss.modeler.create_object_coordinate_system( + obj=box.name, origin=box.vertices[1], x_axis=box.faces[2], y_axis=box.faces[4], name="obj_cs_3" + ) +obj_cs_3.props["MoveToEnd"] = False +obj_cs_3.update() + ############################################################################### # Get all coordinate systems # ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/03-Maxwell/Maxwell2D_NissanLeaf.py b/examples/03-Maxwell/Maxwell2D_NissanLeaf.py index 60bccf51e97..54fc718f5ea 100644 --- a/examples/03-Maxwell/Maxwell2D_NissanLeaf.py +++ b/examples/03-Maxwell/Maxwell2D_NissanLeaf.py @@ -11,6 +11,7 @@ # Perform required imports. from math import sqrt as mysqrt +from pyaedt import generate_unique_folder_name import csv import os import pyaedt @@ -727,6 +728,33 @@ def create_cs_magnets(pm_id, cs_name, point_direction): M2D.save_project() M2D.analyze_setup(sName, use_auto_settings=False) +############################################### +# Get solution data +# ~~~~~~~~~~~~~~~~~ +# Get a simulation result from a solved setup and cast it in a ``SolutionData`` object. +# Plot the desired expression by using Matplotlib plot(). + +solutions = M2D.post.get_solution_data(expressions="Moving1.Torque", + primary_sweep_variable="Time") +solutions.plot() + +############################################### +# Retrieve the data magnitude of an expression +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# List of shaft torque points and compute average. + +mag = solutions.data_magnitude() +avg = sum(mag)/len(mag) + +############################################### +# Export a report to a file +# ~~~~~~~~~~~~~~~~~~~~~~~~~ +# Export a 2D Plot data to a .csv file. + +M2D.post.export_report_to_file(output_dir=M2D.toolkit_directory, + plot_name="TorquePlots", + extension=".csv") + ############################################### # Close AEDT # ~~~~~~~~~~ diff --git a/examples/03-Maxwell/Maxwell_Transformer_Coreloss.py b/examples/03-Maxwell/Maxwell_Transformer_Coreloss.py new file mode 100644 index 00000000000..cab8575bbf2 --- /dev/null +++ b/examples/03-Maxwell/Maxwell_Transformer_Coreloss.py @@ -0,0 +1,91 @@ +""" +Maxwell 3D: Transformer +----------------------- +This example shows how you can use PyAEDT to set core loss given a set +of Power-Volume [kw/m^3] curves at different frequencies. +""" +############################################################################### +# Perform required imports +# ~~~~~~~~~~~~~~~~~~~~~~~~ +# Perform required imports. + +from pyaedt import downloads +from pyaedt import generate_unique_folder_name +from pyaedt import Maxwell3d +from pyaedt.generic.constants import unit_converter +from pyaedt.generic.general_methods import read_csv_pandas + +################################################################################# +# Download .aedt file example +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Set local temporary folder to export the .aedt file to. + +temp_folder = generate_unique_folder_name() +aedt_file = downloads.download_file("core_loss_transformer", "Ex2-PlanarTransformer_2023R2.aedtz", temp_folder) +freq_curve_csv_25kHz = downloads.download_file("core_loss_transformer", "mf3_25kHz.csv", temp_folder) +freq_curve_csv_100kHz = downloads.download_file("core_loss_transformer", "mf3_100kHz.csv", temp_folder) +freq_curve_csv_200kHz = downloads.download_file("core_loss_transformer", "mf3_200kHz.csv", temp_folder) +freq_curve_csv_400kHz = downloads.download_file("core_loss_transformer", "mf3_400kHz.csv", temp_folder) +freq_curve_csv_700kHz = downloads.download_file("core_loss_transformer", "mf3_700kHz.csv", temp_folder) +freq_curve_csv_1MHz = downloads.download_file("core_loss_transformer", "mf3_1MHz.csv", temp_folder) + +data = read_csv_pandas(filename=freq_curve_csv_25kHz) +curves_csv_25kHz = list(zip(data[data.columns[0]], + data[data.columns[1]])) +data = read_csv_pandas(filename=freq_curve_csv_100kHz) +curves_csv_100kHz = list(zip(data[data.columns[0]], + data[data.columns[1]])) +data = read_csv_pandas(filename=freq_curve_csv_200kHz) +curves_csv_200kHz = list(zip(data[data.columns[0]], + data[data.columns[1]])) +data = read_csv_pandas(filename=freq_curve_csv_400kHz) +curves_csv_400kHz = list(zip(data[data.columns[0]], + data[data.columns[1]])) +data = read_csv_pandas(filename=freq_curve_csv_700kHz) +curves_csv_700kHz = list(zip(data[data.columns[0]], + data[data.columns[1]])) +data = read_csv_pandas(filename=freq_curve_csv_1MHz) +curves_csv_1MHz = list(zip(data[data.columns[0]], + data[data.columns[1]])) + +############################################################################### +# Launch AEDT +# ~~~~~~~~~~~ +# Launch AEDT 2023 R2 in graphical mode. + +m3d = Maxwell3d(projectname=aedt_file, + designname="02_3D eddycurrent_CmXY_for_thermal", + specified_version="2023.2", + new_desktop_session=True, + non_graphical=False) + +############################################################################### +# Set core loss at frequencies +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Create a new material, create a dictionary of Power-Volume [kw/m^3] points for a set of frequencies +# retrieved from datasheet provided by supplier and finally set Power-Ferrite core loss model. + +mat = m3d.materials.add_material("newmat") +freq_25kHz = unit_converter(25, unit_system="Freq", input_units="kHz", output_units="Hz") +freq_100kHz = unit_converter(100, unit_system="Freq", input_units="kHz", output_units="Hz") +freq_200kHz = unit_converter(200, unit_system="Freq", input_units="kHz", output_units="Hz") +freq_400kHz = unit_converter(400, unit_system="Freq", input_units="kHz", output_units="Hz") +freq_700kHz = unit_converter(700, unit_system="Freq", input_units="kHz", output_units="Hz") +pv = {freq_25kHz: curves_csv_25kHz, + freq_100kHz: curves_csv_100kHz, + freq_200kHz: curves_csv_200kHz, + freq_400kHz: curves_csv_400kHz, + freq_700kHz: curves_csv_700kHz} +m3d.materials[mat.name].set_coreloss_at_frequency(points_list_at_freq=pv, + coefficient_setup="kw_per_cubic_meter", + core_loss_model_type="Power Ferrite") +coefficients = m3d.materials[mat.name].get_core_loss_coefficients(points_list_at_freq=pv, + coefficient_setup="kw_per_cubic_meter") + +################################################################################### +# Save project and close AEDT +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Save the project and close AEDT. + +m3d.save_project() +m3d.release_desktop() diff --git a/pyaedt/downloads.py b/pyaedt/downloads.py index 012479cd413..f4dc54c02a2 100644 --- a/pyaedt/downloads.py +++ b/pyaedt/downloads.py @@ -31,8 +31,12 @@ def _get_file_url(directory, filename=None): return EXAMPLE_REPO + "/".join([directory, filename]) -def _retrieve_file(url, filename, directory, destination=None, local_paths=[]): - """Download a file from a url""" +def _retrieve_file(url, filename, directory, destination=None, local_paths=None): + """Download a file from a URL.""" + + if local_paths is None: + local_paths = [] + # First check if file has already been downloaded if not destination: destination = EXAMPLES_PATH @@ -85,8 +89,12 @@ def _retrieve_file(url, filename, directory, destination=None, local_paths=[]): local_paths.append(local_path) -def _retrieve_folder(url, directory, destination=None, local_paths=[]): +def _retrieve_folder(url, directory, destination=None, local_paths=None): """Download a folder from a url""" + + if local_paths is None: + local_paths = [] + # First check if folder exists import json import re @@ -125,7 +133,9 @@ def _retrieve_folder(url, directory, destination=None, local_paths=[]): return False -def _download_file(directory, filename=None, destination=None, local_paths=[]): +def _download_file(directory, filename=None, destination=None, local_paths=None): + if local_paths is None: + local_paths = [] if not filename: if not directory.startswith("pyaedt/"): directory = "pyaedt/" + directory diff --git a/pyaedt/edb.py b/pyaedt/edb.py index c33c6f86bee..d158fde3c31 100644 --- a/pyaedt/edb.py +++ b/pyaedt/edb.py @@ -176,7 +176,7 @@ def __init__( if isaedtowned and (inside_desktop or settings.remote_api or settings.remote_rpc_session): self.open_edb_inside_aedt() - elif edbpath[-3:] in ["brd", "mcm", "gds", "xml", "dxf", "tgz"]: + elif edbpath[-3:] in ["brd", "mcm", "sip", "gds", "xml", "dxf", "tgz"]: self.edbpath = edbpath[:-4] + ".aedb" working_dir = os.path.dirname(edbpath) control_file = None @@ -575,7 +575,7 @@ def import_layout_pcb(self, input_file, working_dir, anstranslator_full_path="", ] if not use_ppe: cmd_translator.append("-ppe=false") - if control_file and input_file[-3:] not in ["brd", "mcm"]: + if control_file and input_file[-3:] not in ["brd", "mcm", "sip"]: if is_linux: cmd_translator.append("-c={}".format(control_file)) else: @@ -3834,3 +3834,202 @@ def get_point_terminal(self, name, net_name, location, layer): point_terminal = PointTerminal(self) return point_terminal.create(name, net_name, location, layer) + + @pyaedt_function_handler + def auto_parametrize_design( + self, + layers=True, + materials=True, + via_holes=True, + pads=True, + antipads=True, + traces=True, + layer_filter=None, + material_filter=None, + padstack_definition_filter=None, + trace_net_filter=None, + ): + """Assign automatically design and project variables with current values. + + Parameters + ---------- + layers : bool, optional + ``True`` enable layer thickness parametrization. Default value is ``True``. + materials : bool, optional + ``True`` enable material parametrization. Default value is ``True``. + via_holes : bool, optional + ``True`` enable via diameter parametrization. Default value is ``True``. + pads : bool, optional + ``True`` enable pads size parametrization. Default value is ``True``. + antipads : bool, optional + ``True`` enable anti pads size parametrization. Default value is ``True``. + traces : bool, optional + ``True`` enable trace width parametrization. Default value is ``True``. + layer_filter : str, List(str), optional + Enable layer filter. Default value is ``None``, all layers are parametrized. + material_filter : str, List(str), optional + Enable material filter. Default value is ``None``, all material are parametrized. + padstack_definition_filter : str, List(str), optional + Enable padstack definition filter. Default value is ``None``, all padsatcks are parametrized. + trace_net_filter : str, List(str), optional + Enable nets filter for trace width parametrization. Default value is ``None``, all layers are + parametrized. + Returns + ------- + List(str) + List of all parameters name created. + """ + parameters = [] + if layers: + if not layer_filter: + _layers = self.stackup.stackup_layers + else: + if isinstance(layer_filter, str): + layer_filter = [layer_filter] + _layers = {k: v for k, v in self.stackup.stackup_layers.items() if k in layer_filter} + for layer_name, layer in _layers.items(): + thickness_variable = "${}_thick".format(layer_name) + self._clean_string_for_variable_name(thickness_variable) + if thickness_variable not in self.variables: + self.add_design_variable(thickness_variable, layer.thickness) + layer.thickness = thickness_variable + parameters.append(thickness_variable) + if materials: + if not material_filter: + _materials = self.materials.materials + else: + _materials = {k: v for k, v in self.materials.materials.items() if k in material_filter} + for mat_name, material in _materials.items(): + if material.conductivity < 1e4: + epsr_variable = "$epsr_{}".format(mat_name) + self._clean_string_for_variable_name(epsr_variable) + if epsr_variable not in self.variables: + self.add_design_variable(epsr_variable, material.permittivity) + material.permittivity = epsr_variable + parameters.append(epsr_variable) + loss_tg_variable = "$loss_tangent_{}".format(mat_name) + self._clean_string_for_variable_name(loss_tg_variable) + if not loss_tg_variable in self.variables: + self.add_design_variable(loss_tg_variable, material.loss_tangent) + material.loss_tangent = loss_tg_variable + parameters.append(loss_tg_variable) + else: + sigma_variable = "$sigma_{}".format(mat_name) + self._clean_string_for_variable_name(sigma_variable) + if not sigma_variable in self.variables: + self.add_design_variable(sigma_variable, material.conductivity) + material.conductivity = sigma_variable + parameters.append(sigma_variable) + if traces: + if not trace_net_filter: + paths = self.modeler.paths + else: + paths = [path for path in self.modeler.paths if path.net_name in trace_net_filter] + for path in paths: + trace_width_variable = "trace_w_{}_{}".format(path.net_name, path.id) + self._clean_string_for_variable_name(trace_width_variable) + if trace_width_variable not in self.variables: + self.add_design_variable(trace_width_variable, path.width) + path.width = trace_width_variable + parameters.append(trace_width_variable) + if not padstack_definition_filter: + used_padsatck_defs = list( + set([padstack_inst.padstack_definition for padstack_inst in list(self.padstacks.instances.values())]) + ) + padstack_defs = {k: v for k, v in self.padstacks.definitions.items() if k in used_padsatck_defs} + else: + padstack_defs = {k: v for k, v in self.padstacks.definitions.items() if k in padstack_definition_filter} + for def_name, padstack_def in padstack_defs.items(): + if not padstack_def.via_start_layer == padstack_def.via_stop_layer: + if via_holes: # pragma no cover + hole_variable = self._clean_string_for_variable_name("$hole_diam_{}".format(def_name)) + if hole_variable not in self.variables: + self.add_design_variable(hole_variable, padstack_def.hole_properties[0]) + padstack_def.hole_properties = hole_variable + parameters.append(hole_variable) + if pads: + for layer, pad in padstack_def.pad_by_layer.items(): + if pad.geometry_type == 1: + pad_diameter_variable = self._clean_string_for_variable_name( + "$pad_diam_{}_{}".format(def_name, layer) + ) + if pad_diameter_variable not in self.variables: + self.add_design_variable(pad_diameter_variable, pad.parameters_values[0]) + pad.parameters = {"Diameter": pad_diameter_variable} + parameters.append(pad_diameter_variable) + if pad.geometry_type == 2: # pragma no cover + pad_size_variable = self._clean_string_for_variable_name( + "$pad_size_{}_{}".format(def_name, layer) + ) + if pad_size_variable not in self.variables: + self.add_design_variable(pad_size_variable, pad.parameters_values[0]) + pad.parameters = {"Size": pad_size_variable} + parameters.append(pad_size_variable) + elif pad.geometry_type == 3: # pragma no cover + pad_size_variable_x = self._clean_string_for_variable_name( + "$pad_size_x_{}_{}".format(def_name, layer) + ) + pad_size_variable_y = self._clean_string_for_variable_name( + "$pad_size_y_{}_{}".format(def_name, layer) + ) + if pad_size_variable_x not in self.variables and pad_size_variable_y not in self.variables: + self.add_design_variable(pad_size_variable_x, pad.parameters_values[0]) + self.add_design_variable(pad_size_variable_y, pad.parameters_values[1]) + pad.parameters = {"XSize": pad_size_variable_x, "YSize": pad_size_variable_y} + parameters.append(pad_size_variable_x) + parameters.append(pad_size_variable_y) + if antipads: + for layer, antipad in padstack_def.antipad_by_layer.items(): + if antipad.geometry_type == 1: # pragma no cover + antipad_diameter_variable = self._clean_string_for_variable_name( + "$antipad_diam_{}_{}".format(def_name, layer) + ) + if antipad_diameter_variable not in self.variables: # pragma no cover + self.add_design_variable(antipad_diameter_variable, antipad.parameters_values[0]) + antipad.parameters = {"Diameter": antipad_diameter_variable} + parameters.append(antipad_diameter_variable) + if antipad.geometry_type == 2: # pragma no cover + antipad_size_variable = self._clean_string_for_variable_name( + "$antipad_size_{}_{}".format(def_name, layer) + ) + if antipad_size_variable not in self.variables: # pragma no cover + self.add_design_variable(antipad_size_variable, antipad.parameters_values[0]) + antipad.parameters = {"Size": antipad_size_variable} + parameters.append(antipad_size_variable) + elif antipad.geometry_type == 3: # pragma no cover + antipad_size_variable_x = self._clean_string_for_variable_name( + "$antipad_size_x_{}_{}".format(def_name, layer) + ) + antipad_size_variable_y = self._clean_string_for_variable_name( + "$antipad_size_y_{}_{}".format(def_name, layer) + ) + if ( + antipad_size_variable_x not in self.variables + and antipad_size_variable_y not in self.variables + ): # pragma no cover + self.add_design_variable(antipad_size_variable_x, antipad.parameters_values[0]) + self.add_design_variable(antipad_size_variable_y, antipad.parameters_values[1]) + antipad.parameters = {"XSize": antipad_size_variable_x, "YSize": antipad_size_variable_y} + parameters.append(antipad_size_variable_x) + parameters.append(antipad_size_variable_y) + return parameters + + @pyaedt_function_handler + def _clean_string_for_variable_name(self, variable_name): + """Remove forbidden character for variable name. + + Parameter + ---------- + variable_name : str + Variable name. + + Returns + ------- + str + Edited name. + """ + if "-" in variable_name: + variable_name = variable_name.replace("-", "_") + if "+" in variable_name: + variable_name = variable_name.replace("+", "p") + return variable_name diff --git a/pyaedt/edb_core/components.py b/pyaedt/edb_core/components.py index 92fb5db470e..cfa0e335d15 100644 --- a/pyaedt/edb_core/components.py +++ b/pyaedt/edb_core/components.py @@ -713,7 +713,7 @@ def create_source_on_component(self, sources=None): return True @pyaedt_function_handler() - def create_port_on_pins(self, refdes, pins, reference_pins, impedance=50.0, port_name=None): + def create_port_on_pins(self, refdes, pins, reference_pins, impedance=50.0, port_name=None, pec_boundary=False): """Create circuit port between pins and reference ones. Parameters @@ -732,8 +732,13 @@ def create_port_on_pins(self, refdes, pins, reference_pins, impedance=50.0, port str, [str], EDBPadstackInstance, [EDBPadstackInstance] impedance : Port impedance str, float - port_name : Port Name (Optional) when provided will overwrite the default naming convention - str + port_name : str, optional + Port name. The default is ``None``, in which case a name is automatically assigned. + pec_boundary : bool, optional + Whether to define the PEC boundary, The default is ``False``. If set to ``True``, + a perfect short is created between the pin and impedance is ignored. This + parameter is only supported on a port created between two pins, such as + when there is no pin group. Returns ------- @@ -783,22 +788,40 @@ def create_port_on_pins(self, refdes, pins, reference_pins, impedance=50.0, port if not len([pin for pin in reference_pins if isinstance(pin, EDBPadstackInstance)]) == len(reference_pins): return if len(pins) > 1: + pec_boundary = False + self._logger.info( + "Disabling PEC boundary creation, this feature is supported on single pin " + "ports only, {} pins found".format(len(pins)) + ) group_name = "group_{}_{}".format(pins[0].net_name, pins[0].name) pin_group = self.create_pingroup_from_pins(pins, group_name) term = self._create_pin_group_terminal(pingroup=pin_group, term_name=port_name) else: - term = self._create_terminal(pins[0], term_name=port_name) + term = self._create_terminal(pins[0].primitive_object, term_name=port_name) term.SetIsCircuitPort(True) if len(reference_pins) > 1: + pec_boundary = False + self._logger.info( + "Disabling PEC boundary creation. This feature is supported on single pin" + "ports only {} reference pins found.".format(len(reference_pins)) + ) ref_group_name = "group_{}_{}_ref".format(reference_pins[0].net_name, reference_pins[0].name) ref_pin_group = self.create_pingroup_from_pins(reference_pins, ref_group_name) ref_term = self._create_pin_group_terminal(pingroup=ref_pin_group, term_name=port_name + "_ref") else: - ref_term = self._create_terminal(reference_pins[0], term_name=port_name + "_ref") + ref_term = self._create_terminal(reference_pins[0].primitive_object, term_name=port_name + "_ref") ref_term.SetIsCircuitPort(True) term.SetImpedance(self._edb.utility.value(impedance)) term.SetReferenceTerminal(ref_term) + if pec_boundary: + term.SetIsCircuitPort(False) + ref_term.SetIsCircuitPort(False) + term.SetBoundaryType(self._edb.cell.terminal.BoundaryType.PecBoundary) + ref_term.SetBoundaryType(self._edb.cell.terminal.BoundaryType.PecBoundary) + self._logger.info( + "PEC boundary created between pin {} and reference pin {}".format(pins[0].name, reference_pins[0].name) + ) if term: return term return False @@ -988,11 +1011,9 @@ def _create_terminal(self, pin, term_name=None): Returns ------- - Edb terminal. + EDB terminal. """ - pin_position = self.get_pin_position(pin) # pragma no cover - pin_pos = self._pedb.point_data(*pin_position) res, from_layer, _ = pin.GetLayerRange() cmp_name = pin.GetComponent().GetName() net_name = pin.GetNet().GetName() @@ -1002,8 +1023,8 @@ def _create_terminal(self, pin, term_name=None): for term in list(self._pedb.active_layout.Terminals): if term.GetName() == term_name: return term - term = self._edb.cell.terminal.PointTerminal.Create( - pin.GetLayout(), pin.GetNet(), term_name, pin_pos, from_layer + term = self._edb.cell.terminal.PadstackInstanceTerminal.Create( + pin.GetLayout(), pin.GetNet(), term_name, pin, from_layer ) return term @@ -1082,8 +1103,8 @@ def replace_rlc_by_gap_boundaries(self, component=None): return self.add_rlc_boundary(component.refdes, False) @pyaedt_function_handler() - def deactivate_rlc_component(self, component=None, create_circuit_port=False): - """Deactivate RLC component with a possibility to convert to a circuit port. + def deactivate_rlc_component(self, component=None, create_circuit_port=False, pec_boundary=False): + """Deactivate RLC component with a possibility to convert it to a circuit port. Parameters ---------- @@ -1093,6 +1114,11 @@ def deactivate_rlc_component(self, component=None, create_circuit_port=False): create_circuit_port : bool, optional Whether to replace the deactivated RLC component with a circuit port. The default is ``False``. + pec_boundary : bool, optional + Whether to define the PEC boundary, The default is ``False``. If set to ``True``, + a perfect short is created between the pin and impedance is ignored. This + parameter is only supported on a port created between two pins, such as + when there is no pin group. Returns ------- @@ -1125,12 +1151,14 @@ def deactivate_rlc_component(self, component=None, create_circuit_port=False): self._logger.info("Component %s passed to deactivate is not an RLC.", component.refdes) return False component.is_enabled = False - return self.add_port_on_rlc_component(component=component.refdes, circuit_ports=create_circuit_port) + return self.add_port_on_rlc_component( + component=component.refdes, circuit_ports=create_circuit_port, pec_boundary=pec_boundary + ) @pyaedt_function_handler() - def add_port_on_rlc_component(self, component=None, circuit_ports=True): + def add_port_on_rlc_component(self, component=None, circuit_ports=True, pec_boundary=False): """Deactivate RLC component and replace it with a circuit port. - The circuit port supports only 2-pin components. + The circuit port supports only two-pin components. Parameters ---------- @@ -1141,6 +1169,13 @@ def add_port_on_rlc_component(self, component=None, circuit_ports=True): ``True`` will replace RLC component by circuit ports, ``False`` gap ports compatible with HFSS 3D modeler export. + pec_boundary : bool, optional + pec_boundary : bool, optional + Whether to define the PEC boundary, The default is ``False``. If set to ``True``, + a perfect short is created between the pin and impedance is ignored. This + parameter is only supported on a port created between two pins, such as + when there is no pin group. + Returns ------- bool @@ -1153,9 +1188,6 @@ def add_port_on_rlc_component(self, component=None, circuit_ports=True): self.set_component_rlc(component.refdes) pins = self.get_pin_from_component(component.refdes) if len(pins) == 2: # pragma: no cover - pos_pin_loc = self.get_pin_position(pins[0]) - pt = self._pedb.point_data(*pos_pin_loc) - pin_layers = self._padstack._get_pin_layer_range(pins[0]) pos_pin_term = self._pedb.edb_api.cell.terminal.PadstackInstanceTerminal.Create( self._active_layout, @@ -1167,9 +1199,6 @@ def add_port_on_rlc_component(self, component=None, circuit_ports=True): ) if not pos_pin_term: # pragma: no cover return False - neg_pin_loc = self.get_pin_position(pins[1]) - pt = self._pedb.point_data(*neg_pin_loc) - neg_pin_term = self._pedb.edb_api.cell.terminal.PadstackInstanceTerminal.Create( self._active_layout, pins[1].GetNet(), @@ -1180,17 +1209,23 @@ def add_port_on_rlc_component(self, component=None, circuit_ports=True): ) if not neg_pin_term: # pragma: no cover return False - pos_pin_term.SetBoundaryType(self._pedb.edb_api.cell.terminal.BoundaryType.PortBoundary) + if pec_boundary: + pos_pin_term.SetBoundaryType(self._pedb.edb_api.cell.terminal.BoundaryType.PecBoundary) + neg_pin_term.SetBoundaryType(self._pedb.edb_api.cell.terminal.BoundaryType.PecBoundary) + else: + pos_pin_term.SetBoundaryType(self._pedb.edb_api.cell.terminal.BoundaryType.PortBoundary) + neg_pin_term.SetBoundaryType(self._pedb.edb_api.cell.terminal.BoundaryType.PortBoundary) pos_pin_term.SetName(component.refdes) - neg_pin_term.SetBoundaryType(self._pedb.edb_api.cell.terminal.BoundaryType.PortBoundary) pos_pin_term.SetReferenceTerminal(neg_pin_term) - if circuit_ports: + if circuit_ports and not pec_boundary: pos_pin_term.SetIsCircuitPort(True) neg_pin_term.SetIsCircuitPort(True) + elif pec_boundary: + pos_pin_term.SetIsCircuitPort(False) + neg_pin_term.SetIsCircuitPort(False) else: pos_pin_term.SetIsCircuitPort(False) neg_pin_term.SetIsCircuitPort(False) - self._logger.info("Component {} has been replaced by port".format(component.refdes)) return True return False diff --git a/pyaedt/edb_core/edb_data/primitives_data.py b/pyaedt/edb_core/edb_data/primitives_data.py index 259f8f31b33..0053163de58 100644 --- a/pyaedt/edb_core/edb_data/primitives_data.py +++ b/pyaedt/edb_core/edb_data/primitives_data.py @@ -853,7 +853,7 @@ def create_edge_port( pyaedt_function_handler() - def create_via_fence(self, distance, gap, padstack_name): + def create_via_fence(self, distance, gap, padstack_name, net_name="GND"): """Create via fences on both sides of the trace. Parameters @@ -864,6 +864,8 @@ def create_via_fence(self, distance, gap, padstack_name): Gap between vias. padstack_name: str Name of the via padstack. + net_name: str,optional + Name of the net. Returns ------- @@ -956,7 +958,7 @@ def getParalletLines(pts, distance): # pragma: no cover 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) + self._pedb.padstacks.place([x, y], padstack_name, net_name=net_name) class EdbRectangle(EDBPrimitives, RectangleDotNet): diff --git a/pyaedt/edb_core/edb_data/terminals.py b/pyaedt/edb_core/edb_data/terminals.py index c178821eb70..356ef32be41 100644 --- a/pyaedt/edb_core/edb_data/terminals.py +++ b/pyaedt/edb_core/edb_data/terminals.py @@ -476,6 +476,20 @@ class PadstackInstanceTerminal(Terminal): def __init__(self, pedb, edb_object): super().__init__(pedb, edb_object) + @property + def position(self): + """Return terminal position. + + Returns + ------- + Position [x,y] : [float, float] + + """ + edb_padstack_instance = self._edb_object.GetParameters() + if edb_padstack_instance[0]: + return EDBPadstackInstance(edb_padstack_instance[1], self._pedb).position + return False + def create(self, padstack_instance, name=None, layer=None, is_ref=False): """Create an edge terminal. diff --git a/pyaedt/edb_core/ipc2581/bom/bom.py b/pyaedt/edb_core/ipc2581/bom/bom.py index e3fcd50b20e..0e13e87ebcd 100644 --- a/pyaedt/edb_core/ipc2581/bom/bom.py +++ b/pyaedt/edb_core/ipc2581/bom/bom.py @@ -2,8 +2,9 @@ class Bom(object): - def __init__(self): - self.name = "bom" + def __init__(self, edb): + self._edb = edb + self.name = self._edb.cell_names[0] self.revision = "1.0" self.step_ref = "1.0" self.bom_items = [] diff --git a/pyaedt/edb_core/ipc2581/bom/bom_item.py b/pyaedt/edb_core/ipc2581/bom/bom_item.py index 06c63b18db3..b070ca6d955 100644 --- a/pyaedt/edb_core/ipc2581/bom/bom_item.py +++ b/pyaedt/edb_core/ipc2581/bom/bom_item.py @@ -20,8 +20,8 @@ def write_xml(self, bom): # pragma no cover bom_item.set("category", self.category) bom_item.set("category", self.category) for refdes in self.refdes_list: - refdes.write_xml(bom) - self.charactistics.write_xml(bom) + refdes.write_xml(bom_item) + self.charactistics.write_xml(bom_item) def add_refdes(self, component_name=None, package_def=None, populate=True, placement_layer=""): # pragma no cover refdes = RefDes() diff --git a/pyaedt/edb_core/ipc2581/ecad/cad_data/profile.py b/pyaedt/edb_core/ipc2581/ecad/cad_data/profile.py index 17518303242..02381c44404 100644 --- a/pyaedt/edb_core/ipc2581/ecad/cad_data/profile.py +++ b/pyaedt/edb_core/ipc2581/ecad/cad_data/profile.py @@ -1,9 +1,11 @@ -from pyaedt.edb_core.ipc2581.ecad.cad_data.polygon import Polygon +from pyaedt.edb_core.ipc2581.ecad.cad_data.layer_feature import LayerFeature +from pyaedt.generic.general_methods import ET class Profile(object): - def __init__(self): + def __init__(self, ipc): self._profile = [] + self._ipc = ipc @property def profile(self): @@ -12,12 +14,27 @@ def profile(self): @profile.setter def profile(self, value): # pragma no cover if isinstance(value, list): - if len([poly for poly in value if isinstance(poly, Polygon)]) == len(value): + if len([poly for poly in value if isinstance(poly, LayerFeature)]) == len(value): self._profile = value def add_polygon(self, polygon): # pragma no cover - if isinstance(polygon, Polygon): + if isinstance(polygon, LayerFeature): self._profile.append(polygon) - def xml_writer(self): # pragma no cover - pass + def xml_writer(self, step): # pragma no cover + profile = ET.SubElement(step, "Profile") + for poly in self.profile: + for feature in poly.features: + if feature.feature_type == 0: + polygon = ET.SubElement(profile, "Polygon") + polygon_begin = ET.SubElement(polygon, "PolyBegin") + polygon_begin.set( + "x", str(self._ipc.from_meter_to_units(feature.polygon.poly_steps[0].x, self._ipc.units)) + ) + polygon_begin.set( + "y", str(self._ipc.from_meter_to_units(feature.polygon.poly_steps[0].y, self._ipc.units)) + ) + for poly_step in feature.polygon.poly_steps[1:]: + poly_step.write_xml(polygon, self._ipc) + for cutout in feature.polygon.cutout: + cutout.write_xml(profile, self._ipc) diff --git a/pyaedt/edb_core/ipc2581/ecad/cad_data/step.py b/pyaedt/edb_core/ipc2581/ecad/cad_data/step.py index 5082e591247..61d1031b54d 100644 --- a/pyaedt/edb_core/ipc2581/ecad/cad_data/step.py +++ b/pyaedt/edb_core/ipc2581/ecad/cad_data/step.py @@ -19,7 +19,7 @@ def __init__(self, caddata, edb, units, ipc): self.units = units self._cad_data = caddata self._padstack_defs = {} - self._profile = Profile() + self._profile = Profile(ipc) self._packages = {} self._components = [] self._logical_nets = [] @@ -46,6 +46,10 @@ def packages(self, value): # pragma no cover if len([pkg for pkg in value if isinstance(pkg, Package)]) == len(value): self._packages = value + @property + def profile(self): + return self._profile + @property def components(self): return self._components @@ -204,6 +208,15 @@ def add_layer_feature(self, layer, polys): # pragma no cover layer_feature.add_feature(poly) self._ipc.ecad.cad_data.cad_data_step.layer_features.append(layer_feature) + @pyaedt_function_handler() + def add_profile(self, poly): # pragma no cover + profile = LayerFeature(self._ipc) + profile.layer_name = "profile" + if poly: + if not poly.is_void: + profile.add_feature(poly) + self.profile.add_polygon(profile) + @pyaedt_function_handler() def add_padstack_instances(self, padstack_instances, padstack_defs): # pragma no cover top_bottom_layers = self._ipc.top_bottom_layers @@ -252,6 +265,7 @@ def write_xml(self, cad_data): # pragma no cover step.set("name", self._ipc.design_name) for padsatck_def in list(self.padstack_defs.values()): padsatck_def.write_xml(step) + self.profile.xml_writer(step) for package in list(self.packages.values()): package.write_xml(step) for component in self.components: diff --git a/pyaedt/edb_core/ipc2581/ipc2581.py b/pyaedt/edb_core/ipc2581/ipc2581.py index 94ae892667c..2197b3ffee5 100644 --- a/pyaedt/edb_core/ipc2581/ipc2581.py +++ b/pyaedt/edb_core/ipc2581/ipc2581.py @@ -21,7 +21,7 @@ def __init__(self, pedb, units): self.content = Content(self) self.logistic_header = LogisticHeader() self.history_record = HistoryRecord() - self.bom = Bom() + self.bom = Bom(pedb) self.ecad = Ecad(self, pedb, units) self.file_path = "" self.design_name = "" @@ -37,6 +37,7 @@ def load_ipc_model(self): self.add_bom() self._pedb.logger.info("Parsing Padstack Definitions...") self.add_pdstack_definition() + self.add_profile() self._pedb.logger.info("Parsing Components...") self.add_components() self._pedb.logger.info("Parsing Logical Nets...") @@ -308,9 +309,15 @@ def add_logical_nets(self): for net in nets: self.ecad.cad_data.cad_data_step.add_logical_net(net) + @pyaedt_function_handler() + def add_profile(self): + profile = self._pedb.modeler.primitives_by_layer["Outline"] + for prim in profile: + self.ecad.cad_data.cad_data_step.add_profile(prim) + @pyaedt_function_handler() def add_layer_features(self): - layers = {i: j for i, j in self._pedb.stackup.signal_layers.items()} + layers = {i: j for i, j in self._pedb.stackup.layers.items()} padstack_instances = list(self._pedb.padstacks.instances.values()) padstack_defs = {i: k for i, k in self._pedb.padstacks.definitions.items()} polys = {i: j for i, j in self._pedb.modeler.primitives_by_layer.items()} @@ -368,9 +375,9 @@ def write_xml(self): ipc.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") ipc.set("xmlns:xsd", "http://www.w3.org/2001/XMLSchema") self.content.write_wml(ipc) - # self.logistic_header.write_xml(ipc) - # self.history_record.write_xml(ipc) - # self.bom.write_xml(ipc) + self.logistic_header.write_xml(ipc) + self.history_record.write_xml(ipc) + self.bom.write_xml(ipc) self.ecad.write_xml(ipc) try: ET.indent(ipc) diff --git a/pyaedt/edb_core/layout.py b/pyaedt/edb_core/layout.py index 0b225fe2e70..72275cb1a02 100644 --- a/pyaedt/edb_core/layout.py +++ b/pyaedt/edb_core/layout.py @@ -1103,15 +1103,17 @@ def parametrize_trace_width( return True @pyaedt_function_handler() - def unite_polygons_on_layer(self, layer_name=None, delete_padstack_gemometries=False): + def unite_polygons_on_layer(self, layer_name=None, delete_padstack_gemometries=False, net_list=[]): """Try to unite all Polygons on specified layer. Parameters ---------- layer_name : str, optional - Layer Name on which unite objects. If ``None``, all layers will be taken. + Name of layer name to unite objects on. The default is ``None``, in which case all layers are taken. delete_padstack_gemometries : bool, optional - ``True`` to delete all padstack geometry. + Whether to delete all padstack geometries. The default is ``False``. + net_list : list[str] : optional + Net list filter. The default is ``[]``, in which case all nets are taken. Returns ------- @@ -1135,33 +1137,34 @@ def unite_polygons_on_layer(self, layer_name=None, delete_padstack_gemometries=F if poly.GetNet().GetName(): poly_by_nets[poly.GetNet().GetName()].append(poly) for net in poly_by_nets: - list_polygon_data = [i.GetPolygonData() for i in poly_by_nets[net]] - all_voids = [i.Voids for i in poly_by_nets[net]] - a = self._edb.geometry.polygon_data.unite(convert_py_list_to_net_list(list_polygon_data)) - for item in a: + if net in net_list or not net_list: # pragma no cover + list_polygon_data = [i.GetPolygonData() for i in poly_by_nets[net]] + all_voids = [i.Voids for i in poly_by_nets[net]] + a = self._edb.geometry.polygon_data.unite(convert_py_list_to_net_list(list_polygon_data)) + for item in a: + for v in all_voids: + for void in v: + if int(item.GetIntersectionType(void.GetPolygonData())) == 2: + item.AddHole(void.GetPolygonData()) + poly = self._edb.cell.primitive.polygon.create( + self._active_layout, + lay, + self._pedb.nets.nets[net], + item, + ) + list_to_delete = [i for i in poly_by_nets[net]] for v in all_voids: for void in v: - if int(item.GetIntersectionType(void.GetPolygonData())) == 2: - item.AddHole(void.GetPolygonData()) - poly = self._edb.cell.primitive.polygon.create( - self._active_layout, - lay, - self._pedb.nets.nets[net], - item, - ) - list_to_delete = [i for i in poly_by_nets[net]] - for v in all_voids: - for void in v: - for poly in poly_by_nets[net]: - if int(void.GetPolygonData().GetIntersectionType(poly.GetPolygonData())) >= 2: - try: - id = list_to_delete.index(poly) - except ValueError: - id = -1 - if id >= 0: - list_to_delete.pop(id) - - [i.Delete() for i in list_to_delete] + for poly in poly_by_nets[net]: # pragma no cover + if int(void.GetPolygonData().GetIntersectionType(poly.GetPolygonData())) >= 2: + try: + id = list_to_delete.index(poly) + except ValueError: + id = -1 + if id >= 0: + list_to_delete.pop(id) + + [i.Delete() for i in list_to_delete] # pragma no cover if delete_padstack_gemometries: self._logger.info("Deleting Padstack Definitions") diff --git a/pyaedt/edb_core/nets.py b/pyaedt/edb_core/nets.py index 35747b59830..68e4c1e95b0 100644 --- a/pyaedt/edb_core/nets.py +++ b/pyaedt/edb_core/nets.py @@ -6,7 +6,6 @@ import warnings from pyaedt.edb_core.edb_data.nets_data import EDBNetsData -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 from pyaedt.generic.general_methods import is_ironpython @@ -1182,45 +1181,14 @@ def merge_nets_polygons(self, net_list): Parameters ---------- net_list : str or list[str] - net name of list of net name. + Net name of list of net name. Returns - list of merged polygons. - ------- + operation result : bool + ``True`` when successful, ``False`` when failed. """ if isinstance(net_list, str): net_list = [net_list] - returned_poly = [] - for net in net_list: - if net in self.nets: - net_rtree = self._edb.Geometry.RTree() - paths = [prim for prim in self.nets[net].primitives if prim.type == "Path"] - for path in paths: - path.convert_to_polygon() - polygons = [prim for prim in self.nets[net].primitives if prim.type == "Polygon"] - for polygon in polygons: - polygon_data = polygon.primitive_object.GetPolygonData() - rtree = self._edb.Geometry.RTreeObj(polygon_data, polygon.primitive_object) - net_rtree.Insert(rtree) - connected_polygons = net_rtree.GetConnectedGeometrySets() - void_list = [] - for pp in list(connected_polygons): - for _pp in list(pp): - _voids = list(_pp.Obj.Voids) - void_list.extend(_pp.Obj.Voids) - for poly_list in list(connected_polygons): - layer = list(poly_list)[0].Obj.GetLayer().GetName() - net = list(poly_list)[0].Obj.GetNet() - _poly_list = convert_py_list_to_net_list([obj.Poly for obj in list(poly_list)]) - merged_polygon = list(self._edb.geometry.polygon_data.unite(_poly_list)) - for poly in merged_polygon: - for void in void_list: - poly.AddHole(void.GetPolygonData()) - _new_poly = self._edb.cell.primitive.polygon.create(self._active_layout, layer, net, poly) - returned_poly.append(_new_poly) - for init_poly in list(list(connected_polygons)): - for _pp in list(init_poly): - _pp.Obj.Delete() - return returned_poly + return self._pedb.modeler.unite_polygons_on_layer(net_list=net_list) diff --git a/pyaedt/generic/grpc_plugin_dll.py b/pyaedt/generic/grpc_plugin_dll.py index 85cb2dec80c..0c2a4f19821 100644 --- a/pyaedt/generic/grpc_plugin_dll.py +++ b/pyaedt/generic/grpc_plugin_dll.py @@ -6,6 +6,9 @@ from ctypes import py_object import os +is_linux = os.name == "posix" +is_windows = not is_linux + pathDir = os.environ["DesktopPluginPyAEDT"] # DesktopPlugin pathDir = os.path.dirname(pathDir) # PythonFiles pathDir = os.path.dirname(pathDir) # DesktopPlugin or Win64 @@ -13,7 +16,7 @@ # Plugin filename depends on OS -if os.name != r"nt": +if is_linux: pluginFileName = r"libPyDesktopPlugin.so" else: pluginFileName = r"PyDesktopPlugin.dll" @@ -28,7 +31,7 @@ # AedtAPIDll_file = os.path.join(pathDir, r"PyAedtStub/x64/Debug/PyAedtStub.dll") #develop dir # load dll -if os.name == r"nt": +if is_windows: # on windows, modify path aedtDir = os.path.dirname(AedtAPIDll_file) originalPath = os.environ["PATH"] diff --git a/pyaedt/generic/ibis_v7.json b/pyaedt/generic/ibis_v7.json index b98f09bb366..5d7935077fa 100644 --- a/pyaedt/generic/ibis_v7.json +++ b/pyaedt/generic/ibis_v7.json @@ -62,7 +62,7 @@ "L Series": "", "Rl Series": "", "C Series": "", - "Lc Seeries": "", + "Lc Series": "", "Rc Series": "", "Series Current": "", "Series MOSFET": "", diff --git a/pyaedt/generic/process.py b/pyaedt/generic/process.py index a2325c6894f..ca3786d65d5 100644 --- a/pyaedt/generic/process.py +++ b/pyaedt/generic/process.py @@ -277,7 +277,7 @@ def export_dc_report( command.append("-RunScriptAndExit") command.append('"' + scriptname + '"') print(command) - if os.name == "posix": + if is_linux: p = subprocess.Popen(command) else: diff --git a/pyaedt/icepak.py b/pyaedt/icepak.py index 970404eb93b..9283a67e159 100644 --- a/pyaedt/icepak.py +++ b/pyaedt/icepak.py @@ -548,6 +548,9 @@ def create_conduting_plate( ): """Add a conductive plate thermal assignment on a face. + .. deprecated:: 0.7.8 + This method is deprecated. Use the ``assign_conducting_plate()`` method instead. + Parameters ---------- face_id : int or str or list @@ -592,6 +595,12 @@ def create_conduting_plate( Boundary object when successful or ``None`` when failed. """ + + warnings.warn( + "This method is deprecated in 0.7.8. Use the ``assign_conducting_plate()`` method.", + DeprecationWarning, + ) + if not bc_name: bc_name = generate_unique_name("Source") props = {} @@ -5741,3 +5750,304 @@ def _assign_blower(self, props, faces, inlet_face, fan_curve_flow_unit, fan_curv boundary_name = generate_unique_name("Blower") bound = BoundaryObject(self, boundary_name, props, "Blower") return _create_boundary(bound) + + @pyaedt_function_handler() + def assign_conducting_plate(self, obj_plate, boundary_name=None, total_power="0W", + thermal_specification="Thickness", thickness="1mm", solid_material="Al-Extruded", + conductance="0W_per_Cel", shell_conduction=False, thermal_resistance="0Kel_per_W", + low_side_rad_material=None, high_side_rad_material=None, + thermal_impedance="0celm2_per_W"): + """ + Assign thermal boundary conditions to a conducting plate. + + Parameters + ---------- + obj_plate : str or int or list + Object to assign the boundary to. If a string, specify a surface name. + If an integer, specify a face ID. + boundary_name : str, optional + Boundary name. The default is ``None``, in which case a name is generated + automatically. + total_power : str or float or dict, optional + Power dissipated by the plate. The default is ``"0W"``. If a float, + the default unit is ``"W"``. A transient or temperature-dependent power + can be assigned with a dictionary. + thermal_specification : str, optional + Type of condition to apply. The default is `"Thickness"``. + Options are ``"Conductance"``, ``"Thermal Impedance"``, + ``"Thermal Resistance"``, and ``"Thickness"``. + thickness : str or float, optional + If ``thermal_specification="Thickness"``, this parameter represents the + thickness to model with the plate. The default is ``"1mm"``. If a float, + the default unit is ``"mm"``. + solid_material : str, optional + If ``thermal_specification="Thickness"``, this parameter represents the + material of the conducting plate. The default is ``"Al-Extruded"``. + conductance : str or float, optional + If ``thermal_specification="Conductance"``, this parameter represents the + conductance of the plate. The default is ``"0W_per_Cel"``. If a float, the default + unit is ``"W_per_Cel"``. + thermal_resistance : str or float, optional + If ``thermal_specification="Thermal Resistance"``, this parameter represents the + thermal resistance of the plate. The default is ``"0Kel_per_W"``. If a float, the + default unit is ``"Kel_per_W"``. + thermal_impedance : str or float, optional + If ``thermal_specification="Thermal Impedance"``, this parameter represents the + thermal impedance of the plate. The default is ``"0Cel_m2_per_W"``. If a float, the + default unit is "``Cel_m2_per_W"``. + shell_conduction : bool, optional + Whether to consider shell conduction. The default is ``False``. + low_side_rad_material : str, optional + Material on the low side for radiation. The default is ``None``, in which + case radiation is disabled on the low side. + high_side_rad_material : str, optional + Material on the high side for radiation. The default is ``None``, in which + case radiation is disabled on the high side. + + Returns + ------- + :class:`pyaedt.modules.Boundary.BoundaryObject` + Boundary object when successful or ``None`` when failed. + + """ + props = {} + if not isinstance(obj_plate, list): + obj_plate = [obj_plate] + if all(isinstance(obj, int) for obj in obj_plate): + props["Faces"] = obj_plate + elif all(isinstance(obj, str) for obj in obj_plate): + props["Objects"] = obj_plate + else: + raise AttributeError("Invalid ``obj_plate`` argument.") + + if isinstance(total_power, dict): + assignment = self._parse_variation_data( + "Total Power", + total_power["Type"], + variation_value=total_power["Values"], + function=total_power["Function"], + ) + props.update(assignment) + else: + props["Total Power"] = total_power + props["Thermal Specification"] = thermal_specification + for value, key, unit in zip( + [thickness, conductance, thermal_resistance, thermal_impedance], + ["Thickness", "Conductance", "Thermal Resistance", "Thermal Impedance"], + ["mm", "W_per_Cel", "Kel_per_W", "Cel_m2_per_W"] + ): + if thermal_specification == key: + if not isinstance(value, str): + value = str(value) + unit + props[key] = value + if thermal_specification == "Thickness": + props["Solid Material"] = solid_material + if low_side_rad_material is not None: + props["LowSide"] = {"Radiate": False} + else: + props["LowSide"] = {"Radiate": True, + "RadiateTo": "AllObjects", + "Surface Material": low_side_rad_material} + if high_side_rad_material is not None: + props["LowSide"] = {"Radiate": False} + else: + props["HighSide"] = {"Radiate": True, + "RadiateTo - High": "AllObjects - High", + "Surface Material - High": high_side_rad_material} + props["Shell Conduction"] = shell_conduction + if not boundary_name: + boundary_name = generate_unique_name("Plate") + bound = BoundaryObject(self, boundary_name, props, "Conducting Plate") + return _create_boundary(bound) + + def assign_conducting_plate_with_thickness(self, obj_plate, boundary_name=None, total_power="0W", + thickness="1mm", solid_material="Al-Extruded", + shell_conduction=False, low_side_rad_material=None, + high_side_rad_material=None): + """ + Assign thermal boundary conditions with thickness specification to a conducting plate. + + Parameters + ---------- + obj_plate : str or int or list + Object to assign the boundary to. If a string, specify a surface name. + If an integer, specify a face ID. + boundary_name : str, optional + Boundary name. The default is ``None``, in which case a name is generated + automatically. + total_power : str or float or dict, optional + Power dissipated by the plate. The default is ``"0W"``. If a float, + the default unit is ``"W"``. A transient or temperature-dependent power + can be assigned with a dictionary. + thickness : str or float, optional + If ``thermal_specification="Thickness"``, this parameter represents the + thickness to model with the plate. The default is ``"1mm"``. If a float, + the default unit is ``"mm"``. + solid_material : str, optional + If ``thermal_specification="Thickness"``, this parameter represents the + material of the conducting plate. The default is ``"Al-Extruded"``. + shell_conduction : bool, optional + Whether to consider shell conduction. The default is ``False``. + low_side_rad_material : str, optional + Material on the low side for radiation. The default is ``None``, in which + case radiation is disabled on the low side. + high_side_rad_material : str, optional + Material on the high side for radiation. The default is ``None``, in which + case radiation is disabled on the high side. + + Returns + ------- + :class:`pyaedt.modules.Boundary.BoundaryObject` + Boundary object when successful or ``None`` when failed. + + """ + return self.assign_conducting_plate(obj_plate, + boundary_name=boundary_name, + total_power=total_power, + thermal_specification="Thickness", + thickness=thickness, + solid_material=solid_material, + shell_conduction=shell_conduction, + low_side_rad_material=low_side_rad_material, + high_side_rad_material=high_side_rad_material) + + def assign_conducting_plate_with_resistance(self, obj_plate, boundary_name=None, total_power="0W", + thermal_resistance="0Kel_per_W", + shell_conduction=False, low_side_rad_material=None, + high_side_rad_material=None): + """ + Assign thermal boundary conditions with thermal resistance specification to a conducting plate. + + Parameters + ---------- + obj_plate : str or int or list + Object to assign the boundary to. If a string, specify a surface name. + If an integer, specify a face ID. + boundary_name : str, optional + Boundary name. The default is ``None``, in which case a name is generated + automatically. + total_power : str or float or dict, optional + Power dissipated by the plate. The default is ``"0W"``. If a float, + the default unit is ``"W"``. A transient or temperature-dependent power + can be assigned with a dictionary. + thermal_resistance : str or float, optional + If ``thermal_specification="Thermal Resistance"``, this parameter represents the + thermal resistance of the plate. The default is ``"0Kel_per_W"``. If a float, the + default unit is ``"Kel_per_W"``. + shell_conduction : bool, optional + Whether to consider shell conduction. The default is ``False``. + low_side_rad_material : str, optional + Material on the low side for radiation. The default is ``None``, in which + case radiation is disabled on the low side. + high_side_rad_material : str, optional + Material on the high side for radiation. The default is ``None``, in which + case radiation is disabled on the high side. + + Returns + ------- + :class:`pyaedt.modules.Boundary.BoundaryObject` + Boundary object when successful or ``None`` when failed. + + """ + return self.assign_conducting_plate(obj_plate, + boundary_name=boundary_name, + total_power=total_power, + thermal_specification="Thermal Resistance", + thermal_resistance=thermal_resistance, + shell_conduction=shell_conduction, + low_side_rad_material=low_side_rad_material, + high_side_rad_material=high_side_rad_material) + + def assign_conducting_plate_with_impedance(self, obj_plate, boundary_name=None, total_power="0W", + thermal_impedance="0celm2_per_W", + shell_conduction=False, low_side_rad_material=None, + high_side_rad_material=None): + """ + Assign thermal boundary conditions with thermal impedance specification to a conducting plate. + + Parameters + ---------- + obj_plate : str or int or list + Object to assign the boundary to. If a string, specify a surface name. + If an integer, specify a face ID. + boundary_name : str, optional + Boundary name. The default is ``None``, in which case a name is generated + automatically. + total_power : str or float or dict, optional + Power dissipated by the plate. The default is ``"0W"``. If a float, + the default unit is ``"W"``. A transient or temperature-dependent power + can be assigned with a dictionary. + thermal_impedance : str or float, optional + If ``thermal_specification="Thermal Impedance"``, this parameter represents the + thermal impedance of the plate. The default is ``"0Cel_m2_per_W"``. If a float, the + default unit is "``Cel_m2_per_W"``. + shell_conduction : bool, optional + Whether to consider shell conduction. The default is ``False``. + low_side_rad_material : str, optional + Material on the low side for radiation. The default is ``None``, in which + case radiation is disabled on the low side. + high_side_rad_material : str, optional + Material on the high side for radiation. The default is ``None``, in which + case radiation is disabled on the high side. + + Returns + ------- + :class:`pyaedt.modules.Boundary.BoundaryObject` + Boundary object when successful or ``None`` when failed. + + """ + return self.assign_conducting_plate(obj_plate, + boundary_name=boundary_name, + total_power=total_power, + thermal_specification="Thermal Impedance", + thermal_impedance=thermal_impedance, + shell_conduction=shell_conduction, + low_side_rad_material=low_side_rad_material, + high_side_rad_material=high_side_rad_material) + + def assign_conducting_plate_with_conductance(self, obj_plate, boundary_name=None, total_power="0W", + conductance="0W_per_Cel", + shell_conduction=False, low_side_rad_material=None, + high_side_rad_material=None): + """ + Assign thermal boundary conditions with conductance specification to a conducting plate. + + Parameters + ---------- + obj_plate : str or int or list + Object to assign the boundary to. If a string, specify a surface name. + If an integer, specify a face ID. + boundary_name : str, optional + Boundary name. The default is ``None``, in which case a name is generated + automatically. + total_power : str or float or dict, optional + Power dissipated by the plate. The default is ``"0W"``. If a float, + the default unit is ``"W"``. A transient or temperature-dependent power + can be assigned with a dictionary. + conductance : str or float, optional + If ``thermal_specification="Conductance"``, this parameter represents the + conductance of the plate. The default is ``"0W_per_Cel"``. If a float, the default + unit is ``"W_per_Cel"``. + shell_conduction : bool, optional + Whether to consider shell conduction. The default is ``False``. + low_side_rad_material : str, optional + Material on the low side for radiation. The default is ``None``, in which + case radiation is disabled on the low side. + high_side_rad_material : str, optional + Material on the high side for radiation. The default is ``None``, in which + case radiation is disabled on the high side. + + Returns + ------- + :class:`pyaedt.modules.Boundary.BoundaryObject` + Boundary object when successful or ``None`` when failed. + + """ + return self.assign_conducting_plate(obj_plate, + boundary_name=boundary_name, + total_power=total_power, + thermal_specification="Conductance", + conductance=conductance, + shell_conduction=shell_conduction, + low_side_rad_material=low_side_rad_material, + high_side_rad_material=high_side_rad_material) diff --git a/pyaedt/modules/DesignXPloration.py b/pyaedt/modules/DesignXPloration.py index 3dc5ca7df02..3eb3d702a44 100644 --- a/pyaedt/modules/DesignXPloration.py +++ b/pyaedt/modules/DesignXPloration.py @@ -894,7 +894,8 @@ def add_variation(self, sweep_var, start_point, end_point=None, step=100, unit=N unit : str, optional Variation units. Default is `None`. variation_type : float or int - Variation Type. Admitted values are `"LinearCount"`, `"LinearStep"`, `"LogScale"`, `"SingleValue"`. + Variation Type. Admitted values are `"SingleValue", `"LinearCount"`, `"LinearStep"`, + `"DecadeCount"`, `"OctaveCount"`, `"ExponentialCount"`. Returns ------- @@ -918,8 +919,12 @@ def add_variation(self, sweep_var, start_point, end_point=None, step=100, unit=N sweep_range = "LINC {} {} {}".format(start_point, end_point, step) elif variation_type == "LinearStep": sweep_range = "LIN {} {} {}".format(start_point, end_point, self._app.value_with_units(step, unit)) - elif variation_type == "LogScale": - sweep_range = "DEC {} {} {}".format(start_point, end_point, self._app.value_with_units(step, unit)) + elif variation_type == "DecadeCount": + sweep_range = "DEC {} {} {}".format(start_point, end_point, step) + elif variation_type == "OctaveCount": + sweep_range = "OCT {} {} {}".format(start_point, end_point, step) + elif variation_type == "ExponentialCount": + sweep_range = "ESTP {} {} {}".format(start_point, end_point, step) elif variation_type == "SingleValue": sweep_range = "{}".format(start_point) if not sweep_range: diff --git a/pyaedt/modules/Material.py b/pyaedt/modules/Material.py index 9be36f49b35..1963d72a260 100644 --- a/pyaedt/modules/Material.py +++ b/pyaedt/modules/Material.py @@ -2057,6 +2057,8 @@ def get_core_loss_coefficients( value, unit = decompose_variable_value(thickness) if not is_number(value) and not unit: raise TypeError("Thickness must be provided as a string with value and unit.") + if len(points_list_at_freq) <= 1 and core_loss_model_type == "Power Ferrite": + raise ValueError("At least 2 frequencies must be included.") props = OrderedDict({}) freq_keys = list(points_list_at_freq.keys()) for i in range(0, len(freq_keys)): @@ -2080,7 +2082,7 @@ def get_core_loss_coefficients( elif len(points_list_at_freq) > 1: props["CoreLossMultiCurveData"] = OrderedDict({}) props["CoreLossMultiCurveData"]["property_data"] = "coreloss_multi_curve_data" - props["CoreLossMultiCurveData"]["coreloss_unit"] = "w_per_cubic_meter" + props["CoreLossMultiCurveData"]["coreloss_unit"] = coefficient_setup props["CoreLossMultiCurveData"]["AllCurves"] = OrderedDict({}) props["CoreLossMultiCurveData"]["AllCurves"]["OneCurve"] = [] @@ -2096,9 +2098,15 @@ def get_core_loss_coefficients( props = self._get_args(props) props.pop(0) - props[0][-1][2] = "NAME:Points" - points = props[0][-1].pop(2) - props[0][-1][2].insert(0, points) + if len(points_list_at_freq) == 1: + props[0][-1][2] = "NAME:Points" + points = props[0][-1].pop(2) + props[0][-1][2].insert(0, points) + else: + for p in props[0][-1]: + if isinstance(p, list): + p[3].pop(2) + p[3][2].insert(0, "NAME:Points") coefficients = self.odefinition_manager.ComputeCoreLossCoefficients( core_loss_model_type, self.mass_density.evaluated_value, props[0] ) @@ -2185,6 +2193,8 @@ def set_coreloss_at_frequency( """ if not isinstance(points_list_at_freq, dict): raise TypeError("Points list at frequency must be provided as a dictionary.") + if len(points_list_at_freq) <= 1 and core_loss_model_type == "Power Ferrite": + raise ValueError("At least 2 frequencies must be included.") freq_keys = list(points_list_at_freq.keys()) for i in range(0, len(freq_keys)): if isinstance(freq_keys[i], str): @@ -2194,9 +2204,8 @@ def set_coreloss_at_frequency( points_list_at_freq[value] = points_list_at_freq[freq_keys[i]] del points_list_at_freq[freq_keys[i]] if "core_loss_type" not in self._props: - self._props["core_loss_type"] = OrderedDict( - {"property_type": "ChoiceProperty", "Choice": "Electrical Steel"} - ) + choice = "Electrical Steel" if core_loss_model_type == "Electrical Steel" else "Power Ferrite" + self._props["core_loss_type"] = OrderedDict({"property_type": "ChoiceProperty", "Choice": choice}) else: self._props.pop("core_loss_cm", None) self._props.pop("core_loss_x", None) @@ -2221,7 +2230,7 @@ def set_coreloss_at_frequency( elif len(points_list_at_freq) > 1: self._props["AttachedData"]["CoreLossMultiCurveData"] = OrderedDict({}) self._props["AttachedData"]["CoreLossMultiCurveData"]["property_data"] = "coreloss_multi_curve_data" - self._props["AttachedData"]["CoreLossMultiCurveData"]["coreloss_unit"] = "w_per_cubic_meter" + self._props["AttachedData"]["CoreLossMultiCurveData"]["coreloss_unit"] = coefficient_setup self._props["AttachedData"]["CoreLossMultiCurveData"]["AllCurves"] = OrderedDict({}) self._props["AttachedData"]["CoreLossMultiCurveData"]["AllCurves"]["OneCurve"] = [] @@ -2238,9 +2247,14 @@ def set_coreloss_at_frequency( coefficients = self.get_core_loss_coefficients( points_list_at_freq, thickness=thickness, conductivity=conductivity ) - self._props["core_loss_kh"] = str(coefficients[0]) - self._props["core_loss_kc"] = str(coefficients[1]) - self._props["core_loss_ke"] = str(coefficients[2]) + if core_loss_model_type == "Electrical Steel": + self._props["core_loss_kh"] = str(coefficients[0]) + self._props["core_loss_kc"] = str(coefficients[1]) + self._props["core_loss_ke"] = str(coefficients[2]) + else: + self._props["core_loss_cm"] = str(coefficients[0]) + self._props["core_loss_x"] = str(coefficients[1]) + self._props["core_loss_y"] = str(coefficients[2]) self._props["core_loss_kdc"] = str(kdc) self._props["core_loss_equiv_cut_depth"] = cut_depth return self.update() diff --git a/pyaedt/modules/PostProcessor.py b/pyaedt/modules/PostProcessor.py index 8d69c99dc8d..3acbd18ea3e 100644 --- a/pyaedt/modules/PostProcessor.py +++ b/pyaedt/modules/PostProcessor.py @@ -1623,6 +1623,7 @@ def create_report( variations = {"Freq": ["All"]} elif not variations and domain != "Sweep": variations = self._app.available_variations.nominal_w_values_dict + report.variations = variations if primary_sweep_variable: report.primary_sweep = primary_sweep_variable elif domain == "DCIR": # pragma: no cover @@ -1834,10 +1835,15 @@ def get_solution_data( report.domain = domain if primary_sweep_variable: report.primary_sweep = primary_sweep_variable - if variations: - report.variations = variations - else: - report.variations = self._app.available_variations.nominal_w_values_dict + if not variations and domain == "Sweep": + variations = self._app.available_variations.nominal_w_values_dict + if variations: + variations["Freq"] = "All" + else: + variations = {"Freq": ["All"]} + elif not variations and domain != "Sweep": + variations = self._app.available_variations.nominal_w_values_dict + report.variations = variations report.sub_design_id = subdesign_id report.point_number = polyline_points if context == "Differential Pairs": diff --git a/pyaedt/modules/SolveSetup.py b/pyaedt/modules/SolveSetup.py index a61f7905e85..d2cdda2f666 100644 --- a/pyaedt/modules/SolveSetup.py +++ b/pyaedt/modules/SolveSetup.py @@ -1639,6 +1639,12 @@ def is_solved(self): setup_name=combined_name, expressions=expressions[0], ) + elif self.props.get("SolveSetupType", "HFSS") == "SIwaveDCIR": + expressions = self.p_app.post.available_report_quantities(solution=self.name, is_siwave_dc=True) + sol = self._app.post.reports_by_category.standard( + setup_name=self.name, + expressions=expressions[0], + ) else: expressions = [i for i in self.p_app.post.available_report_quantities(solution=self.name)] diff --git a/pyaedt/modules/SolveSweeps.py b/pyaedt/modules/SolveSweeps.py index 8679d371f0f..bde54e41770 100644 --- a/pyaedt/modules/SolveSweeps.py +++ b/pyaedt/modules/SolveSweeps.py @@ -9,6 +9,7 @@ from pyaedt import pyaedt_function_handler from pyaedt.generic.DataHandlers import _dict2arg from pyaedt.generic.LoadAEDTFile import load_entire_aedt_file +from pyaedt.generic.constants import unit_converter from pyaedt.modules.SetupTemplates import Sweep3DLayout from pyaedt.modules.SetupTemplates import SweepHfss3D from pyaedt.modules.SetupTemplates import SweepSiwave @@ -166,6 +167,12 @@ def basis_frequencies(self): try: new_list = [float(i) for i in v["Fields"]["IDDblMap"][1::2]] new_list.sort() + new_list = unit_converter( + values=new_list, + unit_system="Freq", + input_units="Hz", + output_units=self._app._app.odesktop.GetDefaultUnit("Frequency"), + ) fr.append(new_list) except (KeyError, NameError, IndexError): pass @@ -697,9 +704,15 @@ def basis_frequencies(self): solutions = load_entire_aedt_file(solutions_file) for k, v in solutions.items(): if "SolutionBlock" in k and "SolutionName" in v and v["SolutionName"] == self.name and "Fields" in v: - try: + try: # pragma: no cover new_list = [float(i) for i in v["Fields"]["IDDblMap"][1::2]] new_list.sort() + new_list = unit_converter( + values=new_list, + unit_system="Freq", + input_units="Hz", + output_units=self._app._app.odesktop.GetDefaultUnit("Frequency"), + ) fr.append(new_list) except (KeyError, NameError, IndexError): pass