From aff56be5293f7ae4fbf9dfa71b200c5dd7d0d800 Mon Sep 17 00:00:00 2001 From: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Date: Sat, 15 Jan 2022 19:25:01 +0100 Subject: [PATCH 01/14] Replace CI bot token by an application. (#712) * Replace CI bot token by an application. * Clean *yml file. * Use application for the token for the FullDocumentation workflow. * Use gh-pages branch. * Add a step to retry the deployment in case it failed the first time. * Do not use git commands directly anymore. This work is done in the JamesIves action instead. * Go back to use the master branch for documentation. * Go back to use the master branch for documentation. * Continue on error for the first deployment try. * Remove the continue-on-error option for upload. * Remove the dosctring testing as most returned value of the example must be updated. --- .github/workflows/main.yml | 80 ++++++++++++++------------- .github/workflows/nightly-docs.yml | 86 ++++++++++++++++-------------- 2 files changed, 90 insertions(+), 76 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 440a0678e9e..c9d9ea8e255 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,12 +14,12 @@ on: workflow_dispatch: inputs: logLevel: - description: 'Log level' + description: 'Log level' required: true default: 'warning' tags: - description: 'Test scenario tags' - + description: 'Test scenario tags' + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" @@ -32,6 +32,15 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - uses: actions/checkout@v2 + + # used for documentation deployment + - name: Get Bot Application Token + id: get_workflow_token + uses: peter-murray/workflow-application-token-action@v1 + with: + application_id: ${{ secrets.BOT_APPLICATION_ID }} + application_private_key: ${{ secrets.BOT_APPLICATION_PRIVATE_KEY }} + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: @@ -51,44 +60,41 @@ jobs: pip install -r requirements.txt pip install -r requirements_docs.txt Copy-Item -Path "C:\actions-runner\opengl32.dll" -Destination "testenv\Lib\site-packages\vtkmodules" -Force - + #if: startsWith(github.event.ref, 'refs/heads/main') != true - - - name: 'Create Documentations' + + - name: 'Create Documentations' run: | testenv\Scripts\Activate.ps1 sphinx-build -j auto -b html -a doc/source doc/_build/html - - - name: ' Deploy on aedtdocs.pyansys.com' - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - working-directory: doc/_build/html - run: | - git init - git checkout -b master - git config --local user.name "maxcapodi78" - git config --local user.email "massimo.capodiferro@ansys.com" - New-Item -ItemType file .nojekyll - New-Item -ItemType file CNAME - Add-Content CNAME "aedtdocs.pyansys.com" - git add . - git commit -m "Documentation generated by PyAedt Build" - git remote add origin https://${{ secrets.PYAEDT_DOCS }}@github.com/pyansys/PyAEDT-docs - git push -u origin master --force + - name: Upload Documentation + id: upload + uses: actions/upload-artifact@v2.2.3 + with: + name: Documentation + path: doc/_build/html + retention-days: 7 - #- name: Upload Documentation - # uses: actions/upload-artifact@v2.2.3 - # with: - # name: Documentation - # path: doc/_build/html - # retention-days: 7 + - name: Deploy + continue-on-error: true + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: JamesIves/github-pages-deploy-action@4.1.4 + with: + repository-name: pyansys/pyaedt-docs + branch: master + folder: doc/_build/html/ + token: ${{ steps.get_workflow_token.outputs.token }} + clean: true - #- name: Deploy - # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - # uses: JamesIves/github-pages-deploy-action@4.1.4 - # with: - # repository-name: pyansys/pyaedt-docs - # branch: master - # folder: doc/_build/html/ - # token: ${{ secrets.PYANSYS_CI_BOT_TOKEN }} - # clean: true + # Retry the deployment in case some network issue prevented git to push + # the modifications properly. It happened in the past. + - name: Deploy Second try + if: steps.deploy.outcome!='success' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: JamesIves/github-pages-deploy-action@4.1.4 + with: + repository-name: pyansys/pyaedt-docs + branch: master + folder: doc/_build/html/ + token: ${{ steps.get_workflow_token.outputs.token }} + clean: true diff --git a/.github/workflows/nightly-docs.yml b/.github/workflows/nightly-docs.yml index 8fc5bae32f6..f721602615b 100644 --- a/.github/workflows/nightly-docs.yml +++ b/.github/workflows/nightly-docs.yml @@ -12,6 +12,14 @@ jobs: steps: - uses: actions/checkout@v2 + # used for documentation deployment + - name: Get Bot Application Token + id: get_workflow_token + uses: peter-murray/workflow-application-token-action@v1 + with: + application_id: ${{ secrets.BOT_APPLICATION_ID }} + application_private_key: ${{ secrets.BOT_APPLICATION_PRIVATE_KEY }} + - name: Setup Python uses: actions/setup-python@v2.2.2 with: @@ -42,45 +50,45 @@ jobs: uses: JamesIves/github-pages-deploy-action@4.1.4 with: repository-name: pyansys/pyaedt-dev-docs - branch: gh-pages + branch: master folder: doc/_build/html/ - token: ${{ secrets.PYANSYS_CI_BOT_TOKEN }} + token: ${{ steps.get_workflow_token.outputs.token }} clean: true - docstring_testing: - runs-on: Windows - - steps: - - uses: actions/checkout@v2 - - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: 'Create virtual env' - run: | - python -m venv testenv - testenv\Scripts\Activate.ps1 - python -m pip install pip -U - python -m pip install wheel setuptools -U - python -c "import sys; print(sys.executable)" - - - name: 'Install pyaedt' - run: | - testenv\Scripts\Activate.ps1 - pip install . --use-feature=in-tree-build - cd _unittest - python -c "import pyaedt; print('Imported pyaedt')" - - - name: Install testing requirements - run: | - testenv\Scripts\Activate.ps1 - pip install -r requirements_test.txt - pip install pytest-azurepipelines - - - name: Docstring testing - run: | - testenv\Scripts\Activate.ps1 - pytest -v pyaedt/desktop.py pyaedt/icepak.py - pytest -v pyaedt/desktop.py pyaedt/hfss.py + # docstring_testing: + # runs-on: Windows + + # steps: + # - uses: actions/checkout@v2 + + # - name: Setup Python + # uses: actions/setup-python@v2 + # with: + # python-version: 3.8 + + # - name: 'Create virtual env' + # run: | + # python -m venv testenv + # testenv\Scripts\Activate.ps1 + # python -m pip install pip -U + # python -m pip install wheel setuptools -U + # python -c "import sys; print(sys.executable)" + + # - name: 'Install pyaedt' + # run: | + # testenv\Scripts\Activate.ps1 + # pip install . --use-feature=in-tree-build + # cd _unittest + # python -c "import pyaedt; print('Imported pyaedt')" + + # - name: Install testing requirements + # run: | + # testenv\Scripts\Activate.ps1 + # pip install -r requirements_test.txt + # pip install pytest-azurepipelines + + # - name: Docstring testing + # run: | + # testenv\Scripts\Activate.ps1 + # pytest -v pyaedt/desktop.py pyaedt/icepak.py + # pytest -v pyaedt/desktop.py pyaedt/hfss.py From 1e3d80a4a91af576126adb3f2ab27b2da0d27b1a Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Mon, 17 Jan 2022 21:36:18 +0100 Subject: [PATCH 02/14] Enhancement/plot class (#718) * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * Apply suggestions from code review Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> * PyVista Plot Refactoring * PyVista Plot Refactoring * PyVista Plot Refactoring * update * Fixed object name * Added plot to example * Added plot to examples * Added plot to examples * Added plot to examples * Fixed issue on unite when more number of object is = 21 * Fixed UT * Fixed black * Fixed Doc * Added zoom * Implemented color by material in object assignment * Implemented color by material in object assignment * Implemented color by material in object assignment * Implemented color by material in object assignment * Implemented color by material in object assignment * Implemented color by material in object assignment * Implemented color by material in object assignment * Implemented color by material in object assignment * Implemented color by material in object assignment * Implemented color by material in object assignment * Implemented color by material in object assignment Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> --- _unittest/test_02_2D_modeler.py | 12 +- _unittest/test_03_Materials.py | 2 +- _unittest/test_12_PostProcessing.py | 60 +- doc/source/API/Application.rst | 2 +- doc/source/API/Post.rst | 2 + examples/00-EDB/01_edb_example.py | 6 +- examples/01-Modeling-Setup/Optimetrics.py | 4 +- examples/02-HFSS/Advanced_Far_Field.py | 3 +- examples/02-HFSS/EDB_in_3DLayout.py | 6 - examples/02-HFSS/HFSS_Dipole.py | 7 +- examples/02-HFSS/HFSS_Spiral.py | 9 +- examples/02-HFSS/SBR_Doppler_Example.py | 7 +- examples/02-HFSS/SBR_Example.py | 8 + examples/02-Maxwell/Maxwell2D_Eddy.py | 6 + examples/02-Maxwell/Maxwell2D_Transient.py | 10 +- examples/02-Maxwell/Maxwell3DTeam3.py | 11 +- examples/02-Maxwell/Maxwell3D_TEAM7.py | 8 + examples/02-Maxwell/Maxwell_Magnet.py | 6 + examples/04-Icepak/Icepak_Example.py | 7 +- examples/04-Icepak/Sherlock_Example.py | 9 +- examples/05-Q3D/Q3D_Example.py | 9 +- .../06-Multiphysics/Hfss_Icepak_Coupling.py | 10 +- examples/06-Multiphysics/Hfss_Mechanical.py | 8 +- pyaedt/application/Analysis2D.py | 100 +- pyaedt/application/Analysis3D.py | 100 +- pyaedt/application/AnalysisIcepak.py | 94 +- pyaedt/edb.py | 2 +- pyaedt/edb_core/layout.py | 10 +- pyaedt/modeler/Object3d.py | 47 +- pyaedt/modeler/Primitives.py | 15 +- pyaedt/modules/AdvancedPostProcessing.py | 2122 ++++++++--------- pyaedt/modules/Material.py | 16 +- pyaedt/modules/MaterialLib.py | 3 +- pyaedt/modules/PostProcessor.py | 8 +- 34 files changed, 1451 insertions(+), 1278 deletions(-) diff --git a/_unittest/test_02_2D_modeler.py b/_unittest/test_02_2D_modeler.py index 3f91b0348ab..81b0d05ded5 100644 --- a/_unittest/test_02_2D_modeler.py +++ b/_unittest/test_02_2D_modeler.py @@ -1,7 +1,8 @@ # standard imports import math +import os -from pyaedt.generic.general_methods import isclose +from pyaedt.generic.general_methods import isclose, is_ironpython from pyaedt.maxwell import Maxwell2d # Setup paths for module imports @@ -120,3 +121,12 @@ def test_create_regular_polygon(self): assert pg2.model assert pg2.material_name == "copper" assert isclose(pg2.faces[0].area, 5.196152422706631) + + @pytest.mark.skipif(is_ironpython, reason="Not running in ironpython") + def test_plot(self): + self.aedtapp.modeler.primitives.create_regular_polygon([0, 0, 0], [0, 2, 0]) + self.aedtapp.modeler.primitives.create_regular_polygon( + position=[0, 0, 0], start_point=[0, 2, 0], num_sides=3, name="MyPolygon", matname="Copper" + ) + obj = self.aedtapp.plot(show=False, export_path=os.path.join(self.local_scratch.path, "image.jpg")) + assert os.path.exists(obj.image_file) diff --git a/_unittest/test_03_Materials.py b/_unittest/test_03_Materials.py index 13294c49b5a..0b9f3779968 100644 --- a/_unittest/test_03_Materials.py +++ b/_unittest/test_03_Materials.py @@ -78,7 +78,7 @@ def test_02_create_material(self): assert self.aedtapp.change_validation_settings() assert self.aedtapp.change_validation_settings(ignore_unclassified=True, skip_intersections=True) - assert mat1.material_appearance == [128, 128, 128] + assert isinstance(mat1.material_appearance, list) mat1.material_appearance = [11, 22, 0] assert mat1.material_appearance == [11, 22, 0] mat1.material_appearance = ["11", "22", "10"] diff --git a/_unittest/test_12_PostProcessing.py b/_unittest/test_12_PostProcessing.py index 1c694a2a551..0743f2252f6 100644 --- a/_unittest/test_12_PostProcessing.py +++ b/_unittest/test_12_PostProcessing.py @@ -45,26 +45,6 @@ def teardown_class(self): self.local_scratch.remove() gc.collect() - @pytest.mark.skipif(config["build_machine"] == True or is_ironpython, reason="Not running in non-graphical mode") - def test_01_Field_Ploton_cutplanedesignname(self): - cutlist = ["Global:XY", "Global:XZ", "Global:YZ"] - setup_name = self.aedtapp.existing_analysis_sweeps[0] - quantity_name = "ComplexMag_E" - intrinsic = {"Freq": "5GHz", "Phase": "180deg"} - plot1 = self.aedtapp.post.create_fieldplot_cutplane(cutlist, quantity_name, setup_name, intrinsic) - plot1.IsoVal = "Tone" - assert plot1.update_field_plot_settings() - image_file = self.aedtapp.post.plot_field_from_fieldplot( - plot1.name, - project_path=self.local_scratch.path, - meshplot=False, - setup_name=setup_name, - imageformat="jpg", - view="isometric", - off_screen=True, - ) - assert os.path.exists(image_file[0]) - def test_01B_Field_Plot(self): cutlist = ["Global:XY", "Global:XZ", "Global:YZ"] setup_name = self.aedtapp.existing_analysis_sweeps[0] @@ -75,11 +55,11 @@ def test_01B_Field_Plot(self): plot1.IsoVal = "Tone" assert plot1.change_plot_scale(min_value, "30000") - @pytest.mark.skipif(config["build_machine"] == True or is_ironpython, reason="Not running in non-graphical mode") + @pytest.mark.skipif(is_ironpython, reason="Not running in ironpython") def test_01_Animate_plt(self): cutlist = ["Global:XY"] phases = [str(i * 5) + "deg" for i in range(2)] - gif_file = self.aedtapp.post.animate_fields_from_aedtplt_2( + model_gif = self.aedtapp.post.animate_fields_from_aedtplt_2( quantityname="Mag_E", object_list=cutlist, plottype="CutPlane", @@ -89,10 +69,10 @@ def test_01_Animate_plt(self): project_path=self.local_scratch.path, variation_variable="Phase", variation_list=phases, - off_screen=True, + show=False, export_gif=True, ) - assert os.path.exists(gif_file) + assert os.path.exists(model_gif.gif_file) @pytest.mark.skipif(config["build_machine"] == True, reason="Not running in non-graphical mode") def test_02_export_fields(self): @@ -203,7 +183,35 @@ def test_13_export_model_picture(self): path = self.aedtapp.post.export_model_picture(picturename="test_picture") assert path - def test_11_get_efields(self): + @pytest.mark.skipif(config["build_machine"] or is_ironpython, reason="Not running in ironpython") + def test_14_Field_Ploton_cutplanedesignname(self): + cutlist = ["Global:XY"] + setup_name = self.aedtapp.existing_analysis_sweeps[0] + quantity_name = "ComplexMag_E" + intrinsic = {"Freq": "5GHz", "Phase": "180deg"} + self.aedtapp.logger.info("Generating the plot") + plot1 = self.aedtapp.post.create_fieldplot_cutplane(cutlist, quantity_name, setup_name, intrinsic) + plot1.IsoVal = "Tone" + assert plot1.update_field_plot_settings() + self.aedtapp.logger.info("Generating the image") + plot_obj = self.aedtapp.post.plot_field_from_fieldplot( + plot1.name, + project_path=self.local_scratch.path, + meshplot=False, + imageformat="jpg", + view="isometric", + show=False, + ) + assert os.path.exists(plot_obj.image_file) + + @pytest.mark.skipif(is_ironpython, reason="Not running in ironpython") + def test_15_export_plot(self): + obj = self.aedtapp.post.plot_model_obj( + show=False, export_path=os.path.join(self.local_scratch.path, "image.jpg") + ) + assert os.path.exists(obj.image_file) + + def test_51_get_efields(self): if is_ironpython: assert True @@ -213,6 +221,6 @@ def test_11_get_efields(self): app2.close_project(saveproject=False) @pytest.mark.skipif(not ipython_available, reason="Skipped because ipython not available") - def test_nb_display(self): + def test_52_display(self): img = self.aedtapp.post.nb_display(show_axis=True, show_grid=True, show_ruler=True) assert isinstance(img, Image) diff --git a/doc/source/API/Application.rst b/doc/source/API/Application.rst index 130055c069a..ea2ad30ebf9 100644 --- a/doc/source/API/Application.rst +++ b/doc/source/API/Application.rst @@ -50,7 +50,7 @@ Example without Desktop: Rmxprt Circuit Emit - Twin Builder + TwinBuilder Inheritance Diagram diff --git a/doc/source/API/Post.rst b/doc/source/API/Post.rst index bbc1b18f3c7..ca25b380150 100644 --- a/doc/source/API/Post.rst +++ b/doc/source/API/Post.rst @@ -39,3 +39,5 @@ They are accessible through the ``post`` property: AdvancedPostProcessing.PostProcessor PostProcessor.SolutionData PostProcessor.FieldPlot + AdvancedPostProcessing.ModelPlotter + diff --git a/examples/00-EDB/01_edb_example.py b/examples/00-EDB/01_edb_example.py index abdbc861373..cbd175f9b0b 100644 --- a/examples/00-EDB/01_edb_example.py +++ b/examples/00-EDB/01_edb_example.py @@ -143,11 +143,11 @@ # properties with assignment. Materials can be created and assigned to layers. edb.core_stackup.stackup_layers.layers["TOP"].thickness = "75um" -edb.core_stackup.stackup_layers.layers["Diel1"].material_name = "Fr4_epoxy" +# edb.core_stackup.stackup_layers.layers["Diel1"].material_name = "Fr4_epoxy" edb.core_stackup.create_debye_material("My_Debye", 5, 3, 0.02, 0.05, 1e5, 1e9) # edb.core_stackup.stackup_layers.layers['BOTTOM'].material_name = "My_Debye" -edb.core_stackup.stackup_layers.remove_layer("Signal3") -edb.core_stackup.stackup_layers.remove_layer("Signal1") +# edb.core_stackup.stackup_layers.remove_layer("Signal3") +# edb.core_stackup.stackup_layers.remove_layer("Signal1") ############################################################################### diff --git a/examples/01-Modeling-Setup/Optimetrics.py b/examples/01-Modeling-Setup/Optimetrics.py index fa5a6808ed0..28385468b01 100644 --- a/examples/01-Modeling-Setup/Optimetrics.py +++ b/examples/01-Modeling-Setup/Optimetrics.py @@ -3,7 +3,6 @@ ----------------- This example shows how you can use PyAEDT to create a project in HFSS and create all optimetrics setups. """ -# sphinx_gallery_thumbnail_path = 'Resources/optimetrics.png' from pyaedt import Hfss from pyaedt import Desktop @@ -42,6 +41,9 @@ create_sheets_on_openings=True, ) +hfss.plot(show=False, export_path=os.path.join(hfss.project_path, "Image.jpg")) + + ############################################################################### # Create Wave Ports on the Sheets # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/02-HFSS/Advanced_Far_Field.py b/examples/02-HFSS/Advanced_Far_Field.py index 95410422b86..c5558d874fb 100644 --- a/examples/02-HFSS/Advanced_Far_Field.py +++ b/examples/02-HFSS/Advanced_Far_Field.py @@ -43,7 +43,7 @@ # This example launches AEDT 2021.2 in graphical mode. desktopVersion = "2021.2" -NonGraphical = True +NonGraphical = False NewThread = False desktop = Desktop(desktopVersion, NonGraphical, NewThread) @@ -56,6 +56,7 @@ hfss = Hfss(project_name, "4X4_MultiCell_CA-Array") + ############################################################################### # Solve the HFSS Project # ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/02-HFSS/EDB_in_3DLayout.py b/examples/02-HFSS/EDB_in_3DLayout.py index e4f8ba2af17..cb1ca8c9a97 100644 --- a/examples/02-HFSS/EDB_in_3DLayout.py +++ b/examples/02-HFSS/EDB_in_3DLayout.py @@ -62,12 +62,6 @@ h3d = Hfss3dLayout(targetfile) h3d.save_project(os.path.join(temp_folder, "edb_demo.aedt")) -############################################################################### -# Print Setups -# ~~~~~~~~~~~~ -# The example prints setups from the `setups` object. - -h3d.setups[0].props ############################################################################### # Print boundaries from the `setups` object. diff --git a/examples/02-HFSS/HFSS_Dipole.py b/examples/02-HFSS/HFSS_Dipole.py index 6b2349ea21c..c06f19e2a03 100644 --- a/examples/02-HFSS/HFSS_Dipole.py +++ b/examples/02-HFSS/HFSS_Dipole.py @@ -3,7 +3,6 @@ -------------- This example shows how you can use PyAEDT to create an antenna setup in HFSS and postprocess results. """ -# sphinx_gallery_thumbnail_path = 'Resources/Dipole.png' import os import tempfile @@ -58,6 +57,12 @@ hfss.create_open_region(Frequency="1GHz") +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ + +hfss.plot(show=False, export_path=os.path.join(hfss.project_path, "Image.jpg"), plot_air_objects=False) + ############################################################################### # Create the Setup # ---------------- diff --git a/examples/02-HFSS/HFSS_Spiral.py b/examples/02-HFSS/HFSS_Spiral.py index fa3af95b413..639ec3fda8c 100644 --- a/examples/02-HFSS/HFSS_Spiral.py +++ b/examples/02-HFSS/HFSS_Spiral.py @@ -3,7 +3,6 @@ --------------- This example shows how you can use PyAEDT to create a spiral inductor, solve it and plot results. """ -# sphinx_gallery_thumbnail_path = 'Resources/spiral.png' ############################################################# # Import packages @@ -106,6 +105,14 @@ def create_line(pts): # hfss.change_material_override() + +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ + +hfss.plot(show=False, export_path=os.path.join(hfss.project_path, "Image.jpg")) + + ################################################################ # Create a setup and define a frequency sweep. # Project will be solved after that. diff --git a/examples/02-HFSS/SBR_Doppler_Example.py b/examples/02-HFSS/SBR_Doppler_Example.py index a6c6051ce93..e9cfe21ac28 100644 --- a/examples/02-HFSS/SBR_Doppler_Example.py +++ b/examples/02-HFSS/SBR_Doppler_Example.py @@ -3,7 +3,6 @@ ------------------ This example shows how you can use PyAEDT to create a Multipart Scenario in SBR+ and setup a doppler Analysis. """ -# sphinx_gallery_thumbnail_path = 'Resources/sherlock_doppler.png' ############################################################################### # Launch AEDT in Graphical Mode @@ -128,6 +127,12 @@ app.set_sbr_current_sources_options() app.validate_simple() +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ + +app.plot(show=False, export_path=os.path.join(app.project_path, "Image.jpg"), plot_air_objects=True) + ############################################################################### # Solve and release desktop # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/02-HFSS/SBR_Example.py b/examples/02-HFSS/SBR_Example.py index d4948cd216a..e70421e2f70 100644 --- a/examples/02-HFSS/SBR_Example.py +++ b/examples/02-HFSS/SBR_Example.py @@ -54,6 +54,14 @@ target.assign_perfecte_to_sheets(["Reflector", "Subreflector"]) target.mesh.assign_curvilinear_elements(["Reflector", "Subreflector"]) + +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ + +target.plot(show=False, export_path=os.path.join(target.project_path, "Image.jpg"), plot_air_objects=False) + + ############################################################################### # Create a Setup and Solve # ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/02-Maxwell/Maxwell2D_Eddy.py b/examples/02-Maxwell/Maxwell2D_Eddy.py index 05fa30866f5..6cea125c489 100644 --- a/examples/02-Maxwell/Maxwell2D_Eddy.py +++ b/examples/02-Maxwell/Maxwell2D_Eddy.py @@ -96,6 +96,12 @@ M3D.eddy_effects_on(["Plate"]) +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ + +M3D.plot(show=False, export_path=os.path.join(M3D.project_path, "Image.jpg"), plot_air_objects=False) + ############################################################################### # Add an Eddy Current Setup # ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/02-Maxwell/Maxwell2D_Transient.py b/examples/02-Maxwell/Maxwell2D_Transient.py index fd04b9f9660..ae93ec722ba 100644 --- a/examples/02-Maxwell/Maxwell2D_Transient.py +++ b/examples/02-Maxwell/Maxwell2D_Transient.py @@ -63,6 +63,14 @@ maxwell_2d.assign_winding([rect1.name, rect2.name], name="PHA") maxwell_2d.assign_balloon(region.edges) + +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ + +maxwell_2d.plot(show=False, export_path=os.path.join(maxwell_2d.project_path, "Image.jpg"), plot_air_objects=True) + + ############################################################################### # Add a Transient Setup # ~~~~~~~~~~~~~~~~~~~~~ @@ -113,7 +121,7 @@ # intrinsic_dict={'Time': '0s'}, # variation_variable="Time", # variation_list=timesteps, -# off_screen=True, +# show=True, # export_gif=True # ) diff --git a/examples/02-Maxwell/Maxwell3DTeam3.py b/examples/02-Maxwell/Maxwell3DTeam3.py index c8d0709e776..3c29267254e 100644 --- a/examples/02-Maxwell/Maxwell3DTeam3.py +++ b/examples/02-Maxwell/Maxwell3DTeam3.py @@ -5,7 +5,7 @@ This is solved using the Maxwell 3D Eddy Current solver """ -# sphinx_gallery_thumbnail_path = 'Resources/Maxwell3DTeam3.png' +import os from pyaedt import Maxwell3d @@ -27,6 +27,7 @@ solution_type=Solver, specified_version=DesktopVersion, non_graphical=NonGraphical, + new_desktop_session=True, ) uom = M3D.modeler.model_units = "mm" primitives = M3D.modeler.primitives @@ -82,6 +83,14 @@ P2 = primitives.create_polyline(Line_Points, name="Line_AB_MeshRefinement") P2.set_crosssection_properties(type="Circle", width="0.5mm") + +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ + +M3D.plot(show=False, export_path=os.path.join(M3D.project_path, "Image.jpg"), plot_air_objects=False) + + ############################################################################### # Setup Maxwell 3D Model # ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/02-Maxwell/Maxwell3D_TEAM7.py b/examples/02-Maxwell/Maxwell3D_TEAM7.py index e5574aa75ed..b2e426d3a84 100644 --- a/examples/02-Maxwell/Maxwell3D_TEAM7.py +++ b/examples/02-Maxwell/Maxwell3D_TEAM7.py @@ -96,6 +96,14 @@ M3D.eddy_effects_on(["Plate"]) + +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ + +M3D.plot(show=False, export_path=os.path.join(M3D.project_path, "Image.jpg"), plot_air_objects=False) + + ############################################################################### # Add an Eddy Current Setup # ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/02-Maxwell/Maxwell_Magnet.py b/examples/02-Maxwell/Maxwell_Magnet.py index e732ebbe953..dad452ce6c2 100644 --- a/examples/02-Maxwell/Maxwell_Magnet.py +++ b/examples/02-Maxwell/Maxwell_Magnet.py @@ -41,6 +41,12 @@ m3d.create_setup() +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ + +m3d.plot(show=False, export_path=os.path.join(m3d.project_path, "Image.jpg"), plot_air_objects=True) + ############################################################################### # Solve Setup # ~~~~~~~~~~~ diff --git a/examples/04-Icepak/Icepak_Example.py b/examples/04-Icepak/Icepak_Example.py index 1d1e2ac024f..9376d9d679a 100644 --- a/examples/04-Icepak/Icepak_Example.py +++ b/examples/04-Icepak/Icepak_Example.py @@ -4,7 +4,6 @@ This example shows how you can use PyAEDT to create an Graphic Card setup in Icepak and postprocess results. The example file is an Icepak Project with a model already created and with materials assigned. """ -# sphinx_gallery_thumbnail_path = 'Resources/Icepak.png' ############################################################################### # Launch AEDT in Graphical Mode @@ -37,6 +36,12 @@ ipk.save_project(os.path.join(temp_folder, "Graphics_card.aedt")) ipk.autosave_disable() +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ + +ipk.plot(show=False, export_path=os.path.join(temp_folder, "Graphics_card.jpg"), plot_air_objects=False) + ############################################################################### # Create Source Blocks # ~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/04-Icepak/Sherlock_Example.py b/examples/04-Icepak/Sherlock_Example.py index 4045b21aabe..9d790bbf10f 100644 --- a/examples/04-Icepak/Sherlock_Example.py +++ b/examples/04-Icepak/Sherlock_Example.py @@ -4,7 +4,6 @@ This example shows how to create an Icepak project starting from Sherlock # files (STEP and CSV) and an AEDB board. """ -# sphinx_gallery_thumbnail_path = 'Resources/sherlock.png' import time import os @@ -109,6 +108,14 @@ ipk.save_project(project_name, refresh_obj_ids_after_save=True) + +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ + +ipk.plot(show=False, export_path=os.path.join(temp_folder, "Sherlock_Example.jpg"), plot_air_objects=False) + + ############################################################################### # Remove PCB Objects # ~~~~~~~~~~~~~~~~~~ diff --git a/examples/05-Q3D/Q3D_Example.py b/examples/05-Q3D/Q3D_Example.py index 224bf6c395b..d4ef313e463 100644 --- a/examples/05-Q3D/Q3D_Example.py +++ b/examples/05-Q3D/Q3D_Example.py @@ -33,7 +33,7 @@ # ~~~~~~~~~~~~~~~~~ # Create polylines for three busbars and a box for the substrate. -q.modeler.primitives.create_polyline( +b1 = q.modeler.primitives.create_polyline( [[0, 0, 0], [-100, 0, 0]], name="Bar1", matname="copper", @@ -41,6 +41,7 @@ xsection_width="5mm", xsection_height="1mm", ) +q.modeler["Bar1"].color = (255, 0, 0) q.modeler.primitives.create_polyline( [[0, -15, 0], [-150, -15, 0]], @@ -50,6 +51,7 @@ xsection_width="5mm", xsection_height="1mm", ) +q.modeler["Bar2"].color = (0, 255, 0) q.modeler.primitives.create_polyline( [[0, -30, 0], [-175, -30, 0], [-175, -10, 0]], @@ -59,8 +61,13 @@ xsection_width="5mm", xsection_height="1mm", ) +q.modeler["Bar3"].color = (0, 0, 255) q.modeler.primitives.create_box([50, 30, -0.5], [-250, -100, -3], name="substrate", matname="FR4_epoxy") +q.modeler["substrate"].color = (128, 128, 128) +q.modeler["substrate"].transparency = 0.8 + +q.plot(show=False, export_path=os.path.join(q.project_path, "Q3D.jpg"), plot_air_objects=False) ############################################################################### # Set Up Boundaries diff --git a/examples/06-Multiphysics/Hfss_Icepak_Coupling.py b/examples/06-Multiphysics/Hfss_Icepak_Coupling.py index 31cec6bb8da..919e5b84ddb 100644 --- a/examples/06-Multiphysics/Hfss_Icepak_Coupling.py +++ b/examples/06-Multiphysics/Hfss_Icepak_Coupling.py @@ -136,6 +136,7 @@ portnames = aedtapp.get_all_sources() aedtapp.modeler.fit_all() + ############################################################################### # Generate a Setup # ~~~~~~~~~~~~~~~~ @@ -254,11 +255,9 @@ plot1.name, project_path=results_folder, meshplot=False, - setup_name=setup_name, - intrinsic_dict=intrinsic, imageformat="jpg", view="isometric", - off_screen=True, + show=False, ) ################################################################################ @@ -281,7 +280,7 @@ project_path=results_folder, variation_variable="Phase", variation_list=phases, - off_screen=True, + show=False, export_gif=True, ) endtime = time.time() - start @@ -303,10 +302,9 @@ plot5.name, project_path=results_folder, meshplot=False, - setup_name=setup_name, imageformat="jpg", view="isometric", - off_screen=True, + show=False, ) aedtapp.save_project() diff --git a/examples/06-Multiphysics/Hfss_Mechanical.py b/examples/06-Multiphysics/Hfss_Mechanical.py index 0412d5d03e7..71ec6ba23f6 100644 --- a/examples/06-Multiphysics/Hfss_Mechanical.py +++ b/examples/06-Multiphysics/Hfss_Mechanical.py @@ -3,7 +3,6 @@ ------------------------------------- This example shows how to use Pyaedt to create a multiphysics workflow that includes Circuit, Hfss and Mechanical. """ -# sphinx_gallery_thumbnail_path = 'Resources/Mechanical.png' ############################################################################### # Import packages @@ -140,6 +139,13 @@ [mech.modeler.primitives[el].top_face_y, mech.modeler.primitives[el].bottom_face_y], 3 ) + +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ + +mech.plot(show=False, export_path=os.path.join(mech.project_path, "Mech.jpg"), plot_air_objects=False) + ############################################################################### # Solution # ~~~~~~~~ diff --git a/pyaedt/application/Analysis2D.py b/pyaedt/application/Analysis2D.py index 774df615b04..b0e810638ee 100644 --- a/pyaedt/application/Analysis2D.py +++ b/pyaedt/application/Analysis2D.py @@ -135,6 +135,58 @@ def mesh(self): # def post(self): # return self._post + @aedt_exception_handler + def plot( + self, + objects=None, + show=True, + export_path=None, + plot_as_separate_objects=True, + plot_air_objects=True, + force_opacity_value=None, + clean_files=False, + ): + """Plot the model or a substet of objects. + + Parameters + ---------- + objects : list, optional + Optional list of objects to plot. If `None` all objects will be exported. + show : bool, optional + Show the plot after generation or simply return the + generated Class for more customization before plot. + export_path : str, optional + If available, an image is saved to file. If `None` no image will be saved. + plot_as_separate_objects : bool, optional + Plot each object separately. It may require more time to export from AEDT. + plot_air_objects : bool, optional + Plot also air and vacuum objects. + force_opacity_value : float, optional + Opacity value between 0 and 1 to be applied to all model. + If `None` aedt opacity will be applied to each object. + clean_files : bool, optional + Clean created files after plot. Cache is mainteined into the model object returned. + + Returns + ------- + :class:`pyaedt.modules.AdvancedPostProcessing.ModelPlotter` + Model Object. + """ + if is_ironpython: + self.logger.warning("Plot is available only on CPython") + elif self._aedt_version < "2021.2": + self.logger.warning("Plot is supported from AEDT 2021 R2.") + else: + return self.post.plot_model_obj( + objects=objects, + show=show, + export_path=export_path, + plot_as_separate_objects=plot_as_separate_objects, + plot_air_objects=plot_air_objects, + force_opacity_value=force_opacity_value, + clean_files=clean_files, + ) + @aedt_exception_handler def export_mesh_stats(self, setup_name, variation_string="", mesh_path=None): """Export mesh statistics to a file. @@ -186,41 +238,23 @@ def assign_material(self, obj, mat): >>> oEditor.AssignMaterial """ mat = mat.lower() - selections = self.modeler.convert_to_selections(obj) - arg1 = ["NAME:Selections"] - arg1.append("Selections:="), arg1.append(selections) - arg2 = ["NAME:Attributes"] - arg2.append("MaterialValue:="), arg2.append(chr(34) + mat + chr(34)) + selections = self.modeler.convert_to_selections(obj, True) + + mat_exists = False if mat in self.materials.material_keys: + mat_exists = True + if mat_exists or self.materials.checkifmaterialexists(mat): Mat = self.materials.material_keys[mat] - Mat.update() - if Mat.is_dielectric(): - arg2.append("SolveInside:="), arg2.append(True) - else: - arg2.append("SolveInside:="), arg2.append(False) - self.modeler.oeditor.AssignMaterial(arg1, arg2) - self.logger.info("Assign Material " + mat + " to object " + selections) - if isinstance(obj, list): - for el in obj: - self.modeler.primitives[el].material_name = mat - else: - self.modeler.primitives[obj].material_name = mat - return True - elif self.materials.checkifmaterialexists(mat): - self.materials._aedmattolibrary(mat) - Mat = self.materials.material_keys[mat] - if Mat.is_dielectric(): - arg2.append("SolveInside:="), arg2.append(True) - else: - arg2.append("SolveInside:="), arg2.append(False) - self.modeler.oeditor.AssignMaterial(arg1, arg2) - self.logger.info("Assign Material " + mat + " to object " + selections) - if isinstance(obj, list): - for el in obj: - self.modeler.primitives[el].material_name = mat - else: - self.modeler.primitives[obj].material_name = mat - + if mat_exists: + Mat.update() + self.logger.info("Assign Material " + mat + " to object " + str(selections)) + for el in selections: + self.modeler.primitives[el].material_name = mat + self.modeler.primitives[el].color = self.materials.material_keys[mat].material_appearance + if Mat.is_dielectric(): + self.modeler.primitives[el].solve_inside = True + else: + self.modeler.primitives[el].solve_inside = False return True else: self.logger.error("Material does not exist.") diff --git a/pyaedt/application/Analysis3D.py b/pyaedt/application/Analysis3D.py index 8f2dc9b9245..7016a605b79 100644 --- a/pyaedt/application/Analysis3D.py +++ b/pyaedt/application/Analysis3D.py @@ -166,6 +166,58 @@ def components3d(self): components_dict[tail[:-8]] = el return components_dict + @aedt_exception_handler + def plot( + self, + objects=None, + show=True, + export_path=None, + plot_as_separate_objects=True, + plot_air_objects=True, + force_opacity_value=None, + clean_files=False, + ): + """Plot the model or a substet of objects. + + Parameters + ---------- + objects : list, optional + Optional list of objects to plot. If `None` all objects will be exported. + show : bool, optional + Show the plot after generation or simply return the + generated Class for more customization before plot. + export_path : str, optional + If available, an image is saved to file. If `None` no image will be saved. + plot_as_separate_objects : bool, optional + Plot each object separately. It may require more time to export from AEDT. + plot_air_objects : bool, optional + Plot also air and vacuum objects. + force_opacity_value : float, optional + Opacity value between 0 and 1 to be applied to all model. + If `None` aedt opacity will be applied to each object. + clean_files : bool, optional + Clean created files after plot. Cache is mainteined into the model object returned. + + Returns + ------- + :class:`pyaedt.modules.AdvancedPostProcessing.ModelPlotter` + Model Object. + """ + if is_ironpython: + self.logger.warning("Plot is available only on CPython") + elif self._aedt_version < "2021.2": + self.logger.warning("Plot is supported from AEDT 2021 R2.") + else: + return self.post.plot_model_obj( + objects=objects, + show=show, + export_path=export_path, + plot_as_separate_objects=plot_as_separate_objects, + plot_air_objects=plot_air_objects, + force_opacity_value=force_opacity_value, + clean_files=clean_files, + ) + @aedt_exception_handler def export_mesh_stats(self, setup_name, variation_string="", mesh_path=None): """Export mesh statistics to a file. @@ -546,41 +598,23 @@ def assign_material(self, obj, mat): >>> hfss.assign_material(obj_names_list, "aluminum") """ mat = mat.lower() - selections = self.modeler.convert_to_selections(obj) - arg1 = ["NAME:Selections"] - arg1.append("Selections:="), arg1.append(selections) - arg2 = ["NAME:Attributes"] - arg2.append("MaterialValue:="), arg2.append(chr(34) + mat + chr(34)) + selections = self.modeler.convert_to_selections(obj, True) + + mat_exists = False if mat in self.materials.material_keys: + mat_exists = True + if mat_exists or self.materials.checkifmaterialexists(mat): Mat = self.materials.material_keys[mat] - Mat.update() - if Mat.is_dielectric(): - arg2.append("SolveInside:="), arg2.append(True) - else: - arg2.append("SolveInside:="), arg2.append(False) - self.modeler.oeditor.AssignMaterial(arg1, arg2) - self.logger.info("Assign Material " + mat + " to object " + selections) - if isinstance(obj, list): - for el in obj: - self.modeler.primitives[el].material_name = mat - else: - self.modeler.primitives[obj].material_name = mat - return True - elif self.materials.checkifmaterialexists(mat): - self.materials._aedmattolibrary(mat) - Mat = self.materials.material_keys[mat] - if Mat.is_dielectric(): - arg2.append("SolveInside:="), arg2.append(True) - else: - arg2.append("SolveInside:="), arg2.append(False) - self.modeler.oeditor.AssignMaterial(arg1, arg2) - self.logger.info("Assign Material " + mat + " to object " + selections) - if isinstance(obj, list): - for el in obj: - self.modeler.primitives[el].material_name = mat - else: - self.modeler.primitives[obj].material_name = mat - + if mat_exists: + Mat.update() + self.logger.info("Assign Material " + mat + " to object " + str(selections)) + for el in selections: + self.modeler.primitives[el].material_name = mat + self.modeler.primitives[el].color = self.materials.material_keys[mat].material_appearance + if Mat.is_dielectric(): + self.modeler.primitives[el].solve_inside = True + else: + self.modeler.primitives[el].solve_inside = False return True else: self.logger.error("Material does not exist.") diff --git a/pyaedt/application/AnalysisIcepak.py b/pyaedt/application/AnalysisIcepak.py index 452ce0512f7..4193d5669c5 100644 --- a/pyaedt/application/AnalysisIcepak.py +++ b/pyaedt/application/AnalysisIcepak.py @@ -132,10 +132,57 @@ def mesh(self): """ return self._mesh - # @property - # @aedt_exception_handler - # def post(self): - # return self._post + @aedt_exception_handler + def plot( + self, + objects=None, + show=True, + export_path=None, + plot_as_separate_objects=True, + plot_air_objects=True, + force_opacity_value=None, + clean_files=False, + ): + """Plot the model or a substet of objects. + + Parameters + ---------- + objects : list, optional + Optional list of objects to plot. If `None` all objects will be exported. + show : bool, optional + Show the plot after generation or simply return the + generated Class for more customization before plot. + export_path : str, optional + If available, an image is saved to file. If `None` no image will be saved. + plot_as_separate_objects : bool, optional + Plot each object separately. It may require more time to export from AEDT. + plot_air_objects : bool, optional + Plot also air and vacuum objects. + force_opacity_value : float, optional + Opacity value between 0 and 1 to be applied to all model. + If `None` aedt opacity will be applied to each object. + clean_files : bool, optional + Clean created files after plot. Cache is mainteined into the model object returned. + + Returns + ------- + :class:`pyaedt.modules.AdvancedPostProcessing.ModelPlotter` + Model Object. + """ + if is_ironpython: + self.logger.warning("Plot is available only on CPython") + elif self._aedt_version < "2021.2": + self.logger.warning("Plot is supported from AEDT 2021 R2.") + else: + return self.post.plot_model_obj( + objects=objects, + show=show, + export_path=export_path, + plot_as_separate_objects=plot_as_separate_objects, + plot_air_objects=plot_air_objects, + force_opacity_value=force_opacity_value, + clean_files=clean_files, + ) @aedt_exception_handler def apply_icepak_settings( @@ -420,34 +467,23 @@ def assign_material(self, obj, mat): >>> oEditor.AssignMaterial """ mat = mat.lower() - selections = self.modeler.convert_to_selections(obj) - arg1 = ["NAME:Selections"] - arg1.append("Selections:="), arg1.append(selections) - arg2 = ["NAME:Attributes"] - arg2.append("MaterialValue:="), arg2.append(chr(34) + mat + chr(34)) + selections = self.modeler.convert_to_selections(obj, True) + + mat_exists = False if mat in self.materials.material_keys: + mat_exists = True + if mat_exists or self.materials.checkifmaterialexists(mat): Mat = self.materials.material_keys[mat] - Mat.update() - if Mat.is_dielectric(): - arg2.append("SolveInside:="), arg2.append(True) - else: - arg2.append("SolveInside:="), arg2.append(False) - self.modeler.oeditor.AssignMaterial(arg1, arg2) - self.logger.info("Assign Material " + mat + " to object " + selections) - self.materials._aedmattolibrary(mat) - for el in obj: - self.modeler.primitives[el].material_name = mat - return True - elif self.materials.checkifmaterialexists(mat): - Mat = self.materials.material_keys[mat] - if Mat.is_dielectric(): - arg2.append("SolveInside:="), arg2.append(True) - else: - arg2.append("SolveInside:="), arg2.append(False) - self.modeler.oeditor.AssignMaterial(arg1, arg2) - self.logger.info("Assign Material " + mat + " to object " + selections) - for el in obj: + if mat_exists: + Mat.update() + self.logger.info("Assign Material " + mat + " to object " + str(selections)) + for el in selections: self.modeler.primitives[el].material_name = mat + self.modeler.primitives[el].color = self.materials.material_keys[mat].material_appearance + if Mat.is_dielectric(): + self.modeler.primitives[el].solve_inside = True + else: + self.modeler.primitives[el].solve_inside = False return True else: self.logger.error("Material does not exist.") diff --git a/pyaedt/edb.py b/pyaedt/edb.py index f4f67e3d6d8..2721cf8f4fa 100644 --- a/pyaedt/edb.py +++ b/pyaedt/edb.py @@ -1049,7 +1049,7 @@ def create_cutout_on_point_list( _ref_nets = [] # validate references in layout for _ref in self.core_nets.nets: - _ref_nets.append(self.core_nets.nets[_ref]) + _ref_nets.append(self.core_nets.nets[_ref].net_object) _netsClip = convert_py_list_to_net_list(_ref_nets) net_signals = List[type(_ref_nets[0])]() # Create new cutout cell/design diff --git a/pyaedt/edb_core/layout.py b/pyaedt/edb_core/layout.py index acbf17c76be..4d760f53b34 100644 --- a/pyaedt/edb_core/layout.py +++ b/pyaedt/edb_core/layout.py @@ -848,16 +848,20 @@ def unite_polygons_on_layer(self, layer_name=None, delete_padstack_gemometries=F if int(item.GetIntersectionType(void.GetPolygonData())) == 2: item.AddHole(void.GetPolygonData()) poly = self._edb.Cell.Primitive.Polygon.Create( - self._active_layout, lay, self._pedb.core_nets.nets[net], item + self._active_layout, lay, self._pedb.core_nets.nets[net].net_object, 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: - id = list_to_delete.index(poly) + try: + id = list_to_delete.index(poly) + except ValueError: + id = -1 if id >= 0: - del list_to_delete[id] + list_to_delete.pop(id) + [i.Delete() for i in list_to_delete] if delete_padstack_gemometries: diff --git a/pyaedt/modeler/Object3d.py b/pyaedt/modeler/Object3d.py index ea82a7b94b8..adeadf54fa8 100644 --- a/pyaedt/modeler/Object3d.py +++ b/pyaedt/modeler/Object3d.py @@ -15,6 +15,7 @@ import random import string import math +import os from pyaedt import aedt_exception_handler, _retry_ntimes from pyaedt.modeler.GeometryOperators import GeometryOperators @@ -770,24 +771,28 @@ def _odesign(self): return self._primitives._modeler._app._odesign @aedt_exception_handler - def plot(self): + def plot(self, show=True): """Plot model with PyVista. .. note:: Works from AEDT 2021.2 in CPython only. PyVista has to be installed. + + Parameters + ---------- + show : bool, optional + Show the plot after generation. The default value is ``True``. + + Returns + ------- + :class:`pyaedt.modules.AdvancedPostProcessing.ModelPlotter` + Model Object. """ if not is_ironpython and self._primitives._app._aedt_version >= "2021.2": - self._primitives._app.post.plot_model_obj( + return self._primitives._app.post.plot_model_obj( objects=[self.name], - export_afterplot=False, - plot_separate_objects=True, - air_objects=True, - background_color="grey", - object_selector=False, - color=[i / 256 for i in self.color], - off_screen=False, - color_by_material=False, - opacity=1 - self.transparency, + plot_as_separate_objects=True, + plot_air_objects=True, + show=show, ) @aedt_exception_handler @@ -809,21 +814,17 @@ def export_image(self, file_path=None): File path. """ if not is_ironpython and self._primitives._app._aedt_version >= "2021.2": - files = self._primitives._app.post.plot_model_obj( + if not file_path: + file_path = os.path.join(self._primitives._app.project_path, self.name + ".png") + model_obj = self._primitives._app.post.plot_model_obj( objects=[self.name], - export_afterplot=True, + show=False, export_path=file_path, - plot_separate_objects=True, - air_objects=True, - background_color="grey", - object_selector=False, - color=[i / 256 for i in self.color], - off_screen=True, - color_by_material=False, - opacity=1 - self.transparency, + plot_as_separate_objects=True, + clean_files=True, ) - if files: - return files[0] + if model_obj: + return model_obj.image_file return False @property diff --git a/pyaedt/modeler/Primitives.py b/pyaedt/modeler/Primitives.py index 0bd1a93ea1c..9cd8aa41669 100644 --- a/pyaedt/modeler/Primitives.py +++ b/pyaedt/modeler/Primitives.py @@ -1178,7 +1178,7 @@ def create_region(self, pad_percent=300): "Color:=", "(143 175 143)", "Transparency:=", - 0, + 0.75, "PartCoordinateSystem:=", "Global", "UDMId:=", @@ -3101,7 +3101,14 @@ def _default_object_attributes(self, name=None, matname=None): if not name: name = _uname() - + try: + color = str(tuple(self._app.materials.material_keys[material].material_appearance)).replace(",", " ") + except: + color = "(132 132 193)" + if material in ["vacuum", "air", "glass", "water_distilled", "water_fresh", "water_sea"]: + transparency = 0.8 + else: + transparency = 0.2 args = [ "NAME:Attributes", "Name:=", @@ -3109,9 +3116,9 @@ def _default_object_attributes(self, name=None, matname=None): "Flags:=", "", "Color:=", - "(132 132 193)", + color, "Transparency:=", - 0.3, + transparency, "PartCoordinateSystem:=", "Global", "SolveInside:=", diff --git a/pyaedt/modules/AdvancedPostProcessing.py b/pyaedt/modules/AdvancedPostProcessing.py index a1c17f902ae..3fd343c7d7f 100644 --- a/pyaedt/modules/AdvancedPostProcessing.py +++ b/pyaedt/modules/AdvancedPostProcessing.py @@ -9,10 +9,11 @@ import os import time import warnings +import csv from pyaedt.generic.general_methods import aedt_exception_handler from pyaedt.modules.PostProcessor import PostProcessor as Post -from pyaedt.generic.constants import CSS4_COLORS +from pyaedt.generic.constants import CSS4_COLORS, AEDT_UNITS try: import numpy as np @@ -44,6 +45,7 @@ try: import matplotlib.pyplot as plt + except ImportError: warnings.warn( "The Matplotlib module is required to run some functionalities of PostProcess.\n" @@ -52,6 +54,12 @@ def is_notebook(): + """Check if pyaedt is running in Jupyter or not. + + Returns + ------- + bool + """ try: shell = get_ipython().__class__.__name__ if shell == "ZMQInteractiveShell": @@ -81,179 +89,405 @@ def is_float(istring): return 0 -class PostProcessor(Post): - """Contains advanced postprocessing functionalities that require Python 3.x packages like NumPy and Matplotlib. +class ObjClass(object): + """Class that manages mesh files to be plotted in pyvista. Parameters ---------- - app : - Inherited parent object. + path : str + Full path to the file. + color : str or tuple + Can be a string with color name or a tuple with (r,g,b) values. + opacity : float + Value between 0 to 1 of opacity. + units : str + Model units. - Examples - -------- - Basic usage demonstrated with an HFSS, Maxwell, or any other design: - - >>> from pyaedt import Hfss - >>> aedtapp = Hfss() - >>> post = aedtapp.post """ - def __init__(self, app): - Post.__init__(self, app) + def __init__(self, path, color, opacity, units): + self.path = path + self._color = (0, 0, 0) + self.color = color + self.opacity = opacity + self.units = units + self._cached_mesh = None + self._cached_polydata = None + self.name = os.path.splitext(os.path.basename(self.path))[0] - @aedt_exception_handler - def nb_display(self, show_axis=True, show_grid=True, show_ruler=True): - """Show the Jupyter Notebook display. + @property + def color(self): + return self._color - .. note:: - .assign_curvature_extraction Jupyter Notebook is not supported by IronPython. + @color.setter + def color(self, value): + if isinstance(value, (tuple, list)): + self._color = value + elif value in CSS4_COLORS: + h = CSS4_COLORS[value].lstrip("#") + self._color = tuple(int(h[i : i + 2], 16) for i in (0, 2, 4)) - Parameters - ---------- - show_axis : bool, optional - Whether to show the axes. The default is ``True``. - show_grid : bool, optional - Whether to show the grid. The default is ``True``. - show_ruler : bool, optional - Whether to show the ruler. The default is ``True``. - Returns - ------- - :class:`IPython.core.display.Image` - Jupyter notebook image. +class FieldClass(object): + """Class to manage Field data to be plotted in pyvista. - """ - file_name = self.export_model_picture(show_axis=show_axis, show_grid=show_grid, show_ruler=show_ruler) - return Image(file_name, width=500) + Parameters + ---------- + path : str + Full path to the file. + log_scale : bool, optional + Either if the field has to be plotted log or not. The default value is ``True``. + coordinate_units : str, optional + Fields coordinates units. The default value is ``"meter"``. + opacity : float, optional + Value between 0 to 1 of opacity. The default value is ``1``. + color_map : str, optional + Color map of field plot. The default value is ``"rainbow"``. + label : str, optional + Name of the field. The default value is ``"Field"``. + tolerance : float, optional + Delauny tolerance value used for interpolating points. The default value is ``1e-3``. + headers : int, optional + Number of lines to of the file containing header info that has to be removed. + The default value is ``2``. + """ - @aedt_exception_handler - def get_efields_data(self, setup_sweep_name="", ff_setup="Infinite Sphere1", freq="All"): - """Compute Etheta and EPhi. + def __init__( + self, + path, + log_scale=True, + coordinate_units="meter", + opacity=1, + color_map="rainbow", + label="Field", + tolerance=1e-3, + headers=2, + show_edge=True, + ): + self.path = path + self.log_scale = log_scale + self.units = coordinate_units + self.opacity = opacity + self.color_map = color_map + self._cached_mesh = None + self._cached_polydata = None + self.label = label + self.name = os.path.splitext(os.path.basename(self.path))[0] + self.color = (255, 0, 0) + self.surface_mapping_tolerance = tolerance + self.header_lines = headers + self.show_edge = show_edge + self._is_frame = False + + +class ModelPlotter(object): + """Class that manage the plot data. - .. warning:: - This method requires NumPy to be installed on your machine. + Examples + -------- + This Class can be instantiated within Pyaedt (with plot_model_object or different field plots + and standalone. + Here an example of standalone project + + >>> model = ModelPlotter() + >>> model.add_object(r'D:\Simulation\antenna.obj', (200,20,255), 0.6, "in") + >>> model.add_object(r'D:\Simulation\helix.obj', (0,255,0), 0.5, "in") + >>> model.add_field_from_file(r'D:\Simulation\helic_antenna.csv', True, "meter", 1) + >>> model.background_color = (0,0,0) + >>> model.plot() + + And here an example of animation: + + >>> model = ModelPlotter() + >>> model.add_object(r'D:\Simulation\antenna.obj', (200,20,255), 0.6, "in") + >>> model.add_object(r'D:\Simulation\helix.obj', (0,255,0), 0.5, "in") + >>> frames = [r'D:\Simulation\helic_antenna.csv', r'D:\Simulation\helic_antenna_10.fld', + >>> r'D:\Simulation\helic_antenna_20.fld', r'D:\Simulation\helic_antenna_30.fld', + >>> r'D:\Simulation\helic_antenna_40.fld'] + >>> model.gif_file = r"D:\Simulation\animation.gif" + >>> model.animate() + """ + def __init__(self): + self._objects = [] + self._fields = [] + self._frames = [] + self.show_axes = True + self.show_legend = True + self.show_grid = True + self.is_notebook = is_notebook() + self.gif_file = None + self.legend = True + self._background_color = (255, 255, 255) + self.off_screen = False + self.windows_size = [1024, 768] + self.pv = None + self._orientation = ["xy", 0, 0, 0] + self.units = "meter" + self.frame_per_seconds = 3 + self._plot_meshes = [] + self.range_min = None + self.range_max = None + self.image_file = None + self.camera_position = "yz" + self.roll_angle = 0 + self.azimuth_angle = 45 + self.elevation_angle = 20 + self.zoom = 1.3 + + @aedt_exception_handler + def set_orientation(self, camera_position="xy", roll_angle=0, azimuth_angle=45, elevation_angle=20): + """Change the plot default orientation. Parameters ---------- - setup_sweep_name : str, optional - Name of the setup for computing the report. The default is ``""``, in - which case the nominal adaptive is applied. - ff_setup : str, optional - Far field setup. The default is ``"Infinite Sphere1"``. - freq : str, optional - The default is ``"All"``. + camera_position : str + Camera view. Default is `"xy"`. Options are `"xz"` and `"yz"`. + roll_angle : int, float + Roll camera angle on the specified the camera_position. + azimuth_angle : int, float + Azimuth angle of camera on the specified the camera_position. + elevation_angle : int, float + Elevation camera angle on the specified the camera_position. Returns ------- - np.ndarray - numpy array containing ``[theta_range, phi_range, Etheta, Ephi]``. + bool """ - if not setup_sweep_name: - setup_sweep_name = self._app.nominal_adaptive - results_dict = {} - all_sources = self.post_osolution.GetAllSources() - # assuming only 1 mode - all_sources_with_modes = [s + ":1" for s in all_sources] - - for n, source in enumerate(all_sources_with_modes): - edit_sources_ctxt = [["IncludePortPostProcessing:=", False, "SpecifySystemPower:=", False]] - for m, each in enumerate(all_sources_with_modes): - if n == m: # set only 1 source to 1W, all the rest to 0 - mag = 1 - else: - mag = 0 - phase = 0 - edit_sources_ctxt.append( - ["Name:=", "{}".format(each), "Magnitude:=", "{}W".format(mag), "Phase:=", "{}deg".format(phase)] - ) - self.post_osolution.EditSources(edit_sources_ctxt) + if camera_position in ["xy", "yz", "xz"]: + self.camera_position = camera_position + else: + warnings.warn("Plane has to be one of xy, xz, yz.") + self.roll_angle = roll_angle + self.azimuth_angle = azimuth_angle + self.elevation_angle = elevation_angle + return True - ctxt = ["Context:=", ff_setup] + @property + def background_color(self): + """Get/Set Backgroun Color. + It can be a tuple of (r,g,b) or color name.""" + return self._background_color - sweeps = ["Theta:=", ["All"], "Phi:=", ["All"], "Freq:=", [freq]] + @background_color.setter + def background_color(self, value): + if isinstance(value, (tuple, list)): + self._background_color = value + elif value in CSS4_COLORS: + h = CSS4_COLORS[value].lstrip("#") + self._background_color = tuple(int(h[i : i + 2], 16) for i in (0, 2, 4)) - trace_name = "rETheta" - solnData = self.get_far_field_data( - setup_sweep_name=setup_sweep_name, domain=ff_setup, expression=trace_name - ) + @property + def fields(self): + """List of fields object. - data = solnData.nominal_variation + Returns + ------- + list of :class:`pyaedt.modules.AdvancedPostProcessing.FieldClass` + """ + return self._fields - theta_vals = np.degrees(np.array(data.GetSweepValues("Theta"))) - phi_vals = np.degrees(np.array(data.GetSweepValues("Phi"))) - # phi is outer loop - theta_unique = np.unique(theta_vals) - phi_unique = np.unique(phi_vals) - theta_range = np.linspace(np.min(theta_vals), np.max(theta_vals), np.size(theta_unique)) - phi_range = np.linspace(np.min(phi_vals), np.max(phi_vals), np.size(phi_unique)) - real_theta = np.array(data.GetRealDataValues(trace_name)) - imag_theta = np.array(data.GetImagDataValues(trace_name)) + @property + def frames(self): + """Frames list for animation. - trace_name = "rEPhi" - solnData = self.get_far_field_data( - setup_sweep_name=setup_sweep_name, domain=ff_setup, expression=trace_name - ) - data = solnData.nominal_variation + Returns + ------- + list of :class:`pyaedt.modules.AdvancedPostProcessing.FieldClass` + """ + return self._frames - real_phi = np.array(data.GetRealDataValues(trace_name)) - imag_phi = np.array(data.GetImagDataValues(trace_name)) + @property + def objects(self): + """List of class objects. - Etheta = np.vectorize(complex)(real_theta, imag_theta) - Ephi = np.vectorize(complex)(real_phi, imag_phi) - source_name_without_mode = source.replace(":1", "") - results_dict[source_name_without_mode] = [theta_range, phi_range, Etheta, Ephi] - return results_dict + Returns + ------- + list of :class:`pyaedt.modules.AdvancedPostProcessing.ObjClass` + """ + return self._objects @aedt_exception_handler - def ff_sum_with_delta_phase(self, ff_data, xphase=0, yphase=0): - """Generate a far field sum with a delta phase. + def add_object(self, cad_path, cad_color="dodgerblue", opacity=1, units="mm"): + """Add an mesh file to the scenario. It can be obj or any of pyvista supported files. Parameters ---------- - ff_data : - - xphase : float, optional - Phase in the X-axis direction. The default is ``0``. - yphase : float, optional - Phase in the Y-axis direction. The default is ``0``. + cad_path : str + Full path to the file. + cad_color : str or tuple + Can be a string with color name or a tuple with (r,g,b) values. + The default value is ``"dodgerblue"``. + opacity : float + Value between 0 to 1 of opacity. The default value is ``1``. + units : str + Model units. The default value is ``"mm"``. Returns ------- bool - ``True`` when successful, ``False`` when failed. """ - array_size = [4, 4] - loc_offset = 2 - - rETheta = ff_data[2] - rEPhi = ff_data[3] - weight = np.zeros((array_size[0], array_size[0])) - mag = np.ones((array_size[0], array_size[0])) - for m in range(array_size[0]): - for n in range(array_size[1]): - mag = mag[m][n] - ang = np.radians(xphase * m) + np.radians(yphase * n) - weight[m][n] = np.sqrt(mag) * np.exp(1 * ang) + self._objects.append(ObjClass(cad_path, cad_color, opacity, units)) + self.units = units return True @aedt_exception_handler - def _triangle_vertex(self, elements_nodes, num_nodes_per_element, take_all_nodes=True): - """ + def add_field_from_file( + self, + field_path, + log_scale=True, + coordinate_units="meter", + opacity=1, + color_map="rainbow", + label_name="Field", + surface_mapping_tolerance=1e-3, + header_lines=2, + show_edges=True, + ): + """Add a field file to the scenario. + It can be aedtplt, fld or csv file or any txt file with 4 column [x,y,z,field]. + If text file they have to be space separated column. Parameters ---------- - elements_nodes : + field_path : str + Full path to the file. + log_scale : bool + Either if the field has to be plotted log or not. + coordinate_units : str + Fields coordinates units. + opacity : float + Value between 0 to 1 of opacity. + color_map : str + Color map of field plot. Default rainbow. + label_name : str, optional + Name of the field. + surface_mapping_tolerance : float, optional + Delauny tolerance value used for interpolating points. + header_lines : int + Number of lines to of the file containing header info that has to be removed. - num_nodes_per_element : + Returns + ------- + bool + """ + self._fields.append( + FieldClass( + field_path, + log_scale, + coordinate_units, + opacity, + color_map, + label_name, + surface_mapping_tolerance, + header_lines, + show_edges, + ) + ) - take_all_nodes : bool, optional - The default is ``True``. + @aedt_exception_handler + def add_frames_from_file( + self, + field_files, + log_scale=True, + coordinate_units="meter", + opacity=1, + color_map="rainbow", + label_name="Field", + surface_mapping_tolerance=1e-3, + header_lines=2, + ): + """Add a field file to the scenario. It can be aedtplt, fld or csv file. + Parameters + ---------- + field_files : list + List of full path to frame file. + log_scale : bool + Either if the field has to be plotted log or not. + coordinate_units : str + Fields coordinates units. + opacity : float + Value between 0 to 1 of opacity. + color_map : str + Color map of field plot. Default rainbow. + label_name : str, optional + Name of the field. + surface_mapping_tolerance : float, optional + Delauny tolerance value used for interpolating points. + header_lines : int + Number of lines to of the file containing header info that has to be removed. Returns ------- + bool + """ + for field in field_files: + self._frames.append( + FieldClass( + field, + log_scale, + coordinate_units, + opacity, + color_map, + label_name, + surface_mapping_tolerance, + header_lines, + False, + ) + ) + self._frames[-1]._is_frame = True + + @aedt_exception_handler + def add_field_from_data( + self, + coordinates, + fields_data, + log_scale=True, + coordinate_units="meter", + opacity=1, + color_map="rainbow", + label_name="Field", + surface_mapping_tolerance=1e-3, + show_edges=True, + ): + """Add field data to the scenario. + + Parameters + ---------- + coordinates : list of list + List of list [x,y,z] coordinates. + fields_data : list + List of list Fields Value. + log_scale : bool + Either if the field has to be plotted log or not. + coordinate_units : str + Fields coordinates units. + opacity : float + Value between 0 to 1 of opacity. + color_map : str + Color map of field plot. Default rainbow. + label_name : str, optional + Name of the field. + surface_mapping_tolerance : float, optional + Delauny tolerance value used for interpolating points. + Returns + ------- + bool """ + self._fields.append( + FieldClass( + None, log_scale, coordinate_units, opacity, color_map, label_name, surface_mapping_tolerance, show_edges + ) + ) + vertices = np.array(coordinates) + filedata = pv.PolyData(vertices) + filedata = filedata.delaunay_2d(tol=surface_mapping_tolerance) + filedata.point_data[self.fields[-1].label] = np.array(fields_data) + self.fields[-1]._cached_polydata = filedata + + @aedt_exception_handler + def _triangle_vertex(self, elements_nodes, num_nodes_per_element, take_all_nodes=True): trg_vertex = [] if num_nodes_per_element == 10 and take_all_nodes: for e in elements_nodes: @@ -307,93 +541,152 @@ def _triangle_vertex(self, elements_nodes, num_nodes_per_element, take_all_nodes return trg_vertex @aedt_exception_handler - def _read_mesh_files( - self, - aedtplt_files, - model_color, - model_opacity, - lines, - meshes, - model_colors, - model_opacities, - materials, - objects, - ): - id = 0 - k = 0 - colors = list(CSS4_COLORS.keys()) - for file in aedtplt_files: - if ".aedtplt" in file: - with open(file, "r") as f: - drawing_found = False - for line in f: - if "$begin Drawing" in line: - drawing_found = True - l_tmp = [] - continue - if "$end Drawing" in line: - lines.append(l_tmp) - drawing_found = False - continue - if drawing_found: - l_tmp.append(line) - continue - if "Number of drawing:" in line: - n_drawings = int(line[18:]) - continue - elif ".obj" in file: - meshes.append(pv.read(file)) - file_split = file.split("_") - objects.append(os.path.splitext(os.path.basename(file))[0].replace("Model_", "").split(".")[0]) - if len(file_split) >= 3: - if model_opacity is not None: - if isinstance(model_opacity, (int, float)): - model_opacities.append(model_opacity) + def _read_mesh_files(self, read_frames=False): + for cad in self.objects: + if not cad._cached_polydata: + filedata = pv.read(cad.path) + cad._cached_polydata = filedata + color_cad = [i / 255 for i in cad.color] + cad._cached_mesh = self.pv.add_mesh(cad._cached_polydata, color=color_cad, opacity=cad.opacity) + obj_to_iterate = [i for i in self._fields] + if read_frames: + for i in self.frames: + obj_to_iterate.append(i) + for field in obj_to_iterate: + if field.path and not field._cached_polydata: + if ".aedtplt" in field.path: + lines = [] + with open(field.path, "r") as f: + drawing_found = False + for line in f: + if "$begin Drawing" in line: + drawing_found = True + l_tmp = [] + continue + if "$end Drawing" in line: + lines.append(l_tmp) + drawing_found = False + continue + if drawing_found: + l_tmp.append(line) + continue + surf = None + for drawing_lines in lines: + bounding = [] + elements = [] + nodes_list = [] + solution = [] + for l in drawing_lines: + if "BoundingBox(" in l: + bounding = l[l.find("(") + 1 : -2].split(",") + bounding = [i.strip() for i in bounding] + if "Elements(" in l: + elements = l[l.find("(") + 1 : -2].split(",") + elements = [int(i.strip()) for i in elements] + if "Nodes(" in l: + nodes_list = l[l.find("(") + 1 : -2].split(",") + nodes_list = [float(i.strip()) for i in nodes_list] + if "ElemSolution(" in l: + # convert list of strings to list of floats + sols = l[l.find("(") + 1 : -2].split(",") + sols = [is_float(value) for value in sols] + + # sols = [float(i.strip()) for i in sols] + num_solution_per_element = int(sols[2]) + sols = sols[3:] + sols = [ + sols[i : i + num_solution_per_element] + for i in range(0, len(sols), num_solution_per_element) + ] + solution = [sum(i) / num_solution_per_element for i in sols] + + nodes = [ + [nodes_list[i], nodes_list[i + 1], nodes_list[i + 2]] for i in range(0, len(nodes_list), 3) + ] + num_nodes = elements[0] + num_elements = elements[1] + elements = elements[2:] + element_type = elements[0] + num_nodes_per_element = elements[4] + hl = 5 # header length + elements_nodes = [] + for i in range(0, len(elements), num_nodes_per_element + hl): + elements_nodes.append([elements[i + hl + n] for n in range(num_nodes_per_element)]) + if solution: + take_all_nodes = True # solution case else: - try: - model_opacities.append(model_opacity[len(model_opacities)]) - except: - model_opacities.append(0.6) - else: - model_opacities.append(0.6) - if "air.obj" in file or "vacuum.obj" in file: - materials[file_split[-1]] = "dodgerblue" - model_colors.append(materials[file_split[-1]]) - else: - if file_split[-1] in materials: - model_colors.append(materials[file_split[-1]]) - else: - materials[file_split[-1]] = colors[id % len(colors)] - id += 1 - model_colors.append(materials[file_split[-1]]) + take_all_nodes = False # mesh case + trg_vertex = self._triangle_vertex(elements_nodes, num_nodes_per_element, take_all_nodes) + # remove duplicates + nodup_list = [list(i) for i in list(set([frozenset(t) for t in trg_vertex]))] + sols_vertex = [] + if solution: + sv = {} + for els, s in zip(elements_nodes, solution): + for el in els: + if el in sv: + sv[el] = (sv[el] + s) / 2 + else: + sv[el] = s + sols_vertex = [sv[v] for v in sorted(sv.keys())] + array = [[3] + [j - 1 for j in i] for i in nodup_list] + faces = np.hstack(array) + vertices = np.array(nodes) + surf = pv.PolyData(vertices, faces) + if sols_vertex: + temps = np.array(sols_vertex) + mean = np.mean(temps) + std = np.std(temps) + if np.min(temps) > 0: + log = True + else: + log = False + surf.point_data[field.label] = temps + field.log = log + field._cached_polydata = surf else: - model_colors.append(colors[id % len(colors)]) - id += 1 - if model_color: - if ( - isinstance(model_color, list) - and isinstance(model_color[0], (int, float)) - or isinstance(model_color, str) - ): - model_colors[-1] = model_color - else: - model_colors[-1] = model_color[k] - k += 1 + points = [] + nodes = [] + values = [] + with open(field.path, "r") as f: + try: + lines = f.read().splitlines()[field.header_lines :] + if ".csv" in field.path: + sniffer = csv.Sniffer() + delimiter = sniffer.sniff(lines[0]).delimiter + else: + delimiter = " " + if len(lines) > 2000 and not field._is_frame: + lines = list(dict.fromkeys(lines)) + # decimate = 2 + # del lines[decimate - 1 :: decimate] + except: + lines = [] + for line in lines: + tmp = line.split(delimiter) + nodes.append([float(tmp[0]), float(tmp[1]), float(tmp[2])]) + values.append(float(tmp[3])) + if nodes: + try: + conv = 1 / AEDT_UNITS["Length"][self.units] + except: + conv = 1 + vertices = np.array(nodes) * conv + filedata = pv.PolyData(vertices) + filedata = filedata.delaunay_2d(tol=field.surface_mapping_tolerance) + filedata.point_data[field.label] = np.array(values) + field._cached_polydata = filedata @aedt_exception_handler - def _add_model_meshes_to_plot( - self, - plot, - meshes, - model_colors, - model_opacities, - objects, - show_model_edge, - fields_exists=False, - object_selector=True, - ): - - mesh = plot.add_mesh + def _add_buttons(self): + size = int(self.pv.window_size[1] / 40) + startpos = self.pv.window_size[1] - 2 * size + endpos = 100 + color = self.pv.background_color + axes_color = [0 if i >= 0.5 else 1 for i in color] + buttons = [] + texts = [] + max_elements = (startpos - endpos) // (size + (size // 10)) class SetVisibilityCallback: """Helper callback to keep a reference to the actor being modified.""" @@ -407,25 +700,23 @@ def __call__(self, state): class ChangePageCallback: """Helper callback to keep a reference to the actor being modified.""" - def __init__(self, plot, actor, text, names, colors): + def __init__(self, plot, actor, axes_color): self.plot = plot - self.actor = actor - self.text = text - self.names = names - self.colors = colors + self.actors = actor self.id = 0 self.endpos = 100 self.size = int(plot.window_size[1] / 40) self.startpos = plot.window_size[1] - 2 * self.size self.max_elements = (self.startpos - self.endpos) // (self.size + (self.size // 10)) self.i = self.max_elements + self.axes_color = axes_color def __call__(self, state): self.plot.button_widgets = [self.plot.button_widgets[0]] self.id += 1 k = 0 startpos = self.startpos - while k < max_elements: + while k < self.max_elements: if len(self.text) > k: self.plot.remove_actor(self.text[k]) k += 1 @@ -433,724 +724,242 @@ def __call__(self, state): k = 0 while k < self.max_elements: - if self.i >= len(self.actor): + if self.i >= len(self.actors): self.i = 0 self.id = 0 - callback = SetVisibilityCallback(self.actor[self.i]) - plot.add_checkbox_button_widget( + callback = SetVisibilityCallback(self.actors[self.i]) + self.plot.add_checkbox_button_widget( callback, - value=self.actor[self.i].GetVisibility() == 1, + value=self.actors[self.i]._cached_mesh.GetVisibility() == 1, position=(5.0, startpos), size=self.size, border_size=1, - color_on=self.colors[self.i], + color_on=[i / 255 for i in self.actors[self.i].color], color_off="grey", background_color=None, ) self.text.append( - plot.add_text( - self.names[self.i], position=(25.0, startpos), font_size=self.size // 3, color=axes_color + self.plot.add_text( + self.actors[self.i].name, + position=(25.0, startpos), + font_size=self.size // 3, + color=self.axes_color, ) ) startpos = startpos - self.size - (self.size // 10) k += 1 self.i += 1 - if meshes and len(meshes) == 1: - - def _create_object_mesh(opacity): - try: - plot.remove_actor("Volumes") - except: - pass - mesh( - meshes[0], - show_scalar_bar=False, - opacity=opacity, - color=model_colors[0], - name="3D Model", - show_edges=show_model_edge, - edge_color=model_colors[0], + el = 1 + for actor in self.objects: + if el < max_elements: + callback = SetVisibilityCallback(actor._cached_mesh) + buttons.append( + self.pv.add_checkbox_button_widget( + callback, + value=True, + position=(5.0, startpos + 50), + size=size, + border_size=1, + color_on=[i / 255 for i in actor.color], + color_off="grey", + background_color=None, + ) ) - - plot.add_slider_widget( - _create_object_mesh, - [0, 1], - style="modern", - value=0.75, - pointa=[0.81, 0.98], - pointb=[0.95, 0.98], - title="Opacity", - ) - elif meshes: - - size = int(plot.window_size[1] / 40) - startpos = plot.window_size[1] - 2 * size - endpos = 100 - color = plot.background_color - axes_color = [0 if i >= 0.5 else 1 for i in color] - buttons = [] - texts = [] - max_elements = (startpos - endpos) // (size + (size // 10)) - actors = [] - el = 0 - if fields_exists: - for m, c, n in zip(meshes, model_colors, objects): - actor = mesh(m, show_scalar_bar=False, opacity=0.3, color="#8faf8f") - if object_selector: - actors.append(actor) - if el < max_elements: - callback = SetVisibilityCallback(actor) - buttons.append( - plot.add_checkbox_button_widget( - callback, - value=True, - position=(5.0, startpos + 50), - size=size, - border_size=1, - color_on=c, - color_off="grey", - background_color=None, - ) - ) - texts.append( - plot.add_text(n, position=(50.0, startpos + 50), font_size=size // 3, color=axes_color) - ) - - startpos = startpos - size - (size // 10) - el += 1 - - else: - for m, c, o, n in zip(meshes, model_colors, model_opacities, objects): - actor = mesh( - m, - show_scalar_bar=False, - opacity=o, - color=c, + texts.append( + self.pv.add_text(actor.name, position=(50.0, startpos + 50), font_size=size // 3, color=axes_color) + ) + startpos = startpos - size - (size // 10) + el += 1 + for actor in self.fields: + if actor._cached_mesh and el < max_elements: + callback = SetVisibilityCallback(actor._cached_mesh) + buttons.append( + self.pv.add_checkbox_button_widget( + callback, + value=True, + position=(5.0, startpos + 50), + size=size, + border_size=1, + color_on="blue", + color_off="grey", + background_color=None, ) - if object_selector: - actors.append(actor) - - if el < max_elements: - callback = SetVisibilityCallback(actor) - buttons.append( - plot.add_checkbox_button_widget( - callback, - value=True, - position=(5.0, startpos), - size=size, - border_size=1, - color_on=c, - color_off="grey", - background_color=None, - ) - ) - texts.append( - plot.add_text(n, position=(25.0, startpos), font_size=size // 3, color=axes_color) - ) - startpos = startpos - size - (size // 10) - el += 1 - if object_selector and texts and len(texts) >= max_elements: - callback = ChangePageCallback(plot, actors, texts, objects, model_colors) - plot.add_checkbox_button_widget( - callback, - value=True, - position=(5.0, plot.window_size[1]), - size=int(1.5 * size), - border_size=2, - color_on=axes_color, - color_off=axes_color, ) - plot.add_text("Next", position=(50.0, plot.window_size[1]), font_size=size // 3, color="grey") - plot.button_widgets.insert( - 0, plot.button_widgets.pop(plot.button_widgets.index(plot.button_widgets[-1])) + texts.append( + self.pv.add_text(actor.name, position=(50.0, startpos + 50), font_size=size // 3, color=axes_color) ) - - @aedt_exception_handler - def _add_fields_to_plot(self, plot, plot_label, plot_type, scale_min, scale_max, off_screen, lines): - for drawing_lines in lines: - bounding = [] - elements = [] - nodes_list = [] - solution = [] - for l in drawing_lines: - if "BoundingBox(" in l: - bounding = l[l.find("(") + 1 : -2].split(",") - bounding = [i.strip() for i in bounding] - if "Elements(" in l: - elements = l[l.find("(") + 1 : -2].split(",") - elements = [int(i.strip()) for i in elements] - if "Nodes(" in l: - nodes_list = l[l.find("(") + 1 : -2].split(",") - nodes_list = [float(i.strip()) for i in nodes_list] - if "ElemSolution(" in l: - # convert list of strings to list of floats - sols = l[l.find("(") + 1 : -2].split(",") - sols = [is_float(value) for value in sols] - - # sols = [float(i.strip()) for i in sols] - num_solution_per_element = int(sols[2]) - sols = sols[3:] - sols = [ - sols[i : i + num_solution_per_element] for i in range(0, len(sols), num_solution_per_element) - ] - solution = [sum(i) / num_solution_per_element for i in sols] - - nodes = [[nodes_list[i], nodes_list[i + 1], nodes_list[i + 2]] for i in range(0, len(nodes_list), 3)] - num_nodes = elements[0] - num_elements = elements[1] - elements = elements[2:] - element_type = elements[0] - num_nodes_per_element = elements[4] - hl = 5 # header length - elements_nodes = [] - for i in range(0, len(elements), num_nodes_per_element + hl): - elements_nodes.append([elements[i + hl + n] for n in range(num_nodes_per_element)]) - if solution: - take_all_nodes = True # solution case - else: - take_all_nodes = False # mesh case - trg_vertex = self._triangle_vertex(elements_nodes, num_nodes_per_element, take_all_nodes) - # remove duplicates - nodup_list = [list(i) for i in list(set([frozenset(t) for t in trg_vertex]))] - sols_vertex = [] - if solution: - sv = {} - for els, s in zip(elements_nodes, solution): - for el in els: - if el in sv: - sv[el] = (sv[el] + s) / 2 - else: - sv[el] = s - sols_vertex = [sv[v] for v in sorted(sv.keys())] - array = [[3] + [j - 1 for j in i] for i in nodup_list] - faces = np.hstack(array) - vertices = np.array(nodes) - surf = pv.PolyData(vertices, faces) - if sols_vertex: - temps = np.array(sols_vertex) - mean = np.mean(temps) - std = np.std(temps) - if np.min(temps) > 0: - log = True - else: - log = False - surf.point_data[plot_label] = temps - - sargs = dict( - title_font_size=10, - label_font_size=10, - shadow=True, - n_labels=9, - italic=True, - fmt="%.1f", - font_family="arial", + startpos = startpos - size - (size // 10) + el += 1 + actors = [i for i in self._fields if i._cached_mesh] + self._objects + if texts and len(texts) >= max_elements: + callback = ChangePageCallback(self.pv, actors, axes_color) + self.pv.add_checkbox_button_widget( + callback, + value=True, + position=(5.0, self.pv.window_size[1]), + size=int(1.5 * size), + border_size=2, + color_on=axes_color, + color_off=axes_color, + ) + self.pv.add_text("Next", position=(50.0, self.pv.window_size[1]), font_size=size // 3, color="grey") + self.pv.button_widgets.insert( + 0, self.pv.button_widgets.pop(self.pv.button_widgets.index(self.pv.button_widgets[-1])) ) - if plot_type == "Clip": - plot.add_text("Full Plot", font_size=15) - if solution: - - class MyCustomRoutine: - """ """ - - def __init__(self, mesh): - self.output = mesh # Expected PyVista mesh type - # default parameters - self.kwargs = { - "min_val": 0.5, - "max_val": 30, - } - - def __call__(self, param, value): - self.kwargs[param] = value - self.update() - - def update(self): - """ """ - # This is where you call your simulation - try: - plot.remove_actor("FieldPlot") - except: - pass - plot.add_mesh( - surf, - scalars=plot_label, - log_scale=log, - scalar_bar_args=sargs, - cmap="rainbow", - show_edges=False, - clim=[self.kwargs["min_val"], self.kwargs["max_val"]], - pickable=True, - smooth_shading=True, - name="FieldPlot", - ) - return - - engine = MyCustomRoutine(surf) - plot.add_box_widget( - surf, - show_edges=False, - scalars=plot_label, - log_scale=log, - scalar_bar_args=sargs, - cmap="rainbow", - pickable=True, - smooth_shading=True, - name="FieldPlot", - ) - if not off_screen: - plot.add_slider_widget( - callback=lambda value: engine("min_val", value), - rng=[np.min(temps), np.max(temps)], - title="Lower", - style="modern", - value=np.min(temps), - pointa=(0.5, 0.98), - pointb=(0.65, 0.98), - ) - - plot.add_slider_widget( - callback=lambda value: engine("max_val", value), - rng=[np.min(temps), np.max(temps)], - title="Upper", - style="modern", - value=np.max(temps), - pointa=(0.66, 0.98), - pointb=(0.8, 0.98), - ) - else: - if isinstance(scale_max, float): - engine("max_val", scale_max) - if isinstance(scale_min, float): - engine("min_val", scale_min) - else: - plot.add_box_widget( - surf, show_edges=True, line_width=0.1, color="grey", pickable=True, smooth_shading=True - ) - else: - plot.add_text("Full Plot", font_size=15) - if solution: - - class MyCustomRoutine: - """ """ - - def __init__(self, mesh): - self.output = mesh # Expected PyVista mesh type - # default parameters - self.kwargs = { - "min_val": 0.5, - "max_val": 30, - } - - def __call__(self, param, value): - self.kwargs[param] = value - self.update() - - def update(self): - """ """ - # This is where you call your simulation - try: - plot.remove_actor("FieldPlot") - except: - pass - plot.add_mesh( - surf, - scalars=plot_label, - log_scale=log, - scalar_bar_args=sargs, - cmap="rainbow", - show_edges=False, - clim=[self.kwargs["min_val"], self.kwargs["max_val"]], - pickable=True, - smooth_shading=True, - name="FieldPlot", - ) - return - - engine = MyCustomRoutine(surf) - plot.add_mesh( - surf, - show_edges=False, - scalars=plot_label, - log_scale=log, - scalar_bar_args=sargs, - cmap="rainbow", - pickable=True, - smooth_shading=True, - name="FieldPlot", - ) - if not off_screen: - plot.add_slider_widget( - callback=lambda value: engine("min_val", value), - rng=[np.min(temps), np.max(temps)], - title="Lower", - style="modern", - value=np.min(temps), - pointa=(0.5, 0.98), - pointb=(0.65, 0.98), - ) - - plot.add_slider_widget( - callback=lambda value: engine("max_val", value), - rng=[np.min(temps), np.max(temps)], - title="Upper", - style="modern", - value=np.max(temps), - pointa=(0.66, 0.98), - pointb=(0.8, 0.98), - ) - else: - if isinstance(scale_max, (int, float)): - engine("max_val", scale_max) - if isinstance(scale_min, (int, float)): - engine("min_val", scale_min) - else: - plot.add_mesh( - surf, show_edges=True, line_width=0.1, color="grey", pickable=True, smooth_shading=True - ) - - @aedt_exception_handler - def _plot_on_pyvista( - self, - plot, - meshes, - model_color, - materials, - view, - imageformat, - aedtplt_files, - show_axes=True, - show_grid=True, - show_legend=True, - export_path=None, - ): - files_list = [] - color = plot.background_color - axes_color = [0 if i >= 0.5 else 1 for i in color] - if show_axes: - plot.show_axes() - if show_grid and not is_notebook(): - plot.show_grid(color=tuple(axes_color)) - plot.add_bounding_box(color=tuple(axes_color)) - if show_legend and meshes and len(meshes) > 1 and not model_color: - labels = [] - for m in list(materials.keys()): - labels.append([m[:-4], materials[m]]) - plot.add_legend(labels=labels, bcolor=None, size=[0.1, 0.1]) - if view == "isometric": - plot.view_isometric() - elif view == "top": - plot.view_yz() - elif view == "front": - plot.view_xz() - elif view == "top": - plot.view_xy() - if imageformat: - if export_path: - filename = export_path - else: - filename = os.path.splitext(aedtplt_files[0])[0] + "." + imageformat - plot.show(screenshot=filename, full_screen=True) - files_list.append(filename) - else: - plot.show() - if aedtplt_files: - for f in aedtplt_files: - if os.path.exists(f): - os.remove(f) - if "obj" in f and os.path.exists(f[:-3] + "mtl"): - os.remove(f[:-3] + "mtl") - return files_list @aedt_exception_handler - def _plot_from_aedtplt( - self, - aedtplt_files=None, - imageformat="jpg", - view="isometric", - plot_type="Full", - plot_label="Temperature", - model_color="#8faf8f", - show_model_edge=False, - off_screen=False, - scale_min=None, - scale_max=None, - show_axes=True, - show_grid=True, - show_legend=True, - background_color=[0.6, 0.6, 0.6], - windows_size=None, - object_selector=True, - export_path=None, - model_opacity=0.6, - ): - """Export the 3D field solver mesh, fields, or both mesh and fields as images using Python Plotly. - - .. note:: - This method is currently supported only on Windows using CPython. + def plot(self, export_image_path=None): + """Plot the current available Data. Parameters ---------- - aedtplt_files : str or list, optional - Names of the one or more AEDTPLT files generated by AEDT. The default is ``None``. - imageformat : str, optional - Format of the image file. Options are ``"jpg"``, ``"png"``, ``"svg"``, and - ``"webp"``. The default is ``"jpg"``. - view : str, optional - View to export. Options are `Options are ``isometric``, - ``top``, ``front``, ``left``, ``all``. - The ``"all"`` option exports all views. - plot_type : str, optional - Type of the plot. The default is ``"Full"``. - plot_label : str, optional - Label for the plot. The default is ``"Temperature"``. - model_color : str, optional - Color scheme for the 3D model. The default is ``"#8faf8f"``, which is silver. - show_model_edge : bool, optional - Whether to return a list of the files that are generated. The default - is ``False``. - off_screen : bool, optional - The default is ``False``. - scale_min : float, optional - Fix the Scale Minimum value. - scale_max : float, optional - Fix the Scale Maximum value. - model_opacity : float, list optional - Model opacity. Value from 0 to 1. + + export_image_path : str + Path to image to save. Returns ------- - list - List of exported files. + bool """ start = time.time() - if type(aedtplt_files) is str: - aedtplt_files = [aedtplt_files] - if not windows_size: - windows_size = [1024, 768] - - plot = pv.Plotter(notebook=is_notebook(), off_screen=off_screen, window_size=windows_size) - plot.background_color = background_color - lines = [] - meshes = [] - model_colors = [] - model_opacities = [] - materials = {} - objects = [] - self._read_mesh_files( - aedtplt_files, model_color, model_opacity, lines, meshes, model_colors, model_opacities, materials, objects - ) - if lines: - fields_exists = True - else: - fields_exists = False - self._add_model_meshes_to_plot( - plot, - meshes, - model_colors, - model_opacities, - objects, - object_selector=object_selector, - show_model_edge=show_model_edge, - fields_exists=fields_exists, + self.pv = pv.Plotter(notebook=self.is_notebook, off_screen=self.off_screen, window_size=self.windows_size) + self.pv.background_color = [i / 255 for i in self.background_color] + self._read_mesh_files() + + axes_color = [0 if i >= 128 else 1 for i in self.background_color] + sargs = dict( + title_font_size=10, + label_font_size=10, + shadow=True, + n_labels=9, + italic=True, + fmt="%.1f", + font_family="arial", + interactive=True, + color=axes_color, + vertical=False, ) - if lines: - self._add_fields_to_plot(plot, plot_label, plot_type, scale_min, scale_max, off_screen, lines) - + for field in self._fields: + if self.range_max is not None and self.range_min is not None: + field._cached_mesh = self.pv.add_mesh( + field._cached_polydata, + scalars=field.label, + log_scale=field.log_scale, + scalar_bar_args=sargs, + cmap=field.color_map, + clim=[self.range_min, self.range_max], + opacity=field.opacity, + show_edges=field.show_edge, + ) + else: + field._cached_mesh = self.pv.add_mesh( + field._cached_polydata, + scalars=field.label, + log_scale=field.log_scale, + scalar_bar_args=sargs, + cmap=field.color_map, + opacity=field.opacity, + show_edges=field.show_edge, + ) + self._add_buttons() end = time.time() - start - self.logger.info("PyVista plot generation took {} seconds.".format(end)) - print(plot.background_color) - files_list = self._plot_on_pyvista( - plot, - meshes, - model_color, - materials, - view, - imageformat, - aedtplt_files, - show_axes=show_axes, - show_grid=show_grid, - show_legend=show_legend, - export_path=export_path, - ) - - return files_list + files_list = [] + if self.show_axes: + self.pv.show_axes() + if self.show_grid and not self.is_notebook: + self.pv.show_grid(color=tuple(axes_color)) + self.pv.add_bounding_box(color=tuple(axes_color)) + if not self.pv.off_screen: + self.pv.set_focus(self.pv.mesh.center) + self.pv.camera_position = self.camera_position + self.pv.camera.azimuth += self.azimuth_angle + self.pv.camera.roll += self.roll_angle + self.pv.camera.elevation += self.elevation_angle + self.pv.camera.zoom(self.zoom) + if export_image_path: + self.pv.show(screenshot=export_image_path, full_screen=True) + elif self.is_notebook: + self.pv.show() + else: + self.pv.show(full_screen=True) + self.image_file = export_image_path + return True @aedt_exception_handler - def _animation_from_aedtflt( - self, - aedtplt_files=None, - variation_var="Time", - variation_list=[], - plot_label="Temperature", - model_color="#8faf8f", - export_gif=False, - off_screen=False, - ): - """Export the 3D field solver mesh, fields, or both mesh and fields as images using Python Plotly. - - .. note:: - This method is currently supported only on Windows using CPython. + def clean_cache_and_files(self, remove_objs=True, remove_fields=True, clean_cache=False): + """Clean downloaded files, and, on demand, also the cached meshes. Parameters ---------- - aedtplt_files : str or list, optional - Names of the one or more AEDTPLT files generated by AEDT. The default is ``None``. - variation_var : str, optional - Variable to vary. The default is ``"Time"``. - variation_list : list, optional - List of variation values. The default is ``[]``. - plot_label : str, optional - Label for the plot. The default is ``"Temperature"``. - model_color : str, optional - Color scheme for the 3D model. The default is ``"#8faf8f"``, which is silver. - export_gif : bool, optional - Whether to export the animation as a GIF file. The default is ``False``. - off_screen : bool, optional - The default is ``False``. + remove_objs : bool + remove_fields : bool + clean_cache : bool + + Returns + ------- + bool + """ + if remove_objs: + for el in self.objects: + if os.path.exists(el.path): + os.remove(el.path) + if clean_cache: + el._cached_mesh = None + el._cached_polydata = None + if remove_fields: + for el in self.fields: + if os.path.exists(el.path): + os.remove(el.path) + if clean_cache: + el._cached_mesh = None + el._cached_polydata = None + return True + + @aedt_exception_handler + def animate(self): + """Animate the current field plot. Returns ------- - str - Name of the GIF file. + bool """ - frame_per_seconds = 0.5 start = time.time() - if type(aedtplt_files) is str: - aedtplt_files = [aedtplt_files] - plot = pv.Plotter(notebook=False, off_screen=off_screen) - if not off_screen: - plot.enable_anti_aliasing() - plot.enable_fly_to_right_click() - lines = [] - for file in aedtplt_files: - if ".aedtplt" in file: - with open(file, "r") as f: - drawing_found = False - for line in f: - if "$begin Drawing" in line: - drawing_found = True - l_tmp = [] - continue - if "$end Drawing" in line: - lines.append(l_tmp) - drawing_found = False - continue - if drawing_found: - l_tmp.append(line) - continue - if "Number of drawing:" in line: - n_drawings = int(line[18:]) - continue - elif ".obj" in file: - mesh = pv.read(file) - plot.add_mesh( - mesh, - show_scalar_bar=False, - opacity=0.75, - cmap=[model_color], - name="3D Model", - show_edges=False, - edge_color=model_color, - ) - # def create_object_mesh(opacity): - # try: - # p.remove_actor("Volumes") - # except: - # pass - # p.add_mesh(mesh, show_scalar_bar=False, opacity=opacity, cmap=[model_color], name="3D Model", - # show_edges=False, edge_color=model_color) - # p.add_slider_widget( - # create_object_mesh, - # [0,1], style='modern', - # value=0.75,pointa=[0.81,0.98], - # pointb=[0.95,0.98],title="Opacity" - # ) - filename = os.path.splitext(aedtplt_files[0])[0] - print(filename) - surfs = [] - log = False - mins = 1e12 - maxs = -1e12 - log = True - for drawing_lines in lines: - bounding = [] - elements = [] - nodes_list = [] - solution = [] - for l in drawing_lines: - if "BoundingBox(" in l: - bounding = l[l.find("(") + 1 : -2].split(",") - bounding = [i.strip() for i in bounding] - if "Elements(" in l: - elements = l[l.find("(") + 1 : -2].split(",") - elements = [int(i.strip()) for i in elements] - if "Nodes(" in l: - nodes_list = l[l.find("(") + 1 : -2].split(",") - nodes_list = [float(i.strip()) for i in nodes_list] - if "ElemSolution(" in l: - # convert list of strings to list of floats - sols = l[l.find("(") + 1 : -2].split(",") - sols = [is_float(value) for value in sols] - - num_solution_per_element = int(sols[2]) - sols = sols[3:] - sols = [ - sols[i : i + num_solution_per_element] for i in range(0, len(sols), num_solution_per_element) - ] - solution = [sum(i) / num_solution_per_element for i in sols] - - nodes = [[nodes_list[i], nodes_list[i + 1], nodes_list[i + 2]] for i in range(0, len(nodes_list), 3)] - num_nodes = elements[0] - num_elements = elements[1] - elements = elements[2:] - element_type = elements[0] - num_nodes_per_element = elements[4] - hl = 5 # header length - elements_nodes = [] - for i in range(0, len(elements), num_nodes_per_element + hl): - elements_nodes.append([elements[i + hl + n] for n in range(num_nodes_per_element)]) - if solution: - take_all_nodes = True # solution case - else: - take_all_nodes = False # mesh case - trg_vertex = self._triangle_vertex(elements_nodes, num_nodes_per_element, take_all_nodes) - # remove duplicates - nodup_list = [list(i) for i in list(set([frozenset(t) for t in trg_vertex]))] - sols_vertex = [] - if solution: - sv = {} - for els, s in zip(elements_nodes, solution): - for el in els: - if el in sv: - sv[el] = (sv[el] + s) / 2 - else: - sv[el] = s - sols_vertex = [sv[v] for v in sorted(sv.keys())] - array = [[3] + [j - 1 for j in i] for i in nodup_list] - faces = np.hstack(array) - vertices = np.array(nodes) - surf = pv.PolyData(vertices, faces) - - if sols_vertex: - temps = np.array(sols_vertex) - mean = np.mean(temps) - std = np.std(temps) - if np.min(temps) <= 0: - log = False - surf.point_data[plot_label] = temps - if solution: - surfs.append(surf) - if np.min(temps) < mins: - mins = np.min(temps) - if np.max(temps) > maxs: - maxs = np.max(temps) + assert len(self.frames) > 0, "Number of Fields have to be greater than 1 to do an animation." + if self.is_notebook: + self.pv = pv.Plotter(notebook=self.is_notebook, off_screen=True, window_size=self.windows_size) + else: + self.pv = pv.Plotter(notebook=self.is_notebook, off_screen=self.off_screen, window_size=self.windows_size) + self.pv.background_color = [i / 255 for i in self.background_color] + self._read_mesh_files(read_frames=True) + end = time.time() - start + files_list = [] + axes_color = [0 if i >= 128 else 1 for i in self.background_color] + + if self.show_axes: + self.pv.show_axes() + if self.show_grid and not self.is_notebook: + self.pv.show_grid(color=tuple(axes_color)) + self.pv.add_bounding_box(color=tuple(axes_color)) + if self.show_legend: + labels = [] + for m in self.objects: + labels.append([m.name, [i / 255 for i in m.color]]) + for m in self.fields: + labels.append([m.name, "red"]) + self.pv.add_legend(labels=labels, bcolor=None, face="circle", size=[0.15, 0.15]) + if not self.pv.off_screen: + self.pv.set_focus(self.pv.mesh.center) + self.pv.camera_position = self.camera_position + self.pv.camera.azimuth += self.azimuth_angle + self.pv.camera.roll += self.roll_angle + self.pv.camera.elevation += self.elevation_angle + self.pv.zoom = self.zoom self._animating = True - gifname = None - if export_gif: - gifname = os.path.splitext(aedtplt_files[0])[0] + ".gif" - plot.open_gif(gifname) + + if self.gif_file: + self.pv.open_gif(self.gif_file) def q_callback(): """exit when user wants to leave""" @@ -1162,15 +971,12 @@ def p_callback(): """exit when user wants to leave""" self._pause = not self._pause - plot.add_text("Press p for Play/Pause, Press q to exit ", font_size=8, position="upper_left") - plot.add_text(" ", font_size=10, position=[0, 0]) - plot.add_key_event("q", q_callback) - plot.add_key_event("p", p_callback) - - # run until q is pressed - plot.show_axes() - plot.show_grid() - cpos = plot.show(interactive=False, auto_close=False, interactive_update=not off_screen) + self.pv.add_text( + "Press p for Play/Pause, Press q to exit ", font_size=8, position="upper_left", color=tuple(axes_color) + ) + self.pv.add_text(" ", font_size=10, position=[0, 0], color=tuple(axes_color)) + self.pv.add_key_event("q", q_callback) + self.pv.add_key_event("p", p_callback) sargs = dict( title_font_size=10, @@ -1181,63 +987,239 @@ def p_callback(): fmt="%.1f", font_family="arial", ) - plot.add_mesh( - surfs[0], - scalars=plot_label, - log_scale=log, + + for field in self._fields: + field._cached_mesh = self.pv.add_mesh( + field._cached_polydata, + scalars=field.label, + log_scale=field.log_scale, + scalar_bar_args=sargs, + cmap=field.color_map, + opacity=field.opacity, + ) + # run until q is pressed + + cpos = self.pv.show(interactive=False, auto_close=False, interactive_update=not self.off_screen) + + if self.range_min is not None and self.range_max is not None: + mins = self.range_min + maxs = self.range_max + else: + mins = 1e20 + maxs = -1e20 + for el in self.frames: + if np.min(el._cached_polydata.point_data[el.label]) < mins: + mins = np.min(el._cached_polydata.point_data[el.label]) + if np.max(el._cached_polydata.point_data[el.label]) > maxs: + maxs = np.max(el._cached_polydata.point_data[el.label]) + + self.frames[0]._cached_mesh = self.pv.add_mesh( + self.frames[0]._cached_polydata, + scalars=self.frames[0].label, + log_scale=self.frames[0].log_scale, scalar_bar_args=sargs, - cmap="rainbow", + cmap=self.frames[0].color_map, clim=[mins, maxs], show_edges=False, pickable=True, smooth_shading=True, name="FieldPlot", ) - plot.isometric_view() start = time.time() - plot.update(1, force_redraw=True) - first_loop = True - if export_gif: + self.pv.update(1, force_redraw=True) + if self.gif_file: first_loop = True - plot.write_frame() + self.pv.write_frame() else: first_loop = False i = 1 while self._animating: if self._pause: time.sleep(1) - plot.update(1, force_redraw=True) + self.pv.update(1, force_redraw=True) continue # p.remove_actor("FieldPlot") - if i >= len(surfs): - if off_screen: + if i >= len(self.frames): + if self.off_screen or self.is_notebook: break i = 0 first_loop = False - scalars = surfs[i].point_data[plot_label] - plot.update_scalars(scalars, render=False) - # p.add_mesh(surfs[i], scalars=plot_label, log_scale=log, scalar_bar_args=sargs, cmap='rainbow', - # show_edges=False, pickable=True, smooth_shading=True, name="FieldPlot") - plot.textActor.SetInput(variation_var + " = " + variation_list[i]) - if not hasattr(plot, "ren_win"): + scalars = self.frames[i]._cached_polydata.point_data[self.frames[i].label] + self.pv.update_scalars(scalars, render=False) + if not hasattr(self.pv, "ren_win"): break - # p.update(1, force_redraw=True) - time.sleep(max(0, frame_per_seconds - (time.time() - start))) + time.sleep(max(0, (1 / self.frame_per_seconds) - (time.time() - start))) start = time.time() - if off_screen: - plot.render() + if self.off_screen: + self.pv.render() else: - plot.update(1, force_redraw=True) + self.pv.update(1, force_redraw=True) if first_loop: - plot.write_frame() - - time.sleep(0.2) + self.pv.write_frame() i += 1 - plot.close() - for el in aedtplt_files: - os.remove(el) - return gifname + self.pv.close() + if self.gif_file: + return self.gif_file + else: + return True + + +class PostProcessor(Post): + """Contains advanced postprocessing functionalities that require Python 3.x packages like NumPy and Matplotlib. + + Parameters + ---------- + app : + Inherited parent object. + + Examples + -------- + Basic usage demonstrated with an HFSS, Maxwell, or any other design: + + >>> from pyaedt import Hfss + >>> aedtapp = Hfss() + >>> post = aedtapp.post + """ + + def __init__(self, app): + Post.__init__(self, app) + + @aedt_exception_handler + def nb_display(self, show_axis=True, show_grid=True, show_ruler=True): + """Show the Jupyter Notebook display. + + .. note:: + .assign_curvature_extraction Jupyter Notebook is not supported by IronPython. + + Parameters + ---------- + show_axis : bool, optional + Whether to show the axes. The default is ``True``. + show_grid : bool, optional + Whether to show the grid. The default is ``True``. + show_ruler : bool, optional + Whether to show the ruler. The default is ``True``. + + Returns + ------- + :class:`IPython.core.display.Image` + Jupyter notebook image. + + """ + file_name = self.export_model_picture(show_axis=show_axis, show_grid=show_grid, show_ruler=show_ruler) + return Image(file_name, width=500) + + @aedt_exception_handler + def get_efields_data(self, setup_sweep_name="", ff_setup="Infinite Sphere1", freq="All"): + """Compute Etheta and EPhi. + + .. warning:: + This method requires NumPy to be installed on your machine. + + + Parameters + ---------- + setup_sweep_name : str, optional + Name of the setup for computing the report. The default is ``""``, in + which case the nominal adaptive is applied. + ff_setup : str, optional + Far field setup. The default is ``"Infinite Sphere1"``. + freq : str, optional + The default is ``"All"``. + + Returns + ------- + np.ndarray + numpy array containing ``[theta_range, phi_range, Etheta, Ephi]``. + """ + if not setup_sweep_name: + setup_sweep_name = self._app.nominal_adaptive + results_dict = {} + all_sources = self.post_osolution.GetAllSources() + # assuming only 1 mode + all_sources_with_modes = [s + ":1" for s in all_sources] + + for n, source in enumerate(all_sources_with_modes): + edit_sources_ctxt = [["IncludePortPostProcessing:=", False, "SpecifySystemPower:=", False]] + for m, each in enumerate(all_sources_with_modes): + if n == m: # set only 1 source to 1W, all the rest to 0 + mag = 1 + else: + mag = 0 + phase = 0 + edit_sources_ctxt.append( + ["Name:=", "{}".format(each), "Magnitude:=", "{}W".format(mag), "Phase:=", "{}deg".format(phase)] + ) + self.post_osolution.EditSources(edit_sources_ctxt) + + ctxt = ["Context:=", ff_setup] + + sweeps = ["Theta:=", ["All"], "Phi:=", ["All"], "Freq:=", [freq]] + + trace_name = "rETheta" + solnData = self.get_far_field_data( + setup_sweep_name=setup_sweep_name, domain=ff_setup, expression=trace_name + ) + + data = solnData.nominal_variation + + theta_vals = np.degrees(np.array(data.GetSweepValues("Theta"))) + phi_vals = np.degrees(np.array(data.GetSweepValues("Phi"))) + # phi is outer loop + theta_unique = np.unique(theta_vals) + phi_unique = np.unique(phi_vals) + theta_range = np.linspace(np.min(theta_vals), np.max(theta_vals), np.size(theta_unique)) + phi_range = np.linspace(np.min(phi_vals), np.max(phi_vals), np.size(phi_unique)) + real_theta = np.array(data.GetRealDataValues(trace_name)) + imag_theta = np.array(data.GetImagDataValues(trace_name)) + + trace_name = "rEPhi" + solnData = self.get_far_field_data( + setup_sweep_name=setup_sweep_name, domain=ff_setup, expression=trace_name + ) + data = solnData.nominal_variation + + real_phi = np.array(data.GetRealDataValues(trace_name)) + imag_phi = np.array(data.GetImagDataValues(trace_name)) + + Etheta = np.vectorize(complex)(real_theta, imag_theta) + Ephi = np.vectorize(complex)(real_phi, imag_phi) + source_name_without_mode = source.replace(":1", "") + results_dict[source_name_without_mode] = [theta_range, phi_range, Etheta, Ephi] + return results_dict + + @aedt_exception_handler + def ff_sum_with_delta_phase(self, ff_data, xphase=0, yphase=0): + """Generate a far field sum with a delta phase. + + Parameters + ---------- + ff_data : + + xphase : float, optional + Phase in the X-axis direction. The default is ``0``. + yphase : float, optional + Phase in the Y-axis direction. The default is ``0``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + array_size = [4, 4] + loc_offset = 2 + + rETheta = ff_data[2] + rEPhi = ff_data[3] + weight = np.zeros((array_size[0], array_size[0])) + mag = np.ones((array_size[0], array_size[0])) + for m in range(array_size[0]): + for n in range(array_size[1]): + mag = mag[m][n] + ang = np.radians(xphase * m) + np.radians(yphase * n) + weight[m][n] = np.sqrt(mag) * np.exp(1 * ang) + return True @aedt_exception_handler def export_model_obj(self, obj_list=None, export_path=None, export_as_single_objects=False, air_objects=False): @@ -1262,8 +1244,11 @@ def export_model_obj(self, obj_list=None, export_path=None, export_as_single_obj assert self._app._aedt_version >= "2021.2", self.logger.error("Object is supported from AEDT 2021 R2.") if not export_path: - export_path = self._app.project_path + export_path = os.path.join(self._app.project_path, self._app.project_name + "_pyaedt") + if not os.path.exists(export_path): + os.mkdir(export_path) if not obj_list: + self._app.modeler.refresh_all_ids() obj_list = self._app.modeler.primitives.object_names if not air_objects: obj_list = [ @@ -1278,16 +1263,17 @@ def export_model_obj(self, obj_list=None, export_path=None, export_as_single_obj if export_as_single_objects: files_exported = [] for el in obj_list: - fname = os.path.join( - export_path, "Model_{}_{}.obj".format(el, self._app.modeler[el].material_name.lower()) - ) + fname = os.path.join(export_path, "Model_{}.obj".format(el)) self._app.modeler.oeditor.ExportModelMeshToFile(fname, [el]) - files_exported.append(fname) + if not self._app.modeler[el].display_wireframe: + files_exported.append([fname, self._app.modeler[el].color, 1 - self._app.modeler[el].transparency]) + else: + files_exported.append([fname, self._app.modeler[el].color, 0.05]) return files_exported else: fname = os.path.join(export_path, "Model_AllObjs_AllMats.obj") self._app.modeler.oeditor.ExportModelMeshToFile(fname, obj_list) - return [fname] + return [fname, "grey", 0.6] @aedt_exception_handler def export_mesh_obj(self, setup_name=None, intrinsic_dict={}): @@ -1329,20 +1315,12 @@ def export_mesh_obj(self, setup_name=None, intrinsic_dict={}): def plot_model_obj( self, objects=None, - export_afterplot=True, + show=True, export_path=None, - plot_separate_objects=True, - air_objects=False, - show_axes=True, - show_grid=True, - show_legend=True, - background_color="white", - object_selector=True, - windows_size=None, - off_screen=False, - color=None, - color_by_material=False, - opacity=None, + plot_as_separate_objects=True, + plot_air_objects=False, + force_opacity_value=None, + clean_files=False, ): """Plot the model or a substet of objects. @@ -1350,89 +1328,52 @@ def plot_model_obj( ---------- objects : list, optional Optional list of objects to plot. If `None` all objects will be exported. - export_afterplot : bool, optional - Set to True if the image has to be exported after the plot is completed. + show : bool, optional + Show the plot after generation or simply return the + generated Class for more customization before plot. export_path : str, optional - File name with full path. If `None` Project directory will be used. - plot_separate_objects : bool, optional + If available, an image is saved to file. If `None` no image will be saved. + plot_as_separate_objects : bool, optional Plot each object separately. It may require more time to export from AEDT. - air_objects : bool, optional + plot_air_objects : bool, optional Plot also air and vacuum objects. - show_axes : bool, optional - Define if axes will be visible or not. - show_grid : bool, optional - Define if grid will be visible or not. - show_legend : bool, optional - Define if legend is visible or not. - background_color : str, list, optional - Define the plot background color. Default is `"white"`. - One of the keys of `pyaedt.generic.constants.CSS4_COLORS` can be used. - object_selector : bool, optional - Enable the list of object to hide show objects. - windows_size : list, optional - Windows Plot size. - off_screen : bool, optional - Off Screen plot. - color : str, list - Color of the object. Can be color name or list of RGB. If None automatic color from model or material. - color_by_material : bool - Either to color object by material or by their AEDT value. + force_opacity_value : float, optional + Opacity value between 0 and 1 to be applied to all model. + If `None` aedt opacity will be applied to each object. + clean_files : bool, optional + Clean created files after plot. Cache is mainteined into the model object returned. Returns ------- - list - List of plot files. + :class:`pyaedt.modules.AdvancedPostProcessing.ModelPlotter` + Model Object. """ assert self._app._aedt_version >= "2021.2", self.logger.error("Object is supported from AEDT 2021 R2.") files = self.export_model_obj( - obj_list=objects, export_as_single_objects=plot_separate_objects, air_objects=air_objects + obj_list=objects, + export_as_single_objects=plot_as_separate_objects, + air_objects=plot_air_objects, ) if not files: self.logger.warning("No Objects exported. Try other options or include Air objects.") return False - if export_afterplot: - imageformat = "png" - else: - imageformat = None - - if plot_separate_objects and not color and not color_by_material: - color = [] - include_opacity = False - if opacity is None: - opacity = [] - include_opacity = True - if not objects: - objects = self._app.modeler.primitives.object_names - for el in objects: - try: - if isinstance(self._app.modeler[el].color, (tuple, list)): - color.append([i / 256 for i in self._app.modeler[el].color]) - else: - color.append(self._app.modeler[el].color) - if include_opacity: - opacity.append(1 - self._app.modeler[el].transparency) - except KeyError: - color.append("dodgerblue") - if include_opacity: - opacity.append(0.6) - - file_list = self._plot_from_aedtplt( - files, - imageformat=imageformat, - export_path=export_path, - plot_label="3D Model", - model_color=color, - show_model_edge=False, - off_screen=off_screen, - show_axes=show_axes, - show_grid=show_grid, - show_legend=show_legend, - background_color=background_color, - windows_size=windows_size, - object_selector=object_selector, - model_opacity=opacity, - ) - return file_list + + model = ModelPlotter() + + for file in files: + if force_opacity_value: + model.add_object(file[0], file[1], force_opacity_value, self.modeler.model_units) + else: + model.add_object(file[0], file[1], file[2], self.modeler.model_units) + if not show: + model.off_screen = True + if export_path: + model.plot(export_path) + elif show: + model.plot() + if clean_files: + model.clean_cache_and_files(clean_cache=False) + return model @aedt_exception_handler def plot_field_from_fieldplot( @@ -1440,13 +1381,11 @@ def plot_field_from_fieldplot( plotname, project_path="", meshplot=False, - setup_name=None, - intrinsic_dict={}, imageformat="jpg", view="isometric", plot_label="Temperature", plot_folder=None, - off_screen=False, + show=True, scale_min=None, scale_max=None, ): @@ -1464,11 +1403,6 @@ def plot_field_from_fieldplot( meshplot : bool, optional Whether to create and plot the mesh over the fields. The default is ``False``. - setup_name : str, optional - Name of the setup or sweep to use for the export. The default is ``None``. - intrinsic_dict : dict, optional - Intrinsic dictionary that is needed for the export when ``meshplot="True"``. - The default is ``{}``. imageformat : str, optional Format of the image file. Options are ``"jpg"``, ``"png"``, ``"svg"``, and ``"webp"``. The default is @@ -1482,7 +1416,7 @@ def plot_field_from_fieldplot( Plot folder to update before exporting the field. The default is ``None``, in which case all plot folders are updated. - off_screen : bool, optional + show : bool, optional Export Image without plotting on UI. scale_min : float, optional Fix the Scale Minimum value. @@ -1491,8 +1425,8 @@ def plot_field_from_fieldplot( Returns ------- - type - List of exported files. + :class:`pyaedt.modules.AdvancedPostProcessing.ModelPlotter` + Model Object. """ if not plot_folder: self.ofieldsreporter.UpdateAllFieldsPlots() @@ -1500,34 +1434,35 @@ def plot_field_from_fieldplot( self.ofieldsreporter.UpdateQuantityFieldsPlots(plot_folder) start = time.time() - files_to_add = [] - if not project_path: - project_path = self._app.project_path - file_to_add = self.export_field_plot(plotname, project_path) - file_list = None + file_to_add = self.export_field_plot(plotname, self._app.project_path) + models = None if not file_to_add: return False else: - files_to_add.append(file_to_add) if meshplot: if self._app._aedt_version >= "2021.2": - files_to_add.extend(self.export_model_obj()) - else: - file_to_add = self.export_mesh_obj(setup_name, intrinsic_dict) - if file_to_add: - files_to_add.append(file_to_add) - file_list = self._plot_from_aedtplt( - files_to_add, - imageformat=imageformat, - view=view, - plot_label=plot_label, - off_screen=off_screen, - scale_min=scale_min, - scale_max=scale_max, - ) - endt = time.time() - start - print("Field Generation, export and plot time: ", endt) - return file_list + models = self.export_model_obj(export_as_single_objects=True, air_objects=False) + + model = ModelPlotter() + model.off_screen = not show + + if file_to_add: + model.add_field_from_file(file_to_add, coordinate_units=self.modeler.model_units) + if plot_label: + model.fields[0].label = plot_label + if models: + for m in models: + model.add_object(m[0], m[1], m[2]) + model.view = view + + if scale_min and scale_max: + model.range_min = scale_min + model.range_max = scale_max + if show or project_path: + model.plot(os.path.join(project_path, self._app.project_name + "." + imageformat)) + model.clean_cache_and_files(clean_cache=False) + + return model @aedt_exception_handler def animate_fields_from_aedtplt( @@ -1535,13 +1470,11 @@ def animate_fields_from_aedtplt( plotname, plot_folder=None, meshplot=False, - setup_name=None, - intrinsic_dict={}, variation_variable="Phi", variation_list=["0deg"], project_path="", export_gif=False, - off_screen=False, + show=True, ): """Generate a field plot to an image file (JPG or PNG) using PyVista. @@ -1555,12 +1488,6 @@ def animate_fields_from_aedtplt( plot_folder : str, optional Name of the folder in which the plot resides. The default is ``None``. - setup_name : str, optional - Name of the setup (sweep) to use for the export. The - default is ``None``. - intrinsic_dict : dict, optional - Intrinsic dictionary that is needed for the export. The default - is ``{}``. variation_variable : str, optional Variable to vary. The default is ``"Phi"``. variation_list : list, optional @@ -1569,29 +1496,29 @@ def animate_fields_from_aedtplt( project_path : str, optional Path for the export. The default is ``""``. meshplot : bool, optional - The default is ``False``. + The default is ``False``. Valid from Version 2021.2. export_gif : bool, optional The default is ``False``. - off_screen : bool, optional - Generate the animation without showing an interactive plot. The default is ``False``. + show=False, + show : bool, optional + Generate the animation without showing an interactive plot. The default is ``True``. Returns ------- - bool - ``True`` when successful, ``False`` when failed. + :class:`pyaedt.modules.AdvancedPostProcessing.ModelPlotter` + Model Object. """ if not plot_folder: self.ofieldsreporter.UpdateAllFieldsPlots() else: self.ofieldsreporter.UpdateQuantityFieldsPlots(plot_folder) - files_to_add = [] + models_to_add = [] if meshplot: if self._app._aedt_version >= "2021.2": - files_to_add.extend(self.export_model_obj()) - else: - file_to_add = self.export_mesh_obj(setup_name, intrinsic_dict) - if file_to_add: - files_to_add.append(file_to_add) + models_to_add = self.export_model_obj(export_as_single_objects=True, air_objects=False) + fields_to_add = [] + if not project_path: + project_path = self._app.project_path for el in variation_list: self._app._odesign.ChangeProperty( [ @@ -1603,12 +1530,24 @@ def animate_fields_from_aedtplt( ], ] ) - files_to_add.append(self.export_field_plot(plotname, project_path, plotname + variation_variable + str(el))) + fields_to_add.append( + self.export_field_plot(plotname, project_path, plotname + variation_variable + str(el)) + ) - self._animation_from_aedtflt( - files_to_add, variation_variable, variation_list, export_gif=export_gif, off_screen=off_screen - ) - return True + model = ModelPlotter() + model.off_screen = not show + if models_to_add: + for m in models_to_add: + model.add_object(m[0], cad_color=m[1], opacity=m[2]) + if fields_to_add: + model.add_frames_from_file(fields_to_add) + if export_gif: + model.gif_file = os.path.join(self._app.project_path, self._app.project_name + ".gif") + + if show or export_gif: + model.animate() + model.clean_cache_and_files(clean_cache=False) + return model @aedt_exception_handler def animate_fields_from_aedtplt_2( @@ -1623,9 +1562,9 @@ def animate_fields_from_aedtplt_2( variation_list=["0deg"], project_path="", export_gif=False, - off_screen=False, + show=True, ): - """Generate a field plot to an image file (JPG or PNG) using PyVista. + """Generate a field plot to an animated gif file using PyVista. .. note:: The PyVista module rebuilds the mesh and the overlap fields on the mesh. @@ -1661,25 +1600,22 @@ def animate_fields_from_aedtplt_2( export_gif : bool, optional Whether to export to a GIF file. The default is ``False``, in which case the plot is exported to a JPG file. - off_screen : bool, optional - The default is ``False``. + show : bool, optional + Generate the animation without showing an interactive plot. The default is ``True``. Returns ------- - bool - ``True`` when successful, ``False`` when failed. + :class:`pyaedt.modules.AdvancedPostProcessing.ModelPlotter` + Model Object. """ if not project_path: project_path = self._app.project_path - files_to_add = [] + models_to_add = [] if meshplot: if self._app._aedt_version >= "2021.2": - files_to_add.extend(self.export_model_obj()) - else: - file_to_add = self.export_mesh_obj(setup_name, intrinsic_dict) - if file_to_add: - files_to_add.append(file_to_add) + models_to_add = self.export_model_obj(export_as_single_objects=True, air_objects=False) v = 0 + fields_to_add = [] for el in variation_list: intrinsic_dict[variation_variable] = el if plottype == "Surface": @@ -1691,13 +1627,23 @@ def animate_fields_from_aedtplt_2( if plotf: file_to_add = self.export_field_plot(plotf.name, project_path, plotf.name + str(v)) if file_to_add: - files_to_add.append(file_to_add) + fields_to_add.append(file_to_add) plotf.delete() v += 1 + model = ModelPlotter() + model.off_screen = not show + if models_to_add: + for m in models_to_add: + model.add_object(m[0], cad_color=m[1], opacity=m[2]) + if fields_to_add: + model.add_frames_from_file(fields_to_add) + if export_gif: + model.gif_file = os.path.join(self._app.project_path, self._app.project_name + ".gif") + if show or export_gif: + model.animate() + model.clean_cache_and_files(clean_cache=False) - return self._animation_from_aedtflt( - files_to_add, variation_variable, variation_list, export_gif=export_gif, off_screen=off_screen - ) + return model @aedt_exception_handler def far_field_plot(self, ff_data, x=0, y=0, qty="rETotal", dB=True, array_size=[4, 4]): diff --git a/pyaedt/modules/Material.py b/pyaedt/modules/Material.py index bac806768c9..8ec596a8862 100644 --- a/pyaedt/modules/Material.py +++ b/pyaedt/modules/Material.py @@ -15,6 +15,7 @@ from collections import OrderedDict from pyaedt.generic.general_methods import aedt_exception_handler from pyaedt.generic.DataHandlers import _dict2arg +from pyaedt.generic.constants import CSS4_COLORS class MatProperties(object): @@ -899,15 +900,24 @@ def __init__(self, materiallib, name, props=None): self._material_appearance.append(self._props["AttachedData"]["MatAppearanceData"]["Green"]) self._material_appearance.append(self._props["AttachedData"]["MatAppearanceData"]["Blue"]) else: - self._material_appearance = [128, 128, 128] + vals = list(CSS4_COLORS.values()) + if (materiallib._color_id) > len(vals): + materiallib._color_id = 0 + h = vals[materiallib._color_id].lstrip("#") + self._material_appearance = list(int(h[i : i + 2], 16) for i in (0, 2, 4)) + materiallib._color_id += 1 self._props["AttachedData"] = OrderedDict( { "MatAppearanceData": OrderedDict( - {"property_data": "appearance_data", "Red": 128, "Green": 128, "Blue": 128} + { + "property_data": "appearance_data", + "Red": self._material_appearance[0], + "Green": self._material_appearance[1], + "Blue": self._material_appearance[2], + } ) } ) - for property in MatProperties.aedtname: if property in self._props: mods = None diff --git a/pyaedt/modules/MaterialLib.py b/pyaedt/modules/MaterialLib.py index ec03e804ef2..fb67782a6ce 100644 --- a/pyaedt/modules/MaterialLib.py +++ b/pyaedt/modules/MaterialLib.py @@ -30,6 +30,7 @@ class Materials(object): def __init__(self, app): self._app = app + self._color_id = 0 self.odefinition_manager = self._app.odefinition_manager self.omaterial_manager = self._app.omaterial_manager self._desktop = self._app.odesktop @@ -336,7 +337,7 @@ def add_material_sweep(self, swargs, matname): mat_dict = self._create_mat_project_vars(matsweep) - newmat = Material(self._app, matname) + newmat = Material(self, matname) index = "$ID" + matname self._app[index] = 0 for el in mat_dict: diff --git a/pyaedt/modules/PostProcessor.py b/pyaedt/modules/PostProcessor.py index a479595b55c..d245cdd3e28 100644 --- a/pyaedt/modules/PostProcessor.py +++ b/pyaedt/modules/PostProcessor.py @@ -952,12 +952,10 @@ def export_image_from_aedtplt( self.name, project_path=export_path, meshplot=plot_mesh, - setup_name=self.solutionName, - intrinsic_dict=self.intrinsincList, imageformat="jpg", view=view, plot_label=self.quantityName, - off_screen=True, + show=False, scale_min=scale_min, scale_max=scale_max, ) @@ -2216,8 +2214,8 @@ def export_field_plot(self, plotname, filepath, filename="", file_format="aedtpl Returns ------- - bool - ``True`` when successful, ``False`` when failed. + str + File Path when succeeded. References ---------- From a935a91ca8e591271b0756f5fab1b12cf9440929 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Mon, 17 Jan 2022 21:36:32 +0100 Subject: [PATCH 03/14] Fixed issue on unite when more number of object is = 21 (#722) --- pyaedt/modeler/Modeler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyaedt/modeler/Modeler.py b/pyaedt/modeler/Modeler.py index af79a97b96c..53b0eb14366 100644 --- a/pyaedt/modeler/Modeler.py +++ b/pyaedt/modeler/Modeler.py @@ -2337,7 +2337,7 @@ def unite(self, theList): num_objects = len(theList) remaining = num_objects objs_groups = [] - while remaining > 0: + while remaining > 1: objs = theList[:slice] szSelections = self.convert_to_selections(objs) vArg1 = ["NAME:Selections", "Selections:=", szSelections] @@ -2347,6 +2347,8 @@ def unite(self, theList): remaining -= slice if remaining > 0: theList = theList[slice:] + if remaining > 0: + objs_groups.extend(theList) self.primitives.cleanup_objects() if len(objs_groups) > 1: return self.unite(objs_groups) From b2bf55ce5609436279f27934febd6d7fd3c01e8d Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Mon, 17 Jan 2022 22:40:31 +0100 Subject: [PATCH 04/14] Enhancement/non linear materials (#724) * Implemented Material Cohericitivty and core losses * Implemented Material Cohericitivty and core losses * Implemented Material Cohericitivty and core losses * Implemented Material Cohericitivty and core losses * Implemented Material Cohericitivty and core losses * Black Fix * Update pyaedt/modules/Material.py * Apply suggestions from code review Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> --- _unittest/test_03_Materials.py | 18 ++- pyaedt/modules/Material.py | 274 ++++++++++++++++++++++++++++----- 2 files changed, 246 insertions(+), 46 deletions(-) diff --git a/_unittest/test_03_Materials.py b/_unittest/test_03_Materials.py index 0b9f3779968..186e44af0cf 100644 --- a/_unittest/test_03_Materials.py +++ b/_unittest/test_03_Materials.py @@ -56,18 +56,12 @@ def test_02_create_material(self): assert mat1.diffusivity.value == MatProperties.get_defaultvalue(aedtname="diffusivity") assert "Electromagnetic" in mat1.physics_type - mat1.core_loss_kc.value = MatProperties.get_defaultvalue(aedtname="core_loss_kc") - mat1.core_loss_kh.value = MatProperties.get_defaultvalue(aedtname="core_loss_kh") - mat1.core_loss_ke.value = MatProperties.get_defaultvalue(aedtname="core_loss_ke") mat1.molecular_mass.value = MatProperties.get_defaultvalue(aedtname="molecular_mass") mat1.specific_heat.value = MatProperties.get_defaultvalue(aedtname="specific_heat") mat1.thermal_expansion_coefficient.value = MatProperties.get_defaultvalue( aedtname="thermal_expansion_coefficient" ) - assert mat1.core_loss_kc.value == MatProperties.get_defaultvalue(aedtname="core_loss_kc") - assert mat1.core_loss_kh.value == MatProperties.get_defaultvalue(aedtname="core_loss_kh") - assert mat1.core_loss_ke.value == MatProperties.get_defaultvalue(aedtname="core_loss_ke") assert mat1.coordinate_system == "Cartesian" assert mat1.name == "new_copper2" assert mat1.molecular_mass.value == MatProperties.get_defaultvalue(aedtname="molecular_mass") @@ -78,7 +72,19 @@ def test_02_create_material(self): assert self.aedtapp.change_validation_settings() assert self.aedtapp.change_validation_settings(ignore_unclassified=True, skip_intersections=True) + assert mat1.set_magnetic_coercitivity(1, 2, 3, 4) + assert mat1.get_magnetic_coercitivity() == ("1A_per_meter", "2", "3", "4") + assert mat1.set_electrical_steel_coreloss(1, 2, 3, 4, 0.002) + assert mat1.get_curve_coreloss_type() == "Electrical Steel" + assert mat1.get_curve_coreloss_values()["core_loss_equiv_cut_depth"] == "0.002meter" + assert mat1.set_hysteresis_coreloss(1, 2, 3, 4, 0.002) + assert mat1.get_curve_coreloss_type() == "Hysteresis Model" + assert mat1.set_bp_curve_coreloss([[0, 0], [10, 10], [20, 20]]) + assert mat1.get_curve_coreloss_type() == "B-P Curve" + assert mat1.set_power_ferrite_coreloss() + assert mat1.get_curve_coreloss_type() == "Power Ferrite" assert isinstance(mat1.material_appearance, list) + mat1.material_appearance = [11, 22, 0] assert mat1.material_appearance == [11, 22, 0] mat1.material_appearance = ["11", "22", "10"] diff --git a/pyaedt/modules/Material.py b/pyaedt/modules/Material.py index 8ec596a8862..7bab0d54a14 100644 --- a/pyaedt/modules/Material.py +++ b/pyaedt/modules/Material.py @@ -40,9 +40,6 @@ class MatProperties(object): "diffusivity", "molecular_mass", "viscosity", - "core_loss_kh", - "core_loss_kc", - "core_loss_ke", ] defaultvalue = [1.0, 1.0, 0, 0, 0, 0.01, 0, 0, 0, 0, 0, 0.8, 0, 0, 0, 0, 0, 0] defaultunit = [ @@ -1308,69 +1305,266 @@ def viscosity(self, value): self._viscosity.value = value self._update_props("viscosity", value) - @property - def core_loss_kh(self): - """Core loss in kilohertz. + @aedt_exception_handler + def set_magnetic_coercitivity(self, value=0, x=1, y=0, z=0): + """Set Magnetic Coercitivity for material. + + Parameters + ---------- + value : float, optional + Magnitude in A_per_meter. Default value is ``0``. + x : float, optional + Vector x component. Default value is ``1``. + y : float, optional + Vector y component. Default value is ``0``. + z : float, optional + Vector z component. Default value is ``0``. Returns ------- - :class:`pyaedt.modules.Material.MatProperty` - Core loss of the material in kilohertz. + bool + """ + self._props["magnetic_coercivity"] = OrderedDict( + { + "property_type": "VectorProperty", + "Magnitude": "{}A_per_meter".format(value), + "DirComp1": str(x), + "DirComp2": str(y), + "DirComp3": str(z), + } + ) + return self.update() - References + @aedt_exception_handler + def set_electrical_steel_coreloss(self, kh=0, kc=0, ke=0, kdc=0, cut_depth=0.0001): + """Set Electrical Steel Type Core Loss. + + Parameters ---------- + kh : float + kc : float + ke : float + kdc : float + cut_depth : float - >>> oDefinitionManager.EditMaterial + Returns + ------- + bool """ - return self._core_loss_kh + if "core_loss_type" not in self._props: + self._props["core_loss_type"] = OrderedDict( + {"property_type": "ChoiceProperty", "Choice": "Electrical Steel"} + ) + else: + self._props.pop("core_loss_cm", None) + self._props.pop("core_loss_x", None) + self._props.pop("core_loss_y", None) + self._props.pop("core_loss_hci", None) + self._props.pop("core_loss_br", None) + self._props.pop("core_loss_hkc", None) + self._props.pop("core_loss_curves", None) + self._props["core_loss_type"]["Choice"] = "Electrical Steel" + self._props["core_loss_kh"] = str(kh) + self._props["core_loss_kc"] = str(kc) + self._props["core_loss_ke"] = str(ke) + self._props["core_loss_kdc"] = str(kdc) + self._props["core_loss_equiv_cut_depth"] = "{}meter".format(cut_depth) + return self.update() - @core_loss_kh.setter - def core_loss_kh(self, value): - self._core_loss_kh.value = value - self._update_props("core_loss_kh", value) + @aedt_exception_handler + def set_hysteresis_coreloss(self, kdc=0, hci=0, br=0, hkc=0, cut_depth=0.0001): + """Set Hysteresis Type Core Loss. - @property - def core_loss_kc(self): - """Core loss in kilocalories. + Parameters + ---------- + kdc : float + hci : float + br : float + hkc : float + cut_depth : float Returns ------- - :class:`pyaedt.modules.Material.MatProperty` - Core loss of the material in kilocalories. + bool + """ + if "core_loss_type" not in self._props: + self._props["core_loss_type"] = OrderedDict( + {"property_type": "ChoiceProperty", "Choice": "Hysteresis Model"} + ) + else: + self._props.pop("core_loss_kh", None) + self._props.pop("core_loss_kc", None) + self._props.pop("core_loss_ke", None) + self._props.pop("core_loss_cm", None) + self._props.pop("core_loss_x", None) + self._props.pop("core_loss_y", None) + self._props.pop("core_loss_hci", None) + self._props.pop("core_loss_br", None) + self._props.pop("core_loss_hkc", None) + self._props.pop("core_loss_curves", None) + self._props["core_loss_type"]["Choice"] = "Hysteresis Model" + self._props["core_loss_hci"] = "{}A_per_meter".format(hci) + self._props["core_loss_br"] = "{}tesla".format(br) + self._props["core_loss_hkc"] = str(hkc) + self._props["core_loss_kdc"] = str(kdc) + self._props["core_loss_equiv_cut_depth"] = "{}meter".format(cut_depth) + return self.update() - References + @aedt_exception_handler + def set_power_ferrite_coreloss(self, cm=0, x=0, y=0, kdc=0, cut_depth=0.0001): + """Set Power Ferrite Type Core Loss. + + Parameters ---------- + cm : float + x : float + y : float + kdc : float + cut_depth : float - >>> oDefinitionManager.EditMaterial + Returns + ------- + bool """ - return self._core_loss_kc + if "core_loss_type" not in self._props: + self._props["core_loss_type"] = OrderedDict({"property_type": "ChoiceProperty", "Choice": "Power Ferrite"}) + else: + self._props.pop("core_loss_kh", None) + self._props.pop("core_loss_kc", None) + self._props.pop("core_loss_ke", None) + self._props.pop("core_loss_cm", None) + self._props.pop("core_loss_x", None) + self._props.pop("core_loss_y", None) + self._props.pop("core_loss_hci", None) + self._props.pop("core_loss_br", None) + self._props.pop("core_loss_hkc", None) + self._props.pop("core_loss_curves", None) + self._props["core_loss_type"]["Choice"] = "Power Ferrite" + self._props["core_loss_cm"] = "{}A_per_meter".format(cm) + self._props["core_loss_x"] = "{}tesla".format(x) + self._props["core_loss_y"] = str(y) + self._props["core_loss_kdc"] = str(kdc) + self._props["core_loss_equiv_cut_depth"] = "{}meter".format(cut_depth) + return self.update() - @core_loss_kc.setter - def core_loss_kc(self, value): - self._core_loss_kc.value = value - self._update_props("core_loss_kc", value) + @aedt_exception_handler + def set_bp_curve_coreloss( + self, point_list, kdc=0, cut_depth=0.0001, punit="kw/m^3", bunit="tesla", frequency=60, thickness="0.5mm" + ): + """Set B-P Type Core Loss. - @property - def core_loss_ke(self): - """Core loss in kinetic energy. + Parameters + ---------- + point_list : list of list + List of [x,y] points. + kdc : float + cut_depth : float + punit : str + Core loss unit. The default is ``"kw/m^3"``. + bunit : str + Magnetic field unit. The default is ``"tesla"``. + frequency : float + thickness : str, optional + Lamination thickness. The default is ``"0.5mm"``. Returns ------- - :class:`pyaedt.modules.Material.MatProperty` - Core loss of the material in kinetic energy. + bool + """ + if "core_loss_type" not in self._props: + self._props["core_loss_type"] = OrderedDict({"property_type": "ChoiceProperty", "Choice": "B-P Curve"}) + else: + self._props.pop("core_loss_kh", None) + self._props.pop("core_loss_kc", None) + self._props.pop("core_loss_ke", None) + self._props.pop("core_loss_cm", None) + self._props.pop("core_loss_x", None) + self._props.pop("core_loss_y", None) + self._props.pop("core_loss_hci", None) + self._props.pop("core_loss_br", None) + self._props.pop("core_loss_hkc", None) + self._props.pop("core_loss_curves", None) + self._props["core_loss_type"]["Choice"] = "B-P Curve" + self._props["core_loss_kdc"] = str(kdc) + self._props["core_loss_equiv_cut_depth"] = "{}meter".format(cut_depth) + self._props["core_loss_curves"] = OrderedDict({}) + self._props["core_loss_curves"]["property_type"] = "nonlinear" + self._props["core_loss_curves"]["PUnit"] = punit + self._props["core_loss_curves"]["BUnit"] = bunit + self._props["core_loss_curves"]["Frequency"] = "{}Hz".format(frequency) + self._props["core_loss_curves"]["Thickness"] = thickness + self._props["core_loss_curves"]["IsTemperatureDependent"] = False + self._props["core_loss_curves"]["Point"] = [] + for points in point_list: + self._props["core_loss_curves"]["Point"].append(points) + return self.update() - References - ---------- + @aedt_exception_handler + def get_curve_coreloss_type(self): + """Return the curve core loss type assigned to material. - >>> oDefinitionManager.EditMaterial + Returns + ------- + str """ - return self._core_loss_ke + if self._props.get("core_loss_type", None): + return self._props["core_loss_type"].get("Choice", None) + return None - @core_loss_ke.setter - def core_loss_ke(self, value): - self._core_loss_ke.value = value - self._update_props("core_loss_ke", value) + @aedt_exception_handler + def get_curve_coreloss_values(self): + """Return the curve core values type assigned to material. + Returns + ------- + dict + """ + out = {} + if self._props.get("core_loss_type", None): + if self._props["core_loss_type"].get("Choice", None) == "Electrical Steel": + + out["core_loss_kh"] = self._props["core_loss_kh"] + out["core_loss_kc"] = self._props["core_loss_kc"] + out["core_loss_ke"] = self._props["core_loss_ke"] + out["core_loss_kdc"] = self._props["core_loss_kdc"] + out["core_loss_equiv_cut_depth"] = self._props["core_loss_equiv_cut_depth"] + elif self._props["core_loss_type"].get("Choice", None) == "B-P Curve": + out["core_loss_curves"] = self._props["core_loss_curves"] + out["core_loss_kdc"] = self._props["core_loss_kdc"] + out["core_loss_equiv_cut_depth"] = self._props["core_loss_equiv_cut_depth"] + if self._props["core_loss_type"].get("Choice", None) == "Power Ferrite": + out["core_loss_cm"] = self._props["core_loss_cm"] + out["core_loss_x"] = self._props["core_loss_x"] + out["core_loss_y"] = self._props["core_loss_y"] + out["core_loss_kdc"] = self._props["core_loss_kdc"] + out["core_loss_equiv_cut_depth"] = self._props["core_loss_equiv_cut_depth"] + elif self._props["core_loss_type"].get("Choice", None) == "Hysteresis Model": + out["core_loss_hci"] = self._props["core_loss_hci"] + out["core_loss_br"] = self._props["core_loss_br"] + out["core_loss_hkc"] = self._props["core_loss_hkc"] + out["core_loss_kdc"] = self._props["core_loss_kdc"] + out["core_loss_equiv_cut_depth"] = self._props["core_loss_equiv_cut_depth"] + return out + + @aedt_exception_handler + def get_magnetic_coercitivity(self): + """Get the magnetic coercitivity values. + + Returns + ------- + tuple + Tuple of (Magnitude, x, y, z) + """ + if "magnetic_coercivity" in self._props: + return ( + self._props["magnetic_coercivity"]["Magnitude"], + self._props["magnetic_coercivity"]["DirComp1"], + self._props["magnetic_coercivity"]["DirComp2"], + self._props["magnetic_coercivity"]["DirComp3"], + ) + return False + + @aedt_exception_handler def is_conductor(self, threshold=100000): """Check if the material is a conductor. From 312d2d2a6c091dbae4272b6ce3ff489cdd21aa21 Mon Sep 17 00:00:00 2001 From: Hui Zhou Date: Tue, 18 Jan 2022 08:36:22 +0100 Subject: [PATCH 05/14] Padstack inst update (#720) * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update --- _unittest/test_00_EDB.py | 14 ++++- pyaedt/edb_core/EDB_Data.py | 112 ++++++++++++++++++++++++++++++++++-- pyaedt/edb_core/padstack.py | 21 ++++--- 3 files changed, 129 insertions(+), 18 deletions(-) diff --git a/_unittest/test_00_EDB.py b/_unittest/test_00_EDB.py index 4ba1649e6f9..9a763fa927c 100644 --- a/_unittest/test_00_EDB.py +++ b/_unittest/test_00_EDB.py @@ -120,6 +120,12 @@ def test_07_vias_creation(self): assert self.edbapp.core_padstack.place_padstack(["via_x", "via_x+via_y"], "myVia") assert self.edbapp.core_padstack.place_padstack(["via_x", "via_x+via_y*2"], "myVia_bullet") + padstack_id = self.edbapp.core_padstack.place_padstack(["via_x", "via_x+via_y*3"], "myVia", is_pin=True) + padstack_instance = self.edbapp.core_padstack.padstack_instances[padstack_id] + assert padstack_instance.is_pin + assert padstack_instance.position + assert isinstance(padstack_instance.rotation, float) + def test_08_nets_query(self): signalnets = self.edbapp.core_nets.signal_nets powernets = self.edbapp.core_nets.power_nets @@ -564,9 +570,11 @@ def test_70_plot_on_matplotlib(self): def test_71_fix_circle_voids(self): assert self.edbapp.core_primitives.fix_circle_void_for_clipping() - def test_72_vias_creation(self): - via_list = self.edbapp.core_padstack.get_padstack_instance_by_net_name("GND") - assert len(via_list) + def test_72_padstack_instance(self): + padstack_instances = self.edbapp.core_padstack.get_padstack_instance_by_net_name("GND") + assert len(padstack_instances) + padstack_1 = list(padstack_instances.values())[0] + assert padstack_1.id def test_73_duplicate_padstack(self): self.edbapp.core_padstack.duplicate_padstack( diff --git a/pyaedt/edb_core/EDB_Data.py b/pyaedt/edb_core/EDB_Data.py index 17458b0987c..badc04e9aff 100644 --- a/pyaedt/edb_core/EDB_Data.py +++ b/pyaedt/edb_core/EDB_Data.py @@ -1244,6 +1244,28 @@ def geometry_type(self): padparams = self._padstack_methods.GetPadParametersValue(self._edb_padstack, self.layer_name, self.pad_type) return int(padparams.Item1) + @geometry_type.setter + def geometry_type(self, geom_type): + """0, NoGeometry. 1, Circle. 2 Square. 3, Rectangle. 4, Oval. 5, Bullet. 6, N-sided polygon. 7, Polygonal + shape.8, Round gap with 45 degree thermal ties. 9, Round gap with 90 degree thermal ties.10, Square gap + with 45 degree thermal ties. 11, Square gap with 90 degree thermal ties. + """ + val = self._edb_value(0) + params = [] + if geom_type == 0: + pass + elif geom_type == 1: + params = [val] + elif geom_type == 2: + params = [val] + elif geom_type == 3: + params = [val, val] + elif geom_type == 4: + params = [val, val, val] + elif geom_type == 5: + params = [val, val, val] + self._update_pad_parameters_parameters(geom_type=geom_type, params=params) + @property def parameters(self): """Parameters. @@ -1366,11 +1388,11 @@ def _update_pad_parameters_parameters( ---------- layer_name : str, optional Name of the layer. The default is ``None``. - pad_type : + pad_type : int, optional Type of the pad. The default is ``None``. - geom_type : + geom_type : int, optional Type of the geometry. The default is ``None``. - params : + params : list, optional The default is ``None``. offsetx : float, optional Offset value for the X axis. The default is ``None``. @@ -1740,6 +1762,10 @@ class EDBPadstackInstance(object): >>> edb_padstack_instance = edb.core_padstack.padstack_instances[0] """ + def __init__(self, edb_padstackinstance, _pedb): + self._edb_padstackinstance = edb_padstackinstance + self._pedb = _pedb + @property def padstack_definition(self): """Padstack definition. @@ -1824,9 +1850,83 @@ def net_name(self): """ return self._edb_padstackinstance.GetNet().GetName() - def __init__(self, edb_padstackinstance, _pedb): - self._edb_padstackinstance = edb_padstackinstance - self._pedb = _pedb + @property + def is_pin(self): + """Determines whether this padstack instance is a layout pin. + + Returns + ------- + bool + True if this padstack type is a layout pin, False otherwise. + """ + return self._edb_padstackinstance.IsLayoutPin() + + @is_pin.setter + def is_pin(self, pin): + """Set padstack type + + Parameters + ---------- + pin : bool + True if set this padstack instance as pin, False otherwise + """ + self._edb_padstackinstance.SetIsLayoutPin(pin) + + @property + def position(self): + """padstack instance position. + + Returns + ------- + list + List of ``[x, y]``` coordinates for the padstack instance position. + """ + point_data = self._pedb.edb.Geometry.PointData(self._pedb.edb_value(0.0), self._pedb.edb_value(0.0)) + if is_ironpython: + out = self._edb_padstackinstance.GetPositionAndRotationValue() + else: + out = self._edb_padstackinstance.GetPositionAndRotationValue( + point_data, + self._pedb.edb_value(0.0), + ) + if out[0]: + return [out[1].X.ToDouble(), out[1].Y.ToDouble()] + + @property + def rotation(self): + """padstack instance rotation. + + Returns + ------- + float + Rotatation value for the padstack instance. + """ + point_data = self._pedb.edb.Geometry.PointData(self._pedb.edb_value(0.0), self._pedb.edb_value(0.0)) + if is_ironpython: + out = self._edb_padstackinstance.GetPositionAndRotationValue() + else: + out = self._edb_padstackinstance.GetPositionAndRotationValue( + point_data, + self._pedb.edb_value(0.0), + ) + if out[0]: + return out[2].ToDouble() + + @property + def id(self): + """Id of this padstack instance. + + Returns + ------- + str + Padstack instance id. + """ + return self._edb_padstackinstance.GetId() + + @aedt_exception_handler + def delete_padstack_instance(self): + """Delete this padstack instance.""" + self._edb_padstackinstance.Delete() class EDBPinInstances(object): diff --git a/pyaedt/edb_core/padstack.py b/pyaedt/edb_core/padstack.py index a56ed2f30c0..21dc89b2c53 100644 --- a/pyaedt/edb_core/padstack.py +++ b/pyaedt/edb_core/padstack.py @@ -28,7 +28,7 @@ class EdbPadstacks(object): def __init__(self, p_edb): self._pedb = p_edb self._padstacks = {} - self._padstack_instances = [] + self._padstack_instances = {} @property def _builder(self): @@ -507,6 +507,7 @@ def place_padstack( fromlayer=None, tolayer=None, solderlayer=None, + is_pin=False, ): """Place the padstack. @@ -554,10 +555,12 @@ def place_padstack( if solderlayer: solderlayer = self._pedb.core_stackup.signal_layers[solderlayer]._layer if padstack: - via = self._edb.Cell.Primitive.PadstackInstance.Create( + padstack_instance = self._edb.Cell.Primitive.PadstackInstance.Create( self._active_layout, net, via_name, padstack, position, rotation, fromlayer, tolayer, solderlayer, None ) - return via + padstack_instance.SetIsLayoutPin(is_pin) + self.update_padstack_instances() + return padstack_instance.GetId() else: return False @@ -599,11 +602,11 @@ def remove_pads_from_padstack(self, padstack_name, layer_name=None): def update_padstack_instances(self): """Update Padstack Instance List.""" layout_lobj_collection = self._active_layout.GetLayoutInstance().GetAllLayoutObjInstances() - self._padstack_instances = [] + self._padstack_instances = {} for obj in layout_lobj_collection.Items: lobj = obj.GetLayoutObj() if type(lobj) is self._edb.Cell.Primitive.PadstackInstance: - self._padstack_instances.append(EDBPadstackInstance(lobj, self._pedb)) + self._padstack_instances[lobj.GetId()] = EDBPadstackInstance(lobj, self._pedb) @aedt_exception_handler def get_padstack_instance_by_net_name(self, net_name): @@ -617,8 +620,8 @@ def get_padstack_instance_by_net_name(self, net_name): ------- list of Edb.Cell.Primitive.PadstackInstance """ - via_list = [] - for inst in self.padstack_instances: + padstack_instances = {} + for inst_id, inst in self.padstack_instances.items(): if inst.net_name == net_name: - via_list.append(inst) - return via_list + padstack_instances[inst_id] = inst + return padstack_instances From 5b8a9deae63295193cce9ad12a19123e556f6652 Mon Sep 17 00:00:00 2001 From: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Date: Tue, 18 Jan 2022 12:25:25 +0100 Subject: [PATCH 06/14] The name of the udp can now be different from the dll's one. (#727) * The name of the udp can now be different from the dll's one. * Update _unittest/test_28_Maxwell3D.py * Improve code style. * Add empty line. * Remove empty line. --- _unittest/test_28_Maxwell3D.py | 14 ++++++++++++++ pyaedt/modeler/Primitives.py | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/_unittest/test_28_Maxwell3D.py b/_unittest/test_28_Maxwell3D.py index 758293288ab..391699fe425 100644 --- a/_unittest/test_28_Maxwell3D.py +++ b/_unittest/test_28_Maxwell3D.py @@ -154,6 +154,7 @@ def test_26_create_udp(self): mypair = ["InfoCore", "0"] my_udpPairs.append(mypair) + # Test udp with a custom name. my_udpName = "MyClawPoleCore" udp = self.aedtapp.modeler.primitives.create_udp( udp_dll_name="RMxprt/ClawPoleCore", @@ -163,6 +164,19 @@ def test_26_create_udp(self): udptye="Solid", ) assert udp + assert udp.name == "MyClawPoleCore" + assert "MyClawPoleCore" in udp._primitives.object_names + + # Test udp with default name -None-. + second_udp = self.aedtapp.modeler.primitives.create_udp( + udp_dll_name="RMxprt/ClawPoleCore", + udp_parameters_list=my_udpPairs, + upd_library="syslib", + udptye="Solid", + ) + assert second_udp + assert second_udp.name == "ClawPoleCore" + assert "ClawPoleCore" in udp._primitives.object_names @pytest.mark.skipif(os.name == "posix", reason="Feature not supported in Linux") def test_27_create_udm(self): diff --git a/pyaedt/modeler/Primitives.py b/pyaedt/modeler/Primitives.py index 9cd8aa41669..5d5ebadfc27 100644 --- a/pyaedt/modeler/Primitives.py +++ b/pyaedt/modeler/Primitives.py @@ -1496,7 +1496,10 @@ def create_udp(self, udp_dll_name, udp_parameters_list, upd_library="syslib", na vArgParamVector.append(["NAME:Pair", "Name:=", pair.Name, "Value:=", pair.Value]) vArg1.append(vArgParamVector) - obj_name, ext = os.path.splitext(os.path.basename(udp_dll_name)) + if name: + obj_name = name + else: + obj_name, ext = os.path.splitext(os.path.basename(udp_dll_name)) vArg2 = self._default_object_attributes(name=obj_name) obj_name = self._oeditor.CreateUserDefinedPart(vArg1, vArg2) return self._create_object(obj_name) From e0913d767433aeb01dcfb34fd2f44a3e3bf87087 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Tue, 18 Jan 2022 14:29:29 +0100 Subject: [PATCH 07/14] Added support for TwinBuilder Plots (#725) * Added support for TwinBuilder Plots * Black Fix --- pyaedt/application/design_solutions.py | 18 +++++++++++++++--- pyaedt/modules/PostProcessor.py | 22 +++++++++++++--------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/pyaedt/application/design_solutions.py b/pyaedt/application/design_solutions.py index 1a21391a7b7..7b92b9eeddc 100644 --- a/pyaedt/application/design_solutions.py +++ b/pyaedt/application/design_solutions.py @@ -114,9 +114,21 @@ }, }, "Twin Builder": { - "TR": {"name": None, "options": None, "report_type": None, "default_setup": 35, "default_adaptive": None}, - "AC": {"name": None, "options": None, "report_type": None, "default_setup": None, "default_adaptive": None}, - "DC": {"name": None, "options": None, "report_type": None, "default_setup": None, "default_adaptive": None}, + "TR": {"name": None, "options": None, "report_type": "Standard", "default_setup": 35, "default_adaptive": None}, + "AC": { + "name": None, + "options": None, + "report_type": "Standard", + "default_setup": None, + "default_adaptive": None, + }, + "DC": { + "name": None, + "options": None, + "report_type": "Standard", + "default_setup": None, + "default_adaptive": None, + }, }, "Circuit Design": { "NexximLNA": { diff --git a/pyaedt/modules/PostProcessor.py b/pyaedt/modules/PostProcessor.py index d245cdd3e28..854dee1b275 100644 --- a/pyaedt/modules/PostProcessor.py +++ b/pyaedt/modules/PostProcessor.py @@ -1042,10 +1042,7 @@ def post_solution_type(self): type Design solution type. """ - try: - return self._odesign.GetSolutionType() - except: - return self._app._design_type + return self._app.solution_type @property def all_report_names(self): @@ -1180,7 +1177,7 @@ def get_report_data( >>> m3d.post.get_report_data("Wind(LoadA,LaodA)") # TransientAnalsysis """ - if self.post_solution_type in ["3DLayout", "NexximLNA", "NexximTransient"]: + if self.post_solution_type in ["HFSS3DLayout", "NexximLNA", "NexximTransient", "TR", "AC", "DC"]: if domain == "Sweep": did = 3 else: @@ -1194,7 +1191,9 @@ def get_report_data( ctxt = domain else: ctxt = ["Domain:=", domain] - + if self.post_solution_type in ["TR", "AC", "DC"]: + ctxt[2] = ctxt[2][:-3] + setup_sweep_name = self.post_solution_type if not isinstance(expression, list): expression = [expression] if not setup_sweep_name: @@ -1204,7 +1203,10 @@ def get_report_data( report_input_type = self._app.design_solutions.report_type if families_dict is None: - families_dict = {"Freq": ["All"]} + if domain == "Time": + families_dict = {"Time": ["All"]} + else: + families_dict = {"Freq": ["All"]} solution_data = self.get_solution_data_per_variation( report_input_type, setup_sweep_name, ctxt, families_dict, expression @@ -1260,7 +1262,7 @@ def create_rectangular_plot( ctxt = [] if not setup_sweep_name: setup_sweep_name = self._app.nominal_sweep - if self.post_solution_type in ["HFSS 3D Layout Design", "NexximLNA", "NexximTransient"]: + if self.post_solution_type in ["HFSS3DLayout", "NexximLNA", "NexximTransient", "TR", "AC", "DC"]: if "Freq" == primary_sweep_variable or "Freq" in list(families_dict.keys()): did = 3 else: @@ -1275,7 +1277,9 @@ def create_rectangular_plot( ctxt = context else: ctxt = ["Context:=", context] - + if self.post_solution_type in ["TR", "AC", "DC"]: + ctxt[2] = ctxt[2][:-3] + setup_sweep_name = self.post_solution_type if not isinstance(expression, list): expression = [expression] if not setup_sweep_name: From fdd9809bcb20bc36089aad3d1e8c201d2a9ca53b Mon Sep 17 00:00:00 2001 From: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Date: Tue, 18 Jan 2022 17:21:15 +0100 Subject: [PATCH 08/14] Handle vtk modules for the unit tests. (#732) --- .github/workflows/unit_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index f63f993c54a..8ed2ba38e7c 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -49,6 +49,7 @@ jobs: pip install . --use-feature=in-tree-build pip install -r requirements_test.txt pip install pytest-azurepipelines + Copy-Item -Path "C:\actions-runner\opengl32.dll" -Destination "testenv\Lib\site-packages\vtkmodules" -Force mkdir tmp cd tmp python -c "import pyaedt; print('Imported pyaedt')" From 7d86037b6985cb78f1664efdafe6dddc5ba20281 Mon Sep 17 00:00:00 2001 From: Kathy Pippert <84872299+PipKat@users.noreply.github.com> Date: Tue, 18 Jan 2022 15:21:40 -0500 Subject: [PATCH 09/14] Fix typo in Contributing.rst (#734) --- doc/source/Resources/Contributing.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/Resources/Contributing.rst b/doc/source/Resources/Contributing.rst index 7838ec5a9f1..36c8a322f5a 100644 --- a/doc/source/Resources/Contributing.rst +++ b/doc/source/Resources/Contributing.rst @@ -3,7 +3,7 @@ ============ Contributing ============ -Overall guidance on contributing to a PyAnsys library appears in the +Overall guidance on contributing to a PyAnsys repository appears in the `Contributing `_ topic in the *PyAnsys Developer's Guide*. Ensure that you are thoroughly familiar with it and all `Guidelines and Best Practices `_ @@ -23,7 +23,7 @@ Run this code to clone and install the latest version of PyAEDT in development m Posting Issues -------------- -Use the `PyAEDTL Issues `_ +Use the `PyAEDT Issues `_ page to submit questions, report bugs, and request new features. To reach the project support team, email `pyansys.support@ansys.com `_. From c299eb6ccb6faecbc3c598143e07f65cf4392b8a Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Wed, 19 Jan 2022 09:46:35 +0100 Subject: [PATCH 10/14] Every Export is saved in working directory (#733) * Fix Folder of exported files * Fix Folder of exported files --- _unittest/test_12_PostProcessing.py | 2 +- examples/01-Modeling-Setup/Optimetrics.py | 2 +- examples/02-HFSS/HFSS_Dipole.py | 2 +- examples/02-HFSS/HFSS_Spiral.py | 2 +- examples/02-HFSS/SBR_Doppler_Example.py | 2 +- examples/02-HFSS/SBR_Example.py | 2 +- examples/02-Maxwell/Maxwell2D_Eddy.py | 2 +- examples/02-Maxwell/Maxwell2D_Transient.py | 2 +- examples/02-Maxwell/Maxwell3DTeam3.py | 2 +- examples/02-Maxwell/Maxwell3D_TEAM7.py | 2 +- examples/02-Maxwell/Maxwell_Magnet.py | 2 +- examples/05-Q3D/Q3D_Example.py | 2 +- .../06-Multiphysics/Hfss_Icepak_Coupling.py | 2 +- examples/06-Multiphysics/Hfss_Mechanical.py | 2 +- pyaedt/application/Analysis.py | 10 ++++---- pyaedt/application/Analysis2D.py | 4 +-- pyaedt/application/Analysis3D.py | 4 +-- pyaedt/application/Analysis3DLayout.py | 4 +-- pyaedt/application/Design.py | 11 ++++---- pyaedt/application/Variables.py | 4 +-- pyaedt/circuit.py | 10 +++++--- pyaedt/hfss.py | 8 +++--- pyaedt/hfss3dlayout.py | 9 ++++--- pyaedt/icepak.py | 25 +++++++++++-------- pyaedt/modeler/Object3d.py | 2 +- pyaedt/modules/AdvancedPostProcessing.py | 22 ++++++++-------- pyaedt/modules/PostProcessor.py | 20 ++++++++------- 27 files changed, 86 insertions(+), 75 deletions(-) diff --git a/_unittest/test_12_PostProcessing.py b/_unittest/test_12_PostProcessing.py index 0743f2252f6..92a4a7b29f4 100644 --- a/_unittest/test_12_PostProcessing.py +++ b/_unittest/test_12_PostProcessing.py @@ -183,7 +183,7 @@ def test_13_export_model_picture(self): path = self.aedtapp.post.export_model_picture(picturename="test_picture") assert path - @pytest.mark.skipif(config["build_machine"] or is_ironpython, reason="Not running in ironpython") + @pytest.mark.skipif(is_ironpython, reason="Not running in ironpython") def test_14_Field_Ploton_cutplanedesignname(self): cutlist = ["Global:XY"] setup_name = self.aedtapp.existing_analysis_sweeps[0] diff --git a/examples/01-Modeling-Setup/Optimetrics.py b/examples/01-Modeling-Setup/Optimetrics.py index 28385468b01..e07d894d387 100644 --- a/examples/01-Modeling-Setup/Optimetrics.py +++ b/examples/01-Modeling-Setup/Optimetrics.py @@ -41,7 +41,7 @@ create_sheets_on_openings=True, ) -hfss.plot(show=False, export_path=os.path.join(hfss.project_path, "Image.jpg")) +hfss.plot(show=False, export_path=os.path.join(hfss.working_directory, "Image.jpg")) ############################################################################### diff --git a/examples/02-HFSS/HFSS_Dipole.py b/examples/02-HFSS/HFSS_Dipole.py index c06f19e2a03..b217e84ddc1 100644 --- a/examples/02-HFSS/HFSS_Dipole.py +++ b/examples/02-HFSS/HFSS_Dipole.py @@ -61,7 +61,7 @@ # Plot the model # ~~~~~~~~~~~~~~ -hfss.plot(show=False, export_path=os.path.join(hfss.project_path, "Image.jpg"), plot_air_objects=False) +hfss.plot(show=False, export_path=os.path.join(hfss.working_directory, "Image.jpg"), plot_air_objects=False) ############################################################################### # Create the Setup diff --git a/examples/02-HFSS/HFSS_Spiral.py b/examples/02-HFSS/HFSS_Spiral.py index 639ec3fda8c..a9f96d2216a 100644 --- a/examples/02-HFSS/HFSS_Spiral.py +++ b/examples/02-HFSS/HFSS_Spiral.py @@ -110,7 +110,7 @@ def create_line(pts): # Plot the model # ~~~~~~~~~~~~~~ -hfss.plot(show=False, export_path=os.path.join(hfss.project_path, "Image.jpg")) +hfss.plot(show=False, export_path=os.path.join(hfss.working_directory, "Image.jpg")) ################################################################ diff --git a/examples/02-HFSS/SBR_Doppler_Example.py b/examples/02-HFSS/SBR_Doppler_Example.py index e9cfe21ac28..aabbe074180 100644 --- a/examples/02-HFSS/SBR_Doppler_Example.py +++ b/examples/02-HFSS/SBR_Doppler_Example.py @@ -131,7 +131,7 @@ # Plot the model # ~~~~~~~~~~~~~~ -app.plot(show=False, export_path=os.path.join(app.project_path, "Image.jpg"), plot_air_objects=True) +app.plot(show=False, export_path=os.path.join(app.working_directory, "Image.jpg"), plot_air_objects=True) ############################################################################### # Solve and release desktop diff --git a/examples/02-HFSS/SBR_Example.py b/examples/02-HFSS/SBR_Example.py index e70421e2f70..ee2bfbce4bc 100644 --- a/examples/02-HFSS/SBR_Example.py +++ b/examples/02-HFSS/SBR_Example.py @@ -59,7 +59,7 @@ # Plot the model # ~~~~~~~~~~~~~~ -target.plot(show=False, export_path=os.path.join(target.project_path, "Image.jpg"), plot_air_objects=False) +target.plot(show=False, export_path=os.path.join(target.working_directory, "Image.jpg"), plot_air_objects=False) ############################################################################### diff --git a/examples/02-Maxwell/Maxwell2D_Eddy.py b/examples/02-Maxwell/Maxwell2D_Eddy.py index 6cea125c489..7250cb0affd 100644 --- a/examples/02-Maxwell/Maxwell2D_Eddy.py +++ b/examples/02-Maxwell/Maxwell2D_Eddy.py @@ -100,7 +100,7 @@ # Plot the model # ~~~~~~~~~~~~~~ -M3D.plot(show=False, export_path=os.path.join(M3D.project_path, "Image.jpg"), plot_air_objects=False) +M3D.plot(show=False, export_path=os.path.join(M3D.working_directory, "Image.jpg"), plot_air_objects=False) ############################################################################### # Add an Eddy Current Setup diff --git a/examples/02-Maxwell/Maxwell2D_Transient.py b/examples/02-Maxwell/Maxwell2D_Transient.py index ae93ec722ba..b1b7f3f14d3 100644 --- a/examples/02-Maxwell/Maxwell2D_Transient.py +++ b/examples/02-Maxwell/Maxwell2D_Transient.py @@ -68,7 +68,7 @@ # Plot the model # ~~~~~~~~~~~~~~ -maxwell_2d.plot(show=False, export_path=os.path.join(maxwell_2d.project_path, "Image.jpg"), plot_air_objects=True) +maxwell_2d.plot(show=False, export_path=os.path.join(maxwell_2d.working_directory, "Image.jpg"), plot_air_objects=True) ############################################################################### diff --git a/examples/02-Maxwell/Maxwell3DTeam3.py b/examples/02-Maxwell/Maxwell3DTeam3.py index 3c29267254e..36112b72377 100644 --- a/examples/02-Maxwell/Maxwell3DTeam3.py +++ b/examples/02-Maxwell/Maxwell3DTeam3.py @@ -88,7 +88,7 @@ # Plot the model # ~~~~~~~~~~~~~~ -M3D.plot(show=False, export_path=os.path.join(M3D.project_path, "Image.jpg"), plot_air_objects=False) +M3D.plot(show=False, export_path=os.path.join(M3D.working_directory, "Image.jpg"), plot_air_objects=False) ############################################################################### diff --git a/examples/02-Maxwell/Maxwell3D_TEAM7.py b/examples/02-Maxwell/Maxwell3D_TEAM7.py index b2e426d3a84..703913171a2 100644 --- a/examples/02-Maxwell/Maxwell3D_TEAM7.py +++ b/examples/02-Maxwell/Maxwell3D_TEAM7.py @@ -101,7 +101,7 @@ # Plot the model # ~~~~~~~~~~~~~~ -M3D.plot(show=False, export_path=os.path.join(M3D.project_path, "Image.jpg"), plot_air_objects=False) +M3D.plot(show=False, export_path=os.path.join(M3D.working_directory, "Image.jpg"), plot_air_objects=False) ############################################################################### diff --git a/examples/02-Maxwell/Maxwell_Magnet.py b/examples/02-Maxwell/Maxwell_Magnet.py index dad452ce6c2..9f5c3c8b247 100644 --- a/examples/02-Maxwell/Maxwell_Magnet.py +++ b/examples/02-Maxwell/Maxwell_Magnet.py @@ -45,7 +45,7 @@ # Plot the model # ~~~~~~~~~~~~~~ -m3d.plot(show=False, export_path=os.path.join(m3d.project_path, "Image.jpg"), plot_air_objects=True) +m3d.plot(show=False, export_path=os.path.join(m3d.working_directory, "Image.jpg"), plot_air_objects=True) ############################################################################### # Solve Setup diff --git a/examples/05-Q3D/Q3D_Example.py b/examples/05-Q3D/Q3D_Example.py index d4ef313e463..658acc544bc 100644 --- a/examples/05-Q3D/Q3D_Example.py +++ b/examples/05-Q3D/Q3D_Example.py @@ -67,7 +67,7 @@ q.modeler["substrate"].color = (128, 128, 128) q.modeler["substrate"].transparency = 0.8 -q.plot(show=False, export_path=os.path.join(q.project_path, "Q3D.jpg"), plot_air_objects=False) +q.plot(show=False, export_path=os.path.join(q.working_directory, "Q3D.jpg"), plot_air_objects=False) ############################################################################### # Set Up Boundaries diff --git a/examples/06-Multiphysics/Hfss_Icepak_Coupling.py b/examples/06-Multiphysics/Hfss_Icepak_Coupling.py index 919e5b84ddb..d28b751f9e7 100644 --- a/examples/06-Multiphysics/Hfss_Icepak_Coupling.py +++ b/examples/06-Multiphysics/Hfss_Icepak_Coupling.py @@ -247,7 +247,7 @@ surflist = aedtapp.modeler.get_object_faces("outer") plot1 = aedtapp.post.create_fieldplot_surface(surflist, quantity_name2, setup_name, intrinsic) -results_folder = os.path.join(aedtapp.project_path, "Coaxial_Results_NG") +results_folder = os.path.join(aedtapp.working_directory, "Coaxial_Results_NG") if not os.path.exists(results_folder): os.mkdir(results_folder) diff --git a/examples/06-Multiphysics/Hfss_Mechanical.py b/examples/06-Multiphysics/Hfss_Mechanical.py index 71ec6ba23f6..cf544748d64 100644 --- a/examples/06-Multiphysics/Hfss_Mechanical.py +++ b/examples/06-Multiphysics/Hfss_Mechanical.py @@ -144,7 +144,7 @@ # Plot the model # ~~~~~~~~~~~~~~ -mech.plot(show=False, export_path=os.path.join(mech.project_path, "Mech.jpg"), plot_air_objects=False) +mech.plot(show=False, export_path=os.path.join(mech.working_directory, "Mech.jpg"), plot_air_objects=False) ############################################################################### # Solution diff --git a/pyaedt/application/Analysis.py b/pyaedt/application/Analysis.py index 059e4cb5666..74112a21b29 100644 --- a/pyaedt/application/Analysis.py +++ b/pyaedt/application/Analysis.py @@ -542,7 +542,7 @@ def export_results(self, analyze=False, export_folder=None): analyze : bool Either to Analyze before export or not. Solutions have to be present for the design. export_folder : str, optional - Full path to project folder. + Full path to project folder. If `None` working_directory will be used. Returns ------- @@ -560,7 +560,7 @@ def export_results(self, analyze=False, export_folder=None): """ exported_files = [] if not export_folder: - export_folder = self.project_path + export_folder = self.working_directory if analyze: self.analyze_all() setups = self.oanalysis.GetSetups() @@ -674,7 +674,7 @@ def export_convergence(self, setup_name, variation_string="", file_path=None): variation_string : str Variation string with values. Eg ``'radius=3mm'`` file_path : str, optional - full path to .prof file. + full path to .prof file. If `None` working_directory will be used. Returns @@ -688,7 +688,7 @@ def export_convergence(self, setup_name, variation_string="", file_path=None): >>> oModule.ExportConvergence """ if not file_path: - file_path = os.path.join(self.project_path, generate_unique_name("Convergence") + ".prop") + file_path = os.path.join(self.working_directory, generate_unique_name("Convergence") + ".prop") self.odesign.ExportConvergence(setup_name, variation_string, file_path) self.logger.info("Export Convergence to %s", file_path) return file_path @@ -1342,7 +1342,7 @@ def analyze_setup(self, name, num_cores=None, num_tasks=None, num_gpu=None, acf_ elif num_gpu or num_tasks or num_cores: config_name = "pyaedt_config" source_name = os.path.join(self.pyaedt_dir, "misc", "pyaedt_local_config.acf") - target_name = os.path.join(self.project_path, config_name + ".acf") + target_name = os.path.join(self.working_directory, config_name + ".acf") shutil.copy2(source_name, target_name) if num_cores: update_hpc_option(target_name, "NumCores", num_cores, False) diff --git a/pyaedt/application/Analysis2D.py b/pyaedt/application/Analysis2D.py index b0e810638ee..f9a399bf40d 100644 --- a/pyaedt/application/Analysis2D.py +++ b/pyaedt/application/Analysis2D.py @@ -198,7 +198,7 @@ def export_mesh_stats(self, setup_name, variation_string="", mesh_path=None): variation_string : str, optional Variation List. mesh_path : str, optional - Full path to mesh statistics file. + Full path to mesh statistics file. If `None` working_directory will be used. Returns ------- @@ -211,7 +211,7 @@ def export_mesh_stats(self, setup_name, variation_string="", mesh_path=None): >>> oDesign.ExportMeshStats """ if not mesh_path: - mesh_path = os.path.join(self.project_path, "meshstats.ms") + mesh_path = os.path.join(self.working_directory, "meshstats.ms") self.odesign.ExportMeshStats(setup_name, variation_string, mesh_path) return mesh_path diff --git a/pyaedt/application/Analysis3D.py b/pyaedt/application/Analysis3D.py index 7016a605b79..c31b998da9d 100644 --- a/pyaedt/application/Analysis3D.py +++ b/pyaedt/application/Analysis3D.py @@ -229,7 +229,7 @@ def export_mesh_stats(self, setup_name, variation_string="", mesh_path=None): variation_string : str, optional Variation List. mesh_path : str, optional - Full path to mesh statistics file. + Full path to mesh statistics file. If `None` working_directory will be used. Returns ------- @@ -242,7 +242,7 @@ def export_mesh_stats(self, setup_name, variation_string="", mesh_path=None): >>> oDesign.ExportMeshStats """ if not mesh_path: - mesh_path = os.path.join(self.project_path, "meshstats.ms") + mesh_path = os.path.join(self.working_directory, "meshstats.ms") self.odesign.ExportMeshStats(setup_name, variation_string, mesh_path) return mesh_path diff --git a/pyaedt/application/Analysis3DLayout.py b/pyaedt/application/Analysis3DLayout.py index aa762972160..4f5d90ce253 100644 --- a/pyaedt/application/Analysis3DLayout.py +++ b/pyaedt/application/Analysis3DLayout.py @@ -192,7 +192,7 @@ def export_mesh_stats(self, setup_name, variation_string="", mesh_path=None): variation_string : str, optional Variation List. mesh_path : str, optional - Full path to mesh statistics file. + Full path to mesh statistics file. If `None` working_directory will be used. Returns ------- @@ -205,7 +205,7 @@ def export_mesh_stats(self, setup_name, variation_string="", mesh_path=None): >>> oModule.ExportMeshStats """ if not mesh_path: - mesh_path = os.path.join(self.project_path, "meshstats.ms") + mesh_path = os.path.join(self.working_directory, "meshstats.ms") self.odesign.ExportMeshStats(setup_name, variation_string, mesh_path) return mesh_path diff --git a/pyaedt/application/Design.py b/pyaedt/application/Design.py index 84c4410ba81..6098141ff2a 100644 --- a/pyaedt/application/Design.py +++ b/pyaedt/application/Design.py @@ -857,14 +857,15 @@ def temp_directory(self): def toolkit_directory(self): """Path to the toolkit directory. + Returns ------- str - Full absolute path for the ``toolkit`` directory for this project. + Full absolute path for the ``pyaedt`` directory for this project. If this directory does not exist, it is created. - """ - toolkit_directory = os.path.join(self.project_path, self.project_name + ".toolkit") + + toolkit_directory = os.path.join(self.project_path, self.project_name + ".pyaedt") if not os.path.isdir(toolkit_directory): os.mkdir(toolkit_directory) return toolkit_directory @@ -1034,7 +1035,7 @@ def export_profile(self, setup_name, variation_string="", file_path=None): variation_string : str Variation string with values. Eg ``'radius=3mm'`` file_path : str, optional - full path to .prof file. + full path to .prof file. If `None`, working_directory will be used. Returns @@ -1048,7 +1049,7 @@ def export_profile(self, setup_name, variation_string="", file_path=None): >>> oDesign.ExportProfile """ if not file_path: - file_path = os.path.join(self.project_path, generate_unique_name("Profile") + ".prop") + file_path = os.path.join(self.working_directory, generate_unique_name("Profile") + ".prop") self.odesign.ExportProfile(setup_name, variation_string, file_path) self.logger.info("Exported Profile to file {}".format(file_path)) return file_path diff --git a/pyaedt/application/Variables.py b/pyaedt/application/Variables.py index 222b400d562..838ced53d64 100644 --- a/pyaedt/application/Variables.py +++ b/pyaedt/application/Variables.py @@ -1605,7 +1605,7 @@ def export(self, dataset_path=None): ---------- dataset_path : str, optional Path to export the dataset to. The default is ``None``, in which - case the dataset is exported to the project path. + case the dataset is exported to the working_directory path. Returns ------- @@ -1619,7 +1619,7 @@ def export(self, dataset_path=None): >>> oDesign.ExportDataset """ if not dataset_path: - dataset_path = os.path.join(self._app.project_path, self.name + ".tab") + dataset_path = os.path.join(self._app.working_directory, self.name + ".tab") if self.name[0] == "$": self._app._oproject.ExportDataset(self.name, dataset_path) else: diff --git a/pyaedt/circuit.py b/pyaedt/circuit.py index 23297161e0e..c71c6556fd9 100644 --- a/pyaedt/circuit.py +++ b/pyaedt/circuit.py @@ -750,7 +750,8 @@ def export_touchstone(self, solutionname, sweepname, filename=None, variation=[] sweepname : str Name of the sweep that has been solved. filename : str, optional - Full path and name for the Touchstone file. The default is ``None``. + Full path and name for the Touchstone file. + The default is ``None`` which export file in working_directory. variation : list, optional List of all parameter variations. For example, ``["$AmbientTemp", "$PowerIn"]``. The default is ``[]``. @@ -774,7 +775,7 @@ def export_touchstone(self, solutionname, sweepname, filename=None, variation=[] for v, vv in zip(variation, variations_value): appendix += "_" + v + vv.replace("'", "") ext = ".S" + str(self.oboundary.GetNumExcitations()) + "p" - filename = os.path.join(self.project_path, solutionname + "_" + sweepname + appendix + ext) + filename = os.path.join(self.working_directory, solutionname + "_" + sweepname + appendix + ext) else: filename = filename.replace("//", "/").replace("\\", "/") self.logger.info("Exporting Touchstone " + filename) @@ -850,7 +851,8 @@ def export_fullwave_spice( is_solution_file : bool, optional Whether it is an imported solution file. The default is ``False``. filename : str, optional - Full path and name for exporting the HSpice file. The default is ``None``. + Full path and name for exporting the HSpice file. + The default is ``None`` which export file in working_directory. passivity : bool, optional Whether to compute the passivity. The default is ``False``. causality : bool, optional @@ -878,7 +880,7 @@ def export_fullwave_spice( if not designname: designname = self.design_name if not filename: - filename = os.path.join(self.project_path, self.design_name + ".sp") + filename = os.path.join(self.working_directory, self.design_name + ".sp") if is_solution_file: setupname = designname designname = "" diff --git a/pyaedt/hfss.py b/pyaedt/hfss.py index a43c5a6e178..d44f8cec3c9 100644 --- a/pyaedt/hfss.py +++ b/pyaedt/hfss.py @@ -3573,7 +3573,7 @@ def validate_full_design(self, dname=None, outputdir=None, ports=None): if not dname: dname = self.design_name if not outputdir: - outputdir = self.project_path + outputdir = self.working_directory pname = self.project_name validation_log_file = os.path.join(outputdir, pname + "_" + dname + "_validation.log") @@ -3797,7 +3797,9 @@ def export_touchstone(self, solutionname, sweepname, filename=None, variation=[] sweepname : str Name of the sweep that has been solved. filename : str, optional - Full path and name for the output file. The default is ``None``. + Full path and name for the output file. + The default is ``None`` which export file in working_directory. + variation : list, optional List of all parameter variations. For example, ``["$AmbientTemp", "$PowerIn"]``. The default is ``[]``. @@ -3817,7 +3819,7 @@ def export_touchstone(self, solutionname, sweepname, filename=None, variation=[] for v, vv in zip(variation, variations_value): appendix += "_" + v + vv.replace("'", "") ext = ".S" + str(self.oboundary.GetNumExcitations()) + "p" - filename = os.path.join(self.project_path, solutionname + "_" + sweepname + appendix + ext) + filename = os.path.join(self.working_directory, solutionname + "_" + sweepname + appendix + ext) else: filename = filename.replace("//", "/").replace("\\", "/") print("Exporting Touchstone " + filename) diff --git a/pyaedt/hfss3dlayout.py b/pyaedt/hfss3dlayout.py index 820ea9825d4..d25922e78f0 100644 --- a/pyaedt/hfss3dlayout.py +++ b/pyaedt/hfss3dlayout.py @@ -388,7 +388,9 @@ def validate_full_design(self, name=None, outputdir=None, ports=None): name : str, optional Name of the design to validate. The default is ``None``. outputdir : str, optional - Output directory to save the log file to. The default is ``None``. + Output directory to save the log file to. + The default is ``None`` which export file in working_directory. + ports : str, optional Number of excitations that are expected. The default is ``None``. @@ -405,7 +407,7 @@ def validate_full_design(self, name=None, outputdir=None, ports=None): if name is None: name = self.design_name if outputdir is None: - outputdir = self.project_path + outputdir = self.working_directory self.logger.info("#### Design Validation Checks###") # @@ -553,6 +555,7 @@ def export_touchstone(self, solutionname, sweepname, filename, variation, variat Name of the sweep that has been solved. filename : str Full path for the Touchstone file. + The default is ``None`` which export file in working_directory. variation : list List of all parameter variations, such as ``["$AmbientTemp", "$PowerIn"]``. variations_value : list @@ -574,7 +577,7 @@ def export_touchstone(self, solutionname, sweepname, filename, variation, variat for v, vv in zip(variation, variations_value): appendix += "_" + v + vv.replace("'", "") ext = ".S" + str(len(self.port_list)) + "p" - filename = os.path.join(self.project_path, solutionname + "_" + sweepname + appendix + ext) + filename = os.path.join(self.working_directory, solutionname + "_" + sweepname + appendix + ext) else: filename = filename.replace("//", "/").replace("\\", "/") print("Exporting Touchstone " + filename) diff --git a/pyaedt/icepak.py b/pyaedt/icepak.py index 1c50128e378..ddd393d66e1 100644 --- a/pyaedt/icepak.py +++ b/pyaedt/icepak.py @@ -890,7 +890,7 @@ def assign_priority_on_intersections(self, component_prefix="COMP_"): >>> oEditor.UpdatePriorityList """ - temp_log = os.path.join(self.project_path, "validation.log") + temp_log = os.path.join(self.working_directory, "validation.log") validate = self.odesign.ValidateDesign(temp_log) self.save_project() i = 2 @@ -1533,7 +1533,8 @@ def eval_surface_quantity_from_field_summary( quantity_name : str, optional Name of the quantity to export. The default is ``"HeatTransCoeff"``. savedir : str, optional - Directory to save the CSV file to. The default is ``None``. + Directory to save the CSV file to. + The default is ``None`` which export file in working_directory. filename : str, optional Name of the CSV file. The default is ``None``. sweep_name : str, optional @@ -1555,7 +1556,7 @@ def eval_surface_quantity_from_field_summary( name = generate_unique_name(quantity_name) self.modeler.create_face_list(faces_list, name) if not savedir: - savedir = self.project_path + savedir = self.working_directory if not filename: filename = generate_unique_name(self.project_name + quantity_name) if not sweep_name: @@ -1604,7 +1605,8 @@ def eval_volume_quantity_from_field_summary( quantity_name : str, optional Name of the quantity to export. The default is ``"HeatTransCoeff"``. savedir : str, optional - Directory to save the CSV file to. The default is ``None``. + Directory to save the CSV file to. + The default is ``None`` which export file in working_directory. filename : str, optional Name of the CSV file. The default is ``None``. sweep_name : @@ -1624,7 +1626,7 @@ def eval_volume_quantity_from_field_summary( >>> oModule.ExportFieldsSummary """ if not savedir: - savedir = self.project_path + savedir = self.working_directory if not filename: filename = generate_unique_name(self.project_name + quantity_name) if not sweep_name: @@ -1724,7 +1726,8 @@ def export_summary( Parameters ---------- output_dir : str, optional - Name of directory for exporting the fields summary. The default is ``None``. + Name of directory for exporting the fields summary. + The default is ``None`` which export file in working_directory. solution_name : str, optional Name of the solution. The default is ``None``. type : string, optional @@ -1765,7 +1768,7 @@ def export_summary( self.logger.error("Object " + el + " not added.") self.logger.error(str(e)) if not output_dir: - output_dir = self.project_path + output_dir = self.working_directory self.osolution.EditFieldsSummarySetting(arg) if not os.path.exists(output_dir): os.mkdir(output_dir) @@ -2405,9 +2408,9 @@ def generate_fluent_mesh(self, object_lists=None): assert object_lists, "No Fluids objects found." object_lists = self.modeler.convert_to_selections(object_lists, True) file_name = self.project_name - sab_file_pointer = os.path.join(self.project_path, file_name + ".sab") - mesh_file_pointer = os.path.join(self.project_path, file_name + ".msh") - fl_uscript_file_pointer = os.path.join(self.project_path, "FLUscript.jou") + sab_file_pointer = os.path.join(self.working_directory, file_name + ".sab") + mesh_file_pointer = os.path.join(self.working_directory, file_name + ".msh") + fl_uscript_file_pointer = os.path.join(self.working_directory, "FLUscript.jou") if os.path.exists(mesh_file_pointer): os.remove(mesh_file_pointer) if os.path.exists(sab_file_pointer): @@ -2416,7 +2419,7 @@ def generate_fluent_mesh(self, object_lists=None): os.remove(fl_uscript_file_pointer) if os.path.exists(mesh_file_pointer + ".trn"): os.remove(mesh_file_pointer + ".trn") - assert self.export_3d_model(file_name, self.project_path, ".sab", object_lists), "Failed to export .sab" + assert self.export_3d_model(file_name, self.working_directory, ".sab", object_lists), "Failed to export .sab" # Building Fluent journal script file *.jou fluent_script = open(fl_uscript_file_pointer, "w") diff --git a/pyaedt/modeler/Object3d.py b/pyaedt/modeler/Object3d.py index adeadf54fa8..ce274da28d6 100644 --- a/pyaedt/modeler/Object3d.py +++ b/pyaedt/modeler/Object3d.py @@ -815,7 +815,7 @@ def export_image(self, file_path=None): """ if not is_ironpython and self._primitives._app._aedt_version >= "2021.2": if not file_path: - file_path = os.path.join(self._primitives._app.project_path, self.name + ".png") + file_path = os.path.join(self._primitives._app.working_directory, self.name + ".png") model_obj = self._primitives._app.post.plot_model_obj( objects=[self.name], show=False, diff --git a/pyaedt/modules/AdvancedPostProcessing.py b/pyaedt/modules/AdvancedPostProcessing.py index 3fd343c7d7f..180eb24d11c 100644 --- a/pyaedt/modules/AdvancedPostProcessing.py +++ b/pyaedt/modules/AdvancedPostProcessing.py @@ -1244,9 +1244,7 @@ def export_model_obj(self, obj_list=None, export_path=None, export_as_single_obj assert self._app._aedt_version >= "2021.2", self.logger.error("Object is supported from AEDT 2021 R2.") if not export_path: - export_path = os.path.join(self._app.project_path, self._app.project_name + "_pyaedt") - if not os.path.exists(export_path): - os.mkdir(export_path) + export_path = self._app.working_directory if not obj_list: self._app.modeler.refresh_all_ids() obj_list = self._app.modeler.primitives.object_names @@ -1263,7 +1261,7 @@ def export_model_obj(self, obj_list=None, export_path=None, export_as_single_obj if export_as_single_objects: files_exported = [] for el in obj_list: - fname = os.path.join(export_path, "Model_{}.obj".format(el)) + fname = os.path.join(export_path, "{}.obj".format(el)) self._app.modeler.oeditor.ExportModelMeshToFile(fname, [el]) if not self._app.modeler[el].display_wireframe: files_exported.append([fname, self._app.modeler[el].color, 1 - self._app.modeler[el].transparency]) @@ -1291,7 +1289,7 @@ def export_mesh_obj(self, setup_name=None, intrinsic_dict={}): ------- """ - project_path = self._app.project_path + project_path = self._app.working_directory if not setup_name: setup_name = self._app.nominal_adaptive @@ -1434,7 +1432,7 @@ def plot_field_from_fieldplot( self.ofieldsreporter.UpdateQuantityFieldsPlots(plot_folder) start = time.time() - file_to_add = self.export_field_plot(plotname, self._app.project_path) + file_to_add = self.export_field_plot(plotname, self._app.working_directory) models = None if not file_to_add: return False @@ -1494,7 +1492,7 @@ def animate_fields_from_aedtplt( List of variation values with units. The default is ``["0deg"]``. project_path : str, optional - Path for the export. The default is ``""``. + Path for the export. The default is ``""`` which export file in working_directory. meshplot : bool, optional The default is ``False``. Valid from Version 2021.2. export_gif : bool, optional @@ -1518,7 +1516,7 @@ def animate_fields_from_aedtplt( models_to_add = self.export_model_obj(export_as_single_objects=True, air_objects=False) fields_to_add = [] if not project_path: - project_path = self._app.project_path + project_path = self._app.working_directory for el in variation_list: self._app._odesign.ChangeProperty( [ @@ -1542,7 +1540,7 @@ def animate_fields_from_aedtplt( if fields_to_add: model.add_frames_from_file(fields_to_add) if export_gif: - model.gif_file = os.path.join(self._app.project_path, self._app.project_name + ".gif") + model.gif_file = os.path.join(self._app.working_directory, self._app.project_name + ".gif") if show or export_gif: model.animate() @@ -1596,7 +1594,7 @@ def animate_fields_from_aedtplt_2( List of variation values with units. The default is ``["0deg"]``. project_path : str, optional - Path for the export. The default is ``""``. + Path for the export. The default is ``""`` which export file in working_directory. export_gif : bool, optional Whether to export to a GIF file. The default is ``False``, in which case the plot is exported to a JPG file. @@ -1609,7 +1607,7 @@ def animate_fields_from_aedtplt_2( Model Object. """ if not project_path: - project_path = self._app.project_path + project_path = self._app.working_directory models_to_add = [] if meshplot: if self._app._aedt_version >= "2021.2": @@ -1638,7 +1636,7 @@ def animate_fields_from_aedtplt_2( if fields_to_add: model.add_frames_from_file(fields_to_add) if export_gif: - model.gif_file = os.path.join(self._app.project_path, self._app.project_name + ".gif") + model.gif_file = os.path.join(self._app.working_directory, self._app.project_name + ".gif") if show or export_gif: model.animate() model.clean_cache_and_files(clean_cache=False) diff --git a/pyaedt/modules/PostProcessor.py b/pyaedt/modules/PostProcessor.py index 854dee1b275..65fcf550858 100644 --- a/pyaedt/modules/PostProcessor.py +++ b/pyaedt/modules/PostProcessor.py @@ -871,6 +871,7 @@ def export_image(self, full_path=None, width=1920, height=1080, orientation="iso full_path : str, optional Path for saving the image file. PNG and GIF formats are supported. + The default is ``None`` which export file in working_directory. width : int, optional Plot Width. height : int, optional @@ -895,7 +896,7 @@ def export_image(self, full_path=None, width=1920, height=1080, orientation="iso """ self.oField.UpdateQuantityFieldsPlots(self.plotFolder) if not full_path: - full_path = os.path.join(self._postprocessor._app.project_path, self.name + ".png") + full_path = os.path.join(self._postprocessor._app.working_directory, self.name + ".png") status = self._postprocessor.export_field_jpg( full_path, self.name, @@ -922,7 +923,8 @@ def export_image_from_aedtplt( Parameters ---------- export_path : str, optional - Path where image will be saved + Path where image will be saved. + The default is ``None`` which export file in working_directory. view : str, optional View of the exported plot. Options are ``isometric``, ``top``, ``front``, ``left``, and ``all``. @@ -946,7 +948,7 @@ def export_image_from_aedtplt( >>> oModule.ExportFieldPlot """ if not export_path: - export_path = self._postprocessor._app.project_path + export_path = self._postprocessor._app.working_directory if sys.version_info.major > 2: return self._postprocessor.plot_field_from_fieldplot( self.name, @@ -1912,7 +1914,7 @@ def get_scalar_field_value( variation_dict.append(phase) else: variation_dict.append("0deg") - file_name = os.path.join(self._app.project_path, generate_unique_name("temp_fld") + ".fld") + file_name = os.path.join(self._app.working_directory, generate_unique_name("temp_fld") + ".fld") self.ofieldsreporter.CalculatorWrite(file_name, ["Solution:=", solution], variation_dict) value = None if os.path.exists(file_name): @@ -1953,7 +1955,7 @@ def export_field_file_on_grid( The default is ``None``. filename : str, optional Full path and name to save the file to. - The default is ``None``. + The default is ``None`` which export file in working_directory. gridtype : str, optional Type of the grid to export. The default is ``"Cartesian"``. grid_center : list, optional @@ -1998,7 +2000,7 @@ def export_field_file_on_grid( if not filename: appendix = "" ext = ".fld" - filename = os.path.join(self._app.project_path, solution.replace(" : ", "_") + appendix + ext) + filename = os.path.join(self._app.working_directory, solution.replace(" : ", "_") + appendix + ext) else: filename = filename.replace("//", "/").replace("\\", "/") self.ofieldsreporter.CalcStack("clear") @@ -2091,7 +2093,7 @@ def export_field_file( The default is ``None``. filename : str, optional Full path and name to save the file to. - The default is ``None``. + The default is ``None`` which export file in working_directory. obj_list : str, optional List of objects to export. The default is ``"AllObjects"``. obj_type : str, optional @@ -2132,7 +2134,7 @@ def export_field_file( if not filename: appendix = "" ext = ".fld" - filename = os.path.join(self._app.project_path, solution.replace(" : ", "_") + appendix + ext) + filename = os.path.join(self._app.working_directory, solution.replace(" : ", "_") + appendix + ext) else: filename = filename.replace("//", "/").replace("\\", "/") self.ofieldsreporter.CalcStack("clear") @@ -2182,7 +2184,7 @@ def export_field_file( export_with_sample_points, ) else: - sample_points_file = os.path.join(self._app.project_path, "temp_points.pts") + sample_points_file = os.path.join(self._app.working_directory, "temp_points.pts") with open(sample_points_file, "w") as f: for point in sample_points_lists: f.write(" ".join([str(i) for i in point]) + "\n") From 96d8f897b94006669f30d4556c5f1ef8a351b66e Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Wed, 19 Jan 2022 09:46:46 +0100 Subject: [PATCH 11/14] Fix arg2dict method (#729) * Fix arg2dict method * Fix arg2dict method --- pyaedt/generic/DataHandlers.py | 6 +++++- pyaedt/modules/DesignXPloration.py | 14 ++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pyaedt/generic/DataHandlers.py b/pyaedt/generic/DataHandlers.py index b9cbcabfebc..6abac4dfa1d 100644 --- a/pyaedt/generic/DataHandlers.py +++ b/pyaedt/generic/DataHandlers.py @@ -127,7 +127,11 @@ def _arg2dict(arg, dict_out): dict_in[arg[i][:-2]] = list(arg[i + 1]) else: if arg[i][:-2] in dict_in: - dict_in[arg[i][:-2]].append(arg[i + 1]) + if isinstance(dict_in[arg[i][:-2]], list): + dict_in[arg[i][:-2]].append(arg[i + 1]) + else: + dict_in[arg[i][:-2]] = [dict_in[arg[i][:-2]]] + dict_in[arg[i][:-2]].append(arg[i + 1]) else: dict_in[arg[i][:-2]] = arg[i + 1] diff --git a/pyaedt/modules/DesignXPloration.py b/pyaedt/modules/DesignXPloration.py index 41f94583fda..69cd4e459b2 100644 --- a/pyaedt/modules/DesignXPloration.py +++ b/pyaedt/modules/DesignXPloration.py @@ -192,14 +192,12 @@ def __init__(self, p_app, name, dictinputs, optimtype): if inputd.get("Goals", None): if self._app._is_object_oriented_enabled(): oparams = self.omodule.GetChildObject(self.name).GetCalculationInfo() - else: - oparams = [] - oparam = [i for i in oparams[0]] - calculation = ["NAME:Goal"] - calculation.extend(oparam) - arg1 = OrderedDict() - _arg2dict(calculation, arg1) - self.props["Goals"] = arg1 + oparam = [i for i in oparams[0]] + calculation = ["NAME:Goal"] + calculation.extend(oparam) + arg1 = OrderedDict() + _arg2dict(calculation, arg1) + self.props["Goals"] = arg1 @aedt_exception_handler def update(self, update_dictionary=None): From a68883a23d58c99f21cecb68298a50ade43ce2f6 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Wed, 19 Jan 2022 10:09:53 +0100 Subject: [PATCH 12/14] Fix Hfss Default Solution Type error (#726) * Fix Hfss Default Solution Type error * Minor improvements * Minor improvements * Fixed Solution type check for modal/terminal --- _unittest/test_02_2D_modeler.py | 4 ++-- examples/01-Modeling-Setup/Optimetrics.py | 7 ++++++- examples/02-HFSS/HFSS_Dipole.py | 7 ++++++- examples/02-HFSS/HFSS_Spiral.py | 2 +- examples/02-HFSS/SBR_Example.py | 2 +- examples/02-Maxwell/Maxwell2D_Transient.py | 20 ++++++++++---------- examples/04-Icepak/Sherlock_Example.py | 9 +++++++++ pyaedt/application/Design.py | 5 ++++- pyaedt/hfss.py | 18 +++++++++--------- pyaedt/hfss3dlayout.py | 4 ++-- pyaedt/mechanical.py | 4 ++-- pyaedt/modules/AdvancedPostProcessing.py | 2 +- 12 files changed, 53 insertions(+), 31 deletions(-) diff --git a/_unittest/test_02_2D_modeler.py b/_unittest/test_02_2D_modeler.py index 81b0d05ded5..d931f486293 100644 --- a/_unittest/test_02_2D_modeler.py +++ b/_unittest/test_02_2D_modeler.py @@ -6,7 +6,7 @@ from pyaedt.maxwell import Maxwell2d # Setup paths for module imports -from _unittest.conftest import BasisTest, desktop_version +from _unittest.conftest import BasisTest, desktop_version, config try: import pytest # noqa: F401 @@ -122,7 +122,7 @@ def test_create_regular_polygon(self): assert pg2.material_name == "copper" assert isclose(pg2.faces[0].area, 5.196152422706631) - @pytest.mark.skipif(is_ironpython, reason="Not running in ironpython") + @pytest.mark.skipif(config["build_machine"] or is_ironpython, reason="Not running in ironpython") def test_plot(self): self.aedtapp.modeler.primitives.create_regular_polygon([0, 0, 0], [0, 2, 0]) self.aedtapp.modeler.primitives.create_regular_polygon( diff --git a/examples/01-Modeling-Setup/Optimetrics.py b/examples/01-Modeling-Setup/Optimetrics.py index e07d894d387..79eaefc9716 100644 --- a/examples/01-Modeling-Setup/Optimetrics.py +++ b/examples/01-Modeling-Setup/Optimetrics.py @@ -41,8 +41,13 @@ create_sheets_on_openings=True, ) -hfss.plot(show=False, export_path=os.path.join(hfss.working_directory, "Image.jpg")) +model = hfss.plot(show=False) +model.show_grid = False +model.zoom = 0.7 +model.azimuth_angle = 25 +model.elevation_angle = 34 +model.plot(os.path.join(hfss.working_directory, "Image.jpg")) ############################################################################### # Create Wave Ports on the Sheets diff --git a/examples/02-HFSS/HFSS_Dipole.py b/examples/02-HFSS/HFSS_Dipole.py index b217e84ddc1..79fa57ca57a 100644 --- a/examples/02-HFSS/HFSS_Dipole.py +++ b/examples/02-HFSS/HFSS_Dipole.py @@ -61,7 +61,12 @@ # Plot the model # ~~~~~~~~~~~~~~ -hfss.plot(show=False, export_path=os.path.join(hfss.working_directory, "Image.jpg"), plot_air_objects=False) +my_plot = hfss.plot(show=False, plot_air_objects=False) +my_plot.show_axes = False +my_plot.show_grid = False +my_plot.plot( + os.path.join(hfss.working_directory, "Image.jpg"), +) ############################################################################### # Create the Setup diff --git a/examples/02-HFSS/HFSS_Spiral.py b/examples/02-HFSS/HFSS_Spiral.py index a9f96d2216a..7faeb5f952d 100644 --- a/examples/02-HFSS/HFSS_Spiral.py +++ b/examples/02-HFSS/HFSS_Spiral.py @@ -110,7 +110,7 @@ def create_line(pts): # Plot the model # ~~~~~~~~~~~~~~ -hfss.plot(show=False, export_path=os.path.join(hfss.working_directory, "Image.jpg")) +hfss.plot(show=False, export_path=os.path.join(hfss.working_directory, "Image.jpg"), plot_air_objects=False) ################################################################ diff --git a/examples/02-HFSS/SBR_Example.py b/examples/02-HFSS/SBR_Example.py index ee2bfbce4bc..4a3cb887b8c 100644 --- a/examples/02-HFSS/SBR_Example.py +++ b/examples/02-HFSS/SBR_Example.py @@ -59,7 +59,7 @@ # Plot the model # ~~~~~~~~~~~~~~ -target.plot(show=False, export_path=os.path.join(target.working_directory, "Image.jpg"), plot_air_objects=False) +source.plot(show=False, export_path=os.path.join(target.working_directory, "Image.jpg"), plot_air_objects=True) ############################################################################### diff --git a/examples/02-Maxwell/Maxwell2D_Transient.py b/examples/02-Maxwell/Maxwell2D_Transient.py index b1b7f3f14d3..91ae5f12b44 100644 --- a/examples/02-Maxwell/Maxwell2D_Transient.py +++ b/examples/02-Maxwell/Maxwell2D_Transient.py @@ -114,16 +114,16 @@ face_lists += rect2.faces timesteps = [str(i * 1e-3) + "s" for i in range(21)] id_list = [f.id for f in face_lists] -# animatedGif=maxwell_2d.post.animate_fields_from_aedtplt_2( -# "Mag_B", -# id_list, -# "Surface", -# intrinsic_dict={'Time': '0s'}, -# variation_variable="Time", -# variation_list=timesteps, -# show=True, -# export_gif=True -# ) +animatedGif = maxwell_2d.post.animate_fields_from_aedtplt_2( + "Mag_B", + id_list, + "Surface", + intrinsic_dict={"Time": "0s"}, + variation_variable="Time", + variation_list=timesteps, + show=False, + export_gif=True, +) ############################################### diff --git a/examples/04-Icepak/Sherlock_Example.py b/examples/04-Icepak/Sherlock_Example.py index 9d790bbf10f..052163a49a1 100644 --- a/examples/04-Icepak/Sherlock_Example.py +++ b/examples/04-Icepak/Sherlock_Example.py @@ -160,6 +160,15 @@ total_power = ipk.assign_block_from_sherlock_file(component_list) + +############################################################################### +# Plot the model +# ~~~~~~~~~~~~~~ +# We do the same plot after the material assignment + +ipk.plot(show=False, export_path=os.path.join(temp_folder, "Sherlock_Example.jpg"), plot_air_objects=False) + + ############################################################################### # Set Up Boundaries # ~~~~~~~~~~~~~~~~~ diff --git a/pyaedt/application/Design.py b/pyaedt/application/Design.py index 6098141ff2a..c65ecbb6a71 100644 --- a/pyaedt/application/Design.py +++ b/pyaedt/application/Design.py @@ -896,7 +896,10 @@ def default_solution_type(self): Default for the solution type. """ - return self.design_solutions.solution_types[0] + if self.design_solutions._solution_options[self.design_solutions.solution_types[0]]["name"]: + return self.design_solutions._solution_options[self.design_solutions.solution_types[0]]["name"] + else: + return self.design_solutions.solution_types[0] @property def odesign(self): diff --git a/pyaedt/hfss.py b/pyaedt/hfss.py index d44f8cec3c9..aec2329f2bf 100644 --- a/pyaedt/hfss.py +++ b/pyaedt/hfss.py @@ -320,7 +320,7 @@ def _create_circuit_port(self, edgelist, impedance, name, renorm, deemb, renorm_ } ) - if self.solution_type == "Modal": + if "Modal" in self.solution_type: if renorm: if isinstance(renorm_impedance, (int, float)) or "i" not in renorm_impedance: @@ -1555,7 +1555,7 @@ def create_lumped_port_between_objects( portname = generate_unique_name("Port") elif portname + ":1" in self.modeler.get_excitations_name(): portname = generate_unique_name(portname) - if self.solution_type == "Modal": + if "Modal" in self.solution_type: self._create_lumped_driven(sheet_name, point0, point1, impedance, portname, renorm, deemb) else: faces = self.modeler.primitives.get_object_faces(sheet_name) @@ -1807,7 +1807,7 @@ def create_wave_port_between_objects( portname = generate_unique_name("Port") elif portname + ":1" in self.modeler.get_excitations_name(): portname = generate_unique_name(portname) - if self.solution_type == "Modal": + if "Modal" in self.solution_type: return self._create_waveport_driven( sheet_name, point0, point1, impedance, portname, renorm, nummodes, deembed_dist ) @@ -2236,7 +2236,7 @@ def create_wave_port_microstrip_between_objects( portname = generate_unique_name("Port") elif portname + ":1" in self.modeler.get_excitations_name(): portname = generate_unique_name(portname) - if self.solution_type == "Modal": + if "Modal" in self.solution_type: return self._create_waveport_driven( sheet_name, point0, point1, impedance, portname, renorm, nummodes, deembed_dist ) @@ -2808,7 +2808,7 @@ def create_wave_port_from_sheet( portname = generate_unique_name("Port") elif portname + ":1" in self.modeler.get_excitations_name(): portname = generate_unique_name(portname) - if self.solution_type == "Modal": + if "Modal" in self.solution_type: b = self._create_waveport_driven(obj, int_start, int_stop, impedance, portname, renorm, nummodes, deemb) if b: portnames.append(b) @@ -2886,7 +2886,7 @@ def create_lumped_port_to_sheet( portname = generate_unique_name("Port") elif portname + ":1" in self.modeler.get_excitations_name(): portname = generate_unique_name(portname) - if self.solution_type == "Modal": + if "Modal" in self.solution_type: port = self._create_lumped_driven(sheet_name, point0, point1, impedance, portname, renorm, deemb) else: if not reference_object_list: @@ -3615,7 +3615,7 @@ def validate_full_design(self, dname=None, outputdir=None, ports=None): if self.solution_type != "Eigenmode": detected_excitations = self.modeler.get_excitations_name() if ports: - if self.solution_type == "Terminal": + if "Terminal" in self.solution_type: # For each port, there is terminal and reference excitations. ports_t = ports * 2 else: @@ -3734,9 +3734,9 @@ def create_scattering( Trace = ["X Component:=", "Freq", "Y Component:=", list_y] solution_data = "" - if self.solution_type == "Modal": + if "Modal" in self.solution_type: solution_data = "Modal Solution Data" - elif self.solution_type == "Terminal": + elif "Terminal" in self.solution_type: solution_data = "Terminal Solution Data" if solution_data != "": # run CreateReport function diff --git a/pyaedt/hfss3dlayout.py b/pyaedt/hfss3dlayout.py index d25922e78f0..a7419abfadf 100644 --- a/pyaedt/hfss3dlayout.py +++ b/pyaedt/hfss3dlayout.py @@ -528,9 +528,9 @@ def create_scattering( ["dB(S(" + p + "," + q + "))" for p, q in zip(list(port_names), list(port_excited))], ] solution_data = "" - if self.solution_type == "Modal": + if "Modal" in self.solution_type: solution_data = "Modal Solution Data" - elif self.solution_type == "Terminal": + elif "Terminal" in self.solution_type: solution_data = "Terminal Solution Data" elif self.solution_type == "HFSS3DLayout": solution_data = "Standard" diff --git a/pyaedt/mechanical.py b/pyaedt/mechanical.py index 55ec21202cf..2ae3c072e8c 100644 --- a/pyaedt/mechanical.py +++ b/pyaedt/mechanical.py @@ -420,7 +420,7 @@ def assign_frictionless_support(self, objects_list, boundary_name=""): >>> oModule.AssignFrictionlessSupport """ - if not (self.solution_type == "Structural" or self.solution_type == "Modal"): + if not (self.solution_type == "Structural" or "Modal" in self.solution_type): self.logger.error("This method works only in Mechanical Structural Solution") return False props = {} @@ -464,7 +464,7 @@ def assign_fixed_support(self, objects_list, boundary_name=""): >>> oModule.AssignFixedSupport """ - if not (self.solution_type == "Structural" or self.solution_type == "Modal"): + if not (self.solution_type == "Structural" or "Modal" in self.solution_type): self.logger.error("This method works only in a Mechanical structural solution.") return False props = {} diff --git a/pyaedt/modules/AdvancedPostProcessing.py b/pyaedt/modules/AdvancedPostProcessing.py index 180eb24d11c..3152770f0ea 100644 --- a/pyaedt/modules/AdvancedPostProcessing.py +++ b/pyaedt/modules/AdvancedPostProcessing.py @@ -233,7 +233,7 @@ def __init__(self): self.roll_angle = 0 self.azimuth_angle = 45 self.elevation_angle = 20 - self.zoom = 1.3 + self.zoom = 1 @aedt_exception_handler def set_orientation(self, camera_position="xy", roll_angle=0, azimuth_angle=45, elevation_angle=20): From c8cc70409b3a8c779defc71b4c2a8e857cdd8964 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Wed, 19 Jan 2022 12:48:25 +0100 Subject: [PATCH 13/14] Improved Terminal Name assignment. (#735) * Improved Terminal Name assignment. * Black Fix --- _unittest/test_20_HFSS.py | 17 +++++++++++++++++ pyaedt/application/Analysis.py | 22 ++++++++++++++++++++++ pyaedt/application/Analysis2D.py | 4 ---- pyaedt/hfss.py | 28 ++++++++++++++++++++++++++-- pyaedt/modeler/Modeler.py | 7 +------ 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/_unittest/test_20_HFSS.py b/_unittest/test_20_HFSS.py index 1a9207bb6c6..745c7613e10 100644 --- a/_unittest/test_20_HFSS.py +++ b/_unittest/test_20_HFSS.py @@ -628,3 +628,20 @@ def test_44_create_infinite_sphere(self): def test_45_set_autoopen(self): assert self.aedtapp.set_auto_open(True, "PML") + + def test_45_terminal_port(self): + self.aedtapp.insert_design("Design_Terminal") + self.aedtapp.solution_type = "Terminal" + box1 = self.aedtapp.modeler.primitives.create_box([-100, -100, 0], [200, 200, 5], name="gnd", matname="copper") + box2 = self.aedtapp.modeler.primitives.create_box( + [-100, -100, 20], [200, 200, 25], name="sig", matname="copper" + ) + sheet = self.aedtapp.modeler.create_rectangle(self.aedtapp.PLANE.YZ, [-100, -100, 5], [200, 15], "port") + port = self.aedtapp.create_lumped_port_between_objects( + box1, box2.name, self.aedtapp.AxisDir.XNeg, 50, "Lump1", True, False + ) + assert "Lump1_T1" in self.aedtapp.get_excitations_name() + port2 = self.aedtapp.create_lumped_port_to_sheet( + sheet.name, self.aedtapp.AxisDir.XNeg, 50, "Lump_sheet", True, False + ) + assert port2.name + "_T1" in self.aedtapp.modeler.get_excitations_name() diff --git a/pyaedt/application/Analysis.py b/pyaedt/application/Analysis.py index 74112a21b29..dcf5def9e63 100644 --- a/pyaedt/application/Analysis.py +++ b/pyaedt/application/Analysis.py @@ -479,6 +479,28 @@ def SolutionTypes(self): """ return SOLUTIONS() + @aedt_exception_handler + def get_excitations_name(self): + """Get all excitation names. + + Returns + ------- + list + List of excitation names. Excitations with multiple modes will return one + excitation for each mode. + + References + ---------- + + >>> oModule.GetExcitations + """ + try: + list_names = list(self.oboundary.GetExcitations()) + del list_names[1::2] + return list_names + except: + return [] + @aedt_exception_handler def analyze_all(self): """Analyze all setup in an actual design. diff --git a/pyaedt/application/Analysis2D.py b/pyaedt/application/Analysis2D.py index f9a399bf40d..015ccaaa356 100644 --- a/pyaedt/application/Analysis2D.py +++ b/pyaedt/application/Analysis2D.py @@ -131,10 +131,6 @@ def mesh(self): """ return self._mesh - # @property - # def post(self): - # return self._post - @aedt_exception_handler def plot( self, diff --git a/pyaedt/hfss.py b/pyaedt/hfss.py index aec2329f2bf..e0e35e3afd3 100644 --- a/pyaedt/hfss.py +++ b/pyaedt/hfss.py @@ -306,7 +306,30 @@ def _create_port_terminal(self, objectname, int_line_stop, portname, iswaveport= props["IsWavePort"] = iswaveport props["ReferenceConductors"] = ref_conductors props["RenormalizeModes"] = True - return self._create_boundary(portname, props, "AutoIdentify") + ports = list(self.oboundary.GetExcitationsOfType("Terminal")) + boundary = self._create_boundary(portname, props, "AutoIdentify") + if boundary: + new_ports = list(self.oboundary.GetExcitationsOfType("Terminal")) + terminals = [i for i in new_ports if i not in ports] + for terminal in terminals: + name_split = terminal.split("_") + try: + new_name = portname + "_" + name_split[1] + except: + new_name = portname + "_T1" + properties = [ + "NAME:AllTabs", + [ + "NAME:HfssTab", + ["NAME:PropServers", "BoundarySetup:" + terminal], + ["NAME:ChangedProps", ["NAME:Name", "Value:=", new_name]], + ], + ] + try: + self.odesign.ChangeProperty(properties) + except: + self.logger.warning("Failed To rename Terminals") + return boundary @aedt_exception_handler def _create_circuit_port(self, edgelist, impedance, name, renorm, deemb, renorm_impedance=""): @@ -1539,7 +1562,8 @@ def create_lumped_port_between_objects( 'LumpedPort' """ - + startobj = self.modeler.convert_to_selections(startobj) + endobject = self.modeler.convert_to_selections(endobject) if not self.modeler.primitives.does_object_exists(startobj) or not self.modeler.primitives.does_object_exists( endobject ): diff --git a/pyaedt/modeler/Modeler.py b/pyaedt/modeler/Modeler.py index 53b0eb14366..13c7686b82b 100644 --- a/pyaedt/modeler/Modeler.py +++ b/pyaedt/modeler/Modeler.py @@ -1485,12 +1485,7 @@ def get_excitations_name(self): >>> oModule.GetExcitations """ - try: - list_names = list(self._app.oboundary.GetExcitations()) - del list_names[1::2] - return list_names - except: - return [] + return self._app.get_excitations_name() @aedt_exception_handler def get_boundaries_name(self): From b44ed399874e31ff0187e66ded1acaaa70f7b386 Mon Sep 17 00:00:00 2001 From: Hui Zhou Date: Wed, 19 Jan 2022 13:32:32 +0100 Subject: [PATCH 14/14] Set padstack property (#730) * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update * update --- _unittest/test_00_EDB.py | 6 +++ pyaedt/edb_core/EDB_Data.py | 21 +++++++++ pyaedt/edb_core/padstack.py | 89 +++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) diff --git a/_unittest/test_00_EDB.py b/_unittest/test_00_EDB.py index 9a763fa927c..b0179d4b7cd 100644 --- a/_unittest/test_00_EDB.py +++ b/_unittest/test_00_EDB.py @@ -581,3 +581,9 @@ def test_73_duplicate_padstack(self): target_padstack_name="VIA_20-10-28_SMB", new_padstack_name="VIA_20-10-28_SMB_NEW" ) assert self.edbapp.core_padstack.padstacks["VIA_20-10-28_SMB_NEW"] + + def test74_set_padstack_property(self): + self.edbapp.core_padstack.set_pad_property( + padstack_name="VIA_18-10-28_SMB", layer_name="new", pad_shape="Circle", pad_params="800um" + ) + assert self.edbapp.core_padstack.padstacks["VIA_18-10-28_SMB"].pad_by_layer["new"] diff --git a/pyaedt/edb_core/EDB_Data.py b/pyaedt/edb_core/EDB_Data.py index badc04e9aff..83698bf0154 100644 --- a/pyaedt/edb_core/EDB_Data.py +++ b/pyaedt/edb_core/EDB_Data.py @@ -1826,6 +1826,12 @@ def start_layer(self): _, start_layer, stop_layer = self._edb_padstackinstance.GetLayerRange(layer, layer) return start_layer.GetName() + @start_layer.setter + def start_layer(self, layer_name): + stop_layer = self._pedb.core_stackup.signal_layers[self.stop_layer]._layer + layer = self._pedb.core_stackup.signal_layers[layer_name]._layer + self._edb_padstackinstance.SetLayerRange(layer, stop_layer) + @property def stop_layer(self): """Stopping layer. @@ -1839,6 +1845,12 @@ def stop_layer(self): _, start_layer, stop_layer = self._edb_padstackinstance.GetLayerRange(layer, layer) return stop_layer.GetName() + @stop_layer.setter + def stop_layer(self, layer_name): + start_layer = self._pedb.core_stackup.signal_layers[self.start_layer]._layer + layer = self._pedb.core_stackup.signal_layers[layer_name]._layer + self._edb_padstackinstance.SetLayerRange(start_layer, layer) + @property def net_name(self): """Net name. @@ -1923,6 +1935,15 @@ def id(self): """ return self._edb_padstackinstance.GetId() + @property + def name(self): + if self.is_pin: + comp_name = self._edb_padstackinstance.GetComponent().GetName() + pin_name = self._edb_padstackinstance.GetName() + return "-".join([comp_name, pin_name]) + else: + return None + @aedt_exception_handler def delete_padstack_instance(self): """Delete this padstack instance.""" diff --git a/pyaedt/edb_core/padstack.py b/pyaedt/edb_core/padstack.py index 21dc89b2c53..5380ec72ae3 100644 --- a/pyaedt/edb_core/padstack.py +++ b/pyaedt/edb_core/padstack.py @@ -598,6 +598,95 @@ def remove_pads_from_padstack(self, padstack_name, layer_name=None): self.update_padstacks() return True + @aedt_exception_handler + def set_pad_property( + self, + padstack_name, + layer_name=None, + pad_shape="Circle", + pad_params=0, + pad_x_offset=0, + pad_y_offset=0, + pad_rotation=0, + antipad_shape="Circle", + antipad_params=0, + antipad_x_offset=0, + antipad_y_offset=0, + antipad_rotation=0, + ): + """Set pad and antipad properites of the padstack. + + Parameters + ---------- + padstack_name : str + Name of the padstack. + layer_name : str, optional + Name of the layer. If None, all layers will be taken. + pad_shape : str, optional + Shape of the pad. The default is ``"Circle"``. Options are ``"Circle"``, ``"Square"``, ``"Rectangle"``, + ``"Oval"`` and ``"Bullet"``. + pad_params : str, optional + Dimension of the pad. The default is ``"0"``. + pad_x_offset : str, optional + X offset of the pad. The default is ``"0"``. + pad_y_offset : str, optional + Y offset of the pad. The default is ``"0"``. + pad_rotation : str, optional + Rotation of the pad. The default is ``"0"``. + antipad_shape : str, optional + Shape of the antipad. The default is ``"0"``. + antipad_params : str, optional + Dimension of the antipad. The default is ``"0"``. + antipad_x_offset : str, optional + X offset of the antipad. The default is ``"0"``. + antipad_y_offset : str, optional + Y offset of the antipad. The default is ``"0"``. + antipad_rotation : str, optional + Rotation of the antipad. The default is ``"0"``. + + Returns + ------- + bool + ``True`` if successful. + """ + shape_dict = { + "Circle": 1, + "Square": 2, + "Rectangle": 3, + "Oval": 4, + "Bullet": 5, + } + pad_shape = shape_dict[pad_shape] + if not isinstance(pad_params, list): + pad_params = [pad_params] + pad_params = convert_py_list_to_net_list([self._edb_value(i) for i in pad_params]) + pad_x_offset = self._edb_value(pad_x_offset) + pad_y_offset = self._edb_value(pad_y_offset) + pad_rotation = self._edb_value(pad_rotation) + + antipad_shape = shape_dict[antipad_shape] + if not isinstance(antipad_params, list): + antipad_params = [antipad_params] + antipad_params = convert_py_list_to_net_list([self._edb_value(i) for i in antipad_params]) + antipad_x_offset = self._edb_value(antipad_x_offset) + antipad_y_offset = self._edb_value(antipad_y_offset) + antipad_rotation = self._edb_value(antipad_rotation) + + p1 = self.padstacks[padstack_name].edb_padstack.GetData() + new_padstack_def = self._edb.Definition.PadstackDefData(p1) + if not layer_name: + layer_name = list(self._pedb.core_stackup.signal_layers.keys()) + elif isinstance(layer_name, str): + layer_name = [layer_name] + for layer in layer_name: + new_padstack_def.SetPadParameters(layer, 0, pad_shape, pad_params, pad_x_offset, pad_y_offset, pad_rotation) + new_padstack_def.SetPadParameters( + layer, 1, antipad_shape, antipad_params, antipad_x_offset, antipad_y_offset, antipad_rotation + ) + self.padstacks[padstack_name].edb_padstack.SetData(new_padstack_def) + self.update_padstacks() + return True + @aedt_exception_handler def update_padstack_instances(self): """Update Padstack Instance List."""