From 44045940c750e23e6e409f28f972cfe865525263 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:06:43 +0200 Subject: [PATCH 01/20] FEAT: Report configuration refactor (#5168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: maxcapodi78 Co-authored-by: Sébastien Morais <146729917+SMoraisAnsys@users.noreply.github.com> Co-authored-by: Samuelopez-ansys Co-authored-by: Giulia Malinverno --- _unittest/test_01_3dlayout_edb.py | 3 +- _unittest/test_01_report_file_parser.py | 2 +- _unittest/test_04_SBR.py | 2 +- _unittest/test_11_Setup.py | 12 +- _unittest/test_12_1_PostProcessing.py | 11 +- _unittest/test_12_PostProcessing.py | 25 +- _unittest/test_20_HFSS.py | 4 +- _unittest/test_21_Circuit.py | 2 +- _unittest/test_41_3dlayout_modeler.py | 2 +- _unittest/test_44_TouchstoneParser.py | 4 +- _unittest/test_46_FarField.py | 2 +- _unittest/test_98_Icepak.py | 19 +- .../compliance/ContourEyeDiagram_Custom.json | 6 +- .../general_compliance_template.json | 6 +- _unittest_solvers/test_00_analyze.py | 4 +- _unittest_solvers/test_01_pdf.py | 6 +- doc/source/API/Post.rst | 108 - doc/source/API/Visualization.rst | 190 +- doc/source/API/index.rst | 1 - doc/source/API/visualization/advanced.rst | 150 + doc/source/API/visualization/plot.rst | 82 + doc/source/API/visualization/post.rst | 265 + doc/source/API/visualization/report.rst | 81 + doc/source/Resources/farfield.png | Bin 0 -> 302492 bytes doc/source/User_guide/extensions.rst | 38 +- doc/source/User_guide/postprocessing.rst | 2 +- .../hfss3dlayout/index.rst | 18 + .../icepak/create_power_map.rst | 2 +- .../pyaedt_extensions_doc/project/index.rst | 32 + .../extensions/create_power_map_ui.png | Bin 0 -> 3308 bytes doc/source/release_1_0.rst | 2 +- .../config/vocabularies/ANSYS/accept.txt | 2 + examples/02-HFSS/Array.py | 2 +- examples/03-Maxwell/Maxwell2D_DCConduction.py | 2 +- .../06-Multiphysics/Hfss_Icepak_Coupling.py | 2 +- examples/07-Circuit/Touchstone_Management.py | 2 +- examples/07-Circuit/Virtual_Compliance.py | 2 +- .../aedt/core/application/aedt_objects.py | 4 +- .../aedt/core/application/analysis_3d.py | 14 +- .../core/application/analysis_3d_layout.py | 12 +- .../application/analysis_circuit_netlist.py | 8 +- .../aedt/core/application/analysis_hf.py | 2 +- .../aedt/core/application/analysis_nexxim.py | 9 +- .../core/application/analysis_r_m_xprt.py | 12 +- .../core/application/analysis_twin_builder.py | 12 +- src/ansys/aedt/core/circuit.py | 2 +- src/ansys/aedt/core/desktop.py | 3 +- src/ansys/aedt/core/generic/configurations.py | 62 +- src/ansys/aedt/core/generic/design_types.py | 2 +- .../aedt/core/generic/near_field_import.py | 196 - .../aedt/core/generic/report_file_parser.py | 92 - src/ansys/aedt/core/hfss.py | 8 +- src/ansys/aedt/core/hfss3dlayout.py | 4 +- src/ansys/aedt/core/icepak.py | 2 +- .../aedt/core/modeler/cad/elements_3d.py | 36 +- src/ansys/aedt/core/modeler/cad/polylines.py | 60 +- src/ansys/aedt/core/modeler/modeler_3d.py | 2 +- .../core/modules/advanced_post_processing.py | 1324 ----- .../aedt/core/modules/report_templates.py | 4241 ----------------- src/ansys/aedt/core/modules/solutions.py | 3509 -------------- src/ansys/aedt/core/modules/solve_sweeps.py | 1 + .../{sbrplus => visualization}/__init__.py | 0 .../advanced/__init__.py} | 0 .../advanced}/farfield_visualization.py | 818 +--- .../advanced/hdm_plot.py} | 25 +- .../aedt/core/visualization/advanced/misc.py | 685 +++ .../advanced/sbrplus/__init__.py | 23 + .../advanced}/sbrplus/hdm_parser.py | 2 +- .../advanced}/sbrplus/hdm_utils.py | 2 +- .../advanced}/sbrplus/matlab/HdmObject.m | 0 .../advanced}/sbrplus/matlab/README.md | 0 .../advanced}/sbrplus/matlab/SbrBounceType.m | 0 .../advanced}/sbrplus/matlab/StopWatch.m | 1 - .../advanced}/sbrplus/matlab/add_3dlight.m | 0 .../advanced}/sbrplus/matlab/amp2db.m | 0 .../advanced}/sbrplus/matlab/draw_rays1.m | 8 +- .../advanced}/sbrplus/matlab/draw_wfobj.m | 0 .../advanced}/sbrplus/matlab/filter_rays1.m | 10 +- .../sbrplus/matlab/filtered_tracks.m | 0 .../advanced}/sbrplus/matlab/ld_sbrplushdm.m | 36 +- .../advanced}/sbrplus/matlab/ld_wfobj.m | 6 +- .../advanced}/sbrplus/matlab/pwr2db.m | 0 .../sbrplus/matlab/validate_sfields.m | 0 .../advanced}/touchstone_parser.py | 38 +- .../aedt/core/visualization/plot/__init__.py | 23 + .../core/visualization/plot/matplotlib.py | 471 ++ .../{generic => visualization/plot}/pdf.py | 21 +- .../plot.py => visualization/plot/pyvista.py} | 466 +- .../aedt/core/visualization/post/__init__.py | 61 + .../aedt/core/visualization/post/common.py | 2411 ++++++++++ .../post}/compliance.py | 13 +- .../visualization/post/farfield_exporter.py | 357 ++ .../core/visualization/post/field_data.py | 1771 +++++++ .../core/visualization/post/field_summary.py | 290 ++ .../post}/fields_calculator.py | 77 +- .../post}/monitor_icepak.py | 0 .../core/visualization/post/post_circuit.py | 581 +++ .../post/post_common_3d.py} | 3978 ++++------------ .../core/visualization/post/post_icepak.py | 407 ++ .../core/visualization/post/solution_data.py | 1106 +++++ .../{generic => visualization/post}/spisim.py | 2 +- .../__init__.py | 0 .../com_120d_8.json | 0 .../com_93_8.json | 0 .../com_94_17.json | 0 .../com_parameters.py | 8 +- .../com_settings_mapping.py | 0 .../aedt/core/visualization/post/vrt_data.py | 281 ++ .../core/visualization/report/__init__.py | 23 + .../aedt/core/visualization/report/common.py | 2662 +++++++++++ .../core/visualization/report/constants.py | 75 + .../aedt/core/visualization/report/emi.py | 259 + .../aedt/core/visualization/report/eye.py | 1255 +++++ .../aedt/core/visualization/report/field.py | 178 + .../core/visualization/report/standard.py | 652 +++ .../core/workflows/project/create_report.py | 2 +- .../core/workflows/project/import_nastran.py | 6 +- src/pyaedt/generic/com_parameters.py | 2 +- src/pyaedt/generic/compliance.py | 2 +- src/pyaedt/generic/farfield_visualization.py | 3 +- src/pyaedt/generic/near_field_import.py | 2 +- src/pyaedt/generic/pdf.py | 2 +- src/pyaedt/generic/plot.py | 1 - src/pyaedt/generic/report_file_parser.py | 2 +- src/pyaedt/generic/spisim.py | 2 +- src/pyaedt/generic/touchstone_parser.py | 2 +- .../__init__.py | 1 - .../com_parameters.py | 1 - .../com_settings_mapping.py | 1 - src/pyaedt/modules/AdvancedPostProcessing.py | 1 - src/pyaedt/modules/PostProcessor.py | 1 - src/pyaedt/modules/fields_calculator.py | 2 +- src/pyaedt/modules/monitor_icepak.py | 1 - src/pyaedt/modules/report_templates.py | 1 - src/pyaedt/modules/solutions.py | 1 - src/pyaedt/sbrplus/__init__.py | 2 +- src/pyaedt/sbrplus/hdm_parser.py | 2 +- src/pyaedt/sbrplus/hdm_utils.py | 2 +- src/pyaedt/sbrplus/plot.py | 2 +- src/pyaedt/visualization/__init__.py | 1 + src/pyaedt/visualization/advanced/__init__.py | 1 + .../advanced/farfield_visualization.py | 1 + src/pyaedt/visualization/advanced/hdm_plot.py | 1 + src/pyaedt/visualization/advanced/misc.py | 1 + .../advanced/sprplus/__init__.py | 1 + .../advanced/sprplus/hdm_parser.py | 1 + .../advanced/sprplus/hdm_utils.py | 1 + .../advanced/touchstone_parser.py | 1 + src/pyaedt/visualization/plot/__init__.py | 1 + src/pyaedt/visualization/plot/matplotlib.py | 0 src/pyaedt/visualization/plot/pdf.py | 1 + src/pyaedt/visualization/plot/pyvista.py | 1 + src/pyaedt/visualization/post/__init__.py | 1 + src/pyaedt/visualization/post/common.py | 1 + src/pyaedt/visualization/post/compliance.py | 1 + .../visualization/post/farfield_exporter.py | 1 + src/pyaedt/visualization/post/field_data.py | 1 + .../visualization/post/field_summary.py | 1 + .../visualization/post/fields_calculator.py | 1 + .../visualization/post/monitor_icepak.py | 1 + src/pyaedt/visualization/post/post_circuit.py | 1 + .../visualization/post/post_common_3d.py | 1 + src/pyaedt/visualization/post/post_icepak.py | 1 + .../visualization/post/solution_data.py | 1 + src/pyaedt/visualization/post/spisim.py | 1 + .../__init__.py | 1 + .../com_120d_8.json | 0 .../com_93_8.json | 0 .../com_94_17.json | 0 .../com_parameters.py | 1 + .../com_settings_mapping.py | 1 + src/pyaedt/visualization/post/vrt_data.py | 1 + src/pyaedt/visualization/report/__init__.py | 1 + src/pyaedt/visualization/report/common.py | 1 + src/pyaedt/visualization/report/constants.py | 1 + src/pyaedt/visualization/report/emi.py | 1 + src/pyaedt/visualization/report/eye.py | 1 + src/pyaedt/visualization/report/field.py | 1 + src/pyaedt/visualization/report/standard.py | 1 + 179 files changed, 15858 insertions(+), 14014 deletions(-) delete mode 100644 doc/source/API/Post.rst create mode 100644 doc/source/API/visualization/advanced.rst create mode 100644 doc/source/API/visualization/plot.rst create mode 100644 doc/source/API/visualization/post.rst create mode 100644 doc/source/API/visualization/report.rst create mode 100644 doc/source/Resources/farfield.png create mode 100644 doc/source/User_guide/pyaedt_extensions_doc/hfss3dlayout/index.rst create mode 100644 doc/source/User_guide/pyaedt_extensions_doc/project/index.rst create mode 100644 doc/source/_static/extensions/create_power_map_ui.png delete mode 100644 src/ansys/aedt/core/generic/near_field_import.py delete mode 100644 src/ansys/aedt/core/generic/report_file_parser.py delete mode 100644 src/ansys/aedt/core/modules/advanced_post_processing.py delete mode 100644 src/ansys/aedt/core/modules/report_templates.py delete mode 100644 src/ansys/aedt/core/modules/solutions.py rename src/ansys/aedt/core/{sbrplus => visualization}/__init__.py (100%) rename src/ansys/aedt/core/{generic/com_parameters.py => visualization/advanced/__init__.py} (100%) rename src/ansys/aedt/core/{generic => visualization/advanced}/farfield_visualization.py (71%) rename src/ansys/aedt/core/{sbrplus/plot.py => visualization/advanced/hdm_plot.py} (92%) create mode 100644 src/ansys/aedt/core/visualization/advanced/misc.py create mode 100644 src/ansys/aedt/core/visualization/advanced/sbrplus/__init__.py rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/hdm_parser.py (99%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/hdm_utils.py (97%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/HdmObject.m (100%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/README.md (100%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/SbrBounceType.m (100%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/StopWatch.m (99%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/add_3dlight.m (100%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/amp2db.m (100%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/draw_rays1.m (99%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/draw_wfobj.m (100%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/filter_rays1.m (99%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/filtered_tracks.m (100%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/ld_sbrplushdm.m (98%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/ld_wfobj.m (99%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/pwr2db.m (100%) rename src/ansys/aedt/core/{ => visualization/advanced}/sbrplus/matlab/validate_sfields.m (100%) rename src/ansys/aedt/core/{generic => visualization/advanced}/touchstone_parser.py (93%) create mode 100644 src/ansys/aedt/core/visualization/plot/__init__.py create mode 100644 src/ansys/aedt/core/visualization/plot/matplotlib.py rename src/ansys/aedt/core/{generic => visualization/plot}/pdf.py (98%) rename src/ansys/aedt/core/{generic/plot.py => visualization/plot/pyvista.py} (79%) create mode 100644 src/ansys/aedt/core/visualization/post/__init__.py create mode 100644 src/ansys/aedt/core/visualization/post/common.py rename src/ansys/aedt/core/{generic => visualization/post}/compliance.py (98%) create mode 100644 src/ansys/aedt/core/visualization/post/farfield_exporter.py create mode 100644 src/ansys/aedt/core/visualization/post/field_data.py create mode 100644 src/ansys/aedt/core/visualization/post/field_summary.py rename src/ansys/aedt/core/{modules => visualization/post}/fields_calculator.py (92%) rename src/ansys/aedt/core/{modules => visualization/post}/monitor_icepak.py (100%) create mode 100644 src/ansys/aedt/core/visualization/post/post_circuit.py rename src/ansys/aedt/core/{modules/post_processor.py => visualization/post/post_common_3d.py} (50%) create mode 100644 src/ansys/aedt/core/visualization/post/post_icepak.py create mode 100644 src/ansys/aedt/core/visualization/post/solution_data.py rename src/ansys/aedt/core/{generic => visualization/post}/spisim.py (99%) rename src/ansys/aedt/core/{misc => visualization/post}/spisim_com_configuration_files/__init__.py (100%) rename src/ansys/aedt/core/{misc => visualization/post}/spisim_com_configuration_files/com_120d_8.json (100%) rename src/ansys/aedt/core/{misc => visualization/post}/spisim_com_configuration_files/com_93_8.json (100%) rename src/ansys/aedt/core/{misc => visualization/post}/spisim_com_configuration_files/com_94_17.json (100%) rename src/ansys/aedt/core/{misc => visualization/post}/spisim_com_configuration_files/com_parameters.py (98%) rename src/ansys/aedt/core/{misc => visualization/post}/spisim_com_configuration_files/com_settings_mapping.py (100%) create mode 100644 src/ansys/aedt/core/visualization/post/vrt_data.py create mode 100644 src/ansys/aedt/core/visualization/report/__init__.py create mode 100644 src/ansys/aedt/core/visualization/report/common.py create mode 100644 src/ansys/aedt/core/visualization/report/constants.py create mode 100644 src/ansys/aedt/core/visualization/report/emi.py create mode 100644 src/ansys/aedt/core/visualization/report/eye.py create mode 100644 src/ansys/aedt/core/visualization/report/field.py create mode 100644 src/ansys/aedt/core/visualization/report/standard.py delete mode 100644 src/pyaedt/generic/plot.py delete mode 100644 src/pyaedt/misc/spisim_com_configuration_files/__init__.py delete mode 100644 src/pyaedt/misc/spisim_com_configuration_files/com_parameters.py delete mode 100644 src/pyaedt/misc/spisim_com_configuration_files/com_settings_mapping.py delete mode 100644 src/pyaedt/modules/AdvancedPostProcessing.py delete mode 100644 src/pyaedt/modules/PostProcessor.py delete mode 100644 src/pyaedt/modules/monitor_icepak.py delete mode 100644 src/pyaedt/modules/report_templates.py delete mode 100644 src/pyaedt/modules/solutions.py create mode 100644 src/pyaedt/visualization/__init__.py create mode 100644 src/pyaedt/visualization/advanced/__init__.py create mode 100644 src/pyaedt/visualization/advanced/farfield_visualization.py create mode 100644 src/pyaedt/visualization/advanced/hdm_plot.py create mode 100644 src/pyaedt/visualization/advanced/misc.py create mode 100644 src/pyaedt/visualization/advanced/sprplus/__init__.py create mode 100644 src/pyaedt/visualization/advanced/sprplus/hdm_parser.py create mode 100644 src/pyaedt/visualization/advanced/sprplus/hdm_utils.py create mode 100644 src/pyaedt/visualization/advanced/touchstone_parser.py create mode 100644 src/pyaedt/visualization/plot/__init__.py create mode 100644 src/pyaedt/visualization/plot/matplotlib.py create mode 100644 src/pyaedt/visualization/plot/pdf.py create mode 100644 src/pyaedt/visualization/plot/pyvista.py create mode 100644 src/pyaedt/visualization/post/__init__.py create mode 100644 src/pyaedt/visualization/post/common.py create mode 100644 src/pyaedt/visualization/post/compliance.py create mode 100644 src/pyaedt/visualization/post/farfield_exporter.py create mode 100644 src/pyaedt/visualization/post/field_data.py create mode 100644 src/pyaedt/visualization/post/field_summary.py create mode 100644 src/pyaedt/visualization/post/fields_calculator.py create mode 100644 src/pyaedt/visualization/post/monitor_icepak.py create mode 100644 src/pyaedt/visualization/post/post_circuit.py create mode 100644 src/pyaedt/visualization/post/post_common_3d.py create mode 100644 src/pyaedt/visualization/post/post_icepak.py create mode 100644 src/pyaedt/visualization/post/solution_data.py create mode 100644 src/pyaedt/visualization/post/spisim.py create mode 100644 src/pyaedt/visualization/post/spisim_com_configuration_files/__init__.py rename src/pyaedt/{misc => visualization/post}/spisim_com_configuration_files/com_120d_8.json (100%) rename src/pyaedt/{misc => visualization/post}/spisim_com_configuration_files/com_93_8.json (100%) rename src/pyaedt/{misc => visualization/post}/spisim_com_configuration_files/com_94_17.json (100%) create mode 100644 src/pyaedt/visualization/post/spisim_com_configuration_files/com_parameters.py create mode 100644 src/pyaedt/visualization/post/spisim_com_configuration_files/com_settings_mapping.py create mode 100644 src/pyaedt/visualization/post/vrt_data.py create mode 100644 src/pyaedt/visualization/report/__init__.py create mode 100644 src/pyaedt/visualization/report/common.py create mode 100644 src/pyaedt/visualization/report/constants.py create mode 100644 src/pyaedt/visualization/report/emi.py create mode 100644 src/pyaedt/visualization/report/eye.py create mode 100644 src/pyaedt/visualization/report/field.py create mode 100644 src/pyaedt/visualization/report/standard.py diff --git a/_unittest/test_01_3dlayout_edb.py b/_unittest/test_01_3dlayout_edb.py index c4de3284c8a..45a0b2add6d 100644 --- a/_unittest/test_01_3dlayout_edb.py +++ b/_unittest/test_01_3dlayout_edb.py @@ -377,11 +377,10 @@ def test_19_dcir(self): solution_data = self.dcir_example_project.get_dcir_solution_data("SIwaveDCIR1", "Sources", "Voltage") assert self.dcir_example_project.post.available_report_quantities(is_siwave_dc=True, context="") assert self.dcir_example_project.post.create_report( - self.dcir_example_project.post.available_report_quantities(is_siwave_dc=True, context="RL")[0], + self.dcir_example_project.post.available_report_quantities(is_siwave_dc=True, context="Vias")[0], domain="DCIR", context="RL", ) - assert isinstance(self.dcir_example_project.get_dcir_element_data_loop_resistance("SIwaveDCIR1"), pd.DataFrame) assert isinstance(self.dcir_example_project.get_dcir_element_data_current_source("SIwaveDCIR1"), pd.DataFrame) def test_20_change_options(self): diff --git a/_unittest/test_01_report_file_parser.py b/_unittest/test_01_report_file_parser.py index 813f87539e6..c9e93110ce2 100644 --- a/_unittest/test_01_report_file_parser.py +++ b/_unittest/test_01_report_file_parser.py @@ -24,7 +24,7 @@ import os -from ansys.aedt.core.generic.report_file_parser import parse_rdat_file +from ansys.aedt.core.visualization.advanced.misc import parse_rdat_file import pytest local_path = os.path.dirname(os.path.realpath(__file__)) diff --git a/_unittest/test_04_SBR.py b/_unittest/test_04_SBR.py index aaf57132b6b..d66e2319393 100644 --- a/_unittest/test_04_SBR.py +++ b/_unittest/test_04_SBR.py @@ -26,7 +26,7 @@ import os from unittest.mock import mock_open -from ansys.aedt.core.sbrplus.hdm_parser import Parser +from ansys.aedt.core.visualization.advanced.sbrplus.hdm_parser import Parser from mock import patch import pytest diff --git a/_unittest/test_11_Setup.py b/_unittest/test_11_Setup.py index 7da351b0c71..dbff457d220 100644 --- a/_unittest/test_11_Setup.py +++ b/_unittest/test_11_Setup.py @@ -239,7 +239,7 @@ def test_26_create_optimization(self): calculation = "db(S(1,1))" new_setup = self.aedtapp.create_setup("MyOptimSetup") new_setup.props["Frequency"] = "2.5GHz" - sweep = new_setup.create_linear_step_sweep(freqstart=2, freqstop=10, step_size=0.1) + sweep = new_setup.create_linear_step_sweep(start_frequency=2, stop_frequency=10, step_size=0.1) setup2 = self.aedtapp.optimizations.add( calculation, ranges={"Freq": "2.5GHz"}, solution="{} : {}".format(new_setup.name, sweep.name) ) @@ -281,7 +281,7 @@ def test_27_create_doe(self): calculation = "db(S(1,1))" new_setup = self.aedtapp.create_setup("MyDOESetup") new_setup.props["Frequency"] = "2.5GHz" - sweep = new_setup.create_linear_step_sweep(freqstart=2, freqstop=10, step_size=0.1) + sweep = new_setup.create_linear_step_sweep(start_frequency=2, stop_frequency=10, step_size=0.1) setup2 = self.aedtapp.optimizations.add( calculation, ranges={"Freq": "2.5GHz"}, @@ -307,7 +307,7 @@ def test_27_create_doe(self): def test_28A_create_optislang(self): new_setup = self.aedtapp.create_setup("MyOptisSetup") new_setup.props["Frequency"] = "2.5GHz" - sweep = new_setup.create_linear_step_sweep(freqstart=2, freqstop=10, step_size=0.1) + sweep = new_setup.create_linear_step_sweep(start_frequency=2, stop_frequency=10, step_size=0.1) setup1 = self.aedtapp.optimizations.add( calculation=None, ranges=None, @@ -333,7 +333,7 @@ def test_28A_create_optislang(self): def test_28B_create_dx(self): new_setup = self.aedtapp.create_setup("MyDXSetup") new_setup.props["Frequency"] = "2.5GHz" - sweep = new_setup.create_linear_step_sweep(freqstart=2, freqstop=10, step_size=0.1) + sweep = new_setup.create_linear_step_sweep(start_frequency=2, stop_frequency=10, step_size=0.1) setup1 = self.aedtapp.optimizations.add( None, ranges=None, @@ -359,7 +359,7 @@ def test_29_create_sensitivity(self): calculation = "db(S(1,1))" new_setup = self.aedtapp.create_setup("MySensiSetup") new_setup.props["Frequency"] = "2.5GHz" - sweep = new_setup.create_linear_step_sweep(freqstart=2, freqstop=10, step_size=0.1) + sweep = new_setup.create_linear_step_sweep(start_frequency=2, stop_frequency=10, step_size=0.1) setup2 = self.aedtapp.optimizations.add( calculation, ranges={"Freq": "2.5GHz"}, @@ -376,7 +376,7 @@ def test_29_create_statistical(self): calculation = "db(S(1,1))" new_setup = self.aedtapp.create_setup("MyStatisticsetup") new_setup.props["Frequency"] = "2.5GHz" - sweep = new_setup.create_linear_step_sweep(freqstart=2, freqstop=10, step_size=0.1) + sweep = new_setup.create_linear_step_sweep(start_frequency=2, stop_frequency=10, step_size=0.1) setup2 = self.aedtapp.optimizations.add( calculation, ranges={"Freq": "2.5GHz"}, diff --git a/_unittest/test_12_1_PostProcessing.py b/_unittest/test_12_1_PostProcessing.py index c17da79e79e..70063a11d7e 100644 --- a/_unittest/test_12_1_PostProcessing.py +++ b/_unittest/test_12_1_PostProcessing.py @@ -29,9 +29,9 @@ from _unittest.conftest import config from ansys.aedt.core.generic.general_methods import is_linux from ansys.aedt.core.generic.general_methods import read_json -from ansys.aedt.core.generic.plot import _parse_aedtplt -from ansys.aedt.core.generic.plot import _parse_streamline from ansys.aedt.core.generic.settings import settings +from ansys.aedt.core.visualization.plot.pyvista import _parse_aedtplt +from ansys.aedt.core.visualization.plot.pyvista import _parse_streamline import pytest if config["desktopVersion"] > "2022.2": @@ -297,7 +297,12 @@ def test_08_manipulate_report(self): assert not self.aedtapp.post.rename_report("invalid", "MyNewScattering") def test_09_manipulate_report(self): - assert self.aedtapp.post.create_report("dB(S(1,1))") + plot = self.aedtapp.post.create_report("dB(S(1,1))") + assert plot + assert plot.export_config(os.path.join(self.local_scratch.path, f"{plot.plot_name}.json")) + assert self.aedtapp.post.create_report_from_configuration( + os.path.join(self.local_scratch.path, f"{plot.plot_name}.json"), solution_name=self.aedtapp.nominal_sweep + ) assert self.aedtapp.post.create_report( expressions="MaxMagDeltaS", variations={"Pass": ["All"]}, diff --git a/_unittest/test_12_PostProcessing.py b/_unittest/test_12_PostProcessing.py index 7e6a6a88754..19d56c89f22 100644 --- a/_unittest/test_12_PostProcessing.py +++ b/_unittest/test_12_PostProcessing.py @@ -33,10 +33,10 @@ from ansys.aedt.core import Q2d from ansys.aedt.core import Q3d from ansys.aedt.core.generic.general_methods import is_linux -from ansys.aedt.core.generic.pdf import AnsysReport -from ansys.aedt.core.generic.plot import _parse_aedtplt -from ansys.aedt.core.generic.plot import _parse_streamline from ansys.aedt.core.generic.settings import settings +from ansys.aedt.core.visualization.plot.pdf import AnsysReport +from ansys.aedt.core.visualization.plot.pyvista import _parse_aedtplt +from ansys.aedt.core.visualization.plot.pyvista import _parse_streamline import pandas as pd import pytest @@ -152,7 +152,7 @@ def test_09_manipulate_report(self, field_test): plot_type="3D Polar Plot", context=context, ) - assert field_test.post.create_report( + plot = field_test.post.create_report( "db(GainTotal)", field_test.nominal_adaptive, variations=variations, @@ -162,6 +162,11 @@ def test_09_manipulate_report(self, field_test): plot_type="3D Polar Plot", context="3D", ) + assert plot + assert plot.export_config(os.path.join(self.local_scratch.path, f"{plot.plot_name}.json")) + assert field_test.post.create_report_from_configuration( + os.path.join(self.local_scratch.path, f"{plot.plot_name}.json"), solution_name=field_test.nominal_adaptive + ) report = AnsysReport() report.create() assert report.add_project_info(field_test) @@ -324,7 +329,13 @@ def test_17_circuit(self, circuit_test): assert new_report.create() data1 = circuit_test.post.get_solution_data(["dB(S(Port1,Port1))", "dB(S(Port1,Port2))"], "LNA") assert data1.primary_sweep == "Freq" - assert circuit_test.post.create_report(["V(net_11)"], "Transient", "Time") + plot = circuit_test.post.create_report(["V(net_11)"], "Transient", "Time") + assert plot + assert plot.export_config(os.path.join(self.local_scratch.path, f"{plot.plot_name}.json")) + assert circuit_test.post.create_report_from_configuration( + os.path.join(self.local_scratch.path, f"{plot.plot_name}.json"), solution_name="Transient" + ) + data11 = circuit_test.post.get_solution_data(setup_sweep_name="LNA", math_formula="dB") assert data11.primary_sweep == "Freq" assert "dB(S(Port2,Port1))" in data11.expressions @@ -348,6 +359,10 @@ def test_17_circuit(self, circuit_test): assert new_report.create() new_report = circuit_test.post.reports_by_category.spectral(["dB(V(net_11))"]) assert new_report.create() + assert plot.export_config(os.path.join(self.local_scratch.path, f"{new_report.plot_name}.json")) + assert circuit_test.post.create_report_from_configuration( + os.path.join(self.local_scratch.path, f"{new_report.plot_name}.json"), solution_name="Transient" + ) new_report = circuit_test.post.reports_by_category.spectral(["dB(V(net_11))", "dB(V(Port1))"], "Transient") new_report.window = "Kaiser" new_report.adjust_coherent_gain = False diff --git a/_unittest/test_20_HFSS.py b/_unittest/test_20_HFSS.py index 5a310f07ba2..fd1eb9fc4bd 100644 --- a/_unittest/test_20_HFSS.py +++ b/_unittest/test_20_HFSS.py @@ -33,7 +33,7 @@ small_number = 1e-10 # Used for checking equivalence. -from ansys.aedt.core.generic.near_field_import import convert_nearfield_data +from ansys.aedt.core.visualization.advanced.misc import convert_nearfield_data test_subfolder = "T20" @@ -1432,7 +1432,7 @@ def test_59_test_nastran(self): assert self.aedtapp.modeler.import_nastran(example_project2, decimation=0.1, preview=True, save_only_stl=True) assert self.aedtapp.modeler.import_nastran(example_project2, decimation=0.5) example_project = os.path.join(local_path, "../_unittest/example_models", test_subfolder, "sphere.stl") - from ansys.aedt.core.modules.solutions import simplify_stl + from ansys.aedt.core.visualization.advanced.misc import simplify_stl out = simplify_stl(example_project, decimation=0.8) assert os.path.exists(out) diff --git a/_unittest/test_21_Circuit.py b/_unittest/test_21_Circuit.py index ad17cce8253..a8bb6fd4457 100644 --- a/_unittest/test_21_Circuit.py +++ b/_unittest/test_21_Circuit.py @@ -216,7 +216,7 @@ def test_15_rotate(self): ) def test_16_read_touchstone(self): - from ansys.aedt.core.generic.touchstone_parser import read_touchstone + from ansys.aedt.core.visualization.advanced.touchstone_parser import read_touchstone data = read_touchstone(self.touchstone_file) assert len(data.port_names) > 0 diff --git a/_unittest/test_41_3dlayout_modeler.py b/_unittest/test_41_3dlayout_modeler.py index ecb67dd0dc3..b622d88c8c2 100644 --- a/_unittest/test_41_3dlayout_modeler.py +++ b/_unittest/test_41_3dlayout_modeler.py @@ -32,7 +32,7 @@ from ansys.aedt.core import Maxwell3d from ansys.aedt.core.generic.general_methods import generate_unique_name from ansys.aedt.core.generic.general_methods import is_linux -from ansys.aedt.core.generic.pdf import AnsysReport +from ansys.aedt.core.visualization.plot.pdf import AnsysReport import pytest test_subfolder = "T41" diff --git a/_unittest/test_44_TouchstoneParser.py b/_unittest/test_44_TouchstoneParser.py index ff691a6ec14..557a8759ec7 100644 --- a/_unittest/test_44_TouchstoneParser.py +++ b/_unittest/test_44_TouchstoneParser.py @@ -56,7 +56,7 @@ def test_01_get_touchstone_data(self): assert ts_data.get_fext_xtalk_index_from_prefix("diff1", "diff2") def test_02_read_ts_file(self): - from ansys.aedt.core.generic.touchstone_parser import TouchstoneData + from ansys.aedt.core.visualization.advanced.touchstone_parser import TouchstoneData ts1 = TouchstoneData(touchstone_file=os.path.join(test_T44_dir, "port_order_1234.s8p")) assert ts1.get_mixed_mode_touchstone_data() @@ -67,7 +67,7 @@ def test_02_read_ts_file(self): assert ts1.get_worst_curve(curve_list=ts1.get_return_loss_index(), plot=False) def test_03_check_touchstone_file(self): - from ansys.aedt.core.generic.touchstone_parser import check_touchstone_files + from ansys.aedt.core.visualization.advanced.touchstone_parser import check_touchstone_files check = check_touchstone_files(input_dir=test_T44_dir) assert check diff --git a/_unittest/test_46_FarField.py b/_unittest/test_46_FarField.py index c07d133ef6c..83d053a8090 100644 --- a/_unittest/test_46_FarField.py +++ b/_unittest/test_46_FarField.py @@ -25,7 +25,7 @@ import os import shutil -from ansys.aedt.core.generic.farfield_visualization import FfdSolutionData +from ansys.aedt.core.visualization.advanced.farfield_visualization import FfdSolutionData # from _unittest.conftest import config from matplotlib.figure import Figure diff --git a/_unittest/test_98_Icepak.py b/_unittest/test_98_Icepak.py index 6861186d14a..eadbec20897 100644 --- a/_unittest/test_98_Icepak.py +++ b/_unittest/test_98_Icepak.py @@ -34,8 +34,8 @@ from ansys.aedt.core.modules.boundary import PCBSettingsPackageParts from ansys.aedt.core.modules.mesh_icepak import MeshRegion from ansys.aedt.core.modules.setup_templates import SetupKeys -from ansys.aedt.core.modules.solutions import FolderPlotSettings -from ansys.aedt.core.modules.solutions import SpecifiedScale +from ansys.aedt.core.visualization.post.field_data import FolderPlotSettings +from ansys.aedt.core.visualization.post.field_data import SpecifiedScale import pytest test_subfolder = "T98" @@ -277,7 +277,7 @@ def test_03_AssignPCBRegion(self): pcb_mesh_region.MinGapZ = 1 assert pcb_mesh_region.update() if settings.aedt_version > "2023.2": - assert pcb_mesh_region.assignment.padding_values == ["0"] * 6 + assert [str(i) for i in pcb_mesh_region.assignment.padding_values] == ["0"] * 6 assert pcb_mesh_region.assignment.padding_types == ["Percentage Offset"] * 6 pcb_mesh_region.assignment.negative_x_padding = 1 pcb_mesh_region.assignment.positive_x_padding = 1 @@ -293,10 +293,10 @@ def test_03_AssignPCBRegion(self): pcb_mesh_region.assignment.positive_z_padding_type = "Transverse Percentage Offset" assert pcb_mesh_region.assignment.negative_x_padding == "1mm" assert pcb_mesh_region.assignment.positive_x_padding == "1mm" - assert pcb_mesh_region.assignment.negative_y_padding == "1" + assert str(pcb_mesh_region.assignment.negative_y_padding) == "1" assert pcb_mesh_region.assignment.positive_y_padding == "1mm" assert pcb_mesh_region.assignment.negative_z_padding == "1mm" - assert pcb_mesh_region.assignment.positive_z_padding == "1" + assert str(pcb_mesh_region.assignment.positive_z_padding) == "1" assert pcb_mesh_region.assignment.negative_x_padding_type == "Absolute Offset" assert pcb_mesh_region.assignment.positive_x_padding_type == "Absolute Position" assert pcb_mesh_region.assignment.negative_y_padding_type == "Transverse Percentage Offset" @@ -1867,7 +1867,14 @@ def test_80_global_mesh_region(self): g_m_r = self.aedtapp.mesh.global_mesh_region assert g_m_r assert g_m_r.global_region.object.name == "Region" - assert g_m_r.global_region.padding_values == ["50", "50", "50", "50", "50", "50"] + assert g_m_r.global_region.padding_values == [ + "50", + "50", + "50", + "50", + "50", + "50", + ] or g_m_r.global_region.padding_values == [50, 50, 50, 50, 50, 50] assert g_m_r.global_region.padding_types == [ "Percentage Offset", "Percentage Offset", diff --git a/_unittest_solvers/example_models/T01/compliance/ContourEyeDiagram_Custom.json b/_unittest_solvers/example_models/T01/compliance/ContourEyeDiagram_Custom.json index f322bddc931..8f8bcb6cd26 100644 --- a/_unittest_solvers/example_models/T01/compliance/ContourEyeDiagram_Custom.json +++ b/_unittest_solvers/example_models/T01/compliance/ContourEyeDiagram_Custom.json @@ -6,10 +6,14 @@ "quantity_type": "3", "context": { "Domain": "Time", - "primary_sweep": "Time", + "primary_sweep": "__UnitInterval", "primary_sweep_range": [ "All" ], + "secondary_sweep": "__Amplitude", + "secondary_sweep_range": [ + "All" + ], "variations": { "__UnitInterval": [ "All" diff --git a/_unittest_solvers/example_models/T01/compliance/general_compliance_template.json b/_unittest_solvers/example_models/T01/compliance/general_compliance_template.json index 08af8bae72d..b5858183670 100644 --- a/_unittest_solvers/example_models/T01/compliance/general_compliance_template.json +++ b/_unittest_solvers/example_models/T01/compliance/general_compliance_template.json @@ -39,13 +39,13 @@ "traces": ["dB(S(X1_TX0,X1_RX0))", "dB(S(X1_TX1,X1_RX1))","dB(S(X1_TX2,X1_RX2))", "dB(S(X1_TX3,X1_RX3))"], "pass_fail": false, "group_plots":true - + }, {"name": "eye1", "design_name":"32GTps_circuit", "type": "statistical eye", "config": "StatisticalEyeDiagram_Custom.json", - "quantity_type": 3, + "quantity_type": 3, "traces": ["b_input_67", "b_input_119"], "pass_fail": true }, @@ -53,7 +53,7 @@ "design_name":"32GTps_circuit", "type": "contour eye diagram", "config": "ContourEyeDiagram_Custom.json", - "quantity_type": 3, + "quantity_type": 3, "traces": ["b_input_67", "b_input_119"], "pass_fail": true }, diff --git a/_unittest_solvers/test_00_analyze.py b/_unittest_solvers/test_00_analyze.py index 4bd8a8bd657..b536388fb64 100644 --- a/_unittest_solvers/test_00_analyze.py +++ b/_unittest_solvers/test_00_analyze.py @@ -15,7 +15,7 @@ from ansys.aedt.core import Hfss3dLayout from ansys.aedt.core import Circuit, Maxwell3d from _unittest.conftest import config -from ansys.aedt.core.generic.spisim import SpiSim +from ansys.aedt.core.visualization.post.spisim import SpiSim sbr_platform_name = "satellite_231" array_name = "array_231" @@ -545,7 +545,7 @@ def test_09c_compute_com(self, local_scratch): ) assert com_0 and com_1 - from ansys.aedt.core.misc.spisim_com_configuration_files.com_parameters import COMParametersVer3p4 + from ansys.aedt.core.visualization.post.spisim_com_configuration_files.com_parameters import COMParametersVer3p4 com_param = COMParametersVer3p4() com_param.load(os.path.join(spisim.working_directory, "custom.json"), ) com_param.export_spisim_cfg(str(Path(local_scratch.path) / "test.cfg")) diff --git a/_unittest_solvers/test_01_pdf.py b/_unittest_solvers/test_01_pdf.py index 74208fe3597..69b8294f3fc 100644 --- a/_unittest_solvers/test_01_pdf.py +++ b/_unittest_solvers/test_01_pdf.py @@ -5,8 +5,8 @@ import pytest from ansys.aedt.core import Circuit -from ansys.aedt.core.generic.compliance import VirtualCompliance -from ansys.aedt.core.generic.pdf import AnsysReport +from ansys.aedt.core.visualization.post.compliance import VirtualCompliance +from ansys.aedt.core.visualization.plot.pdf import AnsysReport tol = 1e-12 test_project_name = "ANSYS-HSD_V1_0_test" @@ -85,7 +85,7 @@ def test_virtual_compliance(self, local_scratch, aedtapp): assert v.create_compliance_report() def test_spisim_raw_read(self, local_scratch): - from ansys.aedt.core.generic.spisim import SpiSimRawRead + from ansys.aedt.core.visualization.post.spisim import SpiSimRawRead raw_file = os.path.join(local_path, "example_models", test_subfolder, "SerDes_Demo_02_Thru.s4p_ERL.raw") raw_file = local_scratch.copyfile(raw_file) diff --git a/doc/source/API/Post.rst b/doc/source/API/Post.rst deleted file mode 100644 index a4ac06f7adb..00000000000 --- a/doc/source/API/Post.rst +++ /dev/null @@ -1,108 +0,0 @@ -Post-processing -=============== -This section lists modules for creating and editing -plots in AEDT and shows how to interact with AEDT fields calculator. -They are accessible through the ``post`` property. - -.. note:: - Some capabilities of the ``advanced_post_processing`` module require Python 3 and - installations of the `numpy `_, - `matplotlib `_, and `pyvista `_ - packages. - -.. note:: - Some functionalities are available only when AEDT is running - in graphical mode. - -Advanced post-processing -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. currentmodule:: ansys.aedt.core.modules - -.. autosummary:: - :toctree: _autosummary - :nosignatures: - - advanced_post_processing.PostProcessor - - -.. code:: python - - from ansys.aedt.core import Hfss - app = Hfss(specified_version="2023.1", - non_graphical=False, new_desktop_session=True, - close_on_exit=True, student_version=False) - - # This call returns the PostProcessor class - post = app.post - - # This call returns a FieldPlot object - plotf = post.create_fieldplot_volume(objects, quantity_name, setup_name, intrinsics) - - # This call returns a SolutionData object - my_data = post.get_solution_data(expressions=trace_names) - - # This call returns a new standard report object and creates one or multiple reports from it. - standard_report = post.reports_by_category.standard("db(S(1,1))") - standard_report.create() - sols = standard_report.get_solution_data() - ... - - -AEDT report management -~~~~~~~~~~~~~~~~~~~~~~ -AEDT provides great flexibility in reports. -PyAEDT has classes for manipulating any report property. - - -.. note:: - Some functionalities are available only when AEDT is running - in graphical mode. - - -.. currentmodule:: ansys.aedt.core.modules - -.. autosummary:: - :toctree: _autosummary - :nosignatures: - - report_templates.Trace - report_templates.LimitLine - report_templates.Standard - report_templates.Fields - report_templates.NearField - report_templates.FarField - report_templates.EyeDiagram - report_templates.Emission - report_templates.Spectral - -Icepak monitors -~~~~~~~~~~~~~~~ -The ``monitor_icepak`` module includes the classes listed below to add, modify, and manage monitors during simulations. -Retrieve monitor values for post-processing and analysis to gain insights into key simulation metrics. -Methods and properties are accessible through the ``monitor`` property of the ``Icepak`` class. - -.. currentmodule:: ansys.aedt.core.modules.monitor_icepak - -.. autosummary:: - :toctree: _autosummary - :nosignatures: - - - FaceMonitor - PointMonitor - Monitor - -Advanced fields calculator -~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``fields_calculator`` module includes the ``FieldsCalculator`` class. -It provides methods to interact with AEDT Fields Calculator by adding, loading and deleting custom expressions. - -.. currentmodule:: ansys.aedt.core.modules.fields_calculator - -.. autosummary:: - :toctree: _autosummary - :nosignatures: - - - FieldsCalculator diff --git a/doc/source/API/Visualization.rst b/doc/source/API/Visualization.rst index e6e92bd2e28..dd31524bbb6 100644 --- a/doc/source/API/Visualization.rst +++ b/doc/source/API/Visualization.rst @@ -1,175 +1,73 @@ Visualization ============= -This section lists modules for creating and editing data outside AEDT. -Plot fields and data outside AEDT -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -PyAEDT supports external report capabilities available with installed third-party -packages like `numpy `_, -`pandas `_, `matplotlib `_, -and `pyvista `_. +This section outlines the available modules for creating and editing data within and outside AEDT. +PyAEDT offers four primary levels of visualization: -.. currentmodule:: ansys.aedt.core.modules +* **Reports** +* **Post-processing** +* **Graphics** +* **Advanced Visualization** -.. autosummary:: - :toctree: _autosummary - :nosignatures: +Reports +~~~~~~~ - solutions.SolutionData - solutions.FieldPlot +AEDT provides extensive flexibility for generating reports. +PyAEDT includes dedicated classes to manipulate all report properties, offering full control over report customization. +.. toctree:: + :maxdepth: 1 -Plot 3D objects and fields -~~~~~~~~~~~~~~~~~~~~~~~~~~ -ModelPlotter is a class that benefits of `pyvista `_ package and allows to generate -models and 3D plots. + visualization/report +.. image:: ../Resources/sparams.jpg + :width: 800 + :alt: S-Parameters -.. currentmodule:: ansys.aedt.core.generic.plot -.. autosummary:: - :toctree: _autosummary - :nosignatures: +Post-processing +~~~~~~~~~~~~~~~ - ModelPlotter +AEDT has different post-processing tools. +PyAEDT provides classes to interact with and modify any of these tools, +enhancing data analysis and visualization capabilities. +.. toctree:: + :maxdepth: 1 -Plot touchstone data outside AEDT -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TouchstoneData class is based on `scikit-rf `_ package and allows advanced -touchstone post-processing. -The following methods allows to read and check touchstone files. + visualization/post +.. image:: ../Resources/field_plot.png + :width: 800 + :alt: Postprocessing features -.. currentmodule:: ansys.aedt.core.generic.touchstone_parser -.. autosummary:: - :toctree: _autosummary - :nosignatures: - - read_touchstone - check_touchstone_files - - -Using the above methods you are getting an object of a class TouchstoneData. -The class TouchstoneData is based on `scikit-rf `_, -Additional methods are added to provide easy access to touchstone curves. - - -.. currentmodule:: ansys.aedt.core.generic.touchstone_parser - -.. autosummary:: - :toctree: _autosummary - :nosignatures: - - TouchstoneData.get_insertion_loss_index - TouchstoneData.plot_insertion_losses - TouchstoneData.plot - TouchstoneData.plot_return_losses - TouchstoneData.get_mixed_mode_touchstone_data - TouchstoneData.get_return_loss_index - TouchstoneData.get_insertion_loss_index_from_prefix - TouchstoneData.get_next_xtalk_index - TouchstoneData.get_fext_xtalk_index_from_prefix - TouchstoneData.plot_next_xtalk_losses - TouchstoneData.plot_fext_xtalk_losses - TouchstoneData.get_worst_curve - - - -Here an example on how to use TouchstoneData class. - -.. code:: python - - from ansys.aedt.core.generic.touchstone_parser import TouchstoneData - - ts1 = TouchstoneData(touchstone_file=os.path.join(test_T44_dir, "port_order_1234.s8p")) - assert ts1.get_mixed_mode_touchstone_data() - ts2 = TouchstoneData(touchstone_file=os.path.join(test_T44_dir, "port_order_1324.s8p")) - assert ts2.get_mixed_mode_touchstone_data(port_ordering="1324") - - assert ts1.plot_insertion_losses(plot=False) - assert ts1.get_worst_curve(curve_list=ts1.get_return_loss_index(), plot=False) - ... - - -Farfield +Graphics ~~~~~~~~ -PyAEDT offers sophisticated tools for advanced farfield post-processing. -There are two complementary classes: ``FfdSolutionDataExporter`` and ``FfdSolutionData``. - -- FfdSolutionDataExporter: Enables efficient export and manipulation of farfield data. It allows users to convert simulation results into a standard metadata format for further analysis, or reporting. - -- FfdSolutionData: Focuses on the direct access and processing of farfield solution data. It supports a comprehensive set of postprocessing operations, from visualizing radiation patterns to computing key performance metrics. - - -.. currentmodule:: ansys.aedt.core.generic.farfield_visualization - -.. autosummary:: - :toctree: _autosummary - :nosignatures: - - FfdSolutionDataExporter - FfdSolutionData - -This code shows how you can get the farfield data and perform some post-processing: - -.. code:: python - - import ansys.aedt.core - from ansys.aedt.core.generic.farfield_visualization import FfdSolutionDataExporter - app = ansys.aedt.core.Hfss() - ffdata = app.get_antenna_data(frequencies=None, - setup="Setup1 : Sweep", - sphere="3D", - variations=None, - overwrite=False, - link_to_hfss=True, - export_touchstone=True) - incident_power = ffdata.incident_power - ffdata.plot_cut(primary_sweep="Theta", theta=0) - ffdata.plot_contour(polar=True) - ffdata.plot_3d(show_geometry=False) - app.release_desktop(False, False) - -If you exported the farfield data previously,you can directly get the farfield data: - -.. code:: python - - from ansys.aedt.core.generic.farfield_visualization import FfdSolutionData - input_file = r"path_to_ffd\pyaedt_antenna_metadata.json" - ffdata = FfdSolutionData(input_file) - incident_power = ffdata.incident_power - ffdata.plot_cut(primary_sweep="Theta", theta=0) - ffdata.plot_contour(polar=True) - ffdata.plot_3d(show_geometry=False) - app.release_desktop(False, False) -The following diagram shows both classes work. You can use them independently or from the ``get_antenna_data`` method. +Specialized plotting options. - .. image:: ../_static/farfield_visualization_pyaedt.png - :width: 800 - :alt: Farfield data with PyAEDT +.. toctree:: + :maxdepth: 1 + visualization/plot -If you have existing farfield data, or you want to export it manually, you can still use FfdSolutionData class. +.. image:: ../Resources/pyvista_plot.jpg + :width: 800 + :alt: S-Parameters Matplotlib - .. image:: ../_static/farfield_visualization_aedt.png - :width: 800 - :alt: Farfield data with AEDT +Advanced Visualization +~~~~~~~~~~~~~~~~~~~~~~ -Heterogeneous data message -~~~~~~~~~~~~~~~~~~~~~~~~~~ -Heterogeneous data message (HDM) is the file exported from SBR+ solver containing rays information. -The following methods allows to read and plot rays information. +High-level visualization tools. -.. currentmodule:: ansys.aedt.core.sbrplus +.. toctree:: + :maxdepth: 1 -.. autosummary:: - :toctree: _autosummary - :nosignatures: + visualization/advanced - hdm_parser.Parser - plot.HDMPlotter +.. image:: ../Resources/farfield.png + :width: 800 + :alt: Farfield pyvista diff --git a/doc/source/API/index.rst b/doc/source/API/index.rst index 744f88bc650..0d7dbde7ad4 100644 --- a/doc/source/API/index.rst +++ b/doc/source/API/index.rst @@ -90,7 +90,6 @@ Example with ``Desktop`` class implicit initialization: Boundaries Mesh Setup - Post Visualization DesktopMessenger Optimetrics diff --git a/doc/source/API/visualization/advanced.rst b/doc/source/API/visualization/advanced.rst new file mode 100644 index 00000000000..39dbc5fc16b --- /dev/null +++ b/doc/source/API/visualization/advanced.rst @@ -0,0 +1,150 @@ +Advanced +======== + +You can use PyAEDT for postprocessing of AEDT results to display graphics object and plot data. + + +Touchstone +~~~~~~~~~~ + +TouchstoneData class is based on `scikit-rf `_ package and allows advanced +touchstone post-processing. +The following methods allows to read and check touchstone files. + +.. currentmodule:: ansys.aedt.core.visualization.advanced.touchstone_parser + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + TouchstoneData.get_insertion_loss_index + TouchstoneData.plot_insertion_losses + TouchstoneData.plot + TouchstoneData.plot_return_losses + TouchstoneData.get_mixed_mode_touchstone_data + TouchstoneData.get_return_loss_index + TouchstoneData.get_insertion_loss_index_from_prefix + TouchstoneData.get_next_xtalk_index + TouchstoneData.get_fext_xtalk_index_from_prefix + TouchstoneData.plot_next_xtalk_losses + TouchstoneData.plot_fext_xtalk_losses + TouchstoneData.get_worst_curve + read_touchstone + check_touchstone_files + find_touchstone_files + + +Here an example on how to use TouchstoneData class. + +.. code:: python + + from ansys.aedt.core.visualization.advanced.touchstone_parser import TouchstoneData + + ts1 = TouchstoneData(touchstone_file=os.path.join(test_T44_dir, "port_order_1234.s8p")) + assert ts1.get_mixed_mode_touchstone_data() + ts2 = TouchstoneData(touchstone_file=os.path.join(test_T44_dir, "port_order_1324.s8p")) + assert ts2.get_mixed_mode_touchstone_data(port_ordering="1324") + + assert ts1.plot_insertion_losses(plot=False) + assert ts1.get_worst_curve(curve_list=ts1.get_return_loss_index(), plot=False) + + +Farfield +~~~~~~~~ + +PyAEDT offers sophisticated tools for advanced farfield post-processing. +There are two complementary classes: ``FfdSolutionDataExporter`` and ``FfdSolutionData``. + +- FfdSolutionDataExporter: Enables efficient export and manipulation of farfield data. It allows users to convert simulation results into a standard metadata format for further analysis, or reporting. + +- FfdSolutionData: Focuses on the direct access and processing of farfield solution data. It supports a comprehensive set of postprocessing operations, from visualizing radiation patterns to computing key performance metrics. + + +.. currentmodule:: ansys.aedt.core.visualization.advanced.farfield_visualization + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + FfdSolutionData + + +This code shows how you can get the farfield data and perform some post-processing: + +.. code:: python + + import ansys.aedt.core + from ansys.aedt.core.generic.farfield_visualization import FfdSolutionDataExporter + app = ansys.aedt.core.Hfss() + ffdata = app.get_antenna_data(frequencies=None, + setup="Setup1 : Sweep", + sphere="3D", + variations=None, + overwrite=False, + link_to_hfss=True, + export_touchstone=True) + incident_power = ffdata.incident_power + ffdata.plot_cut(primary_sweep="Theta", theta=0) + ffdata.plot_contour(polar=True) + ffdata.plot_3d(show_geometry=False) + app.release_desktop(False, False) + +If you exported the farfield data previously, you can directly get the farfield data: + +.. code:: python + + from ansys.aedt.core.generic.farfield_visualization import FfdSolutionData + input_file = r"path_to_ffd\pyaedt_antenna_metadata.json" + ffdata = FfdSolutionData(input_file) + incident_power = ffdata.incident_power + ffdata.plot_cut(primary_sweep="Theta", theta=0) + ffdata.plot_contour(polar=True) + ffdata.plot_3d(show_geometry=False) + app.release_desktop(False, False) + +The following diagram shows both classes work. You can use them independently or from the ``get_antenna_data`` method. + + .. image:: ../../_static/farfield_visualization_pyaedt.png + :width: 800 + :alt: Farfield data with PyAEDT + + +If you have existing farfield data, or you want to export it manually, you can still use FfdSolutionData class. + + .. image:: ../../_static/farfield_visualization_aedt.png + :width: 800 + :alt: Farfield data with AEDT + + +Heterogeneous data message +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Heterogeneous data message (HDM) is the file exported from SBR+ solver containing rays information. +The following methods allows to read and plot rays information. + +.. currentmodule:: ansys.aedt.core.visualization.advanced + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + hdm_plot.HDMPlotter + sbrplus.hdm_parser.Parser + + +Miscellaneous +~~~~~~~~~~~~~ + +PyAEDT has additional advanced post-processing features: + +.. currentmodule:: ansys.aedt.core.visualization.advanced.misc + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + convert_nearfield_data + parse_rdat_file + nastran_to_stl + simplify_stl + diff --git a/doc/source/API/visualization/plot.rst b/doc/source/API/visualization/plot.rst new file mode 100644 index 00000000000..ce6d5f786e3 --- /dev/null +++ b/doc/source/API/visualization/plot.rst @@ -0,0 +1,82 @@ +Graphics operations +=================== + +PyAEDT enables powerful post-processing of AEDT results, +allowing you to visualize graphics objects and plot data with ease. + +PyAEDT supports external report capabilities available with installed third-party +packages like `pyvista `_, `matplotlib `_, +`pandas `_, and `numpy `_. + + +There have three main categories: + +* **Three-dimensional visualization** +* **Graph visualization** +* **PDF** + + +Three-dimensional visualization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PyAEDT benefits of `pyvista `_ package and allows to generate +models and 3D plots. + +.. currentmodule:: ansys.aedt.core.visualization.plot.pyvista + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + ModelPlotter + FieldClass + ObjClass + + +Graph visualization +~~~~~~~~~~~~~~~~~~~ + +PyAEDT benefits of `matplotlib `_ package and allows to generate 2D plots. + + +.. currentmodule:: ansys.aedt.core.visualization.plot.matplotlib + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + plot_polar_chart + plot_3d_chart + plot_2d_chart + plot_contour + is_notebook + + +PDF +~~~ + +PyAEDT benefits of `fpdf2 `_ package and allows to generate PDF files. + +.. currentmodule:: ansys.aedt.core.visualization.plot.pdf + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + AnsysReport.read_template + AnsysReport.header + AnsysReport.footer + AnsysReport.create + AnsysReport.add_project_info + AnsysReport.add_section + AnsysReport.add_chapter + AnsysReport.add_sub_chapter + AnsysReport.add_image + AnsysReport.add_caption + AnsysReport.add_empty_line + AnsysReport.add_page_break + AnsysReport.add_table + AnsysReport.add_text + AnsysReport.add_toc + AnsysReport.save_pdf + AnsysReport.add_chart diff --git a/doc/source/API/visualization/post.rst b/doc/source/API/visualization/post.rst new file mode 100644 index 00000000000..fdc7dfbfb60 --- /dev/null +++ b/doc/source/API/visualization/post.rst @@ -0,0 +1,265 @@ +AEDT post-processing +==================== + +AEDT offers a wide range of powerful post-processing tools for advanced data analysis and visualization. +PyAEDT provides dedicated classes that allow you to seamlessly interact with and modify these tools, expanding the scope of your data insights + + +.. note:: + Some functionalities are available only when AEDT is running + in graphical mode. + + +Core +~~~~ + +The following classes grant access to the core post-processing functionalities of AEDT: + +* **PostProcessor3D**: This class is utilized across all 3D applications, including HFSS, HFSS 3D Layout, Maxwell 3D and 2D, Q3D Extractor, and Mechanical AEDT. + +* **PostProcessorIcepak**: A specialized class for Icepak, which extends the ``PostProcessor3D`` class by adding features tailored to thermal analysis. + +* **PostProcessorCircuit**: This class handles schematic post-processing, supporting Circuit and Twin Builder applications. + + +.. currentmodule:: ansys.aedt.core.visualization.post.post_common_3d + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + PostProcessor3D + +.. currentmodule:: ansys.aedt.core.visualization.post.post_icepak + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + PostProcessorIcepak + +.. currentmodule:: ansys.aedt.core.visualization.post.post_circuit + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + PostProcessorCircuit + + +You can access these classes directly from the design object: + +.. code:: python + + from ansys.aedt.core import Hfss + app = Hfss(specified_version="2023.1", + non_graphical=False, new_desktop_session=True, + close_on_exit=True, student_version=False) + + # This call returns the PostProcessor class + post = app.post + + # This call returns a FieldPlot object + plotf = post.create_fieldplot_volume(objects, quantity_name, setup_name, intrinsics) + + # This call returns a SolutionData object + my_data = post.get_solution_data(expressions=trace_names) + + # This call returns a new standard report object and creates one or multiple reports from it. + standard_report = post.reports_by_category.standard("db(S(1,1))") + report_standard.create() + sols = report_standard.get_solution_data() + + +User can get the properties of the default reports using the following class: + +.. currentmodule:: ansys.aedt.core.visualization.post.common + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + Reports + +.. code:: python + + from ansys.aedt.core import Hfss + from ansys.aedt.core.visualization.post.common import Reports + app = Hfss(specified_version="2024.2", + non_graphical=False, new_desktop_session=True, + close_on_exit=True, student_version=False) + reports = Reports(app.post, app.design_type) + app.release_desktop(False, False) + + +AEDT data is returned in a structured format, providing organized and detailed results. +For a comprehensive overview of the data structure and its capabilities, refer to the class definition below: + + +.. currentmodule:: ansys.aedt.core.visualization.post.solution_data + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + SolutionData + + +Field +~~~~~ + +AEDT offers additional specialized post-processing features for enhanced 3D field visualization and control. + + +The following classes manage all aspects of AEDT 3D post-processing and are utilized by the ``PostProcessor3D`` class: + +.. currentmodule:: ansys.aedt.core.visualization.post.field_data + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + FieldPlot + +.. code:: python + + from ansys.aedt.core import Hfss + + app = Hfss(specified_version="2024.2", + non_graphical=False, + new_desktop_session=False + ) + test_points = [["0mm", "0mm", "0mm"], ["100mm", "20mm", "0mm"], + ["71mm", "71mm", "0mm"], ["0mm", "100mm", "0mm"]] + p1 = app.modeler.create_polyline(test_points) + setup = app.create_setup() + + report = app.post.create_fieldplot_line(quantity="Mag_E", assignment=p1.name) + report.create() + app.release_desktop(False, False) + + +Additionally, the following classes control field overlay settings, +enabling precise adjustments to visualization parameters: + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + ColorMapSettings + AutoScale + MinMaxScale + Scale3DSettings + NumberFormat + MarkerSettings + ArrowSettings + FolderPlotSettings + +The ``fields_calculator`` module includes the ``FieldsCalculator`` class. +It provides methods to interact with AEDT Fields Calculator by adding, loading and deleting custom expressions. + +.. currentmodule:: ansys.aedt.core.visualization.post.fields_calculator + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + FieldsCalculator + + +HFSS +~~~~ + +For HFSS solutions, there are two additionally features: virtual ray tracing and farfield exporter. + +To define and control virtual ray tracing (VRT) you can use: + +.. currentmodule:: ansys.aedt.core.visualization.post.vrt_data + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + VRTFieldPlot + + +If you need to export HFSS far field data, then you can use the following feature to obtain the antenna metadata: + +.. currentmodule:: ansys.aedt.core.visualization.post.farfield_exporter + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + FfdSolutionDataExporter + +.. code:: python + + from ansys.aedt.core import Hfss + + app = Hfss() + + antenna_data = app.post.get_antenna_data() + app.release_desktop(False, False) + + +Icepak +~~~~~~ + +The ``monitor_icepak`` module includes the classes listed below to add, modify, and manage monitors during simulations. +Retrieve monitor values for post-processing and analysis to gain insights into key simulation metrics. +Methods and properties are accessible through the ``monitor`` property of the ``Icepak`` class. + +.. currentmodule:: ansys.aedt.core.visualization.post.monitor_icepak + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + Monitor + + +The ``field_summary`` module includes the classes listed below to the ``Icepak`` field summary. + +.. currentmodule:: ansys.aedt.core.visualization.post.field_summary + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + FieldSummary + + +Additional tools +~~~~~~~~~~~~~~~~ + +Finally, users can use additional AEDT postprocessing tools like SPiSim: + +.. currentmodule:: ansys.aedt.core.visualization.post.spisim + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + SpiSim + + +.. currentmodule:: ansys.aedt.core.visualization.post.spisim_com_configuration_files.com_parameters + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + COMParameters + COMParametersVer3p4 + + +If you are looking for Virtual Compliance post processing, you should use this set of features: + +.. currentmodule:: ansys.aedt.core.visualization.post.compliance + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + VirtualCompliance \ No newline at end of file diff --git a/doc/source/API/visualization/report.rst b/doc/source/API/visualization/report.rst new file mode 100644 index 00000000000..d922cd08990 --- /dev/null +++ b/doc/source/API/visualization/report.rst @@ -0,0 +1,81 @@ +Report management +================= + +AEDT provides extensive flexibility for generating reports. + + +PyAEDT includes dedicated classes to manipulate all report properties, +offering full control over report customization. + +.. note:: + Some functionalities are available only when AEDT is running + in graphical mode. + + +.. currentmodule:: ansys.aedt.core.visualization.report + +.. autosummary:: + :toctree: _autosummary + :nosignatures: + + standard.Standard + standard.Spectral + field.AntennaParameters + field.Fields + field.NearField + field.FarField + field.Emission + eye.EyeDiagram + eye.AMIConturEyeDiagram + eye.AMIEyeDiagram + emi.EMIReceiver + + +The following code shows how to use report modules in standalone mode. + +.. code:: python + + # Create `Mag_E` report in a polyline + + from ansys.aedt.core import Hfss + from ansys.aedt.core.visualization.report.field import Fields + + app = Hfss(specified_version="2024.2", + non_graphical=False, + new_desktop_session=False + ) + test_points = [["0mm", "0mm", "0mm"], ["100mm", "20mm", "0mm"], + ["71mm", "71mm", "0mm"], ["0mm", "100mm", "0mm"]] + p1 = app.modeler.create_polyline(test_points) + setup = app.create_setup() + + report = Fields(app=app, report_category="Fields", + setup_name=setup.name + " : LastAdaptive", + expressions="Mag_E") + report.polyline = p1.name + report.create() + + app.release_desktop(False, False) + + +You can use these classes directly from the application object: + +.. code:: python + + # Create `Mag_E` report in a polyline + + from ansys.aedt.core import Hfss + + app = Hfss(specified_version="2024.2", + non_graphical=False, + new_desktop_session=False + ) + test_points = [["0mm", "0mm", "0mm"], ["100mm", "20mm", "0mm"], + ["71mm", "71mm", "0mm"], ["0mm", "100mm", "0mm"]] + p1 = app.modeler.create_polyline(test_points) + setup = app.create_setup() + + report = app.post.reports_by_category.fields("Mag_E", setup.name + " : LastAdaptive", p1.name) + report.create() + + app.release_desktop(False, False) diff --git a/doc/source/Resources/farfield.png b/doc/source/Resources/farfield.png new file mode 100644 index 0000000000000000000000000000000000000000..1741114025b268359eb0f664cdf61a7fcbd5a8f9 GIT binary patch literal 302492 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

wa62mSrbB8DLfyayt{ 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�`wsifrs) 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 +:download:`CSV File example <../../../Resources/icepak_classic_powermap.csv>` diff --git a/doc/source/User_guide/pyaedt_extensions_doc/project/index.rst b/doc/source/User_guide/pyaedt_extensions_doc/project/index.rst new file mode 100644 index 00000000000..e16a10264ce --- /dev/null +++ b/doc/source/User_guide/pyaedt_extensions_doc/project/index.rst @@ -0,0 +1,32 @@ +Project extensions +================== + +.. grid:: 2 + + .. grid-item-card:: Import Nastran + :link: import_nastran + :link-type: doc + :margin: 2 2 0 0 + + Import a Nastran or STL file in any 3D modeler application. + + .. grid-item-card:: Configure Layout + :link: configure_edb + :link-type: doc + :margin: 2 2 0 0 + + Configure layout for PCB & package analysis. + + .. grid-item-card:: Advanced Fields Calculator + :link: advanced_fields_calculator + :link-type: doc + + Lear how to use the Advanced Fields Calculator. + + .. grid-item-card:: Kernel converter + :link: kernel_convert + :link-type: doc + :margin: 2 2 0 0 + + Lear how to convert projects from 2022R2 to newer versions. + diff --git a/doc/source/_static/extensions/create_power_map_ui.png b/doc/source/_static/extensions/create_power_map_ui.png new file mode 100644 index 0000000000000000000000000000000000000000..4c1f7e0a61ec4cbd894be517ac57326b96c8e796 GIT binary patch literal 3308 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 literal 0 HcmV?d00001 diff --git a/doc/source/release_1_0.rst b/doc/source/release_1_0.rst index ff333240574..03f3562fc9f 100644 --- a/doc/source/release_1_0.rst +++ b/doc/source/release_1_0.rst @@ -176,7 +176,7 @@ The following table list the name changes with the old and new paths: +----------------------------------------------------------------+--------------------------------------------------------------------------+ | pyaedt\\modules\\OptimetricsTemplates.py | src\\ansys\\aedt\\core\\modules\\optimetrics_templates.py | +----------------------------------------------------------------+--------------------------------------------------------------------------+ -| pyaedt\\modules\\PostProcessor.py | src\\ansys\\aedt\\core\\modules\\post_processor.py | +| pyaedt\\modules\\PostProcessor.py | src\\ansys\\aedt\\core\\modules\\post_general.py | +----------------------------------------------------------------+--------------------------------------------------------------------------+ | pyaedt\\modules\\SetupTemplates.py | src\\ansys\\aedt\\core\\modules\\setup_templates.py | +----------------------------------------------------------------+--------------------------------------------------------------------------+ diff --git a/doc/styles/config/vocabularies/ANSYS/accept.txt b/doc/styles/config/vocabularies/ANSYS/accept.txt index 0ae1c2e5c92..8e10e5c78c8 100644 --- a/doc/styles/config/vocabularies/ANSYS/accept.txt +++ b/doc/styles/config/vocabularies/ANSYS/accept.txt @@ -34,6 +34,7 @@ file_path getters globals [Gg]RPC +HDM HFSS Huray Icepak @@ -59,6 +60,7 @@ numpydoc padstack padstacks parametrics +PDF PMs [Pp]olyline polylines diff --git a/examples/02-HFSS/Array.py b/examples/02-HFSS/Array.py index 2cf014c912c..268fc00f254 100644 --- a/examples/02-HFSS/Array.py +++ b/examples/02-HFSS/Array.py @@ -12,7 +12,7 @@ import os import ansys.aedt.core -from ansys.aedt.core.generic.farfield_visualization import FfdSolutionData +from ansys.aedt.core.visualization.advanced.farfield_visualization import FfdSolutionData ########################################################## # Set AEDT version diff --git a/examples/03-Maxwell/Maxwell2D_DCConduction.py b/examples/03-Maxwell/Maxwell2D_DCConduction.py index 25cdafdb9cd..05e4861fefd 100644 --- a/examples/03-Maxwell/Maxwell2D_DCConduction.py +++ b/examples/03-Maxwell/Maxwell2D_DCConduction.py @@ -9,7 +9,7 @@ import ansys.aedt.core -from ansys.aedt.core.generic.pdf import AnsysReport +from ansys.aedt.core.post.pdf import AnsysReport ########################################################## # Set AEDT version diff --git a/examples/06-Multiphysics/Hfss_Icepak_Coupling.py b/examples/06-Multiphysics/Hfss_Icepak_Coupling.py index 341aae07c08..556bcc5217f 100644 --- a/examples/06-Multiphysics/Hfss_Icepak_Coupling.py +++ b/examples/06-Multiphysics/Hfss_Icepak_Coupling.py @@ -16,7 +16,7 @@ import os import ansys.aedt.core -from ansys.aedt.core.generic.pdf import AnsysReport +from ansys.aedt.core.post.pdf import AnsysReport ########################################################## # Set AEDT version diff --git a/examples/07-Circuit/Touchstone_Management.py b/examples/07-Circuit/Touchstone_Management.py index 609de2a7dca..bfabb7f8a73 100644 --- a/examples/07-Circuit/Touchstone_Management.py +++ b/examples/07-Circuit/Touchstone_Management.py @@ -22,7 +22,7 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Import Matplotlib, NumPy, and the Touchstone file. -from ansys.aedt.core.generic.touchstone_parser import read_touchstone +from ansys.aedt.core.post.touchstone_parser import read_touchstone ############################################################################### # Read Touchstone file diff --git a/examples/07-Circuit/Virtual_Compliance.py b/examples/07-Circuit/Virtual_Compliance.py index 73ee4b98689..9ba6d67d4c0 100644 --- a/examples/07-Circuit/Virtual_Compliance.py +++ b/examples/07-Circuit/Virtual_Compliance.py @@ -12,7 +12,7 @@ import os.path import ansys.aedt.core -from ansys.aedt.core.generic.compliance import VirtualCompliance +from ansys.aedt.core.post.compliance import VirtualCompliance ########################################################## # Set AEDT version diff --git a/src/ansys/aedt/core/application/aedt_objects.py b/src/ansys/aedt/core/application/aedt_objects.py index 3805cd9ce8b..7a40d704c13 100644 --- a/src/ansys/aedt/core/application/aedt_objects.py +++ b/src/ansys/aedt/core/application/aedt_objects.py @@ -335,7 +335,7 @@ def ofieldsreporter(self): Returns ------- - :attr:`ansys.aedt.core.modules.post_processor.PostProcessor.ofieldsreporter` + :attr:`ansys.aedt.core.modules.post_general.PostProcessor.ofieldsreporter` References ---------- @@ -362,7 +362,7 @@ def oreportsetup(self): Returns ------- - :attr:`ansys.aedt.core.modules.post_processor.PostProcessor.oreportsetup` + :attr:`ansys.aedt.core.modules.post_general.PostProcessor.oreportsetup` References ---------- diff --git a/src/ansys/aedt/core/application/analysis_3d.py b/src/ansys/aedt/core/application/analysis_3d.py index 6511784a716..322acfa218a 100644 --- a/src/ansys/aedt/core/application/analysis_3d.py +++ b/src/ansys/aedt/core/application/analysis_3d.py @@ -184,20 +184,14 @@ def post(self): Returns ------- - :class:`ansys.aedt.core.modules.advanced_post_processing.PostProcessor` + :class:`ansys.aedt.core.visualization.post.post_common_3d.PostProcessor3D` or + :class:`ansys.aedt.core.visualization.post.post_icepak.PostProcessorIcepak` PostProcessor object. """ if self._post is None and self._odesign: - self.logger.reset_timer() - if is_ironpython: # pragma: no cover - from ansys.aedt.core.modules.post_processor import PostProcessor - elif self.design_type == "Icepak": - from ansys.aedt.core.modules.advanced_post_processing import IcepakPostProcessor as PostProcessor - else: - from ansys.aedt.core.modules.advanced_post_processing import PostProcessor - self._post = PostProcessor(self) - self.logger.info_timer("Post class has been initialized!") + from ansys.aedt.core.visualization.post import post_processor + self._post = post_processor(self) return self._post @property diff --git a/src/ansys/aedt/core/application/analysis_3d_layout.py b/src/ansys/aedt/core/application/analysis_3d_layout.py index 182eeab6c03..dfae909eb90 100644 --- a/src/ansys/aedt/core/application/analysis_3d_layout.py +++ b/src/ansys/aedt/core/application/analysis_3d_layout.py @@ -26,7 +26,6 @@ from ansys.aedt.core.application.analysis import Analysis from ansys.aedt.core.generic.configurations import Configurations3DLayout -from ansys.aedt.core.generic.general_methods import is_ironpython from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.generic.settings import settings from ansys.aedt.core.modules.setup_templates import SetupKeys @@ -148,18 +147,13 @@ def post(self): Returns ------- - :class:`ansys.aedt.core.modules.advanced_post_processing.PostProcessor` + :class:`ansys.aedt.core.visualization.post.post_common_3d.PostProcessor3D` PostProcessor object. """ if self._post is None and self._odesign: - self.logger.reset_timer() - if is_ironpython: # pragma: no cover - from ansys.aedt.core.modules.post_processor import PostProcessor - else: - from ansys.aedt.core.modules.advanced_post_processing import PostProcessor - self._post = PostProcessor(self) - self.logger.info_timer("Post class has been initialized!") + from ansys.aedt.core.visualization.post import post_processor + self._post = post_processor(self) return self._post @property diff --git a/src/ansys/aedt/core/application/analysis_circuit_netlist.py b/src/ansys/aedt/core/application/analysis_circuit_netlist.py index 2003d59f0ef..558b0760898 100644 --- a/src/ansys/aedt/core/application/analysis_circuit_netlist.py +++ b/src/ansys/aedt/core/application/analysis_circuit_netlist.py @@ -109,15 +109,13 @@ def post(self): Returns ------- - :class:`ansys.aedt.core.modules.advanced_post_processing.CircuitPostProcessor` + :class:`ansys.aedt.core.visualization.post.post_circuit.PostProcessorCircuit` PostProcessor object. """ if self._post is None and self._odesign: - self.logger.reset_timer() - from ansys.aedt.core.modules.advanced_post_processing import PostProcessor + from ansys.aedt.core.visualization.post import post_processor - self._post = PostProcessor(self) - self.logger.info_timer("Post class has been initialized!") + self._post = post_processor(self) return self._post @property diff --git a/src/ansys/aedt/core/application/analysis_hf.py b/src/ansys/aedt/core/application/analysis_hf.py index fe6a6b594be..baa9e3a5b74 100644 --- a/src/ansys/aedt/core/application/analysis_hf.py +++ b/src/ansys/aedt/core/application/analysis_hf.py @@ -313,7 +313,7 @@ def get_touchstone_data(self, setup=None, sweep=None, variations=None): >>> oModule.GetSolutionDataPerVariation """ - from ansys.aedt.core.generic.touchstone_parser import TouchstoneData + from ansys.aedt.core.visualization.advanced.touchstone_parser import TouchstoneData if not setup: setup = self._app.setups[0].name diff --git a/src/ansys/aedt/core/application/analysis_nexxim.py b/src/ansys/aedt/core/application/analysis_nexxim.py index bea93b6d1fc..08f9d93a8a1 100644 --- a/src/ansys/aedt/core/application/analysis_nexxim.py +++ b/src/ansys/aedt/core/application/analysis_nexxim.py @@ -177,16 +177,13 @@ def post(self): Returns ------- - :class:`ansys.aedt.core.modules.advanced_post_processing.CircuitPostProcessor` + :class:`ansys.aedt.core.visualization.post.post_circuit.PostProcessorCircuit` PostProcessor object. """ if self._post is None and self._odesign: - self.logger.reset_timer() - from ansys.aedt.core.modules.post_processor import CircuitPostProcessor - - self._post = CircuitPostProcessor(self) - self.logger.info_timer("Post class has been initialized!") + from ansys.aedt.core.visualization.post import post_processor + self._post = post_processor(self) return self._post @property diff --git a/src/ansys/aedt/core/application/analysis_r_m_xprt.py b/src/ansys/aedt/core/application/analysis_r_m_xprt.py index a41a528c900..4af51cf8634 100644 --- a/src/ansys/aedt/core/application/analysis_r_m_xprt.py +++ b/src/ansys/aedt/core/application/analysis_r_m_xprt.py @@ -87,15 +87,13 @@ def post(self): Returns ------- - :class:`ansys.aedt.core.modules.post_processor.CircuitPostProcessor` + :class:`ansys.aedt.core.visualization.post.post_circuit.PostProcessorCircuit` + PostProcessor object. """ - if self._post is None and self._odesign: # pragma: no cover - self.logger.reset_timer() - from ansys.aedt.core.modules.post_processor import CircuitPostProcessor - - self._post = CircuitPostProcessor(self) - self.logger.info_timer("Post class has been initialized!") + if self._post is None and self._odesign: + from ansys.aedt.core.visualization.post import post_processor + self._post = post_processor(self) return self._post @property diff --git a/src/ansys/aedt/core/application/analysis_twin_builder.py b/src/ansys/aedt/core/application/analysis_twin_builder.py index e1321176f70..7bfd644282b 100644 --- a/src/ansys/aedt/core/application/analysis_twin_builder.py +++ b/src/ansys/aedt/core/application/analysis_twin_builder.py @@ -135,15 +135,13 @@ def post(self): Returns ------- - :class:`ansys.aedt.core.modules.post_processor.CircuitPostProcessor` + :class:`ansys.aedt.core.visualization.post.post_circuit.PostProcessorCircuit` + PostProcessor object. """ - if self._post is None and self._odesign: # pragma: no cover - self.logger.reset_timer() - from ansys.aedt.core.modules.post_processor import CircuitPostProcessor - - self._post = CircuitPostProcessor(self) - self.logger.info_timer("Post class has been initialized!") + if self._post is None and self._odesign: + from ansys.aedt.core.visualization.post import post_processor + self._post = post_processor(self) return self._post @pyaedt_function_handler(setupname="name", setuptype="setup_type") diff --git a/src/ansys/aedt/core/circuit.py b/src/ansys/aedt/core/circuit.py index 89ef67149f6..946a892ca56 100644 --- a/src/ansys/aedt/core/circuit.py +++ b/src/ansys/aedt/core/circuit.py @@ -146,7 +146,7 @@ class Circuit(FieldAnalysisCircuit, ScatteringMethods): Create an instance of Circuit using the 2023 R2 student version and open the specified project, which is named ``"myfile.aedt"``. - >>> hfss = Circuit(version="2023.2", project="myfile.aedt", student_version=True) + >>> aedtapp = Circuit(version="2023.2", project="myfile.aedt", student_version=True) """ diff --git a/src/ansys/aedt/core/desktop.py b/src/ansys/aedt/core/desktop.py index 0134647459b..5f4061f8a22 100644 --- a/src/ansys/aedt/core/desktop.py +++ b/src/ansys/aedt/core/desktop.py @@ -450,7 +450,7 @@ class Desktop(object): machine : str, optional Machine name to connect the oDesktop session to. This parameter works only in 2022 R2 and later. The remote server must be up and running with the command - `"ansysedt.exe -grpcsrv portnum"`. If the machine is `"localhost"`, the server also + ``"ansysedt.exe -grpcsrv portnum"``. If the machine is `"localhost"`, the server also starts if not present. port : int, optional Port number on which to start the oDesktop communication on the already existing server. @@ -479,6 +479,7 @@ class Desktop(object): PyAEDT INFO: Python version ... >>> hfss = ansys.aedt.core.Hfss(design="HFSSDesign1") PyAEDT INFO: No project is defined. Project... + """ # _sessions = {} diff --git a/src/ansys/aedt/core/generic/configurations.py b/src/ansys/aedt/core/generic/configurations.py index 8a33d090404..a26f1d41dbc 100644 --- a/src/ansys/aedt/core/generic/configurations.py +++ b/src/ansys/aedt/core/generic/configurations.py @@ -2018,26 +2018,47 @@ def apply_operations_to_native_components(obj, operation_dict, native_dict): # self._app.modeler.refresh_all_ids() old_objs = list(self._app.modeler.user_defined_components.keys()) if operation_dict["Props"]["Command"] == "Move": - obj.move( - [decompose_variable_value(operation_dict["Props"]["Move Vector"][2 * i + 1])[0] for i in range(3)] - ) + if len(operation_dict["Props"]["Move Vector"]) == 6: + operation_list = [ + decompose_variable_value(operation_dict["Props"]["Move Vector"][2 * i + 1])[0] for i in range(3) + ] + else: + operation_list = [ + decompose_variable_value(operation_dict["Props"]["Move Vector"][i])[0] for i in range(3) + ] + obj.move(operation_list) elif operation_dict["Props"]["Command"] == "Rotate": rotation = decompose_variable_value(operation_dict["Props"]["Angle"]) obj.rotate(operation_dict["Props"]["Axis"], angle=rotation[0], units=rotation[1]) elif operation_dict["Props"]["Command"] == "Mirror": - obj.mirror( - [ + if len(operation_dict["Props"]["Base Position"]) == 6: + base_list = [ decompose_variable_value(operation_dict["Props"]["Base Position"][2 * i + 1])[0] for i in range(3) - ], - [ + ] + normal_list = [ decompose_variable_value(operation_dict["Props"]["Normal Position"][2 * i + 1])[0] for i in range(3) - ], - ) + ] + + else: + base_list = [ + decompose_variable_value(operation_dict["Props"]["Base Position"][i])[0] for i in range(3) + ] + normal_list = [ + decompose_variable_value(operation_dict["Props"]["Normal Position"][i])[0] for i in range(3) + ] + + obj.mirror(base_list, normal_list) elif operation_dict["Props"]["Command"] == "DuplicateAlongLine": + if len(operation_dict["Props"]["Vector"]) == 6: + vector_list = [ + decompose_variable_value(operation_dict["Props"]["Vector"][2 * i + 1])[0] for i in range(3) + ] + else: + vector_list = [decompose_variable_value(operation_dict["Props"]["Vector"][i])[0] for i in range(3)] new_objs = obj.duplicate_along_line( - [decompose_variable_value(operation_dict["Props"]["Vector"][2 * i + 1])[0] for i in range(3)], + vector_list, nclones=operation_dict["Props"]["Total Number"], attach_object=operation_dict["Props"]["Attach To Original Object"], ) @@ -2047,15 +2068,26 @@ def apply_operations_to_native_components(obj, operation_dict, native_dict): # operation_dict["Props"]["Axis"], angle=rotation[0], clones=operation_dict["Props"]["Total Number"] ) elif operation_dict["Props"]["Command"] == "DuplicateMirror": - new_objs = obj.duplicate_and_mirror( - origin=[ + if len(operation_dict["Props"]["Base Position"]) == 6: + base_list = [ decompose_variable_value(operation_dict["Props"]["Base Position"][2 * i + 1])[0] for i in range(3) - ], - vector=[ + ] + normal_list = [ decompose_variable_value(operation_dict["Props"]["Normal Position"][2 * i + 1])[0] for i in range(3) - ], + ] + + else: + base_list = [ + decompose_variable_value(operation_dict["Props"]["Base Position"][i])[0] for i in range(3) + ] + normal_list = [ + decompose_variable_value(operation_dict["Props"]["Normal Position"][i])[0] for i in range(3) + ] + new_objs = obj.duplicate_and_mirror( + base_list, + normal_list, ) else: # pragma: no cover raise ValueError("Operation not supported") diff --git a/src/ansys/aedt/core/generic/design_types.py b/src/ansys/aedt/core/generic/design_types.py index cc3b4453225..dbdf3369447 100644 --- a/src/ansys/aedt/core/generic/design_types.py +++ b/src/ansys/aedt/core/generic/design_types.py @@ -216,7 +216,7 @@ def get_pyaedt_app(project_name=None, design_name=None, desktop=None): odesktop = desktop.odesktop break elif _desktop_sessions: - odesktop = list(_desktop_sessions.values())[-1] + odesktop = list(_desktop_sessions.values())[-1].odesktop elif "oDesktop" in dir(sys.modules["__main__"]): # ironpython odesktop = sys.modules["__main__"].oDesktop # ironpython else: diff --git a/src/ansys/aedt/core/generic/near_field_import.py b/src/ansys/aedt/core/generic/near_field_import.py deleted file mode 100644 index a326ca10d4f..00000000000 --- a/src/ansys/aedt/core/generic/near_field_import.py +++ /dev/null @@ -1,196 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import csv -import os -import re - -from ansys.aedt.core.generic.filesystem import search_files -from ansys.aedt.core.generic.general_methods import open_file - - -class BoxFacePointsAndFields(object): - """Data model class containing field component and coordinates.""" - - def __init__(self): - self.x = [] - self.y = [] - self.z = [] - self.re = {"Ex": [], "Ey": [], "Ez": [], "Hx": [], "Hy": [], "Hz": []} - self.im = {"Ex": [], "Ey": [], "Ez": [], "Hx": [], "Hy": [], "Hz": []} - - def set_xyz_points(self, x, y, z): - self.x = x - self.y = y - self.z = z - - def set_field_component(self, field_component, real, imag, invert): - """Set Field component Real and imaginary parts.""" - if field_component in self.re: - if invert: - self.re[field_component] = [str(-float(i)) for i in real] - self.im[field_component] = [str(-float(i)) for i in imag] - else: - self.re[field_component] = real - self.im[field_component] = imag - else: - print("Error in set_field_component function.") - - def fill_empty_data(self): - for el, val in self.re.items(): - if not val: - zero_field_z_faces = [0] * len(self.x) - self.re[el] = zero_field_z_faces - for el, val in self.im.items(): - if not val: - zero_field_z_faces = [0] * len(self.x) - self.im[el] = zero_field_z_faces - - -def convert_nearfield_data(dat_folder, frequency=6, invert_phase_for_lower_faces=True, output_folder=None): - """Convert a near field data folder to hfss `nfd` file and link it to `and` file. - - Parameters - ---------- - dat_folder : str - Full path to the folder containing near fields data. - Folder will contain 24 files in the following format: `data_Ex_ymin.dat`. Same for H Fields. - frequency : float, int, str - Frequency in `GHz`. - invert_phase_for_lower_faces : bool - Add 180 deg for all fields at 'negative' faces (xmin, ymin, zmin). - output_folder : str, optional - Output folder where files will be saved. - - Returns - ------- - str - Full path to `.and` file. - """ - file_keys = ["xmin", "xmax", "ymin", "ymax", "zmin", "zmax"] - components = { - "xmin": BoxFacePointsAndFields(), - "ymin": BoxFacePointsAndFields(), - "zmin": BoxFacePointsAndFields(), - "xmax": BoxFacePointsAndFields(), - "ymax": BoxFacePointsAndFields(), - "zmax": BoxFacePointsAndFields(), - } - - file_names = search_files(dat_folder, "*.dat") - for data_file in file_names: - match = re.search(r"data_(\S+)_(\S+).dat", os.path.basename(data_file)) - field_component = match.group(1) - face = match.group(2) - - if not os.path.exists(data_file): - continue - # Read in all data for the current file - x, y, z = [], [], [] - real, imag = [], [] - with open_file(data_file, "r") as f: - for line in f: - line = line.strip().split(" ") - if len(line) == 5: - x.append(line[0]) - y.append(line[1]) - z.append(line[2]) - real.append(line[3]) - imag.append(line[4]) - - assert face in components, "Wrong file name format. Face not found." - if not components[face].x: - components[face].set_xyz_points(x, y, z) - components[face].fill_empty_data() - if "min" in face: - components[face].set_field_component(field_component, real, imag, invert_phase_for_lower_faces) - else: - components[face].set_field_component(field_component, real, imag, False) - - full_data = [] - index = 1 - for el in list(file_keys): - for k in range(index, index + len(components[el].x)): - row = [] - row.append(k) - row.append(components[el].x[k - index]) - row.append(components[el].y[k - index]) - row.append(components[el].z[k - index]) - for field in ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"]: - row.append(components[el].re[field][k - index]) - row.append(components[el].im[field][k - index]) - full_data.append(row) - index += len(components[el].x) - - # WRITE .NFD FILE - #################################################################################################### - # .nfd file needs the following 16 columns where index starts with 1: - # index, x, y, z, re_ex, im_ex, re_ey, im_ey, re_ez, im_ez, re_hx, im_hx, re_hy, im_hy, re_hz, im_hz - if not output_folder: - output_folder = os.path.dirname(dat_folder) - directory_name = os.path.basename(dat_folder) - nfd_name = directory_name + ".nfd" - nfd_full_file = os.path.join(output_folder, nfd_name) - and_full_file = os.path.join(output_folder, directory_name + ".and") - - commented_header_line = "#Index, X, Y, Z, Ex(real, imag), Ey(real, imag), Ez(real, imag), " - commented_header_line += "Hx(real, imag), Hy(real, imag), Hz(real, imag)\n" - - with open_file(nfd_full_file, "w") as file: - writer = csv.writer(file, delimiter=",", lineterminator="\n") - file.write(commented_header_line) - file.write("Frequencies 1\n") - file.write("Frequency " + str(frequency) + "GHz\n") - writer.writerows(full_data) - - print(".nfd file written to %s" % nfd_full_file) # Prints if running ipy64 through external editor - - size_x = float(components["xmax"].x[0]) - float(components["xmin"].x[0]) - size_y = float(components["ymax"].y[0]) - float(components["ymin"].y[0]) - size_z = float(components["zmax"].z[0]) - float(components["zmin"].z[0]) - - center_x = float(components["xmin"].x[0]) + float(size_x / 2.0) - center_y = float(components["ymin"].y[0]) + float(size_y / 2.0) - center_z = float(components["zmin"].z[0]) + float(size_z / 2.0) - - sx_mm = size_x * 1000 - sy_mm = size_y * 1000 - sz_mm = size_z * 1000 - cx_mm = center_x * 1000 - cy_mm = center_y * 1000 - cz_mm = center_z * 1000 - - with open_file(and_full_file, "w") as file: - file.write("$begin 'NearFieldHeader'\n") - file.write(" type='nfd'\n") - file.write(" fields='EH'\n") - file.write(" fsweep='" + str(frequency) + "GHz'\n") - file.write(" geometry='box'\n") - file.write(" center='" + str(cx_mm) + "mm," + str(cy_mm) + "mm," + str(cz_mm) + "mm'\n") - file.write(" size='" + str(sx_mm) + "mm," + str(sy_mm) + "mm," + str(sz_mm) + "mm'\n") - file.write("$end 'NearFieldHeader'\n") - file.write("$begin 'NearFieldData'\n") - file.write(' FreqData("' + str(frequency) + 'GHz","' + nfd_name + '")\n') - file.write("$end 'NearFieldData'\n") - return and_full_file diff --git a/src/ansys/aedt/core/generic/report_file_parser.py b/src/ansys/aedt/core/generic/report_file_parser.py deleted file mode 100644 index 63dbf1725d1..00000000000 --- a/src/ansys/aedt/core/generic/report_file_parser.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from ansys.aedt.core.generic.constants import SI_UNITS -from ansys.aedt.core.generic.constants import unit_system -from ansys.aedt.core.generic.load_aedt_file import load_keyword_in_aedt_file - - -def parse_rdat_file(file_path): - """ - Parse Ansys report .rdat file - - Returns: - (dict) report data - """ - report_dict = {} - # data = load_entire_aedt_file(file_path) - data = load_keyword_in_aedt_file(file_path, "ReportsData") - - report_data = data["ReportsData"]["RepMgrRepsData"] - for report_name in report_data: - report_dict[report_name] = {} - for trace, trace_data in report_data[report_name]["Traces"].items(): - all_data = trace_data["TraceComponents"]["TraceDataComps"]["0"] - if all_data["TraceDataCol"]["ParameterType"] == "ComplexParam": - all_data_values = all_data["TraceDataCol"]["ColumnValues"] - all_re_values = all_data_values[0::2] - all_im_values = all_data_values[1::2] - all_x_values = trace_data["PrimarySweepInfo"]["PrimarySweepCol"]["ColumnValues"] - si_unit_x = SI_UNITS[unit_system(trace_data["PrimarySweepInfo"]["PrimarySweepCol"]["Units"])] - si_unit_y = SI_UNITS[unit_system(all_data["TraceDataCol"]["Units"])] - report_dict[report_name][trace_data["TraceName"]] = { - "x_name": all_data["TraceCompExpr"], - "x_unit": si_unit_x, - "y_unit": si_unit_y, - "curves": {}, - } - for curve, curve_data in trace_data["CurvesInfo"].items(): - report_dict[report_name][trace_data["TraceName"]]["curves"][curve_data[1] + "real"] = { - "x_data": all_x_values[0 : curve_data[0]], - "y_data": all_re_values[0 : curve_data[0]], - } - report_dict[report_name][trace_data["TraceName"]]["curves"][curve_data[1] + "imag"] = { - "x_data": all_x_values[0 : curve_data[0]], - "y_data": all_im_values[0 : curve_data[0]], - } - all_x_values = all_x_values[curve_data[0] :] - all_re_values = all_re_values[curve_data[0] :] - all_im_values = all_im_values[curve_data[0] :] - - else: - y_data = trace_data["TraceComponents"]["TraceDataComps"]["1"] - all_x_values = all_data["TraceDataCol"]["ColumnValues"] - all_y_values = y_data["TraceDataCol"]["ColumnValues"] - si_unit_x = SI_UNITS[unit_system(all_data["TraceDataCol"]["Units"])] - si_unit_y = SI_UNITS[unit_system(y_data["TraceDataCol"]["Units"])] - report_dict[report_name][trace_data["TraceName"]] = { - "x_name": all_data["TraceCompExpr"], - "x_unit": si_unit_x, - "y_unit": si_unit_y, - "curves": {}, - } - for curve, curve_data in trace_data["CurvesInfo"].items(): - report_dict[report_name][trace_data["TraceName"]]["curves"][curve_data[1]] = { - "x_data": all_x_values[0 : curve_data[0]], - "y_data": all_y_values[0 : curve_data[0]], - } - all_x_values = all_x_values[curve_data[0] :] - all_y_values = all_y_values[curve_data[0] :] - - return report_dict diff --git a/src/ansys/aedt/core/hfss.py b/src/ansys/aedt/core/hfss.py index f821b3d2efd..fa5a1e7b9e4 100644 --- a/src/ansys/aedt/core/hfss.py +++ b/src/ansys/aedt/core/hfss.py @@ -5913,8 +5913,8 @@ def get_antenna_data( >>> ffdata = hfss.get_antenna_data() >>> ffdata.farfield_data.plot_cut(primary_sweep="theta",theta=0,is_polar=False) """ - from ansys.aedt.core.generic.farfield_visualization import FfdSolutionData - from ansys.aedt.core.generic.farfield_visualization import FfdSolutionDataExporter + from ansys.aedt.core.visualization.advanced.farfield_visualization import FfdSolutionData + from ansys.aedt.core.visualization.post.farfield_exporter import FfdSolutionDataExporter if not variations: variations = self.available_variations.nominal_w_values_dict_w_dependent @@ -6164,7 +6164,7 @@ def parse_hdm_file(self, file_name): :class:`ansys.aedt.core.modules.hdm_parser.Parser` """ - from ansys.aedt.core.sbrplus.hdm_parser import Parser + from ansys.aedt.core.visualization.advanced.sbrplus.hdm_parser import Parser if os.path.exists(file_name): return Parser(file_name).parse_message() @@ -6184,7 +6184,7 @@ def get_hdm_plotter(self, file_name=None): :class:`ansys.aedt.core.sbrplus.plot.HDMPlotter` """ - from ansys.aedt.core.sbrplus.plot import HDMPlotter + from ansys.aedt.core.visualization.advanced.hdm_plot import HDMPlotter hdm = HDMPlotter() files = self.post.export_model_obj(export_as_single_objects=True, air_objects=False) diff --git a/src/ansys/aedt/core/hfss3dlayout.py b/src/ansys/aedt/core/hfss3dlayout.py index 761de8cea7f..cd2d96d27dc 100644 --- a/src/ansys/aedt/core/hfss3dlayout.py +++ b/src/ansys/aedt/core/hfss3dlayout.py @@ -2207,7 +2207,9 @@ def get_dcir_solution_data(self, setup, show="RL", category="Loop_Resistance"): all_quantities = self.post.available_report_quantities( context=show, is_siwave_dc=True, quantities_category=category ) - + if not all_quantities: + self._logger.error("No expressions found.") + return False return self.post.get_solution_data(all_quantities, setup_sweep_name=setup, domain="DCIR", context=show) @pyaedt_function_handler(setup_name="setup") diff --git a/src/ansys/aedt/core/icepak.py b/src/ansys/aedt/core/icepak.py index 6ea377b0d5b..44a116bf73f 100644 --- a/src/ansys/aedt/core/icepak.py +++ b/src/ansys/aedt/core/icepak.py @@ -60,8 +60,8 @@ from ansys.aedt.core.modules.boundary import SinusoidalDictionary from ansys.aedt.core.modules.boundary import SquareWaveDictionary from ansys.aedt.core.modules.boundary import _create_boundary -from ansys.aedt.core.modules.monitor_icepak import Monitor from ansys.aedt.core.modules.setup_templates import SetupKeys +from ansys.aedt.core.visualization.post.monitor_icepak import Monitor class Icepak(FieldAnalysis3D): diff --git a/src/ansys/aedt/core/modeler/cad/elements_3d.py b/src/ansys/aedt/core/modeler/cad/elements_3d.py index 174d8b76a11..b0f1d2446f9 100644 --- a/src/ansys/aedt/core/modeler/cad/elements_3d.py +++ b/src/ansys/aedt/core/modeler/cad/elements_3d.py @@ -1424,14 +1424,44 @@ def __init__(self, node, child_object, first_level=False, get_child_obj_arg=None del self.children[name] else: self.props = {} - for p in self.child_object.GetPropNames(): + if settings.aedt_version >= "2024.2": try: - self.props[p] = self.child_object.GetPropValue(p) + props = self._get_data_model() + for p in self.child_object.GetPropNames(): + if p in props: + self.props[p] = props[p] + else: + self.props[p] = None except Exception: - self.props[p] = None + for p in self.child_object.GetPropNames(): + try: + self.props[p] = self.child_object.GetPropValue(p) + except Exception: + self.props[p] = None + else: + for p in self.child_object.GetPropNames(): + try: + self.props[p] = self.child_object.GetPropValue(p) + except Exception: + self.props[p] = None self.props = HistoryProps(self, self.props) self.command = self.props.get("Command", "") + def _get_data_model(self): + import ast + + input_str = self.child_object.GetDataModel(-1, 1, 1).replace("false", "False").replace("true", "True") + props_list = ast.literal_eval(input_str) + props = {} + for prop in props_list["properties"]: + if "value" in prop: + props[prop["name"]] = prop["value"] + elif "values" in prop: + props[prop["name"]] = prop["values"] + else: + props[prop["name"]] = None + return props + def update_property(self, prop_name, prop_value): """Update the property of the binary tree node. diff --git a/src/ansys/aedt/core/modeler/cad/polylines.py b/src/ansys/aedt/core/modeler/cad/polylines.py index 3951624375d..05cd2649725 100644 --- a/src/ansys/aedt/core/modeler/cad/polylines.py +++ b/src/ansys/aedt/core/modeler/cad/polylines.py @@ -426,24 +426,66 @@ def _convert_points(p_in, dest_unit): s_type = c.props["Segment Type"] if i == 0: # append the first point only for the first segment if s_type != "Center Point Arc": - points.append(_convert_points(list(c.props["Point1"])[1::2], self._primitives.model_units)) + p = [ + c.props["Point1/X"], + c.props["Point1/Y"], + c.props["Point1/Z"], + ] + points.append(_convert_points(p, self._primitives.model_units)) else: - points.append(_convert_points(list(c.props["Start Point"])[1::2], self._primitives.model_units)) + p = [ + c.props["Start Point/X"], + c.props["Start Point/Y"], + c.props["Start Point/Z"], + ] + points.append(_convert_points(p, self._primitives.model_units)) if s_type == "Line": segments.append(PolylineSegment("Line")) - points.append(_convert_points(list(c.props["Point2"])[1::2], self._primitives.model_units)) + p = [ + c.props["Point2/X"], + c.props["Point2/Y"], + c.props["Point2/Z"], + ] + points.append(_convert_points(p, self._primitives.model_units)) elif s_type == "3 Point Arc": segments.append(PolylineSegment("Arc")) - points.append(_convert_points(list(c.props["Point2"])[1::2], self._primitives.model_units)) - points.append(_convert_points(list(c.props["Point3"])[1::2], self._primitives.model_units)) + p2 = [ + c.props["Point2/X"], + c.props["Point2/Y"], + c.props["Point2/Z"], + ] + p3 = [ + c.props["Point3/X"], + c.props["Point3/Y"], + c.props["Point3/Z"], + ] + + points.append(_convert_points(p2, self._primitives.model_units)) + points.append(_convert_points(p3, self._primitives.model_units)) elif s_type == "Spline": segments.append(PolylineSegment("Spline", num_points=n_points)) for p in range(2, n_points + 1): - point_attr = c.props["Point" + str(p)] - points.append(_convert_points(list(point_attr)[1::2], self._primitives.model_units)) + point_attr = "Point" + str(p) + p2 = [ + c.props[f"{point_attr}/X"], + c.props[f"{point_attr}/Y"], + c.props[f"{point_attr}/Z"], + ] + + points.append(_convert_points(p2, self._primitives.model_units)) elif s_type == "Center Point Arc": - start = _convert_points(list(c.props["Start Point"])[1::2], self._primitives.model_units) - center = _convert_points(list(c.props["Center Point"])[1::2], self._primitives.model_units) + p2 = [ + c.props["Start Point/X"], + c.props["Start Point/Y"], + c.props["Start Point/Z"], + ] + p3 = [ + c.props["Center Point/X"], + c.props["Center Point/Y"], + c.props["Center Point/Z"], + ] + start = _convert_points(p2, self._primitives.model_units) + center = _convert_points(p3, self._primitives.model_units) plane = c.props["Plane"] angle = c.props["Angle"] arc_seg = PolylineSegment("AngularArc", arc_angle=angle, arc_center=center, arc_plane=plane) diff --git a/src/ansys/aedt/core/modeler/modeler_3d.py b/src/ansys/aedt/core/modeler/modeler_3d.py index 6ce22683626..311eb8748fb 100644 --- a/src/ansys/aedt/core/modeler/modeler_3d.py +++ b/src/ansys/aedt/core/modeler/modeler_3d.py @@ -37,7 +37,7 @@ from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.modeler.cad.primitives_3d import Primitives3D from ansys.aedt.core.modeler.geometry_operators import GeometryOperators -from ansys.aedt.core.modules.solutions import nastran_to_stl +from ansys.aedt.core.visualization.advanced.misc import nastran_to_stl class Modeler3D(Primitives3D): diff --git a/src/ansys/aedt/core/modules/advanced_post_processing.py b/src/ansys/aedt/core/modules/advanced_post_processing.py deleted file mode 100644 index cc2fb24864b..00000000000 --- a/src/ansys/aedt/core/modules/advanced_post_processing.py +++ /dev/null @@ -1,1324 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -""" -This module contains the `PostProcessor` class. - -It contains all advanced postprocessing functionalities that require Python 3.x packages like NumPy and Matplotlib. -""" - -from __future__ import absolute_import # noreorder - -import csv -import os -import re -import warnings - -from ansys.aedt.core import generate_unique_name -from ansys.aedt.core.generic.general_methods import is_ironpython -from ansys.aedt.core.generic.general_methods import open_file -from ansys.aedt.core.generic.general_methods import pyaedt_function_handler -from ansys.aedt.core.generic.plot import ModelPlotter -from ansys.aedt.core.generic.settings import settings -from ansys.aedt.core.modules.fields_calculator import FieldsCalculator -from ansys.aedt.core.modules.post_processor import FieldSummary -from ansys.aedt.core.modules.post_processor import PostProcessor as Post -from ansys.aedt.core.modules.post_processor import TOTAL_QUANTITIES - -if not is_ironpython: - try: - import numpy as np - except ImportError: - warnings.warn( - "The NumPy module is required to run some functionalities of PostProcess.\n" - "Install with \n\npip install numpy\n\nRequires CPython." - ) - - try: - from IPython.display import Image - - ipython_available = True - except ImportError: - ipython_available = False - - -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 ansys.aedt.core import Hfss - >>> aedtapp = Hfss() - >>> post = aedtapp.post - """ - - def __init__(self, app): - Post.__init__(self, app) - self.fields_calculator = FieldsCalculator(app) - - @pyaedt_function_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. - - """ - if ipython_available: - file_name = self.export_model_picture(show_axis=show_axis, show_grid=show_grid, show_ruler=show_ruler) - return Image(file_name, width=500) - else: - warnings.warn("The Ipython package is missing and must be installed.") - - @pyaedt_function_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) - - trace_name = "rETheta" - solnData = self.get_far_field_data( - expressions=trace_name, setup_sweep_name=setup_sweep_name, domain=ff_setup - ) - - 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( - expressions=trace_name, setup_sweep_name=setup_sweep_name, domain=ff_setup - ) - 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 - - @pyaedt_function_handler() - def get_model_plotter_geometries( - self, - objects=None, - plot_as_separate_objects=True, - plot_air_objects=False, - force_opacity_value=None, - array_coordinates=None, - generate_mesh=True, - get_objects_from_aedt=True, - ): - """Initialize the Model Plotter object with actual modeler objects and return it. - - Parameters - ---------- - objects : list, optional - Optional list of objects to plot. If `None` all objects will be exported. - 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. - array_coordinates : list of list - List of array element centers. The modeler objects will be duplicated and translated. - List of [[x1,y1,z1], [x2,y2,z2]...]. - generate_mesh : bool, optional - Whether to generate the mesh after importing objects. The default is ``True``. - get_objects_from_aedt : bool, optional - Whether to export objects from AEDT and initialize them. The default is ``True``. - - Returns - ------- - :class:`ansys.aedt.core.generic.plot.ModelPlotter` - Model Object. - """ - - if self._app._aedt_version < "2021.2": - raise RuntimeError("Object is supported from AEDT 2021 R2.") # pragma: no cover - - files = [] - if get_objects_from_aedt and self._app.solution_type not in ["HFSS3DLayout", "HFSS 3D Layout Design"]: - files = self.export_model_obj( - assignment=objects, export_as_single_objects=plot_as_separate_objects, air_objects=plot_air_objects - ) - - model = ModelPlotter() - model.off_screen = True - units = self.modeler.model_units - for file in files: - if force_opacity_value: - model.add_object(file[0], file[1], force_opacity_value, units) - else: - model.add_object(file[0], file[1], file[2], units) - model.array_coordinates = array_coordinates - if generate_mesh: - model.generate_geometry_mesh() - return model - - @pyaedt_function_handler() - def plot_model_obj( - self, - objects=None, - show=True, - export_path=None, - plot_as_separate_objects=True, - plot_air_objects=False, - force_opacity_value=None, - clean_files=False, - array_coordinates=None, - view="isometric", - show_legend=True, - dark_mode=False, - show_bounding=False, - show_grid=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. - array_coordinates : list of list - List of array element centers. The modeler objects will be duplicated and translated. - List of [[x1,y1,z1], [x2,y2,z2]...]. - view : str, optional - View to export. Options are ``"isometric"``, ``"xy"``, ``"xz"``, ``"yz"``. - The default is ``"isometric"``. - show_legend : bool, optional - Whether to display the legend or not. The default is ``True``. - dark_mode : bool, optional - Whether to display the model in dark mode or not. The default is ``False``. - show_grid : bool, optional - Whether to display the axes grid or not. The default is ``False``. - show_bounding : bool, optional - Whether to display the axes bounding box or not. The default is ``False``. - - Returns - ------- - :class:`ansys.aedt.core.generic.plot.ModelPlotter` - Model Object. - """ - model = self.get_model_plotter_geometries( - objects=objects, - plot_as_separate_objects=plot_as_separate_objects, - plot_air_objects=plot_air_objects, - force_opacity_value=force_opacity_value, - array_coordinates=array_coordinates, - generate_mesh=False, - ) - - model.show_legend = show_legend - model.off_screen = not show - if dark_mode: - model.background_color = [40, 40, 40] - model.bounding_box = show_bounding - model.show_grid = show_grid - if view != "isometric" and view in ["xy", "xz", "yz"]: - model.camera_position = view - elif view != "isometric": - self.logger.warning("Wrong view setup. It has to be one of xy, xz, yz, isometric.") - if export_path: - model.plot(export_path) - elif show: - model.plot() - if clean_files: - model.clean_cache_and_files(clean_cache=False) - return model - - @pyaedt_function_handler(plotname="plot_name", meshplot="mesh_plot", imageformat="image_format") - def plot_field_from_fieldplot( - self, - plot_name, - project_path="", - mesh_plot=False, - image_format="jpg", - view="isometric", - plot_label="Temperature", - plot_folder=None, - show=True, - scale_min=None, - scale_max=None, - plot_cad_objs=True, - log_scale=True, - dark_mode=False, - show_grid=False, - show_bounding=False, - show_legend=True, - plot_as_separate_objects=True, - file_format="case", - ): - """Export a field plot to an image file (JPG or PNG) using Python PyVista. - - This method does not support streamlines plot. - - .. note:: - The PyVista module rebuilds the mesh and the overlap fields on the mesh. - - Parameters - ---------- - plot_name : str - Name of the field plot to export. - project_path : str, optional - Path for saving the image file. The default is ``""``. - mesh_plot : bool, optional - Whether to create and plot the mesh over the fields. The default is ``False``. - image_format : 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 ``"isometric"``, ``"xy"``, ``"xz"``, ``"yz"``. - plot_label : str, optional - Type of the plot. The default is ``"Temperature"``. - plot_folder : str, optional - Plot folder to update before exporting the field. - The default is ``None``, in which case all plot folders are updated. - show : bool, optional - Export Image without plotting on UI. - scale_min : float, optional - Fix the Scale Minimum value. - scale_max : float, optional - Fix the Scale Maximum value. - plot_cad_objs : bool, optional - Whether to include objects in the plot. The default is ``True``. - log_scale : bool, optional - Whether to plot fields in log scale. The default is ``True``. - dark_mode : bool, optional - Whether to display the model in dark mode or not. The default is ``False``. - show_grid : bool, optional - Whether to display the axes grid or not. The default is ``False``. - show_bounding : bool, optional - Whether to display the axes bounding box or not. The default is ``False``. - show_legend : bool, optional - Whether to display the legend. The default is ``True``. - plot_as_separate_objects : bool, optional - Whether to plot each object separately, which can require - more time to export from AEDT. The default is ``True``. - file_format : str, optional - File format to export the plot to. The default is ``"case". - Options are ``"aedtplt"`` and ``"case"``. - If the active design is a Q3D design, the file format is automatically - set to ``"fldplt"``. - - Returns - ------- - :class:`ansys.aedt.core.generic.plot.ModelPlotter` - Model Object. - """ - is_pcb = False - if self._app.solution_type in ["HFSS3DLayout", "HFSS 3D Layout Design"]: - is_pcb = True - if not plot_folder: - self.ofieldsreporter.UpdateAllFieldsPlots() - else: - self.ofieldsreporter.UpdateQuantityFieldsPlots(plot_folder) - - file_to_add = self.export_field_plot(plot_name, self._app.working_directory, file_format=file_format) - model = self.get_model_plotter_geometries( - generate_mesh=False, - get_objects_from_aedt=plot_cad_objs, - plot_as_separate_objects=plot_as_separate_objects, - ) - model.show_legend = show_legend - model.off_screen = not show - if dark_mode: - model.background_color = [40, 40, 40] - model.bounding_box = show_bounding - model.show_grid = show_grid - if file_to_add: - model.add_field_from_file( - file_to_add, - coordinate_units=self.modeler.model_units, - show_edges=mesh_plot, - log_scale=log_scale, - ) - if plot_label: - model.fields[0].label = plot_label - - if view != "isometric" and view in ["xy", "xz", "yz"]: - model.camera_position = view - elif view != "isometric": - self.logger.warning("Wrong view setup. It has to be one of xy, xz, yz, isometric.") - if is_pcb: - model.z_scale = 5 - - if scale_min is not None and scale_max is None or scale_min is None and scale_max is not None: - self.logger.warning("Invalid scale values: both values must be None or different from None.") - elif scale_min is not None and scale_max is not None and not 0 <= scale_min < scale_max: - self.logger.warning("Invalid scale values: scale_min must be greater than zero and less than scale_max.") - elif log_scale and scale_min == 0: - self.logger.warning("Invalid scale minimum value for logarithm scale.") - else: - model.range_min = scale_min - model.range_max = scale_max - if project_path: - model.plot(os.path.join(project_path, plot_name + "." + image_format)) - elif show: - model.plot() - return model - - @pyaedt_function_handler(object_list="assignment", imageformat="image_format", setup_name="setup") - def plot_field( - self, - quantity, - assignment, - plot_type="Surface", - setup=None, - intrinsics=None, - mesh_on_fields=False, - view="isometric", - plot_label=None, - show=True, - scale_min=None, - scale_max=None, - plot_cad_objs=True, - log_scale=False, - export_path="", - image_format="jpg", - keep_plot_after_generation=False, - dark_mode=False, - show_bounding=False, - show_grid=False, - show_legend=True, - filter_objects=None, - plot_as_separate_objects=True, - ): - """Create a field plot using Python PyVista and export to an image file (JPG or PNG). - - .. note:: - The PyVista module rebuilds the mesh and the overlap fields on the mesh. - - Parameters - ---------- - quantity : str - Quantity to plot. For example, ``"Mag_E"``. - assignment : str, list - One or more objects or faces to apply the field plot to. - plot_type : str, optional - Plot type. The default is ``Surface``. Options are - ``"CutPlane"``, ``"Surface"``, and ``"Volume"``. - setup : str, optional - Setup and sweep name on which create the field plot. Default is None for nominal setup usage. - intrinsics : dict, str, optional - Intrinsic variables required to compute the field before the export. - These are typically: frequency, time and phase. - It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed as: - - ``"Freq"`` or ``"Frequency"`` - - ``"Time"`` - - ``"Phase"`` - in lower or camel case. - If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. - The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. - mesh_on_fields : bool, optional - Whether to create and plot the mesh over the fields. The - default is ``False``. - view : str, optional - View to export. Options are ``"isometric"``, ``"xy"``, ``"xz"``, ``"yz"``. - plot_label : str, optional - Type of the plot. The default is ``"Temperature"``. - show : bool, optional - Export Image without plotting on UI. - scale_min : float, optional - Fix the Scale Minimum value. - scale_max : float, optional - Fix the Scale Maximum value. - plot_cad_objs : bool, optional - Whether to include objects in the plot. The default is ``True``. - log_scale : bool, optional - Whether to plot fields in log scale. The default is ``False``. - export_path : str, optional - Image export path. Default is ``None`` to not export the image. - image_format : str, optional - Format of the image file. Options are ``"jpg"``, ``"png"``, ``"svg"``, and ``"webp"``. - The default is ``"jpg"``. - keep_plot_after_generation : bool, optional - Either to keep the Field Plot in AEDT after the generation is completed. Default is ``False``. - dark_mode : bool, optional - Whether to display the model in dark mode or not. The default is ``False``. - show_grid : bool, optional - Whether to display the axes grid or not. The default is ``False``. - show_bounding : bool, optional - Whether to display the axes bounding box or not. The default is ``False``. - show_legend : bool, optional - Whether to display the legend or not. The default is ``True``. - filter_objects : list, optional - Objects list for filtering the ``CutPlane`` plots. - plot_as_separate_objects : bool, optional - Plot each object separately. It may require more time to export from AEDT. - - Returns - ------- - :class:`ansys.aedt.core.generic.plot.ModelPlotter` - Model Object. - """ - intrinsics = self._app._check_intrinsics(intrinsics, setup=setup) - if filter_objects is None: - filter_objects = [] - if os.getenv("PYAEDT_DOC_GENERATION", "False").lower() in ("true", "1", "t"): # pragma: no cover - show = False - if not setup: - setup = self._app.existing_analysis_sweeps[0] - - # file_to_add = [] - if plot_type == "Surface": - plotf = self.create_fieldplot_surface(assignment, quantity, setup, intrinsics) - elif plot_type == "Volume": - plotf = self.create_fieldplot_volume(assignment, quantity, setup, intrinsics) - else: - plotf = self.create_fieldplot_cutplane( - assignment, quantity, setup, intrinsics, filter_objects=filter_objects - ) - # if plotf: - # file_to_add = self.export_field_plot(plotf.name, self._app.working_directory, plotf.name) - - model = self.plot_field_from_fieldplot( - plotf.name, - export_path, - mesh_on_fields, - image_format, - view, - plot_label if plot_label else quantity, - None, - show, - scale_min, - scale_max, - plot_cad_objs, - log_scale, - dark_mode=dark_mode, - show_grid=show_grid, - show_bounding=show_bounding, - show_legend=show_legend, - plot_as_separate_objects=plot_as_separate_objects, - ) - if not keep_plot_after_generation: - plotf.delete() - return model - - @pyaedt_function_handler(object_list="assignment", variation_list="variations", setup_name="setup") - def plot_animated_field( - self, - quantity, - assignment, - plot_type="Surface", - setup=None, - intrinsics=None, - variation_variable="Phi", - variations=None, - view="isometric", - show=True, - scale_min=None, - scale_max=None, - plot_cad_objs=True, - log_scale=True, - zoom=None, - export_gif=False, - export_path="", - force_opacity_value=0.1, - dark_mode=False, - show_grid=False, - show_bounding=False, - show_legend=True, - filter_objects=None, - ): - """Create an animated field plot using Python PyVista and export to a gif file. - - .. note:: - The PyVista module rebuilds the mesh and the overlap fields on the mesh. - - Parameters - ---------- - quantity : str - Quantity to plot (for example, ``"Mag_E"``). - assignment : list, str - One or more objects or faces to apply the field plot to. - plot_type : str, optional - Plot type. The default is ``Surface``. Options are - ``"CutPlane"``, ``"Surface"``, and ``"Volume"``. - setup : str, optional - Setup and sweep name on which create the field plot. Default is None for nominal setup usage. - intrinsics : dict, str, optional - Intrinsic variables required to compute the field before the export. - These are typically: frequency, time and phase. - It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed as: - - ``"Freq"`` or ``"Frequency"`` - - ``"Time"`` - - ``"Phase"`` - in lower or camel case. - If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. - The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. - variation_variable : str, optional - Variable to vary. The default is ``"Phi"``. - variations : list, optional - List of variation values with units. The default is ``["0deg"]``. - view : str, optional - View to export. Options are ``"isometric"``, ``"xy"``, ``"xz"``, ``"yz"``. - show : bool, optional - Export Image without plotting on UI. - scale_min : float, optional - Fix the Scale Minimum value. - scale_max : float, optional - Fix the Scale Maximum value. - plot_cad_objs : bool, optional - Whether to include objects in the plot. The default is ``True``. - log_scale : bool, optional - Whether to plot fields in log scale. The default is ``True``. - zoom : float, optional - Zoom factor. - export_gif : bool, optional - Whether to export an animated gif or not. The default is ``False``. - export_path : str, optional - Image export path. Default is ``None`` to not ``working_directory`` will be used to save the image. - 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. - dark_mode : bool, optional - Whether to display the model in dark mode or not. The default is ``False``. - show_grid : bool, optional - Whether to display the axes grid or not. The default is ``False``. - show_bounding : bool, optional - Whether to display the axes bounding box or not. The default is ``False``. - show_legend : bool, optional - Whether to display the legend or not. The default is ``True``. - filter_objects : list, optional - Objects list for filtering the ``CutPlane`` plots. - The default is ``None`` in which case an empty list is passed. - - Returns - ------- - :class:`ansys.aedt.core.generic.plot.ModelPlotter` - Model Object. - """ - intrinsics = self._app._check_intrinsics(intrinsics, setup=setup) - if variations is None: - variations = ["0deg"] - if os.getenv("PYAEDT_DOC_GENERATION", "False").lower() in ("true", "1", "t"): # pragma: no cover - show = False - if not export_path: - export_path = self._app.working_directory - if not filter_objects: - filter_objects = [] - - v = 0 - fields_to_add = [] - is_intrinsics = True - if variation_variable in self._app.variable_manager.independent_variables: - is_intrinsics = False - for el in variations: - if is_intrinsics: - intrinsics[variation_variable] = el - else: - self._app[variation_variable] = el - if plot_type == "Surface": - plotf = self.create_fieldplot_surface(assignment, quantity, setup, intrinsics) - elif plot_type == "Volume": - plotf = self.create_fieldplot_volume(assignment, quantity, setup, intrinsics) - else: - plotf = self.create_fieldplot_cutplane( - assignment, quantity, setup, intrinsics, filter_objects=filter_objects - ) - if plotf: - file_to_add = self.export_field_plot(plotf.name, export_path, plotf.name + str(v)) - if file_to_add: - fields_to_add.append(file_to_add) - plotf.delete() - v += 1 - model = self.get_model_plotter_geometries( - generate_mesh=False, get_objects_from_aedt=plot_cad_objs, force_opacity_value=force_opacity_value - ) - model.off_screen = not show - if dark_mode: - model.background_color = [40, 40, 40] - model.bounding_box = show_bounding - model.show_grid = show_grid - model.show_legend = show_legend - if fields_to_add: - model.add_frames_from_file(fields_to_add, log_scale=log_scale) - if export_gif: - model.gif_file = os.path.join(self._app.working_directory, self._app.project_name + ".gif") - if view != "isometric" and view in ["xy", "xz", "yz"]: - model.camera_position = view - elif view != "isometric": - self.logger.warning("Wrong view setup. It has to be one of xy, xz, yz, isometric.") - - if scale_min and scale_max: - model.range_min = scale_min - model.range_max = scale_max - if zoom: - model.zoom = zoom - if show or export_gif: - model.animate() - return model - - @pyaedt_function_handler(plotname="plot_name", variation_list="variations") - def animate_fields_from_aedtplt( - self, - plot_name, - plot_folder=None, - variation_variable="Phase", - variations=["0deg"], - project_path="", - export_gif=False, - show=True, - dark_mode=False, - show_bounding=False, - show_grid=False, - ): - """Generate a field plot to an image file (JPG or PNG) using PyVista. - - .. note:: - The PyVista module rebuilds the mesh and the overlap fields on the mesh. - - Parameters - ---------- - plot_name : str - Name of the plot or the name of the object. - plot_folder : str, optional - Name of the folder in which the plot resides. The default - is ``None``. - variation_variable : str, optional - Variable to vary. The default is ``"Phase"``. - variations : list, optional - List of variation values with units. The default is - ``["0deg"]``. - project_path : str, optional - Path for the export. The default is ``""``, in which case the file is exported - to the working directory. - export_gif : bool, optional - Whether to export the GIF file. The default is ``False``. - show : bool, optional - Generate the animation without showing an interactive plot. The default is ``True``. - dark_mode : bool, optional - Whether to display the model in dark mode or not. The default is ``False``. - show_grid : bool, optional - Whether to display the axes grid or not. The default is ``False``. - show_bounding : bool, optional - Whether to display the axes bounding box or not. The default is ``False``. - - Returns - ------- - :class:`ansys.aedt.core.generic.plot.ModelPlotter` - Model Object. - """ - if not plot_folder: - self.ofieldsreporter.UpdateAllFieldsPlots() - else: - self.ofieldsreporter.UpdateQuantityFieldsPlots(plot_folder) - - fields_to_add = [] - if not project_path: - project_path = self._app.working_directory - for el in variations: - if plot_name in self.field_plots and variation_variable in self.field_plots[plot_name].intrinsics: - self.field_plots[plot_name].intrinsics[variation_variable] = el - self.field_plots[plot_name].update() - else: - self._app._odesign.ChangeProperty( - [ - "NAME:AllTabs", - [ - "NAME:FieldsPostProcessorTab", - ["NAME:PropServers", "FieldsReporter:" + plot_name], - ["NAME:ChangedProps", ["NAME:" + variation_variable, "Value:=", el]], - ], - ] - ) - fields_to_add.append( - self.export_field_plot( - plot_name, project_path, plot_name + variation_variable + str(el), file_format="case" - ) - ) - - model = self.get_model_plotter_geometries(generate_mesh=False) - model.off_screen = not show - if dark_mode: - model.background_color = [40, 40, 40] - model.bounding_box = show_bounding - model.show_grid = show_grid - if fields_to_add: - model.add_frames_from_file(fields_to_add) - if export_gif: - model.gif_file = os.path.join(self._app.working_directory, self._app.project_name + ".gif") - - if show or export_gif: - model.animate() - return model - - @pyaedt_function_handler() - def create_3d_plot( - self, - solution_data, - nominal_sweep=None, - nominal_value=None, - primary_sweep="Theta", - secondary_sweep="Phi", - snapshot_path=None, - show=True, - ): - """Create a 3D plot using Matplotlib. - - Parameters - ---------- - solution_data : :class:`ansys.aedt.core.modules.solutions.SolutionData` - Input data for the solution. - nominal_sweep : str, optional - Name of the nominal sweep. The default is ``None``. - nominal_value : str, optional - Value for the nominal sweep. The default is ``None``. - primary_sweep : str, optional - Primary sweep. The default is ``"Theta"``. - secondary_sweep : str, optional - Secondary sweep. The default is ``"Phi"``. - snapshot_path : str, optional - Full path to image file if a snapshot is needed. - The default is ``None``. - show : bool, optional - Whether if show the plot or not. Default is set to `True`. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if nominal_value: - solution_data.intrinsics[nominal_sweep] = nominal_value - if nominal_value: - solution_data.primary_sweep = primary_sweep - return solution_data.plot_3d( - x_axis=primary_sweep, y_axis=secondary_sweep, snapshot_path=snapshot_path, show=show - ) - - @pyaedt_function_handler(frames_list="frames", output_gif_path="gif_path") - def plot_scene( - self, - frames, - gif_path, - norm_index=0, - dy_rng=0, - fps=30, - show=True, - view="yz", - zoom=2.0, - convert_fields_in_db=False, - log_multiplier=10.0, - ): - """Plot the current model 3D scene with overlapping animation coming from a file list and save the gif. - - - Parameters - ---------- - frames : list or str - File list containing animation frames to plot in CSV format or - path to a text index file containing the full path to CSV files. - gif_path : str - Full path for outputting the GIF file. - norm_index : int, optional - Frame to use to normalize your images. - Data is already saved as dB : 100 for usual traffic scenes. - dy_rng : int, optional - Specify how many dB below you would like to specify the range_min. - Tweak this a couple of times with small number of frames. - fps : int, optional - Frames per Second. - show : bool, optional - Either if show or only export gif. - view : str, optional - View to export. Options are ``"isometric"``, ``"xy"``, ``"xz"``, and ``"yz"``. - The default is ``"isometric"``. - zoom : float, optional - Default zoom. Default Value is `2`. - convert_fields_in_db : bool, optional - Either if convert the fields before plotting in dB. Default Value is `False`. - log_multiplier : float, optional - Field multiplier if field in dB. Default Value is `10.0`. - - Returns - ------- - - """ - if isinstance(frames, str) and os.path.exists(frames): - with open_file(frames, "r") as f: - lines = f.read() - temp_list = lines.splitlines() - frames_paths_list = [i for i in temp_list] - elif isinstance(frames, str): - self.logger.error("Path doesn't exists") - return False - else: - frames_paths_list = frames - scene = self.get_model_plotter_geometries(generate_mesh=False) - - norm_data = np.loadtxt(frames_paths_list[norm_index], skiprows=1, delimiter=",") - norm_val = norm_data[:, -1] - v_max = np.max(norm_val) - v_min = v_max - dy_rng - scene.add_frames_from_file(frames_paths_list, log_scale=False, color_map="jet", header_lines=1, opacity=0.8) - - # Specifying the attributes of the scene through the ModelPlotter object - scene.off_screen = not show - if view != "isometric" and view in ["xy", "xz", "yz"]: - scene.camera_position = view - scene.range_min = v_min - scene.range_max = v_max - scene.show_grid = False - scene.windows_size = [1920, 1080] - scene.show_legend = False - scene.show_boundingbox = False - scene.legend = False - scene.frame_per_seconds = fps - scene.zoom = zoom - scene.bounding_box = False - scene.color_bar = False - scene.gif_file = gif_path # This GIF file may be a bit slower so it can be speed it up a bit - scene.convert_fields_in_db = convert_fields_in_db - scene.log_multiplier = log_multiplier - scene.animate() - - -class IcepakPostProcessor(PostProcessor, object): - def __init__(self, app): - PostProcessor.__init__(self, app) - - @pyaedt_function_handler() - def create_field_summary(self): - return FieldSummary(self._app) - - @pyaedt_function_handler(timestep="time_step", design_variation="variation") - def get_fans_operating_point(self, export_file=None, setup_name=None, time_step=None, variation=None): - """ - Get the operating point of the fans in the design. - - Parameters - ---------- - export_file : str, optional - Name of the file to save the operating point of the fans to. The default is - ``None``, in which case the filename is automatically generated. - setup_name : str, optional - Setup name to determine the operating point of the fans. The default is - ``None``, in which case the first available setup is used. - time_step : str, optional - Time, with units, at which to determine the operating point of the fans. The default - is ``None``, in which case the first available timestep is used. This parameter is - only relevant in transient simulations. - variation : str, optional - Design variation to determine the operating point of the fans from. The default is - ``None``, in which case the nominal variation is used. - - Returns - ------- - list - First element of the list is the CSV filename. The second and third elements - are the quantities with units describing the operating point of the fans. - The fourth element is a dictionary with the names of the fan instances - as keys and lists with volumetric flow rates and pressure rise floats associated - with the operating point as values. - - References - ---------- - - >>> oModule.ExportFanOperatingPoint - - Examples - -------- - >>> from ansys.aedt.core import Icepak - >>> ipk = Icepak() - >>> ipk.create_fan() - >>> filename, vol_flow_name, p_rise_name, op_dict= ipk.get_fans_operating_point() - """ - - if export_file is None: - path = self._app.temp_directory - base_name = "{}_{}_FanOpPoint".format(self._app.project_name, self._app.design_name) - export_file = os.path.join(path, base_name + ".csv") - while os.path.exists(export_file): - file_name = generate_unique_name(base_name) - export_file = os.path.join(path, file_name + ".csv") - if setup_name is None: - setup_name = "{} : {}".format(self._app.get_setups()[0], self._app.solution_type) - if time_step is None: - time_step = "" - if self._app.solution_type == "Transient": - self._app.logger.warning("No timestep is specified. First timestep is exported.") - else: - if not self._app.solution_type == "Transient": - self._app.logger.warning("Simulation is steady-state. Timestep argument is ignored.") - time_step = "" - if variation is None: - variation = "" - self._app.osolution.ExportFanOperatingPoint( - [ - "SolutionName:=", - setup_name, - "DesignVariationKey:=", - variation, - "ExportFilePath:=", - export_file, - "Overwrite:=", - True, - "TimeStep:=", - time_step, - ] - ) - with open_file(export_file, "r") as f: - reader = csv.reader(f) - for line in reader: - if "Fan Instances" in line: - vol_flow = line[1] - p_rise = line[2] - break - var = {line[0]: [float(line[1]), float(line[2])] for line in reader} - return [export_file, vol_flow, p_rise, var] - - @pyaedt_function_handler - def _parse_field_summary_content(self, fs, setup_name, design_variation, quantity_name): - content = fs.get_field_summary_data(setup=setup_name, variation=design_variation) - pattern = r"\[([^]]*)\]" - match = re.search(pattern, content["Quantity"][0]) - if match: - content["Unit"] = [match.group(1)] - else: # pragma: no cover - content["Unit"] = [None] - - if quantity_name in TOTAL_QUANTITIES: - return {i: content[i][0] for i in ["Total", "Unit"]} - return {i: content[i][0] for i in ["Min", "Max", "Mean", "Stdev", "Unit"]} - - @pyaedt_function_handler(faces_list="faces", quantity_name="quantity", design_variation="variation") - def evaluate_faces_quantity( - self, faces, quantity, side="Default", setup_name=None, variations=None, ref_temperature="", time="0s" - ): - """Export the field surface output. - - Parameters - ---------- - faces : list - List of faces to apply. - quantity : str - Name of the quantity to export. - side : str, optional - Which side of the mesh face to use. The default is ``Default``. - Options are ``"Adjacent"``, ``"Combined"``, and ``"Default"``. - setup_name : str, optional - Name of the setup and name of the sweep. For example, ``"IcepakSetup1 : SteatyState"``. - The default is ``None``, in which case the active setup and active sweep are used. - variations : dict, optional - Dictionary of parameters defined for the specific setup with values. The default is ``{}``. - ref_temperature: str, optional - Reference temperature to use for heat transfer coefficient computation. The default is ``""``. - time : str, optional - Timestep to get the data from. Default is ``"0s"``. - - Returns - ------- - dict - Output dictionary, which depending on the quantity chosen, contains one - of these sets of keys: - - - ``"Min"``, ``"Max"``, ``"Mean"``, ``"Stdev"``, and ``"Unit"`` - - ``"Total"`` and ``"Unit"`` - - References - ---------- - - >>> oModule.ExportFieldsSummary - """ - if variations is None: - variations = {} - facelist_name = generate_unique_name(quantity) - self._app.modeler.create_face_list(faces, facelist_name) - fs = self.create_field_summary() - fs.add_calculation( - "Object", "Surface", facelist_name, quantity, side=side, ref_temperature=ref_temperature, time=time - ) - out = self._parse_field_summary_content(fs, setup_name, variations, quantity) - self._app.oeditor.Delete(["NAME:Selections", "Selections:=", facelist_name]) - return out - - @pyaedt_function_handler(boundary_name="boundary", quantity_name="quantity", design_variation="variations") - def evaluate_boundary_quantity( - self, - boundary, - quantity, - side="Default", - volume=False, - setup_name=None, - variations=None, - ref_temperature="", - time="0s", - ): - """Export the field output on a boundary. - - Parameters - ---------- - boundary : str - Name of boundary to perform the computation on. - quantity : str - Name of the quantity to export. - side : str, optional - Side of the mesh face to use. The default is ``"Default"``. - Options are ``"Adjacent"``, ``"Combined"``, and ``"Default"``. - volume : bool, optional - Whether to compute the quantity on the volume or on the surface. - The default is ``False``, in which case the quantity will be evaluated - only on the surface . - setup_name : str, optional - Name of the setup and name of the sweep. For example, ``"IcepakSetup1 : SteatyState"``. - The default is ``None``, in which case the active setup and active sweep are used. - variations : dict, optional - Dictionary of parameters defined for the specific setup with values. The default is ``{}``. - ref_temperature: str, optional - Reference temperature to use for heat transfer coefficient computation. The default is ``""``. - time : str, optional - Timestep to get the data from. Default is ``"0s"``. - - Returns - ------- - dict - Output dictionary, which depending on the quantity chosen, contains one - of these sets of keys: - - ``"Min"``, ``"Max"``, ``"Mean"``, ``"Stdev"``, and ``"Unit"`` - - ``"Total"`` and ``"Unit"`` - - References - ---------- - - >>> oModule.ExportFieldsSummary - """ - if variations is None: - variations = {} - fs = self.create_field_summary() - fs.add_calculation( - "Boundary", - ["Surface", "Volume"][int(volume)], - boundary, - quantity, - side=side, - ref_temperature=ref_temperature, - time=time, - ) - return self._parse_field_summary_content(fs, setup_name, variations, quantity) - - @pyaedt_function_handler(monitor_name="monitor", quantity_name="quantity", design_variation="variations") - def evaluate_monitor_quantity( - self, monitor, quantity, side="Default", setup_name=None, variations=None, ref_temperature="", time="0s" - ): - """Export monitor field output. - - Parameters - ---------- - monitor : str - Name of monitor to perform the computation on. - quantity : str - Name of the quantity to export. - side : str, optional - Side of the mesh face to use. The default is ``"Default"``. - Options are ``"Adjacent"``, ``"Combined"``, and ``"Default"``. - setup_name : str, optional - Name of the setup and name of the sweep. For example, ``"IcepakSetup1 : SteatyState"``. - The default is ``None``, in which case the active setup and active sweep are used. - variations : dict, optional - Dictionary of parameters defined for the specific setup with values. The default is ``{}``. - ref_temperature: str, optional - Reference temperature to use for heat transfer coefficient computation. The default is ``""``. - time : str, optional - Timestep to get the data from. Default is ``"0s"``. - - Returns - ------- - dict - Output dictionary, which depending on the quantity chosen, contains one - of these sets of keys: - - - ``"Min"``, ``"Max"``, ``"Mean"``, ``"Stdev"``, and ``"Unit"`` - - ``"Total"`` and ``"Unit"`` - - References - ---------- - - >>> oModule.ExportFieldsSummary - """ - if variations is None: - variations = {} - if settings.aedt_version < "2024.1": - raise NotImplementedError("Monitors are not supported in field summary in versions earlier than 2024 R1.") - else: # pragma: no cover - if self._app.monitor.face_monitors.get(monitor, None): - field_type = "Surface" - elif self._app.monitor.point_monitors.get(monitor, None): - field_type = "Volume" - else: - raise AttributeError("Monitor {} is not found in the design.".format(monitor)) - fs = self.create_field_summary() - fs.add_calculation( - "Monitor", field_type, monitor, quantity, side=side, ref_temperature=ref_temperature, time=time - ) - return self._parse_field_summary_content(fs, setup_name, variations, quantity) - - @pyaedt_function_handler(design_variation="variations") - def evaluate_object_quantity( - self, - object_name, - quantity_name, - side="Default", - volume=False, - setup_name=None, - variations=None, - ref_temperature="", - time="0s", - ): - """Export the field output on or in an object. - - Parameters - ---------- - object_name : str - Name of object to perform the computation on. - quantity_name : str - Name of the quantity to export. - side : str, optional - Side of the mesh face to use. The default is ``"Default"``. - Options are ``"Adjacent"``, ``"Combined"``, and ``"Default"``. - volume : bool, optional - Whether to compute the quantity on the volume or on the surface. The default is ``False``. - setup_name : str, optional - Name of the setup and name of the sweep. For example, ``"IcepakSetup1 : SteatyState"``. - The default is ``None``, in which case the active setup and active sweep are used. - variations : dict, optional - Dictionary of parameters defined for the specific setup with values. The default is ``{}``. - ref_temperature: str, optional - Reference temperature to use for heat transfer coefficient computation. The default is ``""``. - time : str, optional - Timestep to get the data from. Default is ``"0s"``. - - Returns - ------- - dict - Output dictionary, which depending on the quantity chosen, contains one - of these sets of keys: - - - ``"Min"``, ``"Max"``, ``"Mean"``, ``"Stdev"``, and ``"Unit"`` - - ``"Total"`` and ``"Unit"`` - - References - ---------- - - >>> oModule.ExportFieldsSummary - """ - if variations is None: - variations = {} - fs = self.create_field_summary() - fs.add_calculation( - "Object", - ["Surface", "Volume"][int(volume)], - object_name, - quantity_name, - side=side, - ref_temperature=ref_temperature, - time=time, - ) - return self._parse_field_summary_content(fs, setup_name, variations, quantity_name) diff --git a/src/ansys/aedt/core/modules/report_templates.py b/src/ansys/aedt/core/modules/report_templates.py deleted file mode 100644 index 8955d5be075..00000000000 --- a/src/ansys/aedt/core/modules/report_templates.py +++ /dev/null @@ -1,4241 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - - -import copy -import os -import re - -from ansys.aedt.core.generic.constants import LineStyle -from ansys.aedt.core.generic.constants import SymbolStyle -from ansys.aedt.core.generic.constants import TraceType -from ansys.aedt.core.generic.general_methods import generate_unique_name -from ansys.aedt.core.generic.general_methods import pyaedt_function_handler -from ansys.aedt.core.modeler.geometry_operators import GeometryOperators - - -def _props_with_default(dict_in, key, default_value=None): - return dict_in[key] if dict_in.get(key, None) is not None else default_value - - -class LimitLine(object): - """Line Limit Management Class.""" - - def __init__(self, report_setup, trace_name): - self._oreport_setup = report_setup - self.line_name = trace_name - self.LINESTYLE = LineStyle() - - @pyaedt_function_handler() - def _change_property(self, props_value): - self._oreport_setup.ChangeProperty( - ["NAME:AllTabs", ["NAME:Limit Line", ["NAME:PropServers", self.line_name], props_value]] - ) - return True - - @pyaedt_function_handler() - def set_line_properties( - self, style=None, width=None, hatch_above=None, violation_emphasis=None, hatch_pixels=None, color=None - ): - """Set trace properties. - - Parameters - ---------- - style : str, optional - Style for the limit line. The default is ``None``. You can also use - the ``LIFESTYLE`` property. - width : int, optional - Width of the limit line. The default is ``None``. - hatch_above : bool - Whether the hatch is above the limit line. The default is ``None``. - violation_emphasis : bool - Whether to add violation emphasis. The default is ``None``. - hatch_pixels : int - Number of pixels for the hatch. The default is ``None``. - color : tuple, list - Trace color as a tuple (R,G,B) or a list of integers [0,255]. - The default is ``None``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - props = ["NAME:ChangedProps"] - if style: - props.append(["NAME:Line Style", "Value:=", style]) - if width and isinstance(width, (int, float, str)): - props.append(["NAME:Line Width", "Value:=", str(width)]) - if hatch_above is not None and isinstance(hatch_pixels, (int, str)): - props.append(["NAME:Hatch Above", "Value:=", hatch_above]) - if hatch_pixels and isinstance(hatch_pixels, (int, str)): - props.append(["NAME:Hatch Pixels", "Value:=", str(hatch_pixels)]) - if violation_emphasis: - props.append(["NAME:Violation Emphasis", "Value:=", violation_emphasis]) - if color and isinstance(color, (list, tuple)) and len(color) == 3: - props.append(["NAME:Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) - return self._change_property(props) - - -class Note(object): - """Note Management Class.""" - - def __init__(self, report_setup, plot_note_name): - self._oreport_setup = report_setup - self.plot_note_name = plot_note_name - - @pyaedt_function_handler() - def _change_property(self, props_value): - prop_server_name = self.plot_note_name - self._oreport_setup.ChangeProperty( - ["NAME:AllTabs", ["NAME:Note", ["NAME:PropServers", prop_server_name], props_value]] - ) - return True - - @pyaedt_function_handler() - def set_note_properties( - self, - text=None, - back_color=None, - background_visibility=None, - border_color=None, - border_visibility=None, - border_width=None, - font="Arial", - font_size=12, - italic=False, - bold=False, - color=(0, 0, 0), - ): - """Set note properties. - - Parameters - ---------- - text : str, optional - Style for the limit line. The default is ``None``. You can also use - the ``LIFESTYLE`` property. - back_color : int - Background color specified as a tuple (R,G,B) or a list of integers [0,255]. - The default is ``None``. - background_visibility : bool - Whether to view background. The default is ``None``. - border_color : int - Trace color specified as a tuple (R,G,B) or a list of integers [0,255]. - The default is ``None``. - border_visibility : bool - Whether to view text border. The default is ``None``. - The default is ``None``. - border_width : int - Text boarder width. - The default is ``None``. - font : str, optional - The default is ``None``. - font_size : int, optional - The default is ``None``. - italic : bool - Whether the text is italic. - The default is ``None``. - bold : bool - Whether the text is bold. - The default is ``None``. - color : int =(0, 0, 0) - Trace color specified as a tuple (R,G,B) or a list of integers [0,255]. - The default is ``None``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - props = ["NAME:ChangedProps"] - if text: - props.append(["NAME:Note Text", "Value:=", text]) - if back_color and isinstance(back_color, (list, tuple)) and len(back_color) == 3: - props.append(["NAME:Back Color", "R:=", back_color[0], "G:=", back_color[1], "B:=", back_color[2]]) - if background_visibility is not None: - props.append(["NAME:Background Visibility", "Value:=", background_visibility]) - if border_color and isinstance(border_color, (list, tuple)) and len(border_color) == 3: - props.append(["NAME:Border Color", "R:=", border_color[0], "G:=", border_color[1], "B:=", border_color[2]]) - if border_visibility is not None: - props.append(["NAME:Border Visibility", "Value:=", border_visibility]) - if border_width and isinstance(border_width, (int, float)): - props.append(["NAME:Border Width", "Value:=", str(border_width)]) - - font_props = [ - "NAME:Note Font", - "Height:=", - -1 * font_size - 2, - "Width:=", - 0, - "Escapement:=", - 0, - "Orientation:=", - 0, - "Weight:=", - 700 if bold else 400, - "Italic:=", - 255 if italic else 0, - "Underline:=", - 0, - "StrikeOut:=", - 0, - "CharSet:=", - 0, - "OutPrecision:=", - 3, - "ClipPrecision:=", - 2, - "Quality:=", - 1, - "PitchAndFamily:=", - 34, - "FaceName:=", - font, - "R:=", - color[0], - "G:=", - color[1], - "B:=", - color[2], - ] - props.append(font_props) - return self._change_property(props) - - -class Trace(object): - """Provides trace management.""" - - def __init__(self, report_setup, aedt_name): - self._oreport_setup = report_setup - self.aedt_name = aedt_name - self._name = None - self.LINESTYLE = LineStyle() - self.TRACETYPE = TraceType() - self.SYMBOLSTYLE = SymbolStyle() - - @property - def name(self): - """Trace name. - - Returns - ------- - str - Trace name. - """ - report_name = self.aedt_name.split(":")[0] - traces_in_report = self._oreport_setup.GetReportTraceNames(report_name) - for trace in traces_in_report: - if trace + ":" in self.aedt_name: - self._name = trace - return self._name - - @name.setter - def name(self, value): - report_name = self.aedt_name.split(":")[0] - prop_name = report_name + ":" + self.name - - self._oreport_setup.ChangeProperty( - [ - "NAME:AllTabs", - [ - "NAME:Trace", - ["NAME:PropServers", prop_name], - ["NAME:ChangedProps", ["NAME:Specify Name", "Value:=", True]], - ], - ] - ) - self._oreport_setup.ChangeProperty( - [ - "NAME:AllTabs", - ["NAME:Trace", ["NAME:PropServers", prop_name], ["NAME:ChangedProps", ["NAME:Name", "Value:=", value]]], - ] - ) - self.aedt_name.replace(self.name, value) - self._name = value - - @pyaedt_function_handler() - def _change_property(self, props_value): - self._oreport_setup.ChangeProperty( - ["NAME:AllTabs", ["NAME:Attributes", ["NAME:PropServers", self.aedt_name], props_value]] - ) - return True - - @pyaedt_function_handler(trace_style="style") - def set_trace_properties(self, style=None, width=None, trace_type=None, color=None): - """Set trace properties. - - Parameters - ---------- - style : str, optional - Style for the trace line. The default is ``None``. You can also use - the ``LINESTYLE`` property. - width : int, optional - Width of the trace line. The default is ``None``. - trace_type : str - Type of the trace line. The default is ``None``. You can also use the ``TRACETYPE`` - property. - color : tuple, list - Trace line color specified as a tuple (R,G,B) or a list of integers [0,255]. - The default is ``None``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - props = ["NAME:ChangedProps"] - if style: - props.append(["NAME:Line Style", "Value:=", style]) - if width and isinstance(width, (int, float, str)): - props.append(["NAME:Line Width", "Value:=", str(width)]) - if trace_type: - props.append(["NAME:Trace Type", "Value:=", trace_type]) - if color and isinstance(color, (list, tuple)) and len(color) == 3: - props.append(["NAME:Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) - return self._change_property(props) - - @pyaedt_function_handler() - def set_symbol_properties(self, show=True, style=None, show_arrows=None, fill=None, color=None): - """Set symbol properties. - - Parameters - ---------- - show : bool, optional - Whether to show the symbol. The default is ``True``. - style : str, optional - Style of the style. The default is ``None``. You can also use the ``SYMBOLSTYLE`` - property. - show_arrows : bool, optional - Whether to show arrows. The default is ``None``. - fill : bool, optional - Whether to fill the symbol with a color. The default is ``None``. - color : tuple, list - Symbol fill color specified as a tuple (R,G,B) or a list of integers [0,255]. - The default is ``None``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - props = ["NAME:ChangedProps", ["NAME:Show Symbol", "Value:=", show]] - if style: - props.append(["NAME:Symbol Style", "Value:=", style]) - if show_arrows: - props.append(["NAME:Show Arrows", "Value:=", show_arrows]) - if fill: - props.append(["NAME:Fill Symbol", "Value:=", fill]) - if color and isinstance(color, (list, tuple)) and len(color) == 3: - props.append(["NAME:Symbol Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) - return self._change_property(props) - - -class CommonReport(object): - """Provides common reports.""" - - def __init__(self, app, report_category, setup_name, expressions=None): - self._post = app - self.props = {} - self.props["report_category"] = report_category - self.setup = setup_name - self.props["report_type"] = "Rectangular Plot" - self.props["context"] = {} - self.props["context"]["domain"] = "Sweep" - self.props["context"]["primary_sweep"] = "Freq" - self.props["context"]["primary_sweep_range"] = ["All"] - self.props["context"]["secondary_sweep_range"] = ["All"] - self.props["context"]["variations"] = {"Freq": ["All"]} - if hasattr(self._post._app, "available_variations") and self._post._app.available_variations: - for el, k in self._post._app.available_variations.nominal_w_values_dict.items(): - self.props["context"]["variations"][el] = k - self.props["expressions"] = None - self.props["plot_name"] = None - if expressions: - self.expressions = expressions - self._is_created = False - self.siwave_dc_category = 0 - - @pyaedt_function_handler() - def delete(self): - """Delete current report.""" - self._post.oreportsetup.DeleteReports([self.plot_name]) - for i in self._post.plots: - if i.plot_name == self.plot_name: - del i - break - return True - - @property - def differential_pairs(self): - """Differential pairs flag. - - Returns - ------- - bool - ``True`` when differential pairs is enabled, ``False`` otherwise. - """ - return self.props["context"].get("differential_pairs", False) - - @differential_pairs.setter - def differential_pairs(self, value): - self.props["context"]["differential_pairs"] = value - - @property - def matrix(self): - """Maxwell 2D/3D or Q2D/Q3D matrix name. - - Returns - ------- - str - Matrix name. - """ - return self.props["context"].get("matrix", None) - - @matrix.setter - def matrix(self, value): - self.props["context"]["matrix"] = value - - @property - def reduced_matrix(self): - """Maxwell 2D/3D reduced matrix name for eddy current solvers. - - Returns - ------- - str - Reduced matrix name. - """ - return self.props["context"].get("reduced_matrix", None) - - @reduced_matrix.setter - def reduced_matrix(self, value): - self.props["context"]["reduced_matrix"] = value - - @property - def polyline(self): - """Polyline name for the field report. - - Returns - ------- - str - Polyline name. - """ - return self.props["context"].get("polyline", None) - - @polyline.setter - def polyline(self, value): - self.props["context"]["polyline"] = value - - @property - def expressions(self): - """Expressions. - - Returns - ------- - str - Expressions. - """ - if self.props.get("expressions", None) is None: - return [] - return [k.get("name", None) for k in self.props["expressions"] if k.get("name", None) is not None] - - @expressions.setter - def expressions(self, value): - if isinstance(value, dict): - self.props["expressions"].append(value) - elif isinstance(value, list): - self.props["expressions"] = [] - for el in value: - if isinstance(el, dict): - self.props["expressions"].append(el) - else: - self.props["expressions"].append({"name": el}) - elif isinstance(value, str): - if isinstance(self.props["expressions"], list): - self.props["expressions"].append({"name": value}) - else: - self.props["expressions"] = [{"name": value}] - - @property - def report_category(self): - """Report category. - - Returns - ------- - str - Report category. - """ - return self.props["report_category"] - - @report_category.setter - def report_category(self, value): - self.props["report_category"] = value - - @property - def report_type(self): - """Report type. Options are ``"3D Polar Plot"``, ``"3D Spherical Plot"``, - ``"Radiation Pattern"``, ``"Rectangular Plot"``, ``"Data Table"``, - ``"Smith Chart"``, and ``"Rectangular Contour Plot"``. - - Returns - ------- - str - Report type. - """ - return self.props["report_type"] - - @report_type.setter - def report_type(self, report): - self.props["report_type"] = report - if not self.primary_sweep: - if self.props["report_type"] in ["3D Polar Plot", "3D Spherical Plot"]: - self.primary_sweep = "Phi" - self.secondary_sweep = "Theta" - elif self.props["report_type"] == "Radiation Pattern": - self.primary_sweep = "Phi" - elif self.domain == "Sweep": - self.primary_sweep = "Freq" - elif self.domain == "Time": - self.primary_sweep = "Time" - - @property - def traces(self): - """List of available traces in the report. - - .. note:: - This property works in version 2022 R1 and later. However, it works only in - non-graphical mode in version 2022 R2 and later. - - Returns - ------- - List of :class:`ansys.aedt.core.modules.report_templates.Trace` - """ - _traces = [] - try: - oo = self._post.oreportsetup.GetChildObject(self.plot_name) - oo_names = self._post.oreportsetup.GetChildObject(self.plot_name).GetChildNames() - except Exception: - return _traces - for el in oo_names: - if el in ["Legend", "Grid", "AxisX", "AxisY1", "Header", "General", "CartesianDisplayTypeProperty"]: - continue - try: - oo1 = oo.GetChildObject(el).GetChildNames() - - for i in oo1: - _traces.append(Trace(self._post.oreportsetup, "{}:{}:{}".format(self.plot_name, el, i))) - except Exception: - self._post._app.logger.debug("Something went wrong while processing element {}.".format(el)) - return _traces - - @pyaedt_function_handler() - def _update_traces(self): - for trace in self.traces[::]: - trace_name = trace.aedt_name.split(":")[1] - if "expressions" in self.props and trace_name in self.props["expressions"]: - trace_val = self.props["expressions"][trace_name] - trace_style = _props_with_default(trace_val, "trace_style") - trace_width = _props_with_default(trace_val, "width") - trace_type = _props_with_default(trace_val, "trace_type") - trace_color = _props_with_default(trace_val, "color") - symbol_show = _props_with_default(trace_val, "show_symbols", False) - symbol_style = _props_with_default(trace_val, "symbol_style", None) - symbol_arrows = _props_with_default(trace_val, "show_arrows", None) - symbol_fill = _props_with_default(trace_val, "symbol_fill", False) - symbol_color = _props_with_default(trace_val, "symbol_color", None) - trace.set_trace_properties( - style=trace_style, width=trace_width, trace_type=trace_type, color=trace_color - ) - if self.report_category in ["Eye Diagram", "Spectrum"]: - continue - trace.set_symbol_properties( - show=symbol_show, - style=symbol_style, - show_arrows=symbol_arrows, - fill=symbol_fill, - color=symbol_color, - ) - if ( - "eye_mask" in self.props - and self.report_category in ["Eye Diagram", "Statistical Eye"] - or ("quantity_type" in self.props and self.report_type == "Rectangular Contour Plot") - ): - eye_xunits = _props_with_default(self.props["eye_mask"], "xunits", "ns") - eye_yunits = _props_with_default(self.props["eye_mask"], "yunits", "mV") - eye_points = _props_with_default(self.props["eye_mask"], "points") - eye_enable = _props_with_default(self.props["eye_mask"], "enable_limits", False) - eye_upper = _props_with_default(self.props["eye_mask"], "upper_limit", 500) - eye_lower = _props_with_default(self.props["eye_mask"], "lower_limit", 0.3) - eye_transparency = _props_with_default(self.props["eye_mask"], "transparency", 0.3) - eye_color = _props_with_default(self.props["eye_mask"], "color", (0, 128, 0)) - eye_xoffset = _props_with_default(self.props["eye_mask"], "X Offset", "0ns") - eye_yoffset = _props_with_default(self.props["eye_mask"], "Y Offset", "0V") - if "quantity_type" in self.props and self.report_type == "Rectangular Contour Plot": - if "contours_number" in self.props.get("general", {}): - self._change_property( - "Contour", - " Plot {}".format(self.traces[0].name), - [ - "NAME:ChangedProps", - ["NAME:Num. Contours", "Value:=", str(self.props["general"]["contours_number"])], - ], - ) - if "contours_scale" in self.props.get("general", {}): - self._change_property( - "Contour", - " Plot {}".format(self.traces[0].name), - [ - "NAME:ChangedProps", - ["NAME:Axis Scale", "Value:=", str(self.props["general"]["contours_scale"])], - ], - ) - if "enable_contours_auto_limit" in self.props.get("general", {}): - self._change_property( - "Contour", - " Plot {}".format(self.traces[0].name), - ["NAME:ChangedProps", ["NAME:Scale Type", "Value:=", "Auto Limits"]], - ) - elif "contours_min_limit" in self.props.get("general", {}): - self._change_property( - "Contour", - " Plot {}".format(self.traces[0].name), - [ - "NAME:ChangedProps", - ["NAME:Min", "Value:=", str(self.props["general"]["contours_min_limit"])], - ], - ) - elif "contours_max_limit" in self.props.get("general", {}): - self._change_property( - "Contour", - " Plot {}".format(self.traces[0].name), - [ - "NAME:ChangedProps", - ["NAME:Max", "Value:=", str(self.props["general"]["contours_max_limit"])], - ], - ) - self.eye_mask( - points=eye_points, - x_units=eye_xunits, - y_units=eye_yunits, - enable_limits=eye_enable, - upper_limit=eye_upper, - lower_limit=eye_lower, - color=eye_color, - transparency=eye_transparency, - x_offset=eye_xoffset, - y_offset=eye_yoffset, - ) - if "limitLines" in self.props and self.report_category not in ["Eye Diagram", "Statistical Eye"]: - for line in self.props["limitLines"].values(): - if "equation" in line: - line_start = _props_with_default(line, "start") - line_stop = _props_with_default(line, "stop") - line_step = _props_with_default(line, "step") - line_equation = _props_with_default(line, "equation") - line_axis = _props_with_default(line, "y_axis", 1) - if not line_start or not line_step or not line_stop or not line_equation: - self._post._app.logger.error( - "Equation Limit Lines needs Start, Stop, Step and Equation fields." - ) - continue - self.add_limit_line_from_equation( - start_x=line_start, stop_x=line_stop, step=line_step, equation=line_equation, y_axis=line_axis - ) - else: - line_x = _props_with_default(line, "xpoints") - line_y = _props_with_default(line, "ypoints") - line_xunits = _props_with_default(line, "xunits") - line_yunits = _props_with_default(line, "yunits", "") - line_axis = _props_with_default(line, "y_axis", "Y1") - self.add_limit_line_from_points(line_x, line_y, line_xunits, line_yunits, line_axis) - line_style = _props_with_default(line, "trace_style") - line_width = _props_with_default(line, "width") - line_hatchabove = _props_with_default(line, "hatch_above") - line_viol = _props_with_default(line, "violation_emphasis") - line_hatchpix = _props_with_default(line, "hatch_pixels") - line_color = _props_with_default(line, "color") - self.limit_lines[-1].set_line_properties( - style=line_style, - width=line_width, - hatch_above=line_hatchabove, - violation_emphasis=line_viol, - hatch_pixels=line_hatchpix, - color=line_color, - ) - if "notes" in self.props: - for note in self.props["notes"].values(): - note_text = _props_with_default(note, "text") - note_position = _props_with_default(note, "position", [0, 0]) - self.add_note(note_text, note_position[0], note_position[1]) - note_back_color = _props_with_default(note, "background_color") - note_background_visibility = _props_with_default(note, "background_visibility") - note_border_color = _props_with_default(note, "border_color") - note_border_visibility = _props_with_default(note, "border_visibility") - note_border_width = _props_with_default(note, "border_width") - note_font = _props_with_default(note, "font", "Arial") - note_font_size = _props_with_default(note, "font_size", 12) - note_italic = _props_with_default(note, "italic") - note_bold = _props_with_default(note, "bold") - note_color = _props_with_default(note, "color", (0, 0, 0)) - - self.notes[-1].set_note_properties( - back_color=note_back_color, - background_visibility=note_background_visibility, - border_color=note_border_color, - border_visibility=note_border_visibility, - border_width=note_border_width, - font=note_font, - font_size=note_font_size, - italic=note_italic, - bold=note_bold, - color=note_color, - ) - if "general" in self.props: - if "show_rectangular_plot" in self.props["general"] and self.report_category in ["Eye Diagram"]: - eye_rectangular = _props_with_default(self.props["general"], "show_rectangular_plot", True) - self.rectangular_plot(eye_rectangular) - if "legend" in self.props["general"] and self.report_type != "Rectangular Contour Plot": - legend = self.props["general"]["legend"] - legend_sol_name = _props_with_default(legend, "show_solution_name", True) - legend_var_keys = _props_with_default(legend, "show_variation_key", True) - leend_trace_names = _props_with_default(legend, "show_trace_name", True) - legend_color = _props_with_default(legend, "back_color", (255, 255, 255)) - legend_font_color = _props_with_default(legend, "font_color", (0, 0, 0)) - self.edit_legend( - show_solution_name=legend_sol_name, - show_variation_key=legend_var_keys, - show_trace_name=leend_trace_names, - back_color=legend_color, - font_color=legend_font_color, - ) - if "grid" in self.props["general"]: - grid = self.props["general"]["grid"] - grid_major_color = _props_with_default(grid, "major_color", (200, 200, 200)) - grid_minor_color = _props_with_default(grid, "minor_color", (230, 230, 230)) - grid_enable_major_x = _props_with_default(grid, "major_x", True) - grid_enable_major_y = _props_with_default(grid, "major_y", True) - grid_enable_minor_x = _props_with_default(grid, "minor_x", True) - grid_enable_minor_y = _props_with_default(grid, "minor_y", True) - grid_style_minor = _props_with_default(grid, "style_minor", "Solid") - grid_style_major = _props_with_default(grid, "style_major", "Solid") - self.edit_grid( - minor_x=grid_enable_minor_x, - minor_y=grid_enable_minor_y, - major_x=grid_enable_major_x, - major_y=grid_enable_major_y, - minor_color=grid_minor_color, - major_color=grid_major_color, - style_minor=grid_style_minor, - style_major=grid_style_major, - ) - if "appearance" in self.props["general"]: - general = self.props["general"]["appearance"] - general_back_color = _props_with_default(general, "background_color", (255, 255, 255)) - general_plot_color = _props_with_default(general, "plot_color", (255, 255, 255)) - enable_y_stripes = _props_with_default(general, "enable_y_stripes", True) - general_field_width = _props_with_default(general, "field_width", 4) - general_precision = _props_with_default(general, "precision", 4) - general_use_scientific_notation = _props_with_default(general, "use_scientific_notation", True) - self.edit_general_settings( - background_color=general_back_color, - plot_color=general_plot_color, - enable_y_stripes=enable_y_stripes, - field_width=general_field_width, - precision=general_precision, - use_scientific_notation=general_use_scientific_notation, - ) - if "header" in self.props["general"]: - header = self.props["general"]["header"] - company_name = _props_with_default(header, "company_name", "") - show_design_name = _props_with_default(header, "show_design_name", True) - header_font = _props_with_default(header, "font", "Arial") - header_title_size = _props_with_default(header, "title_size", 12) - header_subtitle_size = _props_with_default(header, "subtitle_size", 12) - header_italic = _props_with_default(header, "italic", False) - header_bold = _props_with_default(header, "bold", False) - header_color = _props_with_default(header, "color", (0, 0, 0)) - self.edit_header( - company_name=company_name, - show_design_name=show_design_name, - font=header_font, - title_size=header_title_size, - subtitle_size=header_subtitle_size, - italic=header_italic, - bold=header_bold, - color=header_color, - ) - - for i in list(self.props["general"].keys()): - if "axis" in i: - axis = self.props["general"][i] - axis_font = _props_with_default(axis, "font", "Arial") - axis_size = _props_with_default(axis, "font_size", 12) - axis_italic = _props_with_default(axis, "italic", False) - axis_bold = _props_with_default(axis, "bold", False) - axis_color = _props_with_default(axis, "color", (0, 0, 0)) - axis_label = _props_with_default(axis, "label") - axis_linear_scaling = _props_with_default(axis, "linear_scaling", True) - axis_min_scale = _props_with_default(axis, "min_scale") - axis_max_scale = _props_with_default(axis, "max_scale") - axis_min_trick_div = _props_with_default(axis, "minor_tick_divs", 5) - specify_spacing = _props_with_default(axis, "specify_spacing", True) - if not specify_spacing: - axis_min_spacing = None - else: - axis_min_spacing = _props_with_default(axis, "min_spacing") - axis_units = _props_with_default(axis, "units") - if i == "axisx": - self.edit_x_axis( - font=axis_font, - font_size=axis_size, - italic=axis_italic, - bold=axis_bold, - color=axis_color, - label=axis_label, - ) - if self.report_category in ["Eye Diagram", "Statistical Eye"]: - continue - self.edit_x_axis_scaling( - linear_scaling=axis_linear_scaling, - min_scale=axis_min_scale, - max_scale=axis_max_scale, - minor_tick_divs=axis_min_trick_div, - min_spacing=axis_min_spacing, - units=axis_units, - ) - else: - self.edit_y_axis( - font=axis_font, - font_size=axis_size, - italic=axis_italic, - bold=axis_bold, - color=axis_color, - label=axis_label, - ) - if self.report_category in ["Eye Diagram", "Statistical Eye"]: - continue - self.edit_y_axis_scaling( - name=i.replace("axis", "").upper(), - linear_scaling=axis_linear_scaling, - min_scale=axis_min_scale, - max_scale=axis_max_scale, - minor_tick_divs=axis_min_trick_div, - min_spacing=axis_min_spacing, - units=axis_units, - ) - - @property - def limit_lines(self): - """List of available limit lines in the report. - - .. note:: - This property works in version 2022 R1 and later. However, it works only in - non-graphical mode in version 2022 R2 and later. - - Returns - ------- - List of :class:`ansys.aedt.core.modules.report_templates.LimitLine` - """ - _traces = [] - oo_names = self._post._app.get_oo_name(self._post.oreportsetup, self.plot_name) - for el in oo_names: - if "LimitLine" in el: - _traces.append(LimitLine(self._post.oreportsetup, "{}:{}".format(self.plot_name, el))) - - return _traces - - @property - def notes(self): - """List of available notes in the report. - - .. note:: - This property works in version 2022 R1 and later. However, it works only in - non-graphical mode in version 2022 R2 and later. - - Returns - ------- - List of :class:`ansys.aedt.core.modules.report_templates.Note` - """ - _notes = [] - try: - oo_names = self._post.oreportsetup.GetChildObject(self.plot_name).GetChildNames() - except Exception: - return _notes - for el in oo_names: - if "Note" in el: - _notes.append(Note(self._post.oreportsetup, "{}:{}".format(self.plot_name, el))) - - return _notes - - @property - def plot_name(self): - """Plot name. - - Returns - ------- - str - Plot name. - """ - return self.props["plot_name"] - - @plot_name.setter - def plot_name(self, name): - if self._is_created: - if name not in self._post.oreportsetup.GetAllReportNames(): - self._post.oreportsetup.RenameReport(self.props["plot_name"], name) - self.props["plot_name"] = name - - @property - def variations(self): - """Variations. - - Returns - ------- - str - Variations. - """ - return self.props["context"]["variations"] - - @variations.setter - def variations(self, value): - self.props["context"]["variations"] = value - - @property - def primary_sweep(self): - """Primary sweep report. - - Returns - ------- - str - Primary sweep. - """ - return self.props["context"]["primary_sweep"] - - @primary_sweep.setter - def primary_sweep(self, value): - if value == self.props["context"].get("secondary_sweep", None): - self.props["context"]["secondary_sweep"] = self.props["context"]["primary_sweep"] - self.props["context"]["primary_sweep"] = value - if value == "Time": - self.variations.pop("Freq", None) - self.variations["Time"] = ["All"] - elif value == "Freq": - self.variations.pop("Time", None) - self.variations["Freq"] = ["All"] - - @property - def secondary_sweep(self): - """Secondary sweep report. - - Returns - ------- - str - Secondary sweep. - """ - return self.props["context"].get("secondary_sweep", None) - - @secondary_sweep.setter - def secondary_sweep(self, value): - if value == self.props["context"]["primary_sweep"]: - self.props["context"]["primary_sweep"] = self.props["context"]["secondary_sweep"] - self.props["context"]["secondary_sweep"] = value - if value == "Time": - self.variations.pop("Freq", None) - self.variations["Time"] = ["All"] - elif value == "Freq": - self.variations.pop("Time", None) - self.variations["Freq"] = ["All"] - - @property - def primary_sweep_range(self): - """Primary sweep range report. - - Returns - ------- - str - Primary sweep range. - """ - return self.props["context"]["primary_sweep_range"] - - @primary_sweep_range.setter - def primary_sweep_range(self, value): - self.props["context"]["primary_sweep_range"] = value - - @property - def secondary_sweep_range(self): - """Secondary sweep range report. - - Returns - ------- - str - Secondary sweep range. - """ - return self.props["context"]["secondary_sweep_range"] - - @secondary_sweep_range.setter - def secondary_sweep_range(self, value): - self.props["context"]["secondary_sweep_range"] = value - - @property - def _context(self): - return [] - - @pyaedt_function_handler() - def update_expressions_with_defaults(self, quantities_category=None): - """Update the list of expressions by taking all quantities from a given category. - - Parameters - ---------- - quantities_category : str, optional - Quantities category to use. The default is ``None``, in which case the default - category for the specified report is used. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - self.expressions = self._post.available_report_quantities( - self.report_category, self.report_type, self.setup, quantities_category - ) - - @property - def _trace_info(self): - if not self.expressions: - self.update_expressions_with_defaults() - if isinstance(self.expressions, list): - expr = self.expressions - else: - expr = [self.expressions] - arg = ["X Component:=", self.primary_sweep, "Y Component:=", expr] - if self.report_type in ["3D Polar Plot", "3D Spherical Plot"]: - arg = [ - "Phi Component:=", - self.primary_sweep, - "Theta Component:=", - self.secondary_sweep, - "Mag Component:=", - expr, - ] - elif self.report_type == "Radiation Pattern": - arg = ["Ang Component:=", self.primary_sweep, "Mag Component:=", expr] - elif self.report_type in ["Smith Chart", "Polar Plot"]: - arg = ["Polar Component:=", expr] - elif self.report_type == "Rectangular Contour Plot": - arg = [ - "X Component:=", - self.primary_sweep, - "Y Component:=", - self.secondary_sweep, - "Z Component:=", - expr, - ] - return arg - - @property - def domain(self): - """Plot domain. - - Returns - ------- - str - Plot domain. - """ - return self.props["context"]["domain"] - - @domain.setter - def domain(self, domain): - self.props["context"]["domain"] = domain - if self._post._app.design_type in ["Maxwell 3D", "Maxwell 2D"]: - return - if self.primary_sweep == "Freq" and domain == "Time": - self.primary_sweep = "Time" - self.variations.pop("Freq", None) - self.variations["Time"] = ["All"] - elif self.primary_sweep == "Time" and domain == "Sweep": - self.primary_sweep = "Freq" - self.variations.pop("Time", None) - self.variations["Freq"] = ["All"] - - @property - def use_pulse_in_tdr(self): - """Defines if the TDR should use a pulse or step. - - Returns - ------- - bool - ``True`` when option is enabled, ``False`` otherwise. - """ - return self.props["context"].get("use_pulse_in_tdr", False) - - @use_pulse_in_tdr.setter - def use_pulse_in_tdr(self, val): - self.props["context"]["use_pulse_in_tdr"] = val - - @pyaedt_function_handler() - def _convert_dict_to_report_sel(self, sweeps): - if not sweeps: - return [] - sweep_list = [] - if self.primary_sweep: - sweep_list.append(self.primary_sweep + ":=") - if self.primary_sweep_range == ["All"] and self.primary_sweep in self.variations: - sweep_list.append(self.variations[self.primary_sweep]) - else: - sweep_list.append(self.primary_sweep_range) - if self.secondary_sweep: - sweep_list.append(self.secondary_sweep + ":=") - if self.secondary_sweep_range == ["All"] and self.secondary_sweep in self.variations: - sweep_list.append(self.variations[self.primary_sweep]) - else: - sweep_list.append(self.secondary_sweep_range) - for el in sweeps: - if el in [self.primary_sweep, self.secondary_sweep]: - continue - sweep_list.append(el + ":=") - if isinstance(sweeps[el], list): - sweep_list.append(sweeps[el]) - else: - sweep_list.append([sweeps[el]]) - for el in list(self._post._app.available_variations.nominal_w_values_dict.keys()): - if el not in sweeps: - sweep_list.append(el + ":=") - sweep_list.append(["Nominal"]) - return sweep_list - - @pyaedt_function_handler(plot_name="name") - def create(self, name=None): - """Create a report. - - Parameters - ---------- - name : str, optional - Name for the plot. The default is ``None``, in which case the - default name is used. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if not name: - self.plot_name = generate_unique_name("Plot") - else: - self.plot_name = name - if self.setup not in self._post._app.existing_analysis_sweeps and "AdaptivePass" not in self.setup: - self._post._app.logger.error("Setup doesn't exist in this design.") - return False - self._post.oreportsetup.CreateReport( - self.plot_name, - self.report_category, - self.report_type, - self.setup, - self._context, - self._convert_dict_to_report_sel(self.variations), - self._trace_info, - ) - self._post.plots.append(self) - self._is_created = True - return True - - @pyaedt_function_handler() - def get_solution_data(self): - """Get the report solution data. - - Returns - ------- - :class:`ansys.aedt.core.modules.solutions.SolutionData` - Solution data object. - """ - if not self.expressions: - self.update_expressions_with_defaults() - solution_data = self._post.get_solution_data_per_variation( - self.report_category, self.setup, self._context, self.variations, self.expressions - ) - if not solution_data: - self._post._app.logger.warning("No Data Available. Check inputs") - return False - if self.primary_sweep: - solution_data.primary_sweep = self.primary_sweep - return solution_data - - @pyaedt_function_handler() - def add_limit_line_from_points(self, x_list, y_list, x_units="", y_units="", y_axis="Y1"): # pragma: no cover - """Add a Cartesian limit line from point lists. This method works only in graphical mode. - - Parameters - ---------- - x_list : list - List of float inputs. - y_list : list - List of float y values. - x_units : str, optional - Units for the ``x_list`` parameter. The default is ``""``. - y_units : str, optional - Units for the ``y_list`` parameter. The default is ``""``. - y_axis : int, optional - Y axis. The default is `"Y1"`. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - x_list = [GeometryOperators.parse_dim_arg(str(i) + x_units) for i in x_list] - y_list = [GeometryOperators.parse_dim_arg(str(i) + y_units) for i in y_list] - if self.plot_name and self._is_created: - xvals = ["NAME:XValues"] - xvals.extend(x_list) - yvals = ["NAME:YValues"] - yvals.extend(y_list) - self._post.oreportsetup.AddCartesianLimitLine( - self.plot_name, - [ - "NAME:CartesianLimitLine", - xvals, - "XUnits:=", - x_units, - yvals, - "YUnits:=", - y_units, - "YAxis:=", - y_axis, - ], - ) - return True - return False - - @pyaedt_function_handler() - def add_limit_line_from_equation( - self, start_x, stop_x, step, equation="x", units="GHz", y_axis=1 - ): # pragma: no cover - """Add a Cartesian limit line from point lists. This method works only in graphical mode. - - Parameters - ---------- - start_x : float - Start X value. - stop_x : float - Stop X value. - step : float - X step value. - equation : str, optional - Y equation to apply. The default is Y=X. - units : str - Units for the X axis. The default is ``"GHz"``. - y_axis : str, int, optional - Y axis. The default is ``1``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if self.plot_name and self._is_created: - self._post.oreportsetup.AddCartesianLimitLineFromEquation( - self.plot_name, - [ - "NAME:CartesianLimitLineFromEquation", - "YAxis:=", - int(str(y_axis).replace("Y", "")), - "Start:=", - self._post._app.value_with_units(start_x, units), - "Stop:=", - self._post._app.value_with_units(stop_x, units), - "Step:=", - self._post._app.value_with_units(step, units), - "Equation:=", - equation, - ], - ) - return True - return False - - @pyaedt_function_handler() - def add_note(self, text, x_position=0, y_position=0): # pragma: no cover - """Add a note at a position. - - Parameters - ---------- - text : string - Text of the note. - x_position : float, optional - x position of the note. The default is ``0.0``. - y_position : float, optional - y position of the note. The default is ``0.0``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - note_name = generate_unique_name("Note", n=3) - if self.plot_name and self._is_created: - self._post.oreportsetup.AddNote( - self.plot_name, - [ - "NAME:NoteDataSource", - [ - "NAME:NoteDataSource", - "SourceName:=", - note_name, - "HaveDefaultPos:=", - True, - "DefaultXPos:=", - x_position, - "DefaultYPos:=", - y_position, - "String:=", - text, - ], - ], - ) - return True - return False - - @pyaedt_function_handler(val="value") - def add_cartesian_x_marker(self, value, name=None): # pragma: no cover - """Add a cartesian X marker. - - .. note:: - This method only works in graphical mode. - - Parameters - ---------- - value : str - Value to apply with units. - name : str, optional - Marker name. The default is ``None``. - - Returns - ------- - str - Marker name if created. - """ - if not name: - name = generate_unique_name("MX") - self._post.oreportsetup.AddCartesianXMarker(self.plot_name, name, GeometryOperators.parse_dim_arg(value)) - return name - return "" - - @pyaedt_function_handler(val="value") - def add_cartesian_y_marker(self, value, name=None, y_axis=1): # pragma: no cover - """Add a cartesian Y marker. - - .. note:: - This method only works in graphical mode. - - Parameters - ---------- - value : str, float - Value to apply with units. - name : str, optional - Marker name. The default is ``None``. - y_axis : str, optional - Y axis. The default is ``"Y1"``. - - Returns - ------- - str - Marker name if created. - """ - if not name: - name = generate_unique_name("MY") - self._post.oreportsetup.AddCartesianYMarker( - self.plot_name, name, "Y{}".format(y_axis), GeometryOperators.parse_dim_arg(value), "" - ) - return name - return "" - - @pyaedt_function_handler(tabname="tab_name") - def _change_property(self, tab_name, property_name, property_val): - if not self._is_created: - self._post._app.logger.error("Plot has not been created. Create it and then change the properties.") - return False - arg = [ - "NAME:AllTabs", - ["NAME:" + tab_name, ["NAME:PropServers", "{}:{}".format(self.plot_name, property_name)], property_val], - ] - self._post.oreportsetup.ChangeProperty(arg) - return True - - @pyaedt_function_handler() - def edit_grid( - self, - minor_x=True, - minor_y=True, - major_x=True, - major_y=True, - style_minor="Solid", - style_major="Solid", - minor_color=(0, 0, 0), - major_color=(0, 0, 0), - ): - """Edit the grid settings for the plot. - - Parameters - ---------- - minor_x : bool, optional - Whether to enable the minor X grid. The default is ``True``. - minor_y : bool, optional - Whether to enable the minor Y grid. The default is ``True``. - major_x : bool, optional - Whether to enable the major X grid. The default is ``True``. - major_y : bool, optional - Whether to enable the major Y grid. The default is ``True``. - style_minor : str, optional - Minor grid style. The default is ``"Solid"``. - style_major : str, optional - Major grid style. The default is ``"Solid"``. - minor_color : tuple, optional - Minor grid (R, G, B) color. The default is ``(0, 0, 0)``. - Each color value must be an integer in a range from 0 to 255. - major_color : tuple, optional - Major grid (R, G, B) color. The default is ``(0, 0, 0)``. - Each color value must be an integer in a range from 0 to 255. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - props = [ - "NAME:ChangedProps", - ["NAME:Show minor X grid", "Value:=", minor_x], - ["NAME:Show minor Y grid", "Value:=", minor_y], - ["NAME:Show major X grid", "Value:=", major_x], - ["NAME:Show major Y grid", "Value:=", major_y], - ["NAME:Minor grid line style", "Value:=", style_minor], - ["NAME:Major grid line style", "Value:=", style_major], - ["NAME:Minor grid line color", "R:=", minor_color[0], "G:=", minor_color[1], "B:=", minor_color[2]], - ["NAME:Major grid line color", "R:=", major_color[0], "G:=", major_color[1], "B:=", major_color[2]], - ] - return self._change_property("Grid", "Grid", props) - - @pyaedt_function_handler() - def edit_x_axis(self, font="Arial", font_size=12, italic=False, bold=False, color=(0, 0, 0), label=None): - """Edit the X-axis settings. - - Parameters - ---------- - font : str, optional - Font name. The default is ``"Arial"``. - font_size : int, optional - Font size. The default is ``12``. - italic : bool, optional - Whether to use italic type. The default is ``False``. - bold : bool, optional - Whether to use bold type. The default is ``False``. - color : tuple, optional - Font (R, G, B) color. The default is ``(0, 0, 0)``. Each color value - must be an integer in a range from 0 to 255. - label : str, optional - Label for the Y axis. The default is ``None``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - props = [ - "NAME:ChangedProps", - [ - "NAME:Text Font", - "Height:=", - -1 * font_size - 2, - "Width:=", - 0, - "Escapement:=", - 0, - "Orientation:=", - 0, - "Weight:=", - 700 if bold else 400, - "Italic:=", - 255 if italic else 0, - "Underline:=", - 0, - "StrikeOut:=", - 0, - "CharSet:=", - 0, - "OutPrecision:=", - 3, - "ClipPrecision:=", - 2, - "Quality:=", - 1, - "PitchAndFamily:=", - 34, - "FaceName:=", - font, - "R:=", - color[0], - "G:=", - color[1], - "B:=", - color[2], - ], - ] - if label: - props.append(["NAME:Name", "Value:=", label]) - props.append(["NAME:Axis Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) - return self._change_property("Axis", "AxisX", props) - - @pyaedt_function_handler() - def edit_x_axis_scaling( - self, linear_scaling=True, min_scale=None, max_scale=None, minor_tick_divs=5, min_spacing=None, units=None - ): - """Edit the X-axis scaling settings. - - Parameters - ---------- - linear_scaling : bool, optional - Whether to use the linear scale. The default is ``True``. - When ``False``, the log scale is used. - min_scale : str, optional - Minimum scale value with units. The default is ``None``. - max_scale : str, optional - Maximum scale value with units. The default is ``None``. - minor_tick_divs : int, optional - Minor tick division. The default is ``5``. - min_spacing : str, optional - Minimum spacing with units. The default is ``None``. - units : str, optional - Units in the plot. The default is ``None``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if linear_scaling: - props = ["NAME:ChangedProps", ["NAME:Axis Scaling", "Value:=", "Linear"]] - else: - props = ["NAME:ChangedProps", ["NAME:Axis Scaling", "Value:=", "Log"]] - if min_scale: - props.append(["NAME:Min", "Value:=", min_scale]) - if max_scale: - props.append(["NAME:Max", "Value:=", max_scale]) - if minor_tick_divs and linear_scaling: - props.append(["NAME:Minor Tick Divs", "Value:=", str(minor_tick_divs)]) - if min_spacing: - props.append(["NAME:Spacing", "Value:=", min_spacing]) - if units: - props.append((["NAME:Auto Units", "Value:=", False])) - props.append(["NAME:Units", "Value:=", units]) - return self._change_property("Scaling", "AxisX", props) - - @pyaedt_function_handler() - def edit_legend( - self, - show_solution_name=True, - show_variation_key=True, - show_trace_name=True, - back_color=(255, 255, 255), - font_color=(0, 0, 0), - ): - """Edit the plot legend. - - Parameters - ---------- - show_solution_name : bool, optional - Whether to show the solution name. The default is ``True``. - show_variation_key : bool, optional - Whether to show the variation key. The default is ``True``. - show_trace_name : bool, optional - Whether to show the trace name. The default is ``True``. - back_color : tuple, optional - Background (R, G, B) color. The default is ``(255, 255, 255)``. Each color value - must be an integer in a range from 0 to 255. - font_color : tuple, optional - Legend font (R, G, B) color. The default is ``(0, 0, 0)``. Each color value - must be an integer in a range from 0 to 255. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - props = [ - "NAME:ChangedProps", - ["NAME:Show Solution Name", "Value:=", show_solution_name], - ["NAME:Show Variation Key", "Value:=", show_variation_key], - ["NAME:Show Trace Name", "Value:=", show_trace_name], - ["NAME:Back Color", "R:=", back_color[0], "G:=", back_color[1], "B:=", back_color[2]], - ["NAME:Font", "R:=", font_color[0], "G:=", font_color[1], "B:=", font_color[2]], - ] - return self._change_property("legend", "legend", props) - - @pyaedt_function_handler(font_height="font_size") - def hide_legend(self, solution_name=True, trace_name=True, variation_key=True, font_size=1): - """Hide the Legend. - - Parameters - ---------- - solution_name : bool, optional - Whether to show or hide the solution name. Default is ``True``. - trace_name : bool, optional - Whether to show or hide the trace name. Default is ``True``. - variation_key : bool, optional - Whether to show or hide the variations. Default is ``True``. - font_size : int - Font size. The default is ``1``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - try: - legend = self._post.oreportsetup.GetChildObject(self.plot_name).GetChildObject("Legend") - legend.Show_Solution_Name = not solution_name - legend.Show_Trace_Name = not trace_name - legend.Show_Variation_Key = not variation_key - legend.SetPropValue("Font/Height", font_size) - legend.SetPropValue("Header Row Font/Height", font_size) - return True - except Exception: - self._post._app.logger.error("Failed to hide legend.") - return False - - @pyaedt_function_handler(axis_name="name") - def edit_y_axis(self, name="Y1", font="Arial", font_size=12, italic=False, bold=False, color=(0, 0, 0), label=None): - """Edit the Y-axis settings. - - Parameters - ---------- - name : str, optional - Name for the main Y axis. The default is ``"Y1"``. - font : str, optional - Font name. The default is ``"Arial"``. - font_size : int, optional - Font size. The default is ``12``. - italic : bool, optional - Whether to use italic type. The default is ``False``. - bold : bool, optional - Whether to use bold type. The default is ``False``. - color : tuple, optional - Font (R, G, B) color. The default is ``(0, 0, 0)``. Each color value - must be an integer in a range from 0 to 255. - label : str, optional - Label for the Y axis. The default is ``None``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - props = [ - "NAME:ChangedProps", - [ - "NAME:Text Font", - "Height:=", - -1 * font_size - 2, - "Width:=", - 0, - "Escapement:=", - 0, - "Orientation:=", - 0, - "Weight:=", - 700 if bold else 400, - "Italic:=", - 255 if italic else 0, - "Underline:=", - 0, - "StrikeOut:=", - 0, - "CharSet:=", - 0, - "OutPrecision:=", - 3, - "ClipPrecision:=", - 2, - "Quality:=", - 1, - "PitchAndFamily:=", - 34, - "FaceName:=", - font, - "R:=", - color[0], - "G:=", - color[1], - "B:=", - color[2], - ], - ] - if label: - props.append(["NAME:Name", "Value:=", label]) - props.append(["NAME:Axis Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) - return self._change_property("Axis", "Axis" + name, props) - - @pyaedt_function_handler(axis_name="name") - def edit_y_axis_scaling( - self, - name="Y1", - linear_scaling=True, - min_scale=None, - max_scale=None, - minor_tick_divs=5, - min_spacing=None, - units=None, - ): - """Edit the Y-axis scaling settings. - - Parameters - ---------- - axis name : str, optional - Axis name. The default is ``Y``. - linear_scaling : bool, optional - Whether to use the linear scale. The default is ``True``. - When ``False``, the log scale is used. - min_scale : str, optional - Minimum scale value with units. The default is ``None``. - max_scale : str, optional - Maximum scale value with units. The default is ``None``. - minor_tick_divs : int, optional - Minor tick division. The default is ``5``. - min_spacing : str, optional - Minimum spacing with units. The default is ``None``. - units : str, optional - Units in the plot. The default is ``None``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if linear_scaling: - props = ["NAME:ChangedProps", ["NAME:Axis Scaling", "Value:=", "Linear"]] - else: - props = ["NAME:ChangedProps", ["NAME:Axis Scaling", "Value:=", "Log"]] - if min_scale: - props.append(["NAME:Min", "Value:=", min_scale]) - if max_scale: - props.append(["NAME:Max", "Value:=", max_scale]) - if minor_tick_divs and linear_scaling: - props.append(["NAME:Minor Tick Divs", "Value:=", str(minor_tick_divs)]) - if min_spacing: - props.append(["NAME:Spacing", "Value:=", min_spacing]) - if units: - props.append((["NAME:Auto Units", "Value:=", False])) - props.append(["NAME:Units", "Value:=", units]) - return self._change_property("Scaling", "Axis" + name, props) - - @pyaedt_function_handler() - def edit_general_settings( - self, - background_color=(255, 255, 255), - plot_color=(255, 255, 255), - enable_y_stripes=True, - field_width=4, - precision=4, - use_scientific_notation=True, - ): - """Edit general settings for the plot. - - Parameters - ---------- - background_color : tuple, optional - Backgoround (R, G, B) color. The default is ``(255, 255, 255)``. Each color value - must be an integer in a range from 0 to 255. - plot_color : tuple, optional - Plot (R, G, B) color. The default is ``(255, 255, 255)``. Each color value - must be an integer in a range from 0 to 255. - enable_y_stripes : bool, optional - Whether to enable Y stripes. The default is ``True``. - field_width : int, optional - Field width. The default is ``4``. - precision : int, optional - Field precision. The default is ``4``. - use_scientific_notation : bool, optional - Whether to enable scientific notation. The default is ``True``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - props = [ - "NAME:ChangedProps", - ["NAME:Back Color", "R:=", background_color[0], "G:=", background_color[1], "B:=", background_color[2]], - ["NAME:Plot Area Color", "R:=", plot_color[0], "G:=", plot_color[1], "B:=", plot_color[2]], - ["NAME:Enable Y Axis Stripes", "Value:=", enable_y_stripes], - ["NAME:Field Width", "Value:=", str(field_width)], - ["NAME:Precision", "Value:=", str(precision)], - ["NAME:Use Scientific Notation", "Value:=", use_scientific_notation], - ] - return self._change_property("general", "general", props) - - @pyaedt_function_handler() - def edit_header( - self, - company_name="PyAEDT", - show_design_name=True, - font="Arial", - title_size=12, - subtitle_size=12, - italic=False, - bold=False, - color=(0, 0, 0), - ): - """Edit the plot header. - - Parameters - ---------- - company_name : str, optional - Company name. The default is ``PyAEDT``. - show_design_name : bool, optional - Whether to show the design name in the plot. The default is ``True``. - font : str, optional - Font name. The default is ``"Arial"``. - title_size : int, optional - Title font size. The default is ``12``. - subtitle_size : int, optional - Subtitle font size. The default is ``12``. - italic : bool, optional - Whether to use italic type. The default is ``False``. - bold : bool, optional - Whether to use bold type. The default is ``False``. - color : tuple, optional - Title (R, G, B) color. The default is ``(0, 0, 0)``. - Each color value must be an integer in a range from 0 to 255. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - props = [ - "NAME:ChangedProps", - [ - "NAME:Title Font", - "Height:=", - -1 * title_size - 2, - "Width:=", - 0, - "Escapement:=", - 0, - "Orientation:=", - 0, - "Weight:=", - 700 if bold else 400, - "Italic:=", - 255 if italic else 0, - "Underline:=", - 0, - "StrikeOut:=", - 0, - "CharSet:=", - 0, - "OutPrecision:=", - 3, - "ClipPrecision:=", - 2, - "Quality:=", - 1, - "PitchAndFamily:=", - 34, - "FaceName:=", - font, - "R:=", - color[0], - "G:=", - color[1], - "B:=", - color[2], - ], - [ - "NAME:Sub Title Font", - "Height:=", - -1 * subtitle_size - 2, - "Width:=", - 0, - "Escapement:=", - 0, - "Orientation:=", - 0, - "Weight:=", - 700 if bold else 400, - "Italic:=", - 255 if italic else 0, - "Underline:=", - 0, - "StrikeOut:=", - 0, - "CharSet:=", - 0, - "OutPrecision:=", - 3, - "ClipPrecision:=", - 2, - "Quality:=", - 1, - "PitchAndFamily:=", - 34, - "FaceName:=", - font, - "R:=", - color[0], - "G:=", - color[1], - "B:=", - color[2], - ], - ["NAME:Company Name", "Value:=", company_name], - ["NAME:Show Design Name", "Value:=", show_design_name], - ] - return self._change_property("Header", "Header", props) - - @pyaedt_function_handler(file_path="input_file") - def import_traces(self, input_file, plot_name): - """Import report data from a file into a specified report. - - Parameters - ---------- - input_file : str - Path for the file to import. The extensions supported are ``".csv"``, - ``".tab"``, ``".dat"``, and ``".rdat"``. - plot_name : str - Name of the plot to import the file data into. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if not os.path.exists(input_file): - msg = "File does not exist." - raise FileExistsError(msg) - - if not plot_name: - msg = "Plot name can't be None." - raise ValueError(msg) - else: - if plot_name not in self._post.all_report_names: - msg = "Plot name provided doesn't exists in current report." - raise ValueError(msg) - self.plot_name = plot_name - - split_path = os.path.splitext(input_file) - extension = split_path[1] - - supported_ext = [".csv", ".tab", ".dat", ".rdat"] - if extension not in supported_ext: - msg = "Extension {} is not supported. Use one of {}".format(extension, ", ".join(supported_ext)) - raise ValueError(msg) - - try: - if extension == ".rdat": - self._post.oreportsetup.ImportReportDataIntoReport(self.plot_name, input_file) - else: - self._post.oreportsetup.ImportIntoReport(self.plot_name, input_file) - return True - except Exception: - return False - - @pyaedt_function_handler() - def delete_traces(self, plot_name, traces_list): - """Delete an existing trace or traces. - - Parameters - ---------- - plot_name : str - Plot name. - traces_list : list - List of one or more traces to delete. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if plot_name not in self._post.all_report_names: - raise ValueError("Plot does not exist in current project.") - - for trace in traces_list: - if trace not in self._trace_info[3]: - raise ValueError("Trace does not exist in the selected plot.") - - props = ["{}:=".format(plot_name), traces_list] - try: - self._post.oreportsetup.DeleteTraces(props) - return True - except Exception: - return False - - @pyaedt_function_handler() - def add_trace_to_report(self, traces, setup_name=None, variations=None, context=None): - """Add a trace to a specific report. - - Parameters - ---------- - traces : list - List of traces to add. - setup_name : str, optional - Name of the setup. The default is ``None`` which automatically take ``nominal_adaptive`` setup. - Please make sure to build a setup string in the form of ``"SetupName : SetupSweep"`` - where ``SetupSweep`` is the Sweep name to use in the export or ``LastAdaptive``. - variations : dict, optional - Dictionary of variations. The default is ``None``. - context : list, optional - List of solution context. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - expr = copy.deepcopy(self.expressions) - self.expressions = traces - - try: - self._post.oreportsetup.AddTraces( - self.plot_name, - setup_name if setup_name else self.setup, - context if context else self._context, - self._convert_dict_to_report_sel(variations if variations else self.variations), - self._trace_info, - ) - return True - except Exception: - return False - finally: - self.expressions = expr - - @pyaedt_function_handler() - def update_trace_in_report(self, traces, setup_name=None, variations=None, context=None): - """Update a trace in a specific report. - - Parameters - ---------- - traces : list - List of traces to add. - setup_name : str, optional - Name of the setup. The default is ``None``. - variations : dict, optional - Dictionary of variations. The default is ``None``. - context : list, optional - List of solution context. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - expr = copy.deepcopy(self.expressions) - self.expressions = traces - - try: - self._post.oreportsetup.UpdateTraces( - self.plot_name, - traces, - setup_name if setup_name else self.setup, - context if context else self._context, - self._convert_dict_to_report_sel(variations if variations else self.variations), - self._trace_info, - ) - return True - except Exception: - return False - finally: - self.expressions = expr - - @pyaedt_function_handler() - def apply_report_template(self, input_file, property_type="Graphical"): # pragma: no cover - """Apply report template. - - .. note:: - This method works in only in graphical mode. - - Parameters - ---------- - input_file : str - Path for the file to import. The extension supported is ``".rpt"``. - property_type : str, optional - Property types to apply. Options are ``"Graphical"``, ``"Data"``, and ``"All"``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - - References - ---------- - - >>> oModule.ApplyReportTemplate - """ - if not os.path.exists(input_file): # pragma: no cover - msg = "File does not exist." - self._post.logger.error(msg) - return False - - split_path = os.path.splitext(input_file) - extension = split_path[1] - - supported_ext = [".rpt"] - if extension not in supported_ext: # pragma: no cover - msg = "Extension {} is not supported.".format(extension) - self._post.logger.error(msg) - return False - - if property_type not in ["Graphical", "Data", "All"]: # pragma: no cover - msg = "Invalid value for `property_type`. The value must be 'Graphical', 'Data', or 'All'." - self._post.logger.error(msg) - return False - self._post.oreportsetup.ApplyReportTemplate(self.plot_name, input_file, property_type) - return True - - -class Standard(CommonReport): - """Provides a reporting class that fits most of the app's standard reports.""" - - def __init__(self, app, report_category, setup_name, expressions=None): - CommonReport.__init__(self, app, report_category, setup_name, expressions) - - @property - def sub_design_id(self): - """Sub design ID for a Circuit or HFSS 3D Layout sub design. - - Returns - ------- - int - Number of the sub design ID. - """ - return self.props["context"].get("Sub Design ID", None) - - @sub_design_id.setter - def sub_design_id(self, value): - self.props["context"]["Sub Design ID"] = value - - @property - def time_start(self): - """Time start value. - - Returns - ------- - str - Time start value. - """ - return self.props["context"].get("time_start", None) - - @time_start.setter - def time_start(self, value): - self.props["context"]["time_start"] = value - - @property - def time_stop(self): - """Time stop value. - - Returns - ------- - str - Time stop value. - """ - return self.props["context"].get("time_stop", None) - - @time_stop.setter - def time_stop(self, value): - self.props["context"]["time_stop"] = value - - @property - def _did(self): - if self.domain == "Sweep": - return 3 - elif self.domain == "Clock Times": - return 55827 - else: - return 1 - - @property - def pulse_rise_time(self): - """Value of Pulse rise time for TDR plot. - - Returns - ------- - float - Pulse rise time. - """ - return self.props["context"].get("pulse_rise_time", 0) if self.domain == "Time" else 0 - - @pulse_rise_time.setter - def pulse_rise_time(self, val): - self.props["context"]["pulse_rise_time"] = val - - @property - def time_windowing(self): - """Returns the TDR time windowing. Options are: - * ``0`` : Rectangular - * ``1`` : Bartlett - * ``2`` : Blackman - * ``3`` : Hamming - * ``4`` : Hanning - * ``5`` : Kaiser - * ``6`` : Welch - * ``7`` : Weber - * ``8`` : Lanzcos. - - Returns - ------- - int - Time windowing. - """ - _time_windowing = self.props["context"].get("time_windowing", 0) - return _time_windowing if self.domain == "Time" and self.pulse_rise_time != 0 else 0 - - @time_windowing.setter - def time_windowing(self, val): - available_values = { - "rectangular": 0, - "bartlett": 1, - "blackman": 2, - "hamming": 3, - "hanning": 4, - "kaiser": 5, - "welch": 6, - "weber": 7, - "lanzcos": 8, - } - if isinstance(val, int): - self.props["context"]["time_windowing"] = val - elif isinstance(val, str) and val.lower in available_values: - self.props["context"]["time_windowing"] = available_values[val.lower()] - - @property - def _context(self): - ctxt = [] - if self._post.post_solution_type in ["TR", "AC", "DC"]: - ctxt = [ - "NAME:Context", - "SimValueContext:=", - [self._did, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0], - ] - elif self._post._app.design_type in ["Q3D Extractor", "2D Extractor"]: - if not self.matrix: - ctxt = ["Context:=", "Original"] - else: - ctxt = ["Context:=", self.matrix] - elif ( - self._post._app.design_type in ["Maxwell 2D", "Maxwell 3D"] - and self._post._app.solution_type == "EddyCurrent" - ): - if not self.matrix: - ctxt = ["Context:=", "Original"] - elif not self.reduced_matrix: - ctxt = ["Context:=", self.matrix] - elif self.reduced_matrix: - ctxt = ["Context:=", self.matrix, "Matrix:=", self.reduced_matrix] - elif self._post.post_solution_type in ["HFSS3DLayout"]: - if self.domain == "DCIR": - ctxt = [ - "NAME:Context", - "SimValueContext:=", - [ - 37010, - 0, - 2, - 0, - False, - False, - -1, - 1, - 0, - 1, - 1, - "", - 0, - 0, - "DCIRID", - False, - str(self.siwave_dc_category), - "IDIID", - False, - "1", - ], - ] - elif self.differential_pairs: - ctxt = [ - "NAME:Context", - "SimValueContext:=", - [ - self._did, - 0, - 2, - self.pulse_rise_time, - self.use_pulse_in_tdr if self.pulse_rise_time else False, - False, - -1, - 1, - self.time_windowing, - 1, - 1, - "", - self.pulse_rise_time / 5, - self.pulse_rise_time * 100, - "EnsDiffPairKey", - False, - "1", - "IDIID", - False, - "1" if not self.pulse_rise_time else "3", - ], - ] - else: - ctxt = [ - "NAME:Context", - "SimValueContext:=", - [ - self._did, - 0, - 2, - self.pulse_rise_time, - self.use_pulse_in_tdr if self.pulse_rise_time else False, - False, - -1, - 1, - self.time_windowing, - 1, - 1, - "", - self.pulse_rise_time / 5, - self.pulse_rise_time * 100, - "IDIID", - False, - "1" if not self.pulse_rise_time else "3", - ], - ] - elif self._post.post_solution_type in ["NexximLNA", "NexximTransient"]: - ctxt = [ - "NAME:Context", - "SimValueContext:=", - [ - self._did, - 0, - 2, - self.pulse_rise_time, - self.use_pulse_in_tdr if self.pulse_rise_time else False, - False, - -1, - 1, - self.time_windowing, - 1, - 1, - "", - self.pulse_rise_time / 5, - self.pulse_rise_time * 100, - ], - ] - if self.sub_design_id: - ctxt_temp = ["NUMLEVELS", False, "1", "SUBDESIGNID", False, str(self.sub_design_id)] - for el in ctxt_temp: - ctxt[2].append(el) - if self.differential_pairs: - ctxt_temp = ["USE_DIFF_PAIRS", False, "1"] - for el in ctxt_temp: - ctxt[2].append(el) - if self.domain == "Time": - if self.time_start: - ctxt[2].extend(["WS", False, self.time_start]) - if self.time_stop: - ctxt[2].extend(["WE", False, self.time_stop]) - elif self._post.post_solution_type in ["NexximAMI"]: - ctxt = [ - "NAME:Context", - "SimValueContext:=", - [self._did, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0, "NUMLEVELS", False, "1"], - ] - qty = re.sub("<[^>]+>", "", self.expressions[0]) - if qty == "InitialWave": - ctxt_temp = ["QTID", False, "0", "SCID", False, "-1", "SID", False, "0"] - elif qty == "WaveAfterSource": - ctxt_temp = ["QTID", False, "1", "SCID", False, "-1", "SID", False, "0"] - elif qty == "WaveAfterChannel": - ctxt_temp = [ - "PCID", - False, - "-1", - "PID", - False, - "0", - "QTID", - False, - "2", - "SCID", - False, - "-1", - "SID", - False, - "0", - ] - elif qty == "WaveAfterProbe": - ctxt_temp = [ - "PCID", - False, - "-1", - "PID", - False, - "0", - "QTID", - False, - "3", - "SCID", - False, - "-1", - "SID", - False, - "0", - ] - elif qty == "ClockTics": - ctxt_temp = ["PCID", False, "-1", "PID", False, "0"] - else: - return None - for el in ctxt_temp: - ctxt[2].append(el) - - elif self.differential_pairs: - ctxt = ["Diff:=", "differential_pairs", "Domain:=", self.domain] - else: - ctxt = ["Domain:=", self.domain] - return ctxt - - -class AntennaParameters(Standard): - """Provides a reporting class that fits antenna parameter reports in an HFSS plot.""" - - def __init__(self, app, report_category, setup_name, far_field_sphere=None, expressions=None): - Standard.__init__(self, app, report_category, setup_name, expressions) - self.far_field_sphere = far_field_sphere - - @property - def far_field_sphere(self): - """Far field sphere name. - - Returns - ------- - str - Name of the far field sphere. - """ - return self.props["context"].get("far_field_sphere", None) - - @far_field_sphere.setter - def far_field_sphere(self, value): - self.props["context"]["far_field_sphere"] = value - - @property - def _context(self): - ctxt = ["Context:=", self.far_field_sphere] - return ctxt - - -class Fields(CommonReport): - """Provides for managing fields.""" - - def __init__(self, app, report_category, setup_name, expressions=None): - CommonReport.__init__(self, app, report_category, setup_name, expressions) - self.domain = "Sweep" - self.polyline = None - self.point_number = 1001 - self.primary_sweep = "Distance" - - @property - def point_number(self): - """Polygon point number. - - Returns - ------- - str - Point number. - """ - return self.props["context"].get("point_number", None) - - @point_number.setter - def point_number(self, value): - self.props["context"]["point_number"] = value - - @property - def _context(self): - ctxt = [] - if self.polyline: - ctxt = ["Context:=", self.polyline, "PointCount:=", self.point_number] - return ctxt - - -class NearField(CommonReport): - """Provides for managing near field reports.""" - - def __init__(self, app, report_category, setup_name, expressions=None): - CommonReport.__init__(self, app, report_category, setup_name, expressions) - self.domain = "Sweep" - - @property - def _context(self): - return ["Context:=", self.near_field] - - @property - def near_field(self): - """Near field name. - - Returns - ------- - str - Field name. - """ - return self.props["context"].get("near_field", None) - - @near_field.setter - def near_field(self, value): - self.props["context"]["near_field"] = value - - -class FarField(CommonReport): - """Provides for managing far field reports.""" - - def __init__(self, app, report_category, setup_name, expressions=None): - CommonReport.__init__(self, app, report_category, setup_name, expressions) - self.domain = "Sweep" - self.primary_sweep = "Phi" - self.secondary_sweep = "Theta" - self.source_context = None - self.source_group = None - if "Phi" not in self.variations: - self.variations["Phi"] = ["All"] - if "Theta" not in self.variations: - self.variations["Theta"] = ["All"] - if "Freq" not in self.variations: - self.variations["Freq"] = ["Nominal"] - - @property - def far_field_sphere(self): - """Far field sphere name. - - Returns - ------- - str - Field name. - """ - return self.props.get("far_field_sphere", None) - - @far_field_sphere.setter - def far_field_sphere(self, value): - self.props["far_field_sphere"] = value - - @property - def _context(self): - if self.source_context: - return ["Context:=", self.far_field_sphere, "SourceContext:=", self.source_context] - if self.source_group: - return ["Context:=", self.far_field_sphere, "Source Group:=", self.source_group] - return ["Context:=", self.far_field_sphere] - - -class AMIConturEyeDiagram(CommonReport): - """Provides for managing eye contour diagram reports in AMI analysis.""" - - def __init__(self, app, report_category, setup_name, expressions=None): - CommonReport.__init__(self, app, report_category, setup_name, expressions) - self.domain = "Time" - self.props["report_type"] = "Rectangular Contour Plot" - self.variations.pop("Time", None) - self.props["context"]["variations"]["__UnitInterval"] = "All" - self.props["context"]["variations"]["__Amplitude"] = "All" - self.props["context"]["variations"]["__EyeOpening"] = "0" - self.quantity_type = 0 - self.min_latch_overlay = "0" - self.noise_floor = "1e-16" - self.enable_jitter_distribution = False - self.rx_rj = "0" - self.rx_dj = "0" - self.rx_sj = "0" - self.rx_dcd = "0" - self.rx_gaussian_noise = "0" - self.rx_uniform_noise = "0" - - @property - def expressions(self): - """Expressions. - - Returns - ------- - str - """ - if self.props.get("expressions", None) is None: - return [] - expr_head = "Eye" - new_exprs = [] - for expr_dict in self.props["expressions"]: - expr = expr_dict["name"] - if not ".int_ami" in expr: - qtype = int(self.quantity_type) - if qtype == 0: - new_exprs.append("Initial{}(".format(expr_head) + expr + ".int_ami_tx)") - elif qtype == 1: - new_exprs.append("{}AfterSource(".format(expr_head) + expr + ".int_ami_tx)") - elif qtype == 2: - new_exprs.append("{}AfterChannel(".format(expr_head) + expr + ".int_ami_rx)") - elif qtype == 3: - new_exprs.append("{}AfterProbe(".format(expr_head) + expr + ".int_ami_rx)") - return new_exprs - - @expressions.setter - def expressions(self, value): - if isinstance(value, dict): - self.props["expressions"].append = value - elif isinstance(value, list): - self.props["expressions"] = [] - for el in value: - if isinstance(el, dict): - self.props["expressions"].append(el) - else: - self.props["expressions"].append({"name": el}) - elif isinstance(value, str): - if isinstance(self.props["expressions"], list): - self.props["expressions"].append({"name": value}) - else: - self.props["expressions"] = [{"name": value}] - - @property - def quantity_type(self): - """Quantity type used in the AMI analysis plot. - - Returns - ------- - int - Quantity type. - """ - return self.props.get("quantity_type", 0) - - @quantity_type.setter - def quantity_type(self, value): - self.props["quantity_type"] = value - - @property - def report_category(self): - """Report category. - - Returns - ------- - str - Report category. - """ - return self.props["report_category"] - - @property - def _context(self): - sim_context = [ - 55819, - 0, - 2, - 0, - False, - False, - -1, - 1, - 0, - 1, - 1, - "", - 0, - 0, - "MLO", - False, - str(self.min_latch_overlay), - "NUMLEVELS", - False, - "1", - "ORJ", - False, - "1", - "PCID", - False, - "-1", - "PID", - False, - "0", - "PRIDIST", - False, - "0", - "QTID", - False, - str(self.quantity_type), - "USE_PRI_DIST", - False, - "0" if not self.enable_jitter_distribution else "1", - "SID", - False, - "0", - ] - if self.enable_jitter_distribution and str(self.quantity_type) == "3": - sim_context = [ - 55819, - 0, - 2, - 0, - False, - False, - -1, - 1, - 0, - 1, - 1, - "", - 0, - 0, - "DCD", - False, - str(self.rx_dcd), - "DJ", - False, - str(self.rx_dj), - "GNOI", - False, - "3.5", - "MLO", - False, - str(self.min_latch_overlay), - "NF", - False, - str(self.noise_floor), - "NUMLEVELS", - False, - "1", - "ORJ", - False, - "1", - "PCID", - False, - "-1", - "PID", - False, - "0", - "SID", - False, - "0", - "PRIDIST", - False, - "0", - "QTID", - False, - str(self.quantity_type), - "RJ", - False, - str(self.rx_rj), - "SJ", - False, - str(self.rx_sj), - "UNOI", - False, - str(self.rx_uniform_noise), - "USE_PRI_DIST", - False, - "0" if not self.enable_jitter_distribution else "1", - ] - - arg = [ - "NAME:Context", - "SimValueContext:=", - sim_context, - ] - if len(self.expressions) == 1: - sid = 0 - pid = 0 - expr = self.expressions[0] - category = "Eye" - found = False - while not found: - available_quantities = self._post.available_report_quantities( - self.report_category, self.report_type, self.setup, category, arg - ) - if len(available_quantities) == 1 and available_quantities[0].lower() == expr.lower(): - found = True - else: - sid += 1 - pid += 1 - arg[2][arg[2].index("SID") + 2] = str(sid) - arg[2][arg[2].index("PID") + 2] = str(pid) - # Limited maximum iterations to 1000 in While loop (Too many probes to analyze even in a single design) - if sid > 1000: - self._post.logger.error( - "Failed to find right context for expression : {}".format(",".join(self.expressions)) - ) - # arg[2][arg[2].index("SID") + 2] = "0" - # arg[2][arg[2].index("PID") + 2] = "0" - break - return arg - - @property - def _trace_info(self): - new_exprs = self.expressions if isinstance(self.expressions, list) else [self.expressions] - return ["X Component:=", "__UnitInterval", "Y Component:=", "__Amplitude", "Z Component:=", new_exprs] - - @pyaedt_function_handler() - def create(self, name=None): - """Create an eye diagram report. - - Parameters - ---------- - name : str, optional - Plot name. The default is ``None``, in which case - the default name is used. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if not name: - self.plot_name = generate_unique_name("Plot") - else: - self.plot_name = name - self._post.oreportsetup.CreateReport( - self.plot_name, - self.report_category, - self.report_type, - self.setup, - self._context, - self._convert_dict_to_report_sel(self.variations), - self._trace_info, - ) - self._post.plots.append(self) - self._is_created = True - - return True - - @pyaedt_function_handler(xunits="x_units", yunits="y_units", xoffset="x_offset", yoffset="y_offset") - def eye_mask( - self, - points, - x_units="ns", - y_units="mV", - enable_limits=False, - upper_limit=500, - lower_limit=-500, - color=(0, 255, 0), - x_offset="0ns", - y_offset="0V", - transparency=0.3, - ): - """Create an eye diagram in the plot. - - Parameters - ---------- - points : list - Points of the eye mask in the format ``[[x1,y1,],[x2,y2],...]``. - x_units : str, optional - X points units. The default is ``"ns"``. - y_units : str, optional - Y points units. The default is ``"mV"``. - enable_limits : bool, optional - Whether to enable the upper and lower limits. The default is ``False``. - upper_limit : float, optional - Upper limit if limits are enabled. The default is ``500``. - lower_limit : str, optional - Lower limit if limits are enabled. The default is ``-500``. - color : tuple, optional - Mask in (R, G, B) color. The default is ``(0, 255, 0)``. - Each color value must be an integer in a range from 0 to 255. - x_offset : str, optional - Mask time offset with units. The default is ``"0ns"``. - y_offset : str, optional - Mask value offset with units. The default is ``"0V"``. - transparency : float, optional - Mask transparency. The default is ``0.3``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if "quantity_type" in dir(self) and self.report_type == "Rectangular Contour Plot": - props = [ - "NAME:AllTabs", - ["NAME:Mask", ["NAME:PropServers", "{}: Plot {}".format(self.plot_name, self.traces[0].name)]], - ] - else: - props = [ - "NAME:AllTabs", - ["NAME:Mask", ["NAME:PropServers", "{}:EyeDisplayTypeProperty".format(self.plot_name)]], - ] - arg = [ - "NAME:Mask", - "Version:=", - 1, - "ShowLimits:=", - enable_limits, - "UpperLimit:=", - upper_limit if upper_limit else 1, - "LowerLimit:=", - lower_limit if lower_limit else 0, - "XUnits:=", - x_units, - "YUnits:=", - y_units, - ] - mask_points = ["NAME:MaskPoints"] - for point in points: - mask_points.append(point[0]) - mask_points.append(point[1]) - arg.append(mask_points) - args = ["NAME:ChangedProps", arg] - if not ("quantity_type" in dir(self) and self.report_type == "Rectangular Contour Plot"): - args.append(["NAME:Mask Fill Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) - args.append(["NAME:X Offset", "Value:=", x_offset]) - args.append(["NAME:Y Offset", "Value:=", y_offset]) - args.append(["NAME:Mask Trans", "Transparency:=", transparency]) - props[1].append(args) - self._post.oreportsetup.ChangeProperty(props) - - return True - - @pyaedt_function_handler(value="enable") - def rectangular_plot(self, enable=True): - """Enable or disable the rectangular plot on the chart. - - Parameters - ---------- - enable : bool - Whether to enable the rectangular plot. The default is ``True``. If - ``False``, the rectangular plot is disabled. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - props = [ - "NAME:AllTabs", - ["NAME:Eye", ["NAME:PropServers", "{}:EyeDisplayTypeProperty".format(self.plot_name)]], - ] - args = ["NAME:ChangedProps", ["NAME:Rectangular Plot", "Value:=", enable]] - props[1].append(args) - self._post.oreportsetup.ChangeProperty(props) - - return True - - @pyaedt_function_handler() - def add_all_eye_measurements(self): - """Add all eye measurements to the plot. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - self._post.oreportsetup.AddAllEyeMeasurements(self.plot_name) - return True - - @pyaedt_function_handler() - def clear_all_eye_measurements(self): - """Clear all eye measurements from the plot. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - self._post.oreportsetup.ClearAllTraceCharacteristics(self.plot_name) - return True - - @pyaedt_function_handler(trace_name="name") - def add_trace_characteristics(self, name, arguments=None, solution_range=None): - """Add a trace characteristic to the plot. - - Parameters - ---------- - name : str - Name of the trace characteristic. - arguments : list, optional - Arguments if any. The default is ``None``. - solution_range : list, optional - Output range. The default is ``None``, in which case - the full range is used. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if not arguments: - arguments = [] - if not solution_range: - solution_range = ["Full"] - self._post.oreportsetup.AddTraceCharacteristics(self.plot_name, name, arguments, solution_range) - return True - - @pyaedt_function_handler(out_file="output_file") - def export_mask_violation(self, output_file=None): - """Export the eye diagram mask violations to a TAB file. - - Parameters - ---------- - output_file : str, optional - Full path to the TAB file. The default is ``None``, in which case - the violations are exported to a TAB file in the working directory. - - Returns - ------- - str - Output file path if a TAB file is created. - """ - if not output_file: - output_file = os.path.join(self._post._app.working_directory, "{}_violations.tab".format(self.plot_name)) - self._post.oreportsetup.ExportEyeMaskViolation(self.plot_name, output_file) - return output_file - - -class AMIEyeDiagram(CommonReport): - """Provides for managing eye diagram reports.""" - - def __init__(self, app, report_category, setup_name, expressions=None): - CommonReport.__init__(self, app, report_category, setup_name, expressions) - self.domain = "Time" - if report_category == "Statistical Eye": - self.props["report_type"] = "Statistical Eye Plot" - self.variations.pop("Time", None) - self.variations["__UnitInterval"] = "All" - self.variations["__Amplitude"] = "All" - self.unit_interval = "0s" - self.offset = "0ms" - self.auto_delay = True - self.manual_delay = "0ps" - self.auto_cross_amplitude = True - self.cross_amplitude = "0mV" - self.auto_compute_eye_meas = True - self.eye_measurement_point = "5e-10s" - self.quantity_type = 0 - - @property - def expressions(self): - """Expressions. - - Returns - ------- - str - """ - if self.props.get("expressions", None) is None: - return [] - expr_head = "Wave" - if self.report_category == "Statistical Eye": - expr_head = "Eye" - new_exprs = [] - for expr_dict in self.props["expressions"]: - expr = expr_dict["name"] - if not ".int_ami" in expr: - qtype = int(self.quantity_type) - if qtype == 0: - new_exprs.append("Initial{}<".format(expr_head) + expr + ".int_ami_tx>") - elif qtype == 1: - new_exprs.append("{}AfterSource<".format(expr_head) + expr + ".int_ami_tx>") - elif qtype == 2: - new_exprs.append("{}AfterChannel<".format(expr_head) + expr + ".int_ami_rx>") - elif qtype == 3: - new_exprs.append("{}AfterProbe<".format(expr_head) + expr + ".int_ami_rx>") - return new_exprs - - @expressions.setter - def expressions(self, value): - if isinstance(value, dict): - self.props["expressions"].append = value - elif isinstance(value, list): - self.props["expressions"] = [] - for el in value: - if isinstance(el, dict): - self.props["expressions"].append(el) - else: - self.props["expressions"].append({"name": el}) - elif isinstance(value, str): - if isinstance(self.props["expressions"], list): - self.props["expressions"].append({"name": value}) - else: - self.props["expressions"] = [{"name": value}] - - @property - def quantity_type(self): - """Quantity type used in the AMI analysis plot. - - Returns - ------- - int - Quantity type. - """ - return self.props.get("quantity_type", 0) - - @quantity_type.setter - def quantity_type(self, value): - self.props["quantity_type"] = value - - @property - def report_category(self): - """Report category. - - Returns - ------- - str - Report category. - """ - return self.props["report_category"] - - @report_category.setter - def report_category(self, value): - self.props["report_category"] = value - if self.props["report_category"] == "Statistical Eye" and self.report_type == "Rectangular Plot": - self.props["report_type"] = "Statistical Eye Plot" - self.variations.pop("Time", None) - self.variations["__UnitInterval"] = "All" - self.variations["__Amplitude"] = "All" - elif self.props["report_category"] == "Eye Diagram" and self.report_type == "Statistical Eye Plot": - self.props["report_type"] = "Rectangular Plot" - self.variations.pop("__UnitInterval", None) - self.variations.pop("__Amplitude", None) - self.variations["Time"] = "All" - - @property - def unit_interval(self): - """Unit interval value. - - Returns - ------- - str - Unit interval. - """ - return self.props["context"].get("unit_interval", None) - - @unit_interval.setter - def unit_interval(self, value): - self.props["context"]["unit_interval"] = value - - @property - def offset(self): - """Offset value. - - Returns - ------- - str - Offset value. - """ - return self.props["context"].get("offset", None) - - @offset.setter - def offset(self, value): - self.props["context"]["offset"] = value - - @property - def auto_delay(self): - """Auto-delay flag. - - Returns - ------- - bool - ``True`` if auto-delay is enabled, ``False`` otherwise. - """ - return self.props["context"].get("auto_delay", None) - - @auto_delay.setter - def auto_delay(self, value): - self.props["context"]["auto_delay"] = value - - @property - def manual_delay(self): - """Manual delay value when ``auto_delay=False``. - - Returns - ------- - str - ``True`` if manual-delay is enabled, ``False`` otherwise. - """ - return self.props["context"].get("manual_delay", None) - - @manual_delay.setter - def manual_delay(self, value): - self.props["context"]["manual_delay"] = value - - @property - def auto_cross_amplitude(self): - """Auto-cross amplitude flag. - - Returns - ------- - bool - ``True`` if auto-cross amplitude is enabled, ``False`` otherwise. - """ - return self.props["context"].get("auto_cross_amplitude", None) - - @auto_cross_amplitude.setter - def auto_cross_amplitude(self, value): - self.props["context"]["auto_cross_amplitude"] = value - - @property - def cross_amplitude(self): - """Cross-amplitude value when ``auto_cross_amplitude=False``. - - Returns - ------- - str - Cross-amplitude. - """ - return self.props["context"].get("cross_amplitude", None) - - @cross_amplitude.setter - def cross_amplitude(self, value): - self.props["context"]["cross_amplitude"] = value - - @property - def auto_compute_eye_meas(self): - """Flag for automatically computing eye measurements. - - Returns - ------- - bool - ``True`` to compute eye measurements, ``False`` otherwise. - """ - return self.props["context"].get("auto_compute_eye_meas", None) - - @auto_compute_eye_meas.setter - def auto_compute_eye_meas(self, value): - self.props["context"]["auto_compute_eye_meas"] = value - - @property - def eye_measurement_point(self): - """Eye measurement point. - - Returns - ------- - str - Eye measurement point. - """ - return self.props["context"].get("eye_measurement_point", None) - - @eye_measurement_point.setter - def eye_measurement_point(self, value): - self.props["context"]["eye_measurement_point"] = value - - @property - def _context(self): - arg = [ - "NAME:Context", - "SimValueContext:=", - [ - 1, - 0, - 2, - 0, - False, - False, - -1, - 1, - 0, - 1, - 1, - "", - 0, - 0, - "-1", - False, - "0", - "NUMLEVELS", - False, - "1", - "PCID", - False, - "-1", - "PID", - False, - "0", - "QTID", - False, - str(self.quantity_type), - "SCID", - False, - "-1", - "SID", - False, - "0", - ], - ] - if self.report_category == "Statistical Eye": - arg = [ - "NAME:Context", - "SimValueContext:=", - [ - 55819, - 0, - 2, - 0, - False, - False, - -1, - 1, - 0, - 1, - 1, - "", - 0, - 0, - "NUMLEVELS", - False, - "1", - "PCID", - False, - "-1", - "PID", - False, - "0", - "QTID", - False, - str(self.quantity_type), - "SCID", - False, - "-1", - "SID", - False, - "0", - ], - ] - if len(self.expressions) == 1: - sid = 0 - pid = 0 - expr = self.expressions[0] - category = "Wave" - if self.report_category == "Statistical Eye": - category = "Eye" - if self.report_category == "Eye Diagram" and self.report_type == "Rectangular Plot": - category = "Voltage" - found = False - while not found: - available_quantities = self._post.available_report_quantities( - self.report_category, self.report_type, self.setup, category, arg - ) - if len(available_quantities) == 1 and available_quantities[0].lower() == expr.lower(): - found = True - else: - sid += 1 - pid += 1 - arg[2][arg[2].index("SID") + 2] = str(sid) - arg[2][arg[2].index("PID") + 2] = str(pid) - # Limited maximum iterations to 1000 in While loop (Too many probes to analyze even in a single design) - if sid > 1000: - self._post.logger.error( - "Failed to find right context for expression : {}".format(",".join(self.expressions)) - ) - # arg[2][arg[2].index("SID") + 2] = "0" - # arg[2][arg[2].index("PID") + 2] = "0" - break - return arg - - @property - def _trace_info(self): - new_exprs = self.expressions if isinstance(self.expressions, list) else [self.expressions] - if self.report_category == "Statistical Eye": - return [ - "X Component:=", - "__UnitInterval", - "Y Component:=", - "__Amplitude", - "Eye Diagram Component:=", - new_exprs, - ] - return ["Component:=", new_exprs] - - @pyaedt_function_handler() - def create(self, name=None): - """Create an eye diagram report. - - Parameters - ---------- - name : str, optional - Plot name. The default is ``None``, in which case - the default name is used. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if not name: - self.plot_name = generate_unique_name("Plot") - else: - self.plot_name = name - options = [ - "Unit Interval:=", - self.unit_interval, - "Offset:=", - self.offset, - "Auto Delay:=", - self.auto_delay, - "Manual Delay:=", - self.manual_delay, - "AutoCompCrossAmplitude:=", - self.auto_cross_amplitude, - "CrossingAmplitude:=", - self.cross_amplitude, - "AutoCompEyeMeasurementPoint:=", - self.auto_compute_eye_meas, - "EyeMeasurementPoint:=", - self.eye_measurement_point, - ] - if self.report_category == "Statistical Eye": - self._post.oreportsetup.CreateReport( - self.plot_name, - self.report_category, - self.report_type, - self.setup, - self._context, - self._convert_dict_to_report_sel(self.variations), - self._trace_info, - ) - else: - self._post.oreportsetup.CreateReport( - self.plot_name, - self.report_category, - self.report_type, - self.setup, - self._context, - self._convert_dict_to_report_sel(self.variations), - self._trace_info, - options, - ) - self._post.plots.append(self) - self._is_created = True - - return True - - @pyaedt_function_handler(xunits="x_units", yunits="y_units", xoffset="x_offset", yoffset="y_offset") - def eye_mask( - self, - points, - x_units="ns", - y_units="mV", - enable_limits=False, - upper_limit=500, - lower_limit=-500, - color=(0, 255, 0), - x_offset="0ns", - y_offset="0V", - transparency=0.3, - ): - """Create an eye diagram in the plot. - - Parameters - ---------- - points : list - Points of the eye mask in the format ``[[x1,y1,],[x2,y2],...]``. - x_units : str, optional - X points units. The default is ``"ns"``. - y_units : str, optional - Y points units. The default is ``"mV"``. - enable_limits : bool, optional - Whether to enable the upper and lower limits. The default is ``False``. - upper_limit : float, optional - Upper limit if limits are enabled. The default is ``500``. - lower_limit : str, optional - Lower limit if limits are enabled. The default is ``-500``. - color : tuple, optional - Mask in (R, G, B) color. The default is ``(0, 255, 0)``. - Each color value must be an integer in a range from 0 to 255. - x_offset : str, optional - Mask time offset with units. The default is ``"0ns"``. - y_offset : str, optional - Mask value offset with units. The default is ``"0V"``. - transparency : float, optional - Mask transparency. The default is ``0.3``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - props = [ - "NAME:AllTabs", - ["NAME:Mask", ["NAME:PropServers", "{}:EyeDisplayTypeProperty".format(self.plot_name)]], - ] - arg = [ - "NAME:Mask", - "Version:=", - 1, - "ShowLimits:=", - enable_limits, - "UpperLimit:=", - upper_limit if upper_limit else 1, - "LowerLimit:=", - lower_limit if lower_limit else 0, - "XUnits:=", - x_units, - "YUnits:=", - y_units, - ] - mask_points = ["NAME:MaskPoints"] - for point in points: - mask_points.append(point[0]) - mask_points.append(point[1]) - arg.append(mask_points) - args = ["NAME:ChangedProps", arg] - args.append(["NAME:Mask Fill Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) - args.append(["NAME:X Offset", "Value:=", x_offset]) - args.append(["NAME:Y Offset", "Value:=", y_offset]) - args.append(["NAME:Mask Trans", "Transparency:=", transparency]) - props[1].append(args) - self._post.oreportsetup.ChangeProperty(props) - - return True - - @pyaedt_function_handler(value="enable") - def rectangular_plot(self, enable=True): - """Enable or disable the rectangular plot on the chart. - - Parameters - ---------- - enable : bool - Whether to enable the rectangular plot. The default is ``True``. When - ``False``, the rectangular plot is disabled. - - Returns - ------- - bool - """ - props = [ - "NAME:AllTabs", - ["NAME:Eye", ["NAME:PropServers", "{}:EyeDisplayTypeProperty".format(self.plot_name)]], - ] - args = ["NAME:ChangedProps", ["NAME:Rectangular Plot", "Value:=", enable]] - props[1].append(args) - self._post.oreportsetup.ChangeProperty(props) - - return True - - @pyaedt_function_handler() - def add_all_eye_measurements(self): - """Add all eye measurements to the plot. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - self._post.oreportsetup.AddAllEyeMeasurements(self.plot_name) - return True - - @pyaedt_function_handler() - def clear_all_eye_measurements(self): - """Clear all eye measurements from the plot. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - self._post.oreportsetup.ClearAllTraceCharacteristics(self.plot_name) - return True - - @pyaedt_function_handler(trace_name="name") - def add_trace_characteristics(self, name, arguments=None, solution_range=None): - """Add a trace characteristic to the plot. - - Parameters - ---------- - name : str - Name of the trace characteristic. - arguments : list, optional - Arguments if any. The default is ``None``. - solution_range : list, optional - Output range. The default is ``None``, in which case - the full range is used. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if not arguments: - arguments = [] - if not solution_range: - solution_range = ["Full"] - self._post.oreportsetup.AddTraceCharacteristics(self.plot_name, name, arguments, solution_range) - return True - - @pyaedt_function_handler(out_file="output_file") - def export_mask_violation(self, output_file=None): - """Export the eye diagram mask violations to a TAB file. - - Parameters - ---------- - output_file : str, optional - Full path to the TAB file. The default is ``None``, in which case - the violations are exported to a TAB file in the working directory. - - Returns - ------- - str - Output file path if a TAB file is created. - """ - if not output_file: - output_file = os.path.join(self._post._app.working_directory, "{}_violations.tab".format(self.plot_name)) - self._post.oreportsetup.ExportEyeMaskViolation(self.plot_name, output_file) - return output_file - - -class EyeDiagram(AMIEyeDiagram): - """Provides for managing eye diagram reports.""" - - def __init__(self, app, report_category, setup_name, expressions=None): - AMIEyeDiagram.__init__(self, app, report_category, setup_name, expressions) - self.time_start = "0ns" - self.time_stop = "200ns" - self.thinning = False - self.dy_dx_tolerance = 0.001 - self.thinning_points = 500000000 - - @property - def expressions(self): - """Expressions. - - Returns - ------- - str - Expressions. - """ - if self.props.get("expressions", None) is None: - return [] - return [k.get("name", None) for k in self.props["expressions"] if k.get("name", None) is not None] - - @expressions.setter - def expressions(self, value): - if isinstance(value, dict): - self.props["expressions"].append = value - elif isinstance(value, list): - self.props["expressions"] = [] - for el in value: - if isinstance(el, dict): - self.props["expressions"].append(el) - else: - self.props["expressions"].append({"name": el}) - elif isinstance(value, str): - if isinstance(self.props["expressions"], list): - self.props["expressions"].append({"name": value}) - else: - self.props["expressions"] = [{"name": value}] - - @property - def time_start(self): - """Time start value. - - Returns - ------- - str - Time start. - """ - return self.props["context"].get("time_start", None) - - @time_start.setter - def time_start(self, value): - self.props["context"]["time_start"] = value - - @property - def time_stop(self): - """Time stop value. - - Returns - ------- - str - Time stop. - """ - return self.props["context"].get("time_stop", None) - - @time_stop.setter - def time_stop(self, value): - self.props["context"]["time_stop"] = value - - @property - def thinning(self): - """Thinning flag. - - Returns - ------- - bool - ``True`` if thinning is enabled, ``False`` otherwise. - """ - return self.props["context"].get("thinning", None) - - @thinning.setter - def thinning(self, value): - self.props["context"]["thinning"] = value - - @property - def dy_dx_tolerance(self): - """DY DX tolerance. - - Returns - ------- - float - DY DX tolerance. - """ - return self.props["context"].get("dy_dx_tolerance", None) - - @dy_dx_tolerance.setter - def dy_dx_tolerance(self, value): - self.props["context"]["dy_dx_tolerance"] = value - - @property - def thinning_points(self): - """Number of thinning points. - - Returns - ------- - int - Number of thinning points. - """ - return self.props["context"].get("thinning_points", None) - - @thinning_points.setter - def thinning_points(self, value): - self.props["context"]["thinning_points"] = value - - @property - def _context(self): - if self.thinning: - val = "1" - else: - val = "0" - arg = [ - "NAME:Context", - "SimValueContext:=", - [ - 1, - 0, - 2, - 0, - False, - False, - -1, - 1, - 0, - 1, - 1, - "", - 0, - 0, - "DE", - False, - val, - "DP", - False, - str(self.thinning_points), - "DT", - False, - str(self.dy_dx_tolerance), - "NUMLEVELS", - False, - "0", - "WE", - False, - self.time_stop, - "WM", - False, - "200ns", - "WN", - False, - "0ps", - "WS", - False, - self.time_start, - ], - ] - return arg - - @property - def _trace_info(self): - if isinstance(self.expressions, list): - return ["Component:=", self.expressions] - else: - return ["Component:=", [self.expressions]] - - -class Emission(CommonReport): - """Provides for managing emission reports.""" - - def __init__(self, app, report_category, setup_name, expressions=None): - CommonReport.__init__(self, app, report_category, setup_name, expressions) - self.domain = "Sweep" - - -class Spectral(CommonReport): - """Provides for managing spectral reports from transient data.""" - - def __init__(self, app, report_category, setup_name, expressions=None): - CommonReport.__init__(self, app, report_category, setup_name, expressions) - self.domain = "Spectrum" - self.algorithm = "FFT" - self.time_start = "0ns" - self.time_stop = "200ns" - self.window = "Rectangular" - self.kaiser_coeff = 0 - self.adjust_coherent_gain = True - self.max_frequency = "10MHz" - self.plot_continous_spectrum = False - self.primary_sweep = "Spectrum" - - @property - def time_start(self): - """Time start value. - - Returns - ------- - str - Time start. - """ - return self.props["context"].get("time_start", None) - - @time_start.setter - def time_start(self, value): - self.props["context"]["time_start"] = value - - @property - def time_stop(self): - """Time stop value. - - Returns - ------- - str - Time stop. - """ - return self.props["context"].get("time_stop", None) - - @time_stop.setter - def time_stop(self, value): - self.props["context"]["time_stop"] = value - - @property - def window(self): - """Window value. - - Returns - ------- - str - Window. - """ - return self.props["context"].get("window", None) - - @window.setter - def window(self, value): - self.props["context"]["window"] = value - - @property - def kaiser_coeff(self): - """Kaiser value. - - Returns - ------- - str - Kaiser coefficient. - """ - return self.props["context"].get("kaiser_coeff", None) - - @kaiser_coeff.setter - def kaiser_coeff(self, value): - self.props["context"]["kaiser_coeff"] = value - - @property - def adjust_coherent_gain(self): - """Coherent gain flag. - - Returns - ------- - bool - ``True`` if coherent gain is enabled, ``False`` otherwise. - """ - return self.props["context"].get("adjust_coherent_gain", None) - - @adjust_coherent_gain.setter - def adjust_coherent_gain(self, value): - self.props["context"]["adjust_coherent_gain"] = value - - @property - def plot_continous_spectrum(self): - """Continuous spectrum flag. - - Returns - ------- - bool - ``True`` if continuous spectrum is enabled, ``False`` otherwise. - """ - return self.props["context"].get("plot_continous_spectrum", None) - - @plot_continous_spectrum.setter - def plot_continous_spectrum(self, value): - self.props["context"]["plot_continous_spectrum"] = value - - @property - def max_frequency(self): - """Maximum spectrum frequency. - - Returns - ------- - str - Maximum spectrum frequency. - """ - return self.props["context"].get("max_frequency", None) - - @max_frequency.setter - def max_frequency(self, value): - self.props["context"]["max_frequency"] = value - - @property - def _context(self): - if self.algorithm == "FFT": - it = "1" - elif self.algorithm == "Fourier Integration": - it = "0" - else: - it = "2" - WT = { - "Rectangular": "0", - "Bartlett": "1", - "Blackman": "2", - "Hamming": "3", - "Hanning": "4", - "Kaiser": "5", - "Welch": "6", - "Weber": "7", - "Lanzcos": "8", - } - wt = WT[self.window] - arg = [ - "NAME:Context", - "SimValueContext:=", - [ - 2, - 0, - 2, - 0, - False, - False, - -1, - 1, - 0, - 1, - 1, - "", - 0, - 0, - "CP", - False, - "1" if self.plot_continous_spectrum else "0", - "IT", - False, - it, - "MF", - False, - self.max_frequency, - "NUMLEVELS", - False, - "0", - "TE", - False, - self.time_stop, - "TS", - False, - self.time_start, - "WT", - False, - wt, - "WW", - False, - "100", - "KP", - False, - str(self.kaiser_coeff), - "CG", - False, - "1" if self.adjust_coherent_gain else "0", - ], - ] - return arg - - @property - def _trace_info(self): - if isinstance(self.expressions, list): - return self.expressions - else: - return [self.expressions] - - @pyaedt_function_handler() - def create(self, name=None): - """Create an eye diagram report. - - Parameters - ---------- - name : str, optional - Plot name. The default is ``None``, in which case - the default name is used. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if not name: - self.plot_name = generate_unique_name("Plot") - else: - self.plot_name = name - self._post.oreportsetup.CreateReport( - self.plot_name, - "Standard", - self.report_type, - self.setup, - self._context, - self._convert_dict_to_report_sel(self.variations), - [ - "X Component:=", - self.primary_sweep, - "Y Component:=", - self._trace_info, - ], - ) - self._post.plots.append(self) - self._is_created = True - return True - - -class EMIReceiver(CommonReport): - """Provides for managing EMI receiver reports.""" - - def __init__(self, app, setup_name, expressions=None): - CommonReport.__init__(self, app, "EMIReceiver", setup_name, expressions) - self.logger = app.logger - self.domain = "EMI Receiver" - self.available_nets = [] - self._net = "0" - for comp in app.modeler.components.components.values(): - if comp.name == "CompInst@EMI_RCVR": - self.available_nets.append(comp.pins[0].net) - if self.available_nets: - self._net = self.available_nets[0] - self.time_start = "0ns" - self.time_stop = "200ns" - self._emission = "CE" - self.overlap_rate = 95 - self.band = "0" - self.primary_sweep = "Freq" - - @property - def net(self): - """Net attached to the EMI receiver. - - Returns - ------- - str - Net name. - """ - return self._net - - @net.setter - def net(self, value): - if value not in self.available_nets: - self.logger.error("Net not available.") - else: - self._net = value - - @property - def band(self): - """Band attached to the EMI receiver. - - Returns - ------- - str - Band name. - """ - return self.props["context"].get("band", None) - - @band.setter - def band(self, value): - self.props["context"]["band"] = value - - @property - def emission(self): - """Emission test. - - Options are ``"CE"`` and ``"RE"``. - - Returns - ------- - str - Emission. - """ - return self._emission - - @emission.setter - def emission(self, value): - if value == "CE": - self._emission = value - self.props["context"]["emission"] = "0" - elif value == "RE": - self._emission = value - self.props["context"]["emission"] = "1" - else: - self.logger.error("Emission must be 'CE' or 'RE', value '{}' is not valid.".format(value)) - - @property - def time_start(self): - """Time start value. - - Returns - ------- - str - Time start. - """ - return self.props["context"].get("time_start", None) - - @time_start.setter - def time_start(self, value): - self.props["context"]["time_start"] = value - - @property - def time_stop(self): - """Time stop value. - - Returns - ------- - str - Time stop. - """ - return self.props["context"].get("time_stop", None) - - @time_stop.setter - def time_stop(self, value): - self.props["context"]["time_stop"] = value - - @property - def _context(self): - - if self.emission == "CE": - em = "0" - else: - em = "1" - - arg = [ - "NAME:Context", - "SimValueContext:=", - [ - 55830, - 0, - 2, - 0, - False, - False, - -1, - 1, - 0, - 1, - 1, - self.net, - 0, - 0, - "BAND", - False, - self.band, - "CG", - False, - "1", - "EM", - False, - em, - "KP", - False, - "0", - "NUMLEVELS", - False, - "0", - "OR", - False, - str(self.overlap_rate), - "RBW", - False, - "9000Hz", - "SIG", - False, - "0", - "TCT", - False, - "1ms", - "TDT", - False, - "160ms", - "TE", - False, - self.time_stop, - "TS", - False, - self.time_start, - "WT", - False, - "6", - "WW", - False, - "100", - ], - ] - return arg - - @property - def _trace_info(self): - if isinstance(self.expressions, list): - return self.expressions - else: - return [self.expressions] - - @pyaedt_function_handler() - def create(self, name=None): - """Create an EMI receiver report. - - Parameters - ---------- - name : str, optional - Plot name. The default is ``None``, in which case - the default name is used. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if not name: - self.plot_name = generate_unique_name("Plot") - else: - self.plot_name = name - self._post.oreportsetup.CreateReport( - self.plot_name, - "Standard", - self.report_type, - self.setup, - self._context, - self._convert_dict_to_report_sel(self.variations), - [ - "X Component:=", - self.primary_sweep, - "Y Component:=", - self._trace_info, - ], - ) - self._post.plots.append(self) - self._is_created = True - return self diff --git a/src/ansys/aedt/core/modules/solutions.py b/src/ansys/aedt/core/modules/solutions.py deleted file mode 100644 index 5e64b0fa8a6..00000000000 --- a/src/ansys/aedt/core/modules/solutions.py +++ /dev/null @@ -1,3509 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from abc import abstractmethod -from collections import defaultdict -import csv -import itertools -import logging -import math -import os -import shutil -import sys -import tempfile - -from ansys.aedt.core.generic.constants import AEDT_UNITS -from ansys.aedt.core.generic.constants import AllowedMarkers -from ansys.aedt.core.generic.constants import CSS4_COLORS -from ansys.aedt.core.generic.constants import EnumUnits -from ansys.aedt.core.generic.constants import db10 -from ansys.aedt.core.generic.constants import db20 -from ansys.aedt.core.generic.data_handlers import _dict2arg -from ansys.aedt.core.generic.general_methods import GrpcApiError -from ansys.aedt.core.generic.general_methods import check_and_download_file -from ansys.aedt.core.generic.general_methods import is_ironpython -from ansys.aedt.core.generic.general_methods import open_file -from ansys.aedt.core.generic.general_methods import pyaedt_function_handler -from ansys.aedt.core.generic.general_methods import write_csv -from ansys.aedt.core.generic.load_aedt_file import load_keyword_in_aedt_file -from ansys.aedt.core.generic.plot import plot_2d_chart -from ansys.aedt.core.generic.plot import plot_3d_chart -from ansys.aedt.core.generic.plot import plot_polar_chart -from ansys.aedt.core.generic.settings import settings -from ansys.aedt.core.modeler.cad.elements_3d import FacePrimitive -from ansys.aedt.core.modeler.geometry_operators import GeometryOperators - -np = None -pd = None -pv = None -if not is_ironpython: - try: - import numpy as np - except ImportError: - np = None - try: - import pandas as pd - except ImportError: - pd = None - try: - import pyvista as pv - except ImportError: - pv = None - try: - import matplotlib.pyplot as plt - except ImportError: - plt = None - - -@pyaedt_function_handler() -def _parse_nastran(file_path): - logger = logging.getLogger("Global") - nas_to_dict = {"Points": [], "PointsId": {}, "Assemblies": {}} - includes = [] - - def parse_lines(input_lines, input_pid=0, in_assembly="Main"): - if in_assembly not in nas_to_dict["Assemblies"]: - nas_to_dict["Assemblies"][in_assembly] = {"Triangles": {}, "Solids": {}, "Lines": {}} - - def get_point(ll, start, length): - n = ll[start : start + length].strip() - if "-" in n[1:] and "e" not in n[1:].lower(): - n = n[0] + n[1:].replace("-", "e-") - return n - - for lk in range(len(input_lines)): - line = input_lines[lk] - line_type = line[:8].strip() - obj_type = "Triangles" - if line.startswith("$") or line.startswith("*"): - continue - elif line_type in ["GRID", "GRID*"]: - num_points = 3 - obj_type = "Grid" - elif line_type in [ - "CTRIA3", - "CTRIA3*", - ]: - num_points = 3 - obj_type = "Triangles" - elif line_type in ["CROD", "CBEAM", "CBAR", "CROD*", "CBEAM*", "CBAR*"]: - num_points = 2 - obj_type = "Lines" - elif line_type in [ - "CQUAD4", - "CQUAD4*", - ]: - num_points = 4 - obj_type = "Triangles" - elif line_type in ["CTETRA", "CTETRA*"]: - num_points = 4 - obj_type = "Solids" - elif line_type in ["CPYRA", "CPYRAM", "CPYRA*", "CPYRAM*"]: - num_points = 5 - obj_type = "Solids" - else: - continue - - points = [] - start_pointer = 8 - word_length = 8 - if line_type.endswith("*"): - word_length = 16 - grid_id = int(line[start_pointer : start_pointer + word_length]) - pp = 0 - start_pointer = start_pointer + word_length - object_id = line[start_pointer : start_pointer + word_length] - if obj_type != "Grid": - object_id = int(object_id) - if object_id not in nas_to_dict["Assemblies"][in_assembly][obj_type]: - nas_to_dict["Assemblies"][in_assembly][obj_type][object_id] = [] - while pp < num_points: - start_pointer = start_pointer + word_length - if start_pointer >= 72: - lk += 1 - line = input_lines[lk] - start_pointer = 8 - points.append(get_point(line, start_pointer, word_length)) - pp += 1 - - if line_type in ["GRID", "GRID*"]: - nas_to_dict["PointsId"][grid_id] = input_pid - nas_to_dict["Points"].append([float(i) for i in points]) - input_pid += 1 - elif line_type in [ - "CTRIA3", - "CTRIA3*", - ]: - tri = [nas_to_dict["PointsId"][int(i)] for i in points] - nas_to_dict["Assemblies"][in_assembly]["Triangles"][object_id].append(tri) - elif line_type in ["CROD", "CBEAM", "CBAR", "CROD*", "CBEAM*", "CBAR*"]: - tri = [nas_to_dict["PointsId"][int(i)] for i in points] - nas_to_dict["Assemblies"][in_assembly]["Lines"][object_id].append(tri) - elif line_type in ["CQUAD4", "CQUAD4*"]: - tri = [ - nas_to_dict["PointsId"][int(points[0])], - nas_to_dict["PointsId"][int(points[1])], - nas_to_dict["PointsId"][int(points[2])], - ] - nas_to_dict["Assemblies"][in_assembly]["Triangles"][object_id].append(tri) - tri = [ - nas_to_dict["PointsId"][int(points[0])], - nas_to_dict["PointsId"][int(points[2])], - nas_to_dict["PointsId"][int(points[3])], - ] - nas_to_dict["Assemblies"][in_assembly]["Triangles"][object_id].append(tri) - else: - from itertools import combinations - - for k in list(combinations(points, 3)): - tri = [ - nas_to_dict["PointsId"][int(k[0])], - nas_to_dict["PointsId"][int(k[1])], - nas_to_dict["PointsId"][int(k[2])], - ] - tri.sort() - tri = tuple(tri) - nas_to_dict["Assemblies"][in_assembly]["Solids"][object_id].append(tri) - - return input_pid - - logger.info("Loading file") - with open_file(file_path, "r") as f: - lines = f.read().splitlines() - for line in lines: - if line.startswith("INCLUDE"): - includes.append(line.split(" ")[1].replace("'", "").strip()) - pid = parse_lines(lines) - for include in includes: - with open_file(os.path.join(os.path.dirname(file_path), include), "r") as f: - lines = f.read().splitlines() - name = include.split(".")[0] - pid = parse_lines(lines, pid, name) - logger.info("File loaded") - for assembly in list(nas_to_dict["Assemblies"].keys())[::]: - if ( - nas_to_dict["Assemblies"][assembly]["Triangles"] - == nas_to_dict["Assemblies"][assembly]["Solids"] - == nas_to_dict["Assemblies"][assembly]["Lines"] - == {} - ): - del nas_to_dict["Assemblies"][assembly] - for _, assembly_object in nas_to_dict["Assemblies"].items(): - - def domino(segments): - - def check_new_connection(s, polylines, exclude_index=-1): - s = s[:] - polylines = [poly[:] for poly in polylines] - attached = False - p_index = None - for i, p in enumerate(polylines): - if i == exclude_index: - continue - if s[0] == p[-1]: - p.extend(s[1:]) # the new segment attaches to the end - attached = True - elif s[-1] == p[0]: - for item in reversed(s[:-1]): - p.insert(0, item) # the new segment attaches to the beginning - attached = True - elif s[0] == p[0]: - for item in s[1:]: - p.insert(0, item) # the new segment attaches to the beginning in reverse order - attached = True - elif s[-1] == p[-1]: - p.extend(s[-2::-1]) # the new segment attaches to the end in reverse order - attached = True - if attached: - p_index = i - break - if not attached: - polylines.append(s) - return polylines, attached, p_index - - polylines = [] - for segment in segments: - polylines, attached_flag, attached_p_index = check_new_connection(segment, polylines) - if attached_flag: - other_polylines = polylines[:attached_p_index] + polylines[attached_p_index + 1 :] - polylines, _, _ = check_new_connection( - polylines[attached_p_index], other_polylines, attached_p_index - ) - - return polylines - - def remove_self_intersections(polylines): - polylines = [poly[:] for poly in polylines] - new_polylines = [] - for p in polylines: - if p[0] in p[1:]: - new_polylines.append([p[0], p[1]]) - p.pop(0) - if p[-1] in p[:-1]: - new_polylines.append([p[-2], p[-1]]) - p.pop(-1) - new_polylines.append(p) - return new_polylines - - if assembly_object["Lines"]: - for lname, lines in assembly_object["Lines"].items(): - new_lines = lines[::] - new_lines = remove_self_intersections(domino(new_lines)) - assembly_object["Lines"][lname] = new_lines - - return nas_to_dict - - -@pyaedt_function_handler() -def _write_stl(nas_to_dict, decimation, working_directory, enable_planar_merge=True): - logger = logging.getLogger("Global") - - def _write_solid_stl(triangle, pp): - try: - # points = [nas_to_dict["Points"][id] for id in triangle] - points = [pp[i] for i in triangle] - except KeyError: # pragma: no cover - return - fc = GeometryOperators.get_polygon_centroid(points) - v1 = points[0] - v2 = points[1] - cv1 = GeometryOperators.v_points(fc, v1) - cv2 = GeometryOperators.v_points(fc, v2) - if cv2[0] == cv1[0] == 0.0 and cv2[1] == cv1[1] == 0.0: - n = [0, 0, 1] # pragma: no cover - elif cv2[0] == cv1[0] == 0.0 and cv2[2] == cv1[2] == 0.0: - n = [0, 1, 0] # pragma: no cover - elif cv2[1] == cv1[1] == 0.0 and cv2[2] == cv1[2] == 0.0: - n = [1, 0, 0] # pragma: no cover - else: - n = GeometryOperators.v_cross(cv1, cv2) - - normal = GeometryOperators.normalize_vector(n) - if normal: - f.write(" facet normal {} {} {}\n".format(normal[0], normal[1], normal[2])) - f.write(" outer loop\n") - f.write(" vertex {} {} {}\n".format(points[0][0], points[0][1], points[0][2])) - f.write(" vertex {} {} {}\n".format(points[1][0], points[1][1], points[1][2])) - f.write(" vertex {} {} {}\n".format(points[2][0], points[2][1], points[2][2])) - f.write(" endloop\n") - f.write(" endfacet\n") - - logger.info("Creating STL file with detected faces") - enable_stl_merge = False if enable_planar_merge == "False" or enable_planar_merge is False else True - - def decimate(points_in, faces_in): - fin = [[3] + list(i) for i in faces_in] - mesh = pv.PolyData(points_in, faces=fin) - new_mesh = mesh.decimate_pro(decimation, preserve_topology=True, boundary_vertex_deletion=False) - points_out = list(new_mesh.points) - faces_out = [i[1:] for i in new_mesh.faces.reshape(-1, 4) if i[0] == 3] - return points_out, faces_out - - output_stls = [] - for assembly_name, assembly in nas_to_dict["Assemblies"].items(): - output_stl = os.path.join(working_directory, assembly_name + ".stl") - f = open(output_stl, "w") - for tri_id, triangles in assembly["Triangles"].items(): - tri_out = triangles - p_out = nas_to_dict["Points"][::] - if decimation > 0 and len(triangles) > 20: - p_out, tri_out = decimate(nas_to_dict["Points"], tri_out) - f.write("solid Sheet_{}\n".format(tri_id)) - if enable_planar_merge == "Auto" and len(tri_out) > 50000: - enable_stl_merge = False # pragma: no cover - for triangle in tri_out: - _write_solid_stl(triangle, p_out) - f.write("endsolid\n") - for solidid, solid_triangles in assembly["Solids"].items(): - f.write("solid Solid_{}\n".format(solidid)) - import pandas as pd - - df = pd.Series(solid_triangles) - tri_out = df.drop_duplicates(keep=False).to_list() - p_out = nas_to_dict["Points"][::] - if decimation > 0 and len(solid_triangles) > 20: - p_out, tri_out = decimate(nas_to_dict["Points"], tri_out) - if enable_planar_merge == "Auto" and len(tri_out) > 50000: - enable_stl_merge = False # pragma: no cover - for triangle in tri_out: - _write_solid_stl(triangle, p_out) - f.write("endsolid\n") - f.close() - output_stls.append(output_stl) - logger.info("STL file created") - return output_stls, enable_stl_merge - - -def nastran_to_stl(input_file, output_folder=None, decimation=0, enable_planar_merge="True", preview=False): - logger = logging.getLogger("Global") - nas_to_dict = _parse_nastran(input_file) - - empty = True - for assembly in nas_to_dict["Assemblies"].values(): - if assembly["Triangles"] or assembly["Solids"] or assembly["Lines"]: - empty = False - break - if empty: # pragma: no cover - logger.error("Failed to import file. Check the model and retry") - return False - if output_folder is None: - output_folder = os.path.dirname(input_file) - output_stls, enable_stl_merge = _write_stl(nas_to_dict, decimation, output_folder, enable_planar_merge) - if preview: - logger.info("Generating preview...") - if decimation > 0: - pl = pv.Plotter(shape=(1, 2)) - else: # pragma: no cover - pl = pv.Plotter() - dargs = dict(show_edges=True) - colors = [] - color_by_assembly = True - if len(nas_to_dict["Assemblies"]) == 1: - color_by_assembly = False - - def preview_pyvista(dict_in): - css4_colors = list(CSS4_COLORS.values()) - k = 0 - p_out = nas_to_dict["Points"][::] - for assembly in dict_in["Assemblies"].values(): - if color_by_assembly: - h = css4_colors[k].lstrip("#") - colors.append(tuple(int(h[i : i + 2], 16) for i in (0, 2, 4))) - k += 1 - - for triangles in assembly["Triangles"].values(): - tri_out = triangles - fin = [[3] + list(i) for i in tri_out] - if not color_by_assembly: - h = css4_colors[k].lstrip("#") - colors.append(tuple(int(h[i : i + 2], 16) for i in (0, 2, 4))) - k = k + 1 if k < len(css4_colors) - 1 else 0 - pl.add_mesh(pv.PolyData(p_out, faces=fin), color=colors[-1], **dargs) - - for triangles in assembly["Solids"].values(): - import pandas as pd - - df = pd.Series(triangles) - tri_out = df.drop_duplicates(keep=False).to_list() - p_out = nas_to_dict["Points"][::] - fin = [[3] + list(i) for i in tri_out] - if not color_by_assembly: - h = css4_colors[k].lstrip("#") - colors.append(tuple(int(h[i : i + 2], 16) for i in (0, 2, 4))) - k = k + 1 if k < len(css4_colors) - 1 else 0 - - pl.add_mesh(pv.PolyData(p_out, faces=fin), color=colors[-1], **dargs) - - preview_pyvista(nas_to_dict) - pl.add_text("Input mesh", font_size=24) - pl.reset_camera() - if decimation > 0 and output_stls: - k = 0 - pl.reset_camera() - pl.subplot(0, 1) - css4_colors = list(CSS4_COLORS.values()) - for output_stl in output_stls: - mesh = pv.read(output_stl) - h = css4_colors[k].lstrip("#") - colors.append(tuple(int(h[i : i + 2], 16) for i in (0, 2, 4))) - pl.add_mesh(mesh, color=colors[-1], **dargs) - k = k + 1 if k < len(css4_colors) - 1 else 0 - pl.add_text("Decimated mesh", font_size=24) - pl.reset_camera() - pl.link_views() - if "PYTEST_CURRENT_TEST" not in os.environ: - pl.show() # pragma: no cover - logger.info("STL files created") - return output_stls, nas_to_dict, enable_stl_merge - - -def simplify_stl(input_file, output_file=None, decimation=0.5, preview=False): - """Import and simplify a stl file using pyvista and fast-simplification. - - Parameters - ---------- - input_file : str - Input stl file. - output_file : str, optional - Output stl file. - decimation : float, optional - Fraction of the original mesh to remove before creating the stl file. If set to ``0.9``, - this function will try to reduce the data set to 10% of its - original size and will remove 90% of the input triangles. - preview : bool, optional - Whether to preview the model in pyvista or skip it. - Returns - ------- - str - Full path to output stl. - """ - mesh = pv.read(input_file) - if not output_file: - output_file = os.path.splitext(input_file)[0] + "_output.stl" - simple = mesh.decimate_pro(decimation, preserve_topology=True, boundary_vertex_deletion=False) - simple.save(output_file) - if preview: - pl = pv.Plotter(shape=(1, 2)) - dargs = dict(show_edges=True, color=True) - pl.add_mesh(mesh, **dargs) - pl.add_text("Input mesh", font_size=24) - pl.reset_camera() - pl.subplot(0, 1) - pl.add_mesh(simple, **dargs) - pl.add_text("Decimated mesh", font_size=24) - pl.reset_camera() - pl.link_views() - if "PYTEST_CURRENT_TEST" not in os.environ: - pl.show() - return output_file - - -class SolutionData(object): - """Contains information from the :func:`GetSolutionDataPerVariation` method.""" - - def __init__(self, aedtdata): - self._original_data = aedtdata - self.number_of_variations = len(aedtdata) - self._enable_pandas_output = True if settings.enable_pandas_output and pd else False - self._expressions = None - self._intrinsics = None - self._nominal_variation = None - self._nominal_variation = self._original_data[0] - self.active_expression = self.expressions[0] - self._sweeps_names = [] - self.update_sweeps() - self.variations = self._get_variations() - self.active_intrinsic = {} - for k, v in self.intrinsics.items(): - self.active_intrinsic[k] = v[0] - if len(self.intrinsics) > 0: - self._primary_sweep = list(self.intrinsics.keys())[0] - else: - self._primary_sweep = self._sweeps_names[0] - self.active_variation = self.variations[0] - self.units_sweeps = {} - for intrinsic in self.intrinsics: - try: - self.units_sweeps[intrinsic] = self.nominal_variation.GetSweepUnits(intrinsic) - except Exception: - self.units_sweeps[intrinsic] = None - self.init_solutions_data() - self._ifft = None - - @property - def enable_pandas_output(self): - """ - Set/Get a flag to use Pandas to export dict and lists. This applies to Solution data output. - If ``True`` the property or method will return a pandas object in CPython environment. - Default is ``False``. - - Returns - ------- - bool - """ - return True if self._enable_pandas_output and pd else False - - @enable_pandas_output.setter - def enable_pandas_output(self, val): - if val != self._enable_pandas_output and pd: - self._enable_pandas_output = val - self.init_solutions_data() - - @pyaedt_function_handler() - def set_active_variation(self, var_id=0): - """Set the active variations to one of available variations in self.variations. - - Parameters - ---------- - var_id : int - Index of Variations to assign. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - if var_id < len(self.variations): - self.active_variation = self.variations[var_id] - self.nominal_variation = var_id - self._expressions = None - self._intrinsics = None - return True - return False - - @pyaedt_function_handler() - def _get_variations(self): - variations_lists = [] - for data in self._original_data: - variations = {} - for v in data.GetDesignVariableNames(): - variations[v] = data.GetDesignVariableValue(v) - variations_lists.append(variations) - return variations_lists - - @pyaedt_function_handler(variation_name="variation") - def variation_values(self, variation): - """Get the list of the specific variation available values. - - Parameters - ---------- - variation : str - Name of variation to return. - - Returns - ------- - list - List of variation values. - """ - if variation in self.intrinsics: - return self.intrinsics[variation] - else: - vars_vals = [] - for el in self.variations: - if variation in el and el[variation] not in vars_vals: - vars_vals.append(el[variation]) - return vars_vals - - @property - def intrinsics(self): - """Get intrinsics dictionary on active variation.""" - if not self._intrinsics: - self._intrinsics = {} - intrinsics = [i for i in self._sweeps_names if i not in self.nominal_variation.GetDesignVariableNames()] - for el in intrinsics: - values = list(self.nominal_variation.GetSweepValues(el, False)) - self._intrinsics[el] = [i for i in values] - self._intrinsics[el] = list(dict.fromkeys(self._intrinsics[el])) - return self._intrinsics - - @property - def nominal_variation(self): - """Nominal variation.""" - return self._nominal_variation - - @nominal_variation.setter - def nominal_variation(self, val): - if 0 <= val <= self.number_of_variations: - self._nominal_variation = self._original_data[val] - else: - print(str(val) + " not in Variations") - - @property - def primary_sweep(self): - """Primary sweep. - - Parameters - ---------- - ps : float - Perimeter of the source. - """ - return self._primary_sweep - - @primary_sweep.setter - def primary_sweep(self, ps): - if ps in self._sweeps_names: - self._primary_sweep = ps - - @property - def expressions(self): - """Expressions.""" - if not self._expressions: - mydata = [i for i in self._nominal_variation.GetDataExpressions()] - self._expressions = list(dict.fromkeys(mydata)) - return self._expressions - - @pyaedt_function_handler() - def update_sweeps(self): - """Update sweeps. - - Returns - ------- - dict - Updated sweeps. - """ - - names = list(self.nominal_variation.GetSweepNames()) - for data in self._original_data: - for v in data.GetDesignVariableNames(): - if v not in self._sweeps_names: - self._sweeps_names.append(v) - self._sweeps_names.extend((reversed(names))) - - @staticmethod - @pyaedt_function_handler() - def _quantity(unit): - """Get the corresponding AEDT units. - - Parameters - ---------- - unit : str - The unit to be looked among the available AEDT units. - - Returns - ------- - str - The AEDT units. - - """ - for el in AEDT_UNITS: - keys_units = [i.lower() for i in list(AEDT_UNITS[el].keys())] - if unit.lower() in keys_units: - return el - return None - - @pyaedt_function_handler() - def init_solutions_data(self): - """Initialize the database and store info in variables.""" - self._solutions_real = self._init_solution_data_real() - self._solutions_imag = self._init_solution_data_imag() - self._solutions_mag = self._init_solution_data_mag() - self._solutions_phase = self._init_solution_data_phase() - - @pyaedt_function_handler() - def _init_solution_data_mag(self): - _solutions_mag = {} - self.units_data = {} - - for expr in self.expressions: - _solutions_mag[expr] = {} - self.units_data[expr] = self.nominal_variation.GetDataUnits(expr) - if self.enable_pandas_output: - _solutions_mag[expr] = np.sqrt(self._solutions_real[expr]) - else: - for i in self._solutions_real[expr]: - _solutions_mag[expr][i] = abs(complex(self._solutions_real[expr][i], self._solutions_imag[expr][i])) - if self.enable_pandas_output: - return pd.DataFrame.from_dict(_solutions_mag) - else: - return _solutions_mag - - @pyaedt_function_handler() - def _init_solution_data_real(self): - """ """ - sols_data = {} - - for expression in self.expressions: - solution_data = {} - - for data, comb in zip(self._original_data, self.variations): - solution = list(data.GetRealDataValues(expression, False)) - values = [] - for el in list(self.intrinsics.keys()): - values.append(list(dict.fromkeys(data.GetSweepValues(el, False)))) - - i = 0 - c = [comb[v] for v in list(comb.keys())] - for t in itertools.product(*values): - solution_data[tuple(c + list(t))] = solution[i] - i += 1 - sols_data[expression] = solution_data - if self.enable_pandas_output: - return pd.DataFrame.from_dict(sols_data) - else: - return sols_data - - @pyaedt_function_handler() - def _init_solution_data_imag(self): - """ """ - sols_data = {} - - for expression in self.expressions: - solution_data = {} - for data, comb in zip(self._original_data, self.variations): - if data.IsDataComplex(expression): - solution = list(data.GetImagDataValues(expression, False)) - else: - l = len(list(data.GetRealDataValues(expression, False))) - solution = [0] * l - values = [] - for el in list(self.intrinsics.keys()): - values.append(list(dict.fromkeys(data.GetSweepValues(el, False)))) - i = 0 - c = [comb[v] for v in list(comb.keys())] - for t in itertools.product(*values): - solution_data[tuple(c + list(t))] = solution[i] - i += 1 - sols_data[expression] = solution_data - if self.enable_pandas_output: - return pd.DataFrame.from_dict(sols_data) - else: - return sols_data - - @pyaedt_function_handler() - def _init_solution_data_phase(self): - data_phase = {} - for expr in self.expressions: - data_phase[expr] = {} - if self.enable_pandas_output: - data_phase[expr] = np.arctan2(self._solutions_imag[expr], self._solutions_real[expr]) - else: - for i in self._solutions_real[expr]: - data_phase[expr][i] = math.atan2(self._solutions_imag[expr][i], self._solutions_real[expr][i]) - if self.enable_pandas_output: - return pd.DataFrame.from_dict(data_phase) - else: - return data_phase - - @property - def full_matrix_real_imag(self): - """Get the full available solution data in Real and Imaginary parts. - - Returns - ------- - tuple of dicts - (Real Dict, Imag Dict) - """ - return self._solutions_real, self._solutions_imag - - @property - def full_matrix_mag_phase(self): - """Get the full available solution data magnitude and phase in radians. - - Returns - ------- - tuple of dicts - (Mag Dict, Phase Dict). - """ - return self._solutions_mag, self._solutions_phase - - @staticmethod - @pyaedt_function_handler() - def to_degrees(input_list): - """Convert an input list from radians to degrees. - - Parameters - ---------- - input_list : list - List of inputs in radians. - - Returns - ------- - list - List of inputs in degrees. - - """ - if isinstance(input_list, (tuple, list)): - return [i * 360 / (2 * math.pi) for i in input_list] - else: - return input_list * 360 / (2 * math.pi) - - @staticmethod - @pyaedt_function_handler() - def to_radians(input_list): - """Convert an input list from degrees to radians. - - Parameters - ---------- - input_list : list - List of inputs in degrees. - - Returns - ------- - type - List of inputs in radians. - """ - if isinstance(input_list, (tuple, list)): - return [i * 2 * math.pi / 360 for i in input_list] - else: - return input_list * 2 * math.pi / 360 - - @pyaedt_function_handler() - def _variation_tuple(self): - temp = [] - for it in self._sweeps_names: - try: - temp.append(self.active_variation[it]) - except KeyError: - temp.append(self.active_intrinsic[it]) - return temp - - @pyaedt_function_handler() - def data_magnitude(self, expression=None, convert_to_SI=False): - """Retrieve the data magnitude of an expression. - - Parameters - ---------- - expression : str, optional - Name of the expression. The default is ``None``, in which case the - active expression is used. - convert_to_SI : bool, optional - Whether to convert the data to the SI unit system. - The default is ``False``. - - Returns - ------- - list - List of data. - """ - if not expression: - expression = self.active_expression - elif expression not in self.expressions: - return False - temp = self._variation_tuple() - solution_data = self._solutions_mag[expression] - sol = [] - position = list(self._sweeps_names).index(self.primary_sweep) - sw = self.variation_values(self.primary_sweep) - for el in sw: - temp[position] = el - try: - sol.append(solution_data[tuple(temp)]) - except KeyError: - sol.append(None) - if convert_to_SI and self._quantity(self.units_data[expression]): - sol = self._convert_list_to_SI( - sol, self._quantity(self.units_data[expression]), self.units_data[expression] - ) - if self.enable_pandas_output: - return pd.Series(sol) - return sol - - @staticmethod - @pyaedt_function_handler(datalist="data", dataunits="data_units") - def _convert_list_to_SI(data, data_units, units): - """Convert a data list to the SI unit system. - - Parameters - ---------- - data : list - List of data to convert. - data_units : str - Data units. - units : str - SI units to convert data into. - - - Returns - ------- - list - List of the data converted to the SI unit system. - - """ - sol = data - if data_units in AEDT_UNITS and units in AEDT_UNITS[data_units]: - sol = [i * AEDT_UNITS[data_units][units] for i in data] - return sol - - @pyaedt_function_handler() - def data_db10(self, expression=None, convert_to_SI=False): - """Retrieve the data in the database for an expression and convert in db10. - - Parameters - ---------- - expression : str, optional - Name of the expression. The default is ``None``, - in which case the active expression is used. - convert_to_SI : bool, optional - Whether to convert the data to the SI unit system. - The default is ``False``. - - Returns - ------- - list - List of the data in the database for the expression. - """ - if not expression: - expression = self.active_expression - if self.enable_pandas_output: - return 10 * np.log10(self.data_magnitude(expression, convert_to_SI)) - return [db10(i) for i in self.data_magnitude(expression, convert_to_SI)] - - @pyaedt_function_handler() - def data_db20(self, expression=None, convert_to_SI=False): - """Retrieve the data in the database for an expression and convert in db20. - - Parameters - ---------- - expression : str, optional - Name of the expression. The default is ``None``, - in which case the active expression is used. - convert_to_SI : bool, optional - Whether to convert the data to the SI unit system. - The default is ``False``. - - Returns - ------- - list - List of the data in the database for the expression. - """ - if not expression: - expression = self.active_expression - if self.enable_pandas_output: - return 20 * np.log10(self.data_magnitude(expression, convert_to_SI)) - return [db20(i) for i in self.data_magnitude(expression, convert_to_SI)] - - @pyaedt_function_handler() - def data_phase(self, expression=None, radians=True): - """Retrieve the phase part of the data for an expression. - - Parameters - ---------- - expression : str, None - Name of the expression. The default is ``None``, - in which case the active expression is used. - radians : bool, optional - Whether to convert the data into radians or degree. - The default is ``True`` for radians. - - Returns - ------- - list - Phase data for the expression. - """ - if not expression: - expression = self.active_expression - coefficient = 1 - if not radians: - coefficient = 180 / math.pi - if self.enable_pandas_output: - return coefficient * np.arctan2(self.data_imag(expression), self.data_real(expression)) - return [coefficient * math.atan2(k, i) for i, k in zip(self.data_real(expression), self.data_imag(expression))] - - @property - def primary_sweep_values(self): - """Retrieve the primary sweep for a given data and primary variable. - - Returns - ------- - list - List of the primary sweep valid points for the expression. - """ - if self.enable_pandas_output: - return pd.Series(self.variation_values(self.primary_sweep)) - return self.variation_values(self.primary_sweep) - - @property - def primary_sweep_variations(self): - """Retrieve the variations lists for a given primary variable. - - Returns - ------- - list - List of the primary sweep valid points for the expression. - - """ - expression = self.active_expression - temp = self._variation_tuple() - - solution_data = list(self._solutions_real[expression].keys()) - sol = [] - position = list(self._sweeps_names).index(self.primary_sweep) - - for el in self.primary_sweep_values: - temp[position] = el - if tuple(temp) in solution_data: - sol_dict = {} - i = 0 - for sn in self._sweeps_names: - sol_dict[sn] = temp[i] - i += 1 - sol.append(sol_dict) - else: - sol.append(None) - if self.enable_pandas_output: - return pd.Series(sol) - return sol - - @pyaedt_function_handler() - def data_real(self, expression=None, convert_to_SI=False): - """Retrieve the real part of the data for an expression. - - Parameters - ---------- - expression : str, None - Name of the expression. The default is ``None``, - in which case the active expression is used. - convert_to_SI : bool, optional - Whether to convert the data to the SI unit system. - The default is ``False``. - - Returns - ------- - list - List of the real data for the expression. - """ - if not expression: - expression = self.active_expression - temp = self._variation_tuple() - - solution_data = self._solutions_real[expression] - sol = [] - position = list(self._sweeps_names).index(self.primary_sweep) - - for el in self.primary_sweep_values: - temp[position] = el - try: - sol.append(solution_data[tuple(temp)]) - except KeyError: - sol.append(None) - - if convert_to_SI and self._quantity(self.units_data[expression]): - sol = self._convert_list_to_SI( - sol, self._quantity(self.units_data[expression]), self.units_data[expression] - ) - if self.enable_pandas_output: - return pd.Series(sol) - return sol - - @pyaedt_function_handler() - def data_imag(self, expression=None, convert_to_SI=False): - """Retrieve the imaginary part of the data for an expression. - - Parameters - ---------- - expression : str, optional - Name of the expression. The default is ``None``, - in which case the active expression is used. - convert_to_SI : bool, optional - Whether to convert the data to the SI unit system. - The default is ``False``. - - Returns - ------- - list - List of the imaginary data for the expression. - """ - if not expression: - expression = self.active_expression - temp = self._variation_tuple() - - solution_data = self._solutions_imag[expression] - sol = [] - position = list(self._sweeps_names).index(self.primary_sweep) - for el in self.primary_sweep_values: - temp[position] = el - try: - sol.append(solution_data[tuple(temp)]) - except KeyError: - sol.append(None) - if convert_to_SI and self._quantity(self.units_data[expression]): - sol = self._convert_list_to_SI( - sol, self._quantity(self.units_data[expression]), self.units_data[expression] - ) - if self.enable_pandas_output: - return pd.Series(sol) - return sol - - @pyaedt_function_handler() - def is_real_only(self, expression=None): - """Check if the expression has only real values or not. - - Parameters - ---------- - expression : str, optional - Name of the expression. The default is ``None``, - in which case the active expression is used. - - Returns - ------- - bool - ``True`` if the Solution Data for specific expression contains only real values. - """ - if not expression: - expression = self.active_expression - if self.enable_pandas_output: - return True if self._solutions_imag[expression].abs().sum() > 0.0 else False - for v in list(self._solutions_imag[expression].values()): - if float(v) != 0.0: - return False - return True - - @pyaedt_function_handler() - def export_data_to_csv(self, output, delimiter=";"): - """Save to output csv file the Solution Data. - - Parameters - ---------- - output : str, - Full path to csv file. - delimiter : str, - CSV Delimiter. Default is ``";"``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - header = [] - des_var = self._original_data[0].GetDesignVariableNames() - sweep_var = self._original_data[0].GetSweepNames() - for el in self._sweeps_names: - unit = "" - if el in des_var: - unit = self._original_data[0].GetDesignVariableUnits(el) - elif el in sweep_var: - unit = self._original_data[0].GetSweepUnits(el) - if unit == "": - header.append("{}".format(el)) - else: - header.append("{} [{}]".format(el, unit)) - # header = [el for el in self._sweeps_names] - for el in self.expressions: - data_unit = self._original_data[0].GetDataUnits(el) - if data_unit: - data_unit = " [{}]".format(data_unit) - if not self.is_real_only(el): - - header.append(el + " (Real){}".format(data_unit)) - header.append(el + " (Imag){}".format(data_unit)) - else: - header.append(el + "{}".format(data_unit)) - - list_full = [header] - for e, v in self._solutions_real[self.active_expression].items(): - list_full.append(list(e)) - for el in self.expressions: - i = 1 - for e, v in self._solutions_real[el].items(): - list_full[i].extend([v]) - i += 1 - i = 1 - if not self.is_real_only(el): - for e, v in self._solutions_imag[el].items(): - list_full[i].extend([v]) - i += 1 - - return write_csv(output, list_full, delimiter=delimiter) - - @pyaedt_function_handler(math_formula="formula", xlabel="x_label", ylabel="y_label") - def plot( - self, - curves=None, - formula=None, - size=(2000, 1000), - show_legend=True, - x_label="", - y_label="", - title="", - snapshot_path=None, - is_polar=False, - show=True, - ): - """Create a matplotlib figure based on a list of data. - - Parameters - ---------- - curves : list - Curves to be plotted. The default is ``None``, in which case - the first curve is plotted. - formula : str , optional - Mathematical formula to apply to the plot curve. The default is ``None``, - in which case only real value of the data stored in the solution data is plotted. - Options are ``"abs"``, ``"db10"``, ``"db20"``, ``"im"``, ``"mag"``, ``"phasedeg"``, - ``"phaserad"``, and ``"re"``. - size : tuple, optional - Image size in pixels (width, height). - show_legend : bool - Whether to show the legend. The default is ``True``. - This parameter is ignored if the number of curves to plot is - greater than 15. - x_label : str - Plot X label. - y_label : str - Plot Y label. - title : str - Plot title label. - snapshot_path : str - Full path to image file if a snapshot is needed. - is_polar : bool, optional - Set to `True` if this is a polar plot. - show : bool, optional - Whether if show the plot or not. Default is set to `True`. - - Returns - ------- - :class:`matplotlib.pyplot.Figure` - Matplotlib figure object. - """ - if is_ironpython: # pragma: no cover - return False - if not curves: - curves = [self.active_expression] - if isinstance(curves, str): - curves = [curves] - data_plot = [] - sweep_name = self.primary_sweep - if is_polar: - sw = self.to_radians(self.primary_sweep_values) - else: - sw = self.primary_sweep_values - for curve in curves: - if not formula: - data_plot.append([sw, self.data_real(curve), curve]) - elif formula == "re": - data_plot.append([sw, self.data_real(curve), "{}({})".format(formula, curve)]) - elif formula == "im": - data_plot.append([sw, self.data_imag(curve), "{}({})".format(formula, curve)]) - elif formula == "db20": - data_plot.append([sw, self.data_db20(curve), "{}({})".format(formula, curve)]) - elif formula == "db10": - data_plot.append([sw, self.data_db10(curve), "{}({})".format(formula, curve)]) - elif formula == "mag": - data_plot.append([sw, self.data_magnitude(curve), "{}({})".format(formula, curve)]) - elif formula == "phasedeg": - data_plot.append([sw, self.data_phase(curve, False), "{}({})".format(formula, curve)]) - elif formula == "phaserad": - data_plot.append([sw, self.data_phase(curve, True), "{}({})".format(formula, curve)]) - if not x_label: - x_label = sweep_name - if not y_label: - y_label = formula - if not title: - title = "Simulation Results Plot" - if len(data_plot) > 15: - show_legend = False - if is_polar: - return plot_polar_chart(data_plot, size, show_legend, x_label, y_label, title, snapshot_path, show=show) - else: - return plot_2d_chart(data_plot, size, show_legend, x_label, y_label, title, snapshot_path, show=show) - - @pyaedt_function_handler(xlabel="x_label", ylabel="y_label", math_formula="formula") - def plot_3d( - self, - curve=None, - x_axis="Theta", - y_axis="Phi", - x_label="", - y_label="", - title="", - formula=None, - size=(2000, 1000), - snapshot_path=None, - show=True, - ): - """Create a matplotlib 3D figure based on a list of data. - - Parameters - ---------- - curve : str - Curve to be plotted. If None, the first curve will be plotted. - x_axis : str, optional - X-axis sweep. The default is ``"Theta"``. - y_axis : str, optional - Y-axis sweep. The default is ``"Phi"``. - x_label : str - Plot X label. - y_label : str - Plot Y label. - title : str - Plot title label. - formula : str , optional - Mathematical formula to apply to the plot curve. The default is ``None``. - Options are `"abs"``, ``"db10"``, ``"db20"``, ``"im"``, ``"mag"``, ``"phasedeg"``, - ``"phaserad"``, and ``"re"``. - size : tuple, optional - Image size in pixels (width, height). The default is ``(2000, 1000)``. - snapshot_path : str, optional - Full path to image file if a snapshot is needed. - The default is ``None``. - show : bool, optional - Whether if show the plot or not. Default is set to `True`. - - Returns - ------- - :class:`matplotlib.figure.Figure` - Matplotlib figure object. - """ - if is_ironpython: - return False # pragma: no cover - if not curve: - curve = self.active_expression - - if not formula: - formula = "mag" - theta = self.variation_values(x_axis) - y_axis_val = self.variation_values(y_axis) - - phi = [] - r = [] - for el in y_axis_val: - self.active_variation[y_axis] = el - phi.append(el * math.pi / 180) - - if formula == "re": - r.append(self.data_real(curve)) - elif formula == "im": - r.append(self.data_imag(curve)) - elif formula == "db20": - r.append(self.data_db20(curve)) - elif formula == "db10": - r.append(self.data_db10(curve)) - elif formula == "mag": - r.append(self.data_magnitude(curve)) - elif formula == "phasedeg": - r.append(self.data_phase(curve, False)) - elif formula == "phaserad": - r.append(self.data_phase(curve, True)) - active_sweep = self.active_intrinsic[self.primary_sweep] - position = self.variation_values(self.primary_sweep).index(active_sweep) - if len(self.variation_values(self.primary_sweep)) > 1: - new_r = [] - for el in r: - new_r.append([el[position]]) - r = new_r - data_plot = [theta, phi, r] - if not x_label: - x_label = x_axis - if not y_label: - y_label = y_axis - if not title: - title = "Simulation Results Plot" - return plot_3d_chart(data_plot, size, x_label, y_label, title, snapshot_path, show=show) - - @pyaedt_function_handler() - def ifft(self, curve_header="NearE", u_axis="_u", v_axis="_v", window=False): - """Create IFFT of given complex data. - - Parameters - ---------- - curve_header : curve header. Solution data must contain 3 curves with X, Y and Z components of curve header. - u_axis : str, optional - U Axis name. Default is Hfss name "_u" - v_axis : str, optional - V Axis name. Default is Hfss name "_v" - window : bool, optional - Either if Hanning windowing has to be applied. - - Returns - ------- - List - IFFT Matrix. - """ - if is_ironpython: - return False - u = self.variation_values(u_axis) - v = self.variation_values(v_axis) - - freq = self.variation_values("Freq") - if self.enable_pandas_output: - e_real_x = np.reshape(self._solutions_real[curve_header + "X"].copy().values, (len(freq), len(v), len(u))) - e_imag_x = np.reshape(self._solutions_imag[curve_header + "X"].copy().values, (len(freq), len(v), len(u))) - e_real_y = np.reshape(self._solutions_real[curve_header + "Y"].copy().values, (len(freq), len(v), len(u))) - e_imag_y = np.reshape(self._solutions_imag[curve_header + "Y"].copy().values, (len(freq), len(v), len(u))) - e_real_z = np.reshape(self._solutions_real[curve_header + "Z"].copy().values, (len(freq), len(v), len(u))) - e_imag_z = np.reshape(self._solutions_imag[curve_header + "Z"].copy().values, (len(freq), len(v), len(u))) - else: - vals_e_real_x = [j for j in self._solutions_real[curve_header + "X"].values()] - vals_e_imag_x = [j for j in self._solutions_imag[curve_header + "X"].values()] - vals_e_real_y = [j for j in self._solutions_real[curve_header + "Y"].values()] - vals_e_imag_y = [j for j in self._solutions_imag[curve_header + "Y"].values()] - vals_e_real_z = [j for j in self._solutions_real[curve_header + "Z"].values()] - vals_e_imag_z = [j for j in self._solutions_imag[curve_header + "Z"].values()] - - e_real_x = np.reshape(vals_e_real_x, (len(freq), len(v), len(u))) - e_imag_x = np.reshape(vals_e_imag_x, (len(freq), len(v), len(u))) - e_real_y = np.reshape(vals_e_real_y, (len(freq), len(v), len(u))) - e_imag_y = np.reshape(vals_e_imag_y, (len(freq), len(v), len(u))) - e_real_z = np.reshape(vals_e_real_z, (len(freq), len(v), len(u))) - e_imag_z = np.reshape(vals_e_imag_z, (len(freq), len(v), len(u))) - - temp_e_comp_x = e_real_x + 1j * e_imag_x # Here is the complex FD data matrix, ready for transforming - temp_e_comp_y = e_real_y + 1j * e_imag_y - temp_e_comp_z = e_real_z + 1j * e_imag_z - - e_comp_x = np.zeros((len(freq), len(v), len(u)), dtype="complex_") - e_comp_y = np.zeros((len(freq), len(v), len(u)), dtype="complex_") - e_comp_z = np.zeros((len(freq), len(v), len(u)), dtype="complex_") - if window: - timewin = np.hanning(len(freq)) - - for row in range(0, len(v)): - for col in range(0, len(u)): - e_comp_x[:, row, col] = np.multiply(temp_e_comp_x[:, row, col], timewin) - e_comp_y[:, row, col] = np.multiply(temp_e_comp_y[:, row, col], timewin) - e_comp_z[:, row, col] = np.multiply(temp_e_comp_z[:, row, col], timewin) - else: - e_comp_x = temp_e_comp_x - e_comp_y = temp_e_comp_y - e_comp_z = temp_e_comp_z - - e_time_x = np.fft.ifft(np.fft.fftshift(e_comp_x, 0), len(freq), 0, None) - e_time_y = np.fft.ifft(np.fft.fftshift(e_comp_y, 0), len(freq), 0, None) - e_time_z = np.fft.ifft(np.fft.fftshift(e_comp_z, 0), len(freq), 0, None) - e_time = np.zeros((np.size(freq), np.size(v), np.size(u))) - for i in range(0, len(freq)): - e_time[i, :, :] = np.abs( - np.sqrt(np.square(e_time_x[i, :, :]) + np.square(e_time_y[i, :, :]) + np.square(e_time_z[i, :, :])) - ) - self._ifft = e_time - - return self._ifft - - @pyaedt_function_handler(csv_dir="csv_path", name_str="csv_file_header") - def ifft_to_file( - self, - u_axis="_u", - v_axis="_v", - coord_system_center=None, - db_val=False, - num_frames=None, - csv_path=None, - csv_file_header="res_", - ): - """Save IFFT matrix to a list of CSV files (one per time step). - - Parameters - ---------- - u_axis : str, optional - U Axis name. Default is Hfss name "_u" - v_axis : str, optional - V Axis name. Default is Hfss name "_v" - coord_system_center : list, optional - List of UV GlobalCS Center. - db_val : bool, optional - Whether data must be exported into a database. The default is ``False``. - num_frames : int, optional - Number of frames to export. The default is ``None``. - csv_path : str, optional - Output path. The default is ``None``. - csv_file_header : str, optional - CSV file header. The default is ``"res_"``. - - Returns - ------- - str - Path to file containing the list of csv files. - """ - if not coord_system_center: - coord_system_center = [0, 0, 0] - t_matrix = self._ifft - x_c_list = self.variation_values(u_axis) - y_c_list = self.variation_values(v_axis) - - adj_x = coord_system_center[0] - adj_y = coord_system_center[1] - adj_z = coord_system_center[2] - if num_frames: - frames = num_frames - else: - frames = t_matrix.shape[0] - csv_list = [] - if os.path.exists(csv_path): - files = [os.path.join(csv_path, f) for f in os.listdir(csv_path) if csv_file_header in f and ".csv" in f] - for file in files: - os.remove(file) - else: - os.mkdir(csv_path) - - for frame in range(frames): - output = os.path.join(csv_path, csv_file_header + str(frame) + ".csv") - list_full = [["x", "y", "z", "val"]] - for i, y in enumerate(y_c_list): - for j, x in enumerate(x_c_list): - y_coord = y + adj_y - x_coord = x + adj_x - z_coord = adj_z - if db_val: - val = 10.0 * np.log10(np.abs(t_matrix[frame, i, j])) - else: - val = t_matrix[frame, i, j] - row_lst = [x_coord, y_coord, z_coord, val] - list_full.append(row_lst) - write_csv(output, list_full, delimiter=",") - csv_list.append(output) - - txt_file_name = csv_path + "fft_list.txt" - textfile = open_file(txt_file_name, "w") - - for element in csv_list: - textfile.write(element + "\n") - textfile.close() - return txt_file_name - - -class BaseFolderPlot: - @abstractmethod - def to_dict(self): - """Convert the settings to a dictionary. - - Returns - ------- - dict - A dictionary containing settings. - """ - - @abstractmethod - def from_dict(self, dictionary): - """Initialize the settings from a dictionary. - - Parameters - ---------- - dictionary : dict - Dictionary containing the configuration settings. - Dictionary syntax must be the same of the AEDT file. - """ - - -class ColorMapSettings(BaseFolderPlot): - """Provides methods and variables for editing color map folder settings. - - Parameters - ---------- - map_type : str, optional - The type of colormap to use. Must be one of the allowed types - (`"Spectrum"`, `"Ramp"`, `"Uniform"`). - Default is `"Spectrum"`. - color : str or list[float], optional - Color to use. If "Spectrum" color map, a string is expected. - Else a list of 3 values (R,G,B). Default is `"Rainbow"`. - """ - - def __init__(self, map_type="Spectrum", color="Rainbow"): - self._map_type = None - self.map_type = map_type - - # Default color settings - self._color_spectrum = "Rainbow" - self._color_ramp = [255, 127, 127] - self._color_uniform = [127, 255, 255] - - # User-provided color settings - self.color = color - - @property - def map_type(self): - """Get the color map type for the field plot.""" - return self._map_type - - @map_type.setter - def map_type(self, value): - """Set the type of color mapping for the field plot. - - Parameters - ---------- - value : str - The type of mapping to set. Must be one of 'Spectrum', 'Ramp', or 'Uniform'. - - Raises - ------ - ValueError - If the provided `value` is not valid, raises a ``ValueError`` with an appropriate message. - """ - if value not in ["Spectrum", "Ramp", "Uniform"]: - raise ValueError(f"{value} is not valid. Only 'Spectrum', 'Ramp', and 'Uniform' are accepted.") - self._map_type = value - - @property - def color(self): - """Get the color based on the map type. - - Returns: - str or list of float: The color scheme based on the map type. - """ - if self.map_type == "Spectrum": - return self._color_spectrum - elif self.map_type == "Ramp": - return self._color_ramp - elif self.map_type == "Uniform": - return self._color_uniform - - @color.setter - def color(self, v): - """Set the colormap based on the map type. - - Parameters: - ----------- - v : str or list[float] - The color value to be set. If a string, it should represent a valid color - spectrum specification (`"Magenta"`, `"Rainbow"`, `"Temperature"` or `"Gray"`). - If a tuple, it should contain three elements representing RGB values. - - Raises: - ------- - ValueError: If the provided color value is not valid for the specified map type. - """ - if self.map_type == "Spectrum": - self._validate_color_spectrum(v) - self._color_spectrum = v - else: - self._validate_color(v) - if self.map_type == "Ramp": - self._color_ramp = v - else: - self._color_uniform = v - - @staticmethod - def _validate_color_spectrum(value): - if value not in ["Magenta", "Rainbow", "Temperature", "Gray"]: - raise ValueError( - f"{value} is not valid. Only 'Magenta', 'Rainbow', 'Temperature', and 'Gray' are accepted." - ) - - @staticmethod - def _validate_color(value): - if not isinstance(value, list) or len(value) != 3: - raise ValueError(f"{value} is not valid. Three values (R, G, B) must be passed.") - - def __repr__(self): - color_repr = self.color - return f"ColorMapSettings(map_type='{self.map_type}', color={color_repr})" - - def to_dict(self): - """Convert the color map settings to a dictionary. - - Returns - ------- - dict - A dictionary containing all the color map settings - for the folder field plot settings. - """ - return { - "ColorMapSettings": { - "ColorMapType": self.map_type, - {"Spectrum": "SpectrumType", "Uniform": "UniformColor", "Ramp": "RampColor"}[self.map_type]: self.color, - } - } - - def from_dict(self, settings): - """Initialize the number format settings of the colormap settings from a dictionary. - - Parameters - ---------- - dictionary : dict - Dictionary containing the configuration for colormap settings. - Dictionary syntax must be the same of relevant portion of the AEDT file. - """ - self._map_type = settings["ColorMapType"] - self._color_spectrum = settings["SpectrumType"] - self._color_ramp = settings["RampColor"] - self._color_uniform = settings["UniformColor"] - - -class AutoScale(BaseFolderPlot): - """Provides methods and variables for editing automatic scale folder settings. - - Parameters - ---------- - n_levels : int, optional - Number of color levels of the scale. Default is `10`. - limit_precision_digits : bool, optional - Whether to limit precision digits. Default is `False`. - precision_digits : int, optional - Precision digits. Default is `3`. - use_current_scale_for_animation : bool, optional - Whether to use the scale for the animation. Default is `False`. - """ - - def __init__( - self, n_levels=10, limit_precision_digits=False, precision_digits=3, use_current_scale_for_animation=False - ): - self.n_levels = n_levels - self.limit_precision_digits = limit_precision_digits - self.precision_digits = precision_digits - self.use_current_scale_for_animation = use_current_scale_for_animation - - def __repr__(self): - return ( - f"AutoScale(n_levels={self.n_levels}, " - f"limit_precision_digits={self.limit_precision_digits}, " - f"precision_digits={self.precision_digits}, " - f"use_current_scale_for_animation={self.use_current_scale_for_animation})" - ) - - def to_dict(self): - """Convert the auto-scale settings to a dictionary. - - Returns - ------- - dict - A dictionary containing all the auto-scale settings - for the folder field plot settings. - """ - return { - "m_nLevels": self.n_levels, - "LimitFieldValuePrecision": self.limit_precision_digits, - "FieldValuePrecisionDigits": self.precision_digits, - "AnimationStaticScale": self.use_current_scale_for_animation, - } - - def from_dict(self, dictionary): - """Initialize the auto-scale settings from a dictionary. - - Parameters - ---------- - dictionary : dict - Dictionary containing the configuration for auto-scale settings. - Dictionary syntax must be the same of relevant portion of the AEDT file. - """ - self.n_levels = dictionary["m_nLevels"] - self.limit_precision_digits = dictionary["LimitFieldValuePrecision"] - self.precision_digits = dictionary["FieldValuePrecisionDigits"] - self.use_current_scale_for_animation = dictionary["AnimationStaticScale"] - - -class MinMaxScale(BaseFolderPlot): - """Provides methods and variables for editing min-max scale folder settings. - - Parameters - ---------- - n_levels : int, optional - Number of color levels of the scale. Default is `10`. - min_value : float, optional - Minimum value of the scale. Default is `0`. - max_value : float, optional - Maximum value of the scale. Default is `1`. - """ - - def __init__(self, n_levels=10, min_value=0, max_value=1): - self.n_levels = n_levels - self.min_value = min_value - self.max_value = max_value - - def __repr__(self): - return f"MinMaxScale(n_levels={self.n_levels}, min_value={self.min_value}, max_value={self.max_value})" - - def to_dict(self): - """Convert the min-max scale settings to a dictionary. - - Returns - ------- - dict - A dictionary containing all the min-max scale settings - for the folder field plot settings. - """ - return {"minvalue": self.min_value, "maxvalue": self.max_value, "m_nLevels": self.n_levels} - - def from_dict(self, dictionary): - """Initialize the min-max scale settings from a dictionary. - - Parameters - ---------- - dictionary : dict - Dictionary containing the configuration for min-max scale settings. - Dictionary syntax must be the same of relevant portion of the AEDT file. - """ - self.min_value = dictionary["minvalue"] - self.max_value = dictionary["maxvalue"] - self.n_levels = dictionary["m_nLevels"] - - -class SpecifiedScale: - """Provides methods and variables for editing min-max scale folder settings. - - Parameters - ---------- - scale_values : int, optional - Scale levels. Default is `None`. - """ - - def __init__(self, scale_values=None): - if scale_values is None: - scale_values = [] - if not isinstance(scale_values, list): - raise ValueError("scale_values must be a list.") - self.scale_values = scale_values - - def __repr__(self): - return f"SpecifiedScale(scale_values={self.scale_values})" - - def to_dict(self): - """Convert the specified scale settings to a dictionary. - - Returns - ------- - dict - A dictionary containing all the specified scale settings - for the folder field plot settings. - """ - return {"UserSpecifyValues": [len(self.scale_values)] + self.scale_values} - - def from_dict(self, dictionary): - """Initialize the specified scale settings from a dictionary. - - Parameters - ---------- - dictionary : dict - Dictionary containing the configuration for specified scale settings. - Dictionary syntax must be the same of relevant portion of the AEDT file. - """ - self.scale_values = dictionary["UserSpecifyValues"][:-1] - - -class NumberFormat(BaseFolderPlot): - """Provides methods and variables for editing number format folder settings. - - Parameters - ---------- - format_type : int, optional - Scale levels. Default is `None`. - width : int, optional - Width of the numbers space. Default is `4`. - precision : int, optional - Precision of the numbers. Default is `4`. - """ - - def __init__(self, format_type="Automatic", width=4, precision=4): - self._format_type = format_type - self.width = width - self.precision = precision - self._accepted = ["Automatic", "Scientific", "Decimal"] - - @property - def format_type(self): - """Get the current number format type.""" - return self._format_type - - @format_type.setter - def format_type(self, v): - """Set the numeric format type of the scale. - - Parameters: - ----------- - v (str): The new format type to be set. Must be one of the accepted values - ("Automatic", "Scientific" or "Decimal"). - - Raises: - ------- - ValueError: If the provided value is not in the list of accepted values. - """ - if v is not None and v in self._accepted: - self._format_type = v - else: - raise ValueError(f"{v} is not valid. Accepted values are {', '.join(self._accepted)}.") - - def __repr__(self): - return f"NumberFormat(format_type={self.format_type}, width={self.width}, precision={self.precision})" - - def to_dict(self): - """Convert the number format settings to a dictionary. - - Returns - ------- - dict - A dictionary containing all the number format settings - for the folder field plot settings. - """ - return { - "ValueNumberFormatTypeAuto": self._accepted.index(self.format_type), - "ValueNumberFormatTypeScientific": self.format_type == "Scientific", - "ValueNumberFormatWidth": self.width, - "ValueNumberFormatPrecision": self.precision, - } - - def from_dict(self, dictionary): - """Initialize the number format settings of the field plot settings from a dictionary. - - Parameters - ---------- - dictionary : dict - Dictionary containing the configuration for number format settings. - Dictionary syntax must be the same of relevant portion of the AEDT file. - """ - self._format_type = self._accepted[dictionary["ValueNumberFormatTypeAuto"]] - self.width = dictionary["ValueNumberFormatWidth"] - self.precision = dictionary["ValueNumberFormatPrecision"] - - -class Scale3DSettings(BaseFolderPlot): - """Provides methods and variables for editing scale folder settings. - - Parameters - ---------- - scale_type : str, optional - Scale type. Default is `"Auto"`. - scale_settings : :class:`ansys.aedt.core.modules.post_processor.AutoScale`, - :class:`ansys.aedt.core.modules.post_processor.MinMaxScale` or - :class:`ansys.aedt.core.modules.post_processor.SpecifiedScale`, optional - Scale settings. Default is `AutoScale()`. - log : bool, optional - Whether to use a log scale. Default is `False`. - db : bool, optional - Whether to use dB scale. Default is `False`. - unit : int, optional - Unit to use in the scale. Default is `None`. - number_format : :class:`ansys.aedt.core.modules.post_processor.NumberFormat`, optional - Number format settings. Default is `NumberFormat()`. - """ - - def __init__( - self, - scale_type="Auto", - scale_settings=AutoScale(), - log=False, - db=False, - unit=None, - number_format=NumberFormat(), - ): - self._scale_type = None # Initialize with None to use the setter for validation - self._scale_settings = None - self._unit = None - self._auto_scale = AutoScale() - self._minmax_scale = MinMaxScale() - self._specified_scale = SpecifiedScale() - self._accepted = ["Auto", "MinMax", "Specified"] - self.number_format = number_format - self.log = log - self.db = db - self.unit = unit - self.scale_type = scale_type # This will trigger the setter and validate the scale_type - self.scale_settings = scale_settings - - @property - def unit(self): - """Get unit used in the plot.""" - return EnumUnits(self._unit).name - - @unit.setter - def unit(self, v): - """Set unit used in the plot. - - Parameters - ---------- - v: str - Unit to be set. - """ - if v is not None: - try: - self._unit = EnumUnits[v].value - except KeyError: - raise KeyError(f"{v} is not a valid unit.") - - @property - def scale_type(self): - """Get type of scale used for the field plot.""" - return self._scale_type - - @scale_type.setter - def scale_type(self, value): - """Set the scale type used for the field plot. - - Parameters: - ----------- - value (str): The type of scaling to set. - Must be one of the accepted values ("Auto", "MinMax" or "Specified"). - - Raises: - ------- - ValueError: If the provided value is not in the list of accepted values. - """ - if value is not None and value not in self._accepted: - raise ValueError(f"{value} is not valid. Accepted values are {', '.join(self._accepted)}.") - self._scale_type = value - # Automatically adjust scale_settings based on scale_type - if value == "Auto": - self._scale_settings = self._auto_scale - elif value == "MinMax": - self._scale_settings = self._minmax_scale - elif value == "Specified": - self._scale_settings = self._specified_scale - - @property - def scale_settings(self): - """Get the current scale settings based on the scale type.""" - self.scale_type = self.scale_type # update correct scale settings - return self._scale_settings - - @scale_settings.setter - def scale_settings(self, value): - """Set the current scale settings based on the scale type.""" - if self.scale_type == "Auto": - if isinstance(value, AutoScale): - self._scale_settings = value - return - elif self.scale_type == "MinMax": - if isinstance(value, MinMaxScale): - self._scale_settings = value - return - elif self.scale_type == "Specified": - if isinstance(value, SpecifiedScale): - self._scale_settings = value - return - raise ValueError("Invalid scale settings for current scale type.") - - def __repr__(self): - return ( - f"Scale3DSettings(scale_type='{self.scale_type}', scale_settings={self.scale_settings}, " - f"log={self.log}, db={self.db})" - ) - - def to_dict(self): - """Convert the scale settings to a dictionary. - - Returns - ------- - dict - A dictionary containing all scale settings - for the folder field plot settings. - """ - arg_out = { - "Scale3DSettings": { - "unit": self._unit, - "ScaleType": self._accepted.index(self.scale_type), - "log": self.log, - "dB": self.db, - } - } - arg_out["Scale3DSettings"].update(self.number_format.to_dict()) - arg_out["Scale3DSettings"].update(self.scale_settings.to_dict()) - return arg_out - - def from_dict(self, dictionary): - """Initialize the scale settings of the field plot settings from a dictionary. - - Parameters - ---------- - dictionary : dict - Dictionary containing the configuration for scale settings. - Dictionary syntax must be the same of relevant portion of the AEDT file. - """ - self._scale_type = self._accepted[dictionary["ScaleType"]] - self.number_format = NumberFormat() - self.number_format.from_dict(dictionary) - self.log = dictionary["log"] - self.db = dictionary["dB"] - self.unit = EnumUnits(int(dictionary["unit"])).name - self._auto_scale = AutoScale() - self._auto_scale.from_dict(dictionary) - self._minmax_scale = MinMaxScale() - self._minmax_scale.from_dict(dictionary) - self._specified_scale = SpecifiedScale() - self._specified_scale.from_dict(dictionary) - - -class MarkerSettings(BaseFolderPlot): - """Provides methods and variables for editing marker folder settings. - - Parameters - ---------- - marker_type : str, optional - The type of maker to use. Must be one of the allowed types - (`"Octahedron"`, `"Tetrahedron"`, `"Sphere"`, `"Box"`, `"Arrow"`). - Default is `"Box"`. - marker_size : float, optional - Size of the marker. Default is `0.005`. - map_size : bool, optional - Whether to map the field magnitude to the arrow type. Default is `False`. - map_color : bool, optional - Whether to map the field magnitude to the arrow color. Default is `True`. - """ - - def __init__(self, marker_type="Box", map_size=False, map_color=True, marker_size=0.005): - self._marker_type = None - self.marker_type = marker_type - self.map_size = map_size - self.map_color = map_color - self.marker_size = marker_size - - @property - def marker_type(self): - """Get the type of maker to use.""" - return AllowedMarkers(self._marker_type).name - - @marker_type.setter - def marker_type(self, v): - """Set the type of maker to use. - - Parameters: - ---------- - v : str - Marker type. Must be one of the allowed types - (`"Octahedron"`, `"Tetrahedron"`, `"Sphere"`, `"Box"`, `"Arrow"`). - """ - try: - self._marker_type = AllowedMarkers[v].value - except KeyError: - raise KeyError(f"{v} is not a valid marker type.") - - def __repr__(self): - return ( - f"MarkerSettings(marker_type='{self.marker_type}', map_size={self.map_size}, " - f"map_color={self.map_color}, marker_size={self.marker_size})" - ) - - def to_dict(self): - """Convert the marker settings to a dictionary. - - Returns - ------- - dict - A dictionary containing all the marker settings - for the folder field plot settings. - """ - return { - "Marker3DSettings": { - "MarkerType": self._marker_type, - "MarkerMapSize": self.map_size, - "MarkerMapColor": self.map_color, - "MarkerSize": self.marker_size, - } - } - - def from_dict(self, dictionary): - """Initialize the marker settings of the field plot settings from a dictionary. - - Parameters - ---------- - dictionary : dict - Dictionary containing the configuration for marker settings. - Dictionary syntax must be the same of relevant portion of the AEDT file. - """ - self.marker_type = AllowedMarkers(int(dictionary["MarkerType"])).name - self.map_size = dictionary["MarkerMapSize"] - self.map_color = dictionary["MarkerMapColor"] - self.marker_size = dictionary["MarkerSize"] - - -class ArrowSettings(BaseFolderPlot): - """Provides methods and variables for editing arrow folder settings. - - Parameters - ---------- - arrow_type : str, optional - The type of arrows to use. Must be one of the allowed types - (`"Line"`, `"Cylinder"`, `"Umbrella"`). Default is `"Line"`. - arrow_size : float, optional - Size of the arrow. Default is `0.005`. - map_size : bool, optional - Whether to map the field magnitude to the arrow type. Default is `False`. - map_color : bool, optional - Whether to map the field magnitude to the arrow color. Default is `True`. - show_arrow_tail : bool, optional - Whether to show the arrow tail. Default is `False`. - magnitude_filtering : bool, optional - Whether to filter the field magnitude for plotting vectors. Default is `False`. - magnitude_threshold : bool, optional - Threshold value for plotting vectors. Default is `0`. - min_magnitude : bool, optional - Minimum value for plotting vectors. Default is `0`. - max_magnitude : bool, optional - Maximum value for plotting vectors. Default is `0.5`. - """ - - def __init__( - self, - arrow_type="Line", - arrow_size=0.005, - map_size=False, - map_color=True, - show_arrow_tail=False, - magnitude_filtering=False, - magnitude_threshold=0, - min_magnitude=0, - max_magnitude=0.5, - ): - self._arrow_type = None - self._allowed_arrow_types = ["Line", "Cylinder", "Umbrella"] - self.arrow_type = arrow_type - self.arrow_size = arrow_size - self.map_size = map_size - self.map_color = map_color - self.show_arrow_tail = show_arrow_tail - self.magnitude_filtering = magnitude_filtering - self.magnitude_threshold = magnitude_threshold - self.min_magnitude = min_magnitude - self.max_magnitude = max_magnitude - - @property - def arrow_type(self): - """Get the type of arrows used in the field plot.""" - return self._arrow_type - - @arrow_type.setter - def arrow_type(self, v): - """Set the type of arrows for the field plot. - - Parameters: - ----------- - v (str): The type of arrows to use. Must be one of the allowed types ("Line", "Cylinder", "Umbrella"). - - Raises: - ------- - ValueError: If the provided value is not in the list of allowed arrow types. - """ - if v in self._allowed_arrow_types: - self._arrow_type = v - else: - raise ValueError(f"{v} is not valid. Accepted values are {','.join(self._allowed_arrow_types)}.") - - def __repr__(self): - return ( - f"Arrow3DSettings(arrow_type='{self.arrow_type}', arrow_size={self.arrow_size}, " - f"map_size={self.map_size}, map_color={self.map_color}, " - f"show_arrow_tail={self.show_arrow_tail}, magnitude_filtering={self.magnitude_filtering}, " - f"magnitude_threshold={self.magnitude_threshold}, min_magnitude={self.min_magnitude}, " - f"max_magnitude={self.max_magnitude})" - ) - - def to_dict(self): - """Convert the arrow settings to a dictionary. - - Returns - ------- - dict - A dictionary containing all the arrow settings - for the folder field plot settings. - """ - return { - "Arrow3DSettings": { - "ArrowType": self._allowed_arrow_types.index(self.arrow_type), - "ArrowMapSize": self.map_size, - "ArrowMapColor": self.map_color, # Missing option in ui - "ShowArrowTail": self.show_arrow_tail, - "ArrowSize": self.arrow_size, - "ArrowMinMagnitude": self.min_magnitude, - "ArrowMaxMagnitude": self.max_magnitude, - "ArrowMagnitudeThreshold": self.magnitude_threshold, - "ArrowMagnitudeFilteringFlag": self.magnitude_filtering, - } - } - - def from_dict(self, dictionary): - """Initialize the arrow settings of the field plot settings from a dictionary. - - Parameters - ---------- - dictionary : dict - Dictionary containing the configuration for arrow settings. - Dictionary syntax must be the same of relevant portion of the AEDT file. - """ - self.arrow_type = self._allowed_arrow_types[dictionary["ArrowType"]] - self.arrow_size = dictionary["ArrowType"] - self.map_size = dictionary["ArrowMapSize"] - self.map_color = dictionary["ArrowMapColor"] - self.show_arrow_tail = dictionary["ShowArrowTail"] - self.magnitude_filtering = dictionary["ArrowMagnitudeFilteringFlag"] - self.magnitude_threshold = dictionary["ArrowMagnitudeThreshold"] - self.min_magnitude = dictionary["ArrowMinMagnitude"] - self.max_magnitude = dictionary["ArrowMaxMagnitude"] - - -class FolderPlotSettings(BaseFolderPlot): - """Provides methods and variables for editing field plots folder settings. - - Parameters - ---------- - postprocessor : :class:`ansys.aedt.core.modules.post_processor.PostProcessor` - folder_name : str - Name of the plot field folder. - arrow_settings : :class:`ansys.aedt.core.modules.solution.ArrowSettings`, optional - Arrow settings. Default is `None`. - marker_settings : :class:`ansys.aedt.core.modules.solution.MarkerSettings`, optional - Marker settings. Default is `None`. - scale_settings : :class:`ansys.aedt.core.modules.solution.Scale3DSettings`, optional - Scale settings. Default is `None`. - color_map_settings : :class:`ansys.aedt.core.modules.solution.ColorMapSettings`, optional - Colormap settings. Default is `None`. - """ - - def __init__( - self, - postprocessor, - folder_name, - arrow_settings=None, - marker_settings=None, - scale_settings=None, - color_map_settings=None, - ): - self.arrow_settings = arrow_settings - self.marker_settings = marker_settings - self.scale_settings = scale_settings - self.color_map_settings = color_map_settings - self._postprocessor = postprocessor - self._folder_name = folder_name - - def update(self): - """ - Update folder plot settings. - """ - out = [] - _dict2arg(self.to_dict(), out) - self._postprocessor.ofieldsreporter.SetPlotFolderSettings(self._folder_name, out[0]) - - def to_dict(self): - """Convert the field plot settings to a dictionary. - - Returns - ------- - dict - A dictionary containing all the settings for the field plot, - including arrow settings, marker settings, - scale settings, and color map settings. - """ - out = {} - out.update(self.arrow_settings.to_dict()) - out.update(self.marker_settings.to_dict()) - out.update(self.scale_settings.to_dict()) - out.update(self.color_map_settings.to_dict()) - return {"FieldsPlotSettings": out} - - def from_dict(self, dictionary): - """Initialize the field plot settings from a dictionary. - - Parameters - ---------- - dictionary : dict - Dictionary containing the configuration for the color map, - scale, arrow, and marker settings. Dictionary syntax must - be the same of the AEDT file. - """ - cmap = ColorMapSettings() - cmap.from_dict(dictionary["ColorMapSettings"]) - self.color_map_settings = cmap - scale = Scale3DSettings() - scale.from_dict(dictionary["Scale3DSettings"]) - self.scale_settings = scale - arrow = ArrowSettings() - arrow.from_dict(dictionary["Arrow3DSettings"]) - marker = MarkerSettings() - marker.from_dict(dictionary["Marker3DSettings"]) - self.arrow_settings = arrow - self.marker_settings = marker - - -class FieldPlot: - """Provides for creating and editing field plots. - - Parameters - ---------- - postprocessor : :class:`ansys.aedt.core.modules.post_processor.PostProcessor` - objects : list - List of objects. - solution : str - Name of the solution. - quantity : str - Name of the plot or the name of the object. - intrinsics : dict, optional - Name of the intrinsic dictionary. The default is ``{}``. - - """ - - @pyaedt_function_handler( - objlist="objects", - surfacelist="surfaces", - linelist="lines", - cutplanelist="cutplanes", - solutionName="solution", - quantityName="quantity", - IntrinsincList="intrinsics", - seedingFaces="seeding_faces", - layers_nets="layer_nets", - layers_plot_type="layer_plot_type", - ) - def __init__( - self, - postprocessor, - objects=None, - surfaces=None, - lines=None, - cutplanes=None, - solution="", - quantity="", - intrinsics=None, - seeding_faces=None, - layer_nets=None, - layer_plot_type="LayerNetsExtFace", - ): - self._postprocessor = postprocessor - self.oField = postprocessor.ofieldsreporter - self.volumes = [] if objects is None else objects - self.surfaces = [] if surfaces is None else surfaces - self.lines = [] if lines is None else lines - self.cutplanes = [] if cutplanes is None else cutplanes - self.layer_nets = [] if layer_nets is None else layer_nets - self.layer_plot_type = layer_plot_type - self.seeding_faces = [] if seeding_faces is None else seeding_faces - self.solution = solution - self.quantity = quantity - self.intrinsics = {} if intrinsics is None else intrinsics - self.name = "Field_Plot" - self.plot_folder = "Field_Plot" - self.Filled = False - self.IsoVal = "Fringe" - self.SmoothShade = True - self.AddGrid = False - self.MapTransparency = True - self.Refinement = 0 - self.Transparency = 0 - self.SmoothingLevel = 0 - self.ArrowUniform = True - self.ArrowSpacing = 0 - self.MinArrowSpacing = 0 - self.MaxArrowSpacing = 0 - self.GridColor = [255, 255, 255] - self.PlotIsoSurface = True - self.PointSize = 1 - self.CloudSpacing = 0.5 - self.CloudMinSpacing = -1 - self.CloudMaxSpacing = -1 - self.LineWidth = 4 - self.LineStyle = "Cylinder" - self.IsoValType = "Tone" - self.NumofPoints = 100 - self.TraceStepLength = "0.001mm" - self.UseAdaptiveStep = True - self.SeedingSamplingOption = True - self.SeedingPointsNumber = 15 - self.FractionOfMaximum = 0.8 - self._filter_boxes = [] - self.field_type = None - self._folder_settings = None - - def _parse_folder_settings(self): - """Parse the folder settings for the field plot from the AEDT file. - - Returns: - FolderPlotSettings or None: An instance of FolderPlotSettings if found, otherwise None. - """ - folder_settings_data = load_keyword_in_aedt_file( - self._postprocessor._app.project_file, - "FieldsPlotManagerID", - design_name=self._postprocessor._app.design_name, - ) - relevant_settings = [ - d - for d in folder_settings_data["FieldsPlotManagerID"].values() - if isinstance(d, dict) and d.get("PlotFolder", False) and d["PlotFolder"] == self.plot_folder - ] - - if not relevant_settings: - self._postprocessor._app.logger.error( - "Could not find settings data in the design properties." - " Define the `FolderPlotSettings` class from scratch or save the project file and try again." - ) - return None - else: - fps = FolderPlotSettings(self._postprocessor, self.plot_folder) - fps.from_dict(relevant_settings[0]) - return fps - - @property - def folder_settings(self): - """Get the folder settings.""" - if self._folder_settings is None: - self._folder_settings = self._parse_folder_settings() - return self._folder_settings - - @folder_settings.setter - def folder_settings(self, v): - """Set the fieldplot folder settings. - - Parameters - ---------- - v : FolderPlotSettings - The new folder plot settings to be set. - - Raises - ------ - ValueError - If the provided value is not an instance of `FolderPlotSettings`. - """ - if isinstance(v, FolderPlotSettings): - self._folder_settings = v - else: - raise ValueError("Invalid type for `folder_settings`, use `FolderPlotSettings` class.") - - @property - def filter_boxes(self): - """Volumes on which filter the plot.""" - return self._filter_boxes - - @filter_boxes.setter - def filter_boxes(self, val): - if isinstance(val, str): - val = [val] - self._filter_boxes = val - - @property - def plotGeomInfo(self): - """Plot geometry information.""" - idx = 0 - if self.volumes: - idx += 1 - if self.surfaces: - idx += 1 - if self.cutplanes: - idx += 1 - if self.lines: - idx += 1 - if self.layer_nets: - idx += 1 - - info = [idx] - if self.volumes: - info.append("Volume") - info.append("ObjList") - info.append(len(self.volumes)) - for index in self.volumes: - info.append(str(index)) - if self.surfaces: - model_faces = [] - nonmodel_faces = [] - if self._postprocessor._app.design_type == "HFSS 3D Layout Design": - model_faces = [str(i) for i in self.surfaces] - else: - models = self._postprocessor.modeler.model_objects - for index in self.surfaces: - try: - if isinstance(index, FacePrimitive): - index = index.id - oname = self._postprocessor.modeler.oeditor.GetObjectNameByFaceID(index) - if oname in models: - model_faces.append(str(index)) - else: - nonmodel_faces.append(str(index)) - except Exception: - self._postprocessor.logger.debug( - "Something went wrong while processing surface {}.".format(index) - ) - info.append("Surface") - if model_faces: - info.append("FacesList") - info.append(len(model_faces)) - for index in model_faces: - info.append(index) - if nonmodel_faces: - info.append("NonModelFaceList") - info.append(len(nonmodel_faces)) - for index in nonmodel_faces: - info.append(index) - if self.cutplanes: - info.append("Surface") - info.append("CutPlane") - info.append(len(self.cutplanes)) - for index in self.cutplanes: - info.append(str(index)) - if self.lines: - info.append("Line") - info.append(len(self.lines)) - for index in self.lines: - info.append(str(index)) - if self.layer_nets: - if self.layer_plot_type == "LayerNets": - info.append("Volume") - info.append("LayerNets") - else: - info.append("Surface") - info.append("LayerNetsExtFace") - info.append(len(self.layer_nets)) - for index in self.layer_nets: - info.append(index[0]) - info.append(len(index[1:])) - info.extend(index[1:]) - return info - - @property - def intrinsicVar(self): - """Intrinsic variable. - - Returns - ------- - list or dict - Variables for the field plot. - """ - var = "" - for a in self.intrinsics: - var += a + "='" + str(self.intrinsics[a]) + "' " - return var - - @property - def plotsettings(self): - """Plot settings. - - Returns - ------- - list - List of plot settings. - """ - if self.surfaces or self.cutplanes or (self.layer_nets and self.layer_plot_type == "LayerNetsExtFace"): - arg = [ - "NAME:PlotOnSurfaceSettings", - "Filled:=", - self.Filled, - "IsoValType:=", - self.IsoVal, - "SmoothShade:=", - self.SmoothShade, - "AddGrid:=", - self.AddGrid, - "MapTransparency:=", - self.MapTransparency, - "Refinement:=", - self.Refinement, - "Transparency:=", - self.Transparency, - "SmoothingLevel:=", - self.SmoothingLevel, - [ - "NAME:Arrow3DSpacingSettings", - "ArrowUniform:=", - self.ArrowUniform, - "ArrowSpacing:=", - self.ArrowSpacing, - "MinArrowSpacing:=", - self.MinArrowSpacing, - "MaxArrowSpacing:=", - self.MaxArrowSpacing, - ], - "GridColor:=", - self.GridColor, - ] - elif self.lines: - arg = [ - "NAME:PlotOnLineSettings", - ["NAME:LineSettingsID", "Width:=", self.LineWidth, "Style:=", self.LineStyle], - "IsoValType:=", - self.IsoValType, - "ArrowUniform:=", - self.ArrowUniform, - "NumofArrow:=", - self.NumofPoints, - "Refinement:=", - self.Refinement, - ] - else: - arg = [ - "NAME:PlotOnVolumeSettings", - "PlotIsoSurface:=", - self.PlotIsoSurface, - "PointSize:=", - self.PointSize, - "Refinement:=", - self.Refinement, - "CloudSpacing:=", - self.CloudSpacing, - "CloudMinSpacing:=", - self.CloudMinSpacing, - "CloudMaxSpacing:=", - self.CloudMaxSpacing, - [ - "NAME:Arrow3DSpacingSettings", - "ArrowUniform:=", - self.ArrowUniform, - "ArrowSpacing:=", - self.ArrowSpacing, - "MinArrowSpacing:=", - self.MinArrowSpacing, - "MaxArrowSpacing:=", - self.MaxArrowSpacing, - ], - ] - return arg - - @pyaedt_function_handler() - def get_points_value(self, points, filename=None, visibility=False): # pragma: no cover - """ - Get points data from field plot. - - .. note:: - This method is working only if the associated field plot is currently visible. - - .. note:: - This method does not work in non-graphical mode. - - Parameters - ---------- - points : list, list of lists or dict - List with [x,y,z] coordinates of a point or list of lists of points or - dictionary with keys containing point names and for each key the point - coordinates [x,y,z]. - filename : str, optional - Full path or relative path with filename. - Default is ``None`` in which case no file is exported. - visibility : bool, optional - Whether to keep the markers visible in the UI. Default is ``False``. - - Returns - ------- - dict or pd.DataFrame - Dict containing 5 keys: point names, x,y,z coordinates and the quantity probed. - Each key is associated with a list with the same length of the argument points. - If pandas is installed, the output is a pandas DataFrame with point names as - index and coordinates and quantity as columns. - """ - self.oField.ClearAllMarkers() - - # Clean inputs - if isinstance(points, dict): - points_name, points_value = list(points.keys()), list(points.values()) - elif isinstance(points, list): - points_name = None - if not isinstance(points[0], list): - points_value = [points] - else: - points_value = points - else: - raise AttributeError("``points`` argument is invalid.") - if filename is not None: - if not os.path.isdir(os.path.dirname(filename)): - raise AttributeError("Specified path ({}) does not exist".format(filename)) - - # Create markers - u = self._postprocessor._app.modeler.model_units - added_points_name = [] - for pt_name_idx, pt in enumerate(points_value): - try: - pt = [c if isinstance(c, str) else "{}{}".format(c, u) for c in pt] - self.oField.AddMarkerToPlot(pt, self.name) - if points_name is not None: - added_points_name.append(points_name[pt_name_idx]) - except (GrpcApiError, SystemExit) as e: # pragma: no cover - self._postprocessor.logger.error( - "Point {} not added. Check if it lies inside the plot.".format(str(pt)) - ) - raise e - - # Export data - temp_file = tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".csv") - temp_file.close() - self.oField.ExportMarkerTable(temp_file.name) - with open_file(temp_file.name, "r") as f: - reader = csv.DictReader(f) - out_dict = defaultdict(list) - for row in reader: - for key in row.keys(): - if key == "Name": - val = row[key] - else: - val = float(row[key].lstrip()) - out_dict[key.lstrip()].append(val) - - # Modify data if needed - if points_name is not None: - out_dict["Name"] = added_points_name - # Export data - if filename is not None: - with open(filename, mode="w") as outfile: - writer = csv.DictWriter(outfile, fieldnames=out_dict.keys()) - writer.writeheader() - for i in range(len(out_dict["Name"])): - row = {field: out_dict[field][i] for field in out_dict} - writer.writerow(row) - elif filename is not None: - # Export data - shutil.copy2(temp_file.name, filename) - os.remove(temp_file.name) - - if not visibility: - self.oField.ClearAllMarkers() - - # Convert to pandas - if pd is not None: - df = pd.DataFrame(out_dict, columns=out_dict.keys()) - df = df.set_index("Name") - return df - else: - return out_dict - - @property - def surfacePlotInstruction(self): - """Surface plot settings. - - Returns - ------- - list - List of surface plot settings. - """ - out = [ - "NAME:" + self.name, - "SolutionName:=", - self.solution, - "QuantityName:=", - self.quantity, - "PlotFolder:=", - self.plot_folder, - ] - if self.field_type: - out.extend(["FieldType:=", self.field_type]) - out.extend( - [ - "UserSpecifyName:=", - 1, - "UserSpecifyFolder:=", - 1, - "StreamlinePlot:=", - False, - "AdjacentSidePlot:=", - False, - "FullModelPlot:=", - False, - "IntrinsicVar:=", - self.intrinsicVar, - "PlotGeomInfo:=", - self.plotGeomInfo, - "FilterBoxes:=", - [len(self.filter_boxes)] + self.filter_boxes, - self.plotsettings, - "EnableGaussianSmoothing:=", - False, - "SurfaceOnly:=", - True if self.surfaces or self.cutplanes else False, - ] - ) - return out - - @property - def surfacePlotInstructionLineTraces(self): - """Surface plot settings for field line traces. - - ..note:: - ``Specify seeding points on selections`` is by default set to ``by sampling``. - - Returns - ------- - list - List of plot settings for line traces. - """ - out = [ - "NAME:" + self.name, - "SolutionName:=", - self.solution, - "UserSpecifyName:=", - 0, - "UserSpecifyFolder:=", - 0, - "QuantityName:=", - "QuantityName_FieldLineTrace", - "PlotFolder:=", - self.plot_folder, - ] - if self.field_type: - out.extend(["FieldType:=", self.field_type]) - out.extend( - [ - "IntrinsicVar:=", - self.intrinsicVar, - "Trace Step Length:=", - self.TraceStepLength, - "Use Adaptive Step:=", - self.UseAdaptiveStep, - "Seeding Faces:=", - self.seeding_faces, - "Seeding Markers:=", - [0], - "Surface Tracing Objects:=", - self.surfaces, - "Volume Tracing Objects:=", - self.volumes, - "Seeding Sampling Option:=", - self.SeedingSamplingOption, - "Seeding Points Number:=", - self.SeedingPointsNumber, - "Fractional of Maximal:=", - self.FractionOfMaximum, - "Discrete Seeds Option:=", - "Marker Point", - [ - "NAME:InceptionEvaluationSettings", - "Gas Type:=", - 0, - "Gas Pressure:=", - 1, - "Use Inception:=", - True, - "Potential U0:=", - 0, - "Potential K:=", - 0, - "Potential A:=", - 1, - ], - self.field_line_trace_plot_settings, - ] - ) - return out - - @property - def field_plot_settings(self): - """Field Plot Settings. - - Returns - ------- - list - Field Plot Settings. - """ - return [ - "NAME:FieldsPlotItemSettings", - [ - "NAME:PlotOnSurfaceSettings", - "Filled:=", - self.Filled, - "IsoValType:=", - self.IsoVal, - "AddGrid:=", - self.AddGrid, - "MapTransparency:=", - self.MapTransparency, - "Refinement:=", - self.Refinement, - "Transparency:=", - self.Transparency, - "SmoothingLevel:=", - self.SmoothingLevel, - "ShadingType:=", - self.SmoothShade, - [ - "NAME:Arrow3DSpacingSettings", - "ArrowUniform:=", - self.ArrowUniform, - "ArrowSpacing:=", - self.ArrowSpacing, - "MinArrowSpacing:=", - self.MinArrowSpacing, - "MaxArrowSpacing:=", - self.MaxArrowSpacing, - ], - "GridColor:=", - self.GridColor, - ], - ] - - @property - def field_line_trace_plot_settings(self): - """Settings for the field line traces in the plot. - - Returns - ------- - list - List of settings for the field line traces in the plot. - """ - return [ - "NAME:FieldLineTracePlotSettings", - ["NAME:LineSettingsID", "Width:=", self.LineWidth, "Style:=", self.LineStyle], - "IsoValType:=", - self.IsoValType, - ] - - @pyaedt_function_handler() - def create(self): - """Create a field plot. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - try: - if self.seeding_faces: - self.oField.CreateFieldPlot(self.surfacePlotInstructionLineTraces, "FieldLineTrace") - else: - self.oField.CreateFieldPlot(self.surfacePlotInstruction, "Field") - if ( - "Maxwell" in self._postprocessor._app.design_type - and "Transient" in self._postprocessor.post_solution_type - ): - self._postprocessor.ofieldsreporter.SetPlotsViewSolutionContext( - [self.name], self.solution, "Time:" + self.intrinsics["Time"] - ) - self._postprocessor.field_plots[self.name] = self - return True - except Exception: - return False - - @pyaedt_function_handler() - def update(self): - """Update the field plot. - - .. note:: - This method works on any plot created inside PyAEDT. - For Plot already existing in AEDT Design it may produce incorrect results. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - try: - if self.seeding_faces: - if self.seeding_faces[0] != len(self.seeding_faces) - 1: - for face in self.seeding_faces[1:]: - if not isinstance(face, int): - self._postprocessor.logger.error("Provide valid object id for seeding faces.") - return False - else: - if face not in list(self._postprocessor._app.modeler.objects.keys()): - self._postprocessor.logger.error("Invalid object id.") - self.seeding_faces.remove(face) - return False - self.seeding_faces[0] = len(self.seeding_faces) - 1 - if self.volumes[0] != len(self.volumes) - 1: - for obj in self.volumes[1:]: - if not isinstance(obj, int): - self._postprocessor.logger.error("Provide valid object id for in-volume object.") - return False - else: - if obj not in list(self._postprocessor._app.modeler.objects.keys()): - self._postprocessor.logger.error("Invalid object id.") - self.volumes.remove(obj) - return False - self.volumes[0] = len(self.volumes) - 1 - if self.surfaces[0] != len(self.surfaces) - 1: - for obj in self.surfaces[1:]: - if not isinstance(obj, int): - self._postprocessor.logger.error("Provide valid object id for surface object.") - return False - else: - if obj not in list(self._postprocessor._app.modeler.objects.keys()): - self._postprocessor.logger.error("Invalid object id.") - self.surfaces.remove(obj) - return False - self.surfaces[0] = len(self.surfaces) - 1 - self.oField.ModifyFieldPlot(self.name, self.surfacePlotInstructionLineTraces) - else: - self.oField.ModifyFieldPlot(self.name, self.surfacePlotInstruction) - return True - except Exception: - return False - - @pyaedt_function_handler() - def update_field_plot_settings(self): - """Modify the field plot settings. - - .. note:: - This method is not available for field plot line traces. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - self.oField.SetFieldPlotSettings(self.name, ["NAME:FieldsPlotItemSettings", self.plotsettings]) - return True - - @pyaedt_function_handler() - def delete(self): - """Delete the field plot.""" - self.oField.DeleteFieldPlot([self.name]) - self._postprocessor.field_plots.pop(self.name, None) - - @pyaedt_function_handler() - def change_plot_scale(self, minimum_value, maximum_value, is_log=False, is_db=False, scale_levels=None): - """Change Field Plot Scale. - - .. deprecated:: 0.10.1 - Use :class:`FieldPlot.folder_settings` methods instead. - - Parameters - ---------- - minimum_value : str, float - Minimum value of the scale. - maximum_value : str, float - Maximum value of the scale. - is_log : bool, optional - Set to ``True`` if Log Scale is setup. - is_db : bool, optional - Set to ``True`` if dB Scale is setup. - scale_levels : int, optional - Set number of color levels. The default is ``None``, in which case the - setting is not changed. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - - References - ---------- - >>> oModule.SetPlotFolderSettings - """ - return self._postprocessor.change_field_plot_scale( - self.plot_folder, minimum_value, maximum_value, is_log, is_db, scale_levels - ) - - @pyaedt_function_handler() - def export_image( - self, - full_path=None, - width=1920, - height=1080, - orientation="isometric", - display_wireframe=True, - selections=None, - show_region=True, - show_axis=True, - show_grid=True, - show_ruler=True, - ): - """Export the active plot to an image file. - - .. note:: - There are some limitations on HFSS 3D Layout plots. - - 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 - Plot height. - orientation : str, optional - View of the exported plot. Options are ``isometric``, - ``top``, ``bottom``, ``right``, ``left``, ``front``, - ``back``, and any custom orientation. - display_wireframe : bool, optional - Whether the objects has to be put in wireframe mode. Default is ``True``. - selections : str or List[str], optional - Objects to fit for the zoom on the exported image. - Default is None in which case all the objects in the design will be shown. - One important note is that, if the fieldplot extension is larger than the - selection extension, the fieldplot extension will be the one considered - for the zoom of the exported image. - show_region : bool, optional - Whether to include the air region in the exported image. Default is ``True``. - show_grid : bool, optional - Whether to display the background grid in the exported image. - Default is ``True``. - show_axis : bool, optional - Whether to display the axis triad in the exported image. Default is ``True``. - show_ruler : bool, optional - Whether to display the ruler in the exported image. Default is ``True``. - - Returns - ------- - str - Full path to exported file if successful. - - References - ---------- - >>> oModule.ExportPlotImageToFile - >>> oModule.ExportModelImageToFile - >>> oModule.ExportPlotImageWithViewToFile - """ - self.oField.UpdateQuantityFieldsPlots(self.plot_folder) - if not full_path: - full_path = os.path.join(self._postprocessor._app.working_directory, self.name + ".png") - status = self._postprocessor.export_field_jpg( - full_path, - self.name, - self.plot_folder, - orientation=orientation, - width=width, - height=height, - display_wireframe=display_wireframe, - selections=selections, - show_region=show_region, - show_axis=show_axis, - show_grid=show_grid, - show_ruler=show_ruler, - ) - full_path = check_and_download_file(full_path) - if status: - return full_path - else: - return False - - @pyaedt_function_handler() - def export_image_from_aedtplt( - self, export_path=None, view="isometric", plot_mesh=False, scale_min=None, scale_max=None - ): - """Save an image of the active plot using PyVista. - - .. note:: - This method only works if the CPython with PyVista module is installed. - - Parameters - ---------- - export_path : str, optional - Path where image will be saved. - The default is ``None`` which export file in working_directory. - view : str, optional - View to export. Options are ``"isometric"``, ``"xy"``, ``"xz"``, ``"yz"``. - plot_mesh : bool, optional - Plot mesh. - scale_min : float, optional - Scale output min. - scale_max : float, optional - Scale output max. - - Returns - ------- - str - Full path to exported file if successful. - - References - ---------- - >>> oModule.UpdateAllFieldsPlots - >>> oModule.UpdateQuantityFieldsPlots - >>> oModule.ExportFieldPlot - """ - if not export_path: - export_path = self._postprocessor._app.working_directory - if sys.version_info.major > 2: - return self._postprocessor.plot_field_from_fieldplot( - self.name, - project_path=export_path, - meshplot=plot_mesh, - imageformat="jpg", - view=view, - plot_label=self.quantity, - show=False, - scale_min=scale_min, - scale_max=scale_max, - ) - else: - self._postprocessor.logger.info("This method works only on CPython with PyVista") - return False - - -class VRTFieldPlot: - """Creates and edits VRT field plots for SBR+ and Creeping Waves. - - Parameters - ---------- - postprocessor : :class:`ansys.aedt.core.modules.post_processor.PostProcessor` - is_creeping_wave : bool - Whether it is a creeping wave model or not. - quantity : str, optional - Name of the plot or the name of the object. - max_frequency : str, optional - Maximum Frequency. The default is ``"1GHz"``. - ray_density : int, optional - Ray Density. The default is ``2``. - bounces : int, optional - Maximum number of bounces. The default is ``5``. - intrinsics : dict, optional - Name of the intrinsic dictionary. The default is ``{}``. - - """ - - @pyaedt_function_handler(quantity_name="quantity") - def __init__( - self, - postprocessor, - is_creeping_wave=False, - quantity="QuantityName_SBR", - max_frequency="1GHz", - ray_density=2, - bounces=5, - intrinsics=None, - ): - self.is_creeping_wave = is_creeping_wave - self._postprocessor = postprocessor - self._ofield = postprocessor.ofieldsreporter - self.quantity = quantity - self.intrinsics = {} if intrinsics is None else intrinsics - self.name = "Field_Plot" - self.plot_folder = "Field_Plot" - self.max_frequency = max_frequency - self.ray_density = ray_density - self.number_of_bounces = bounces - self.multi_bounce_ray_density_control = False - self.mbrd_max_subdivision = 2 - self.shoot_utd_rays = False - self.shoot_type = "All Rays" - self.start_index = 0 - self.stop_index = 1 - self.step_index = 1 - self.is_plane_wave = True - self.incident_theta = "0deg" - self.incident_phi = "0deg" - self.vertical_polarization = False - self.custom_location = [0, 0, 0] - self.ray_box = None - self.ray_elevation = "0deg" - self.ray_azimuth = "0deg" - self.custom_coordinatesystem = 1 - self.ray_cutoff = 40 - self.sample_density = 10 - self.irregular_surface_tolerance = 50 - - @property - def intrinsicVar(self): - """Intrinsic variable. - - Returns - ------- - str - Variables for the field plot. - """ - var = "" - for a in self.intrinsics: - var += a + "='" + str(self.intrinsics[a]) + "' " - return var - - @pyaedt_function_handler() - def _create_args(self): - args = [ - "NAME:" + self.name, - "UserSpecifyName:=", - 0, - "UserSpecifyFolder:=", - 0, - "QuantityName:=", - self.quantity, - "PlotFolder:=", - "Visual Ray Trace SBR", - "IntrinsicVar:=", - self.intrinsicVar, - "MaxFrequency:=", - self.max_frequency, - "RayDensity:=", - self.ray_density, - "NumberBounces:=", - self.number_of_bounces, - "Multi-Bounce Ray Density Control:=", - self.multi_bounce_ray_density_control, - "MBRD Max sub divisions:=", - self.mbrd_max_subdivision, - "Shoot UTD Rays:=", - self.shoot_utd_rays, - "ShootFilterType:=", - self.shoot_type, - ] - if self.shoot_type == "Rays by index": - args.extend( - [ - "start index:=", - self.start_index, - "stop index:=", - self.stop_index, - "index step:=", - self.step_index, - ] - ) - elif self.shoot_type == "Rays in box": - box_id = None - if isinstance(self.ray_box, int): - box_id = self.ray_box - elif isinstance(self.ray_box, str): - box_id = self._postprocessor._primitives.objects[self.ray_box].id - else: - box_id = self.ray_box.id - args.extend("FilterBoxID:=", box_id) - elif self.shoot_type == "Single ray": - args.extend("Ray elevation:=", self.ray_elevation, "Ray azimuth:=", self.ray_azimuth) - args.append("LaunchFrom:=") - if self.is_plane_wave: - args.append("Launch from Plane-Wave") - args.append("Incident direction theta:=") - args.append(self.incident_theta) - args.append("Incident direction phi:=") - args.append(self.incident_phi) - args.append("Vertical Incident Polarization:=") - args.append(self.vertical_polarization) - else: - args.append("Launch from Custom") - args.append("LaunchFromPointID:=") - args.append(-1) - args.append("CustomLocationCoordSystem:=") - args.append(self.custom_coordinatesystem) - args.append("CustomLocation:=") - args.append(self.custom_location) - return args - - @pyaedt_function_handler() - def _create_args_creeping(self): - args = [ - "NAME:" + self.name, - "UserSpecifyName:=", - 0, - "UserSpecifyFolder:=", - 0, - "QuantityName:=", - self.quantity, - "PlotFolder:=", - "Visual Ray Trace CW", - "IntrinsicVar:=", - "", - "MaxFrequency:=", - self.max_frequency, - "SampleDensity:=", - self.sample_density, - "RayCutOff:=", - self.ray_cutoff, - "Irregular Surface Tolerance:=", - self.irregular_surface_tolerance, - "LaunchFrom:=", - ] - if self.is_plane_wave: - args.append("Launch from Plane-Wave") - args.append("Incident direction theta:=") - args.append(self.incident_theta) - args.append("Incident direction phi:=") - args.append(self.incident_phi) - args.append("Vertical Incident Polarization:=") - args.append(self.vertical_polarization) - else: - args.append("Launch from Custom") - args.append("LaunchFromPointID:=") - args.append(-1) - args.append("CustomLocationCoordSystem:=") - args.append(self.custom_coordinatesystem) - args.append("CustomLocation:=") - args.append(self.custom_location) - args.append("SBRRayDensity:=") - args.append(self.ray_density) - return args - - @pyaedt_function_handler() - def create(self): - """Create a field plot. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - try: - if self.is_creeping_wave: - self._ofield.CreateFieldPlot(self._create_args_creeping(), "CreepingWave_VRT") - else: - self._ofield.CreateFieldPlot(self._create_args(), "VRT") - return True - except Exception: - return False - - @pyaedt_function_handler() - def update(self): - """Update the field plot. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - """ - try: - if self.is_creeping_wave: - self._ofield.ModifyFieldPlot(self.name, self._create_args_creeping()) - - else: - self._ofield.ModifyFieldPlot(self.name, self._create_args()) - return True - except Exception: - return False - - @pyaedt_function_handler() - def delete(self): - """Delete the field plot.""" - self._ofield.DeleteFieldPlot([self.name]) - return True - - @pyaedt_function_handler(path_to_hdm_file="path") - def export(self, path=None): - """Export the Visual Ray Tracing to ``hdm`` file. - - Parameters - ---------- - path : str, optional - Full path to the output file. The default is ``None``, in which case the file is - exported to the working directory. - - Returns - ------- - str - Path to the file. - """ - if not path: - path = os.path.join(self._postprocessor._app.working_directory, self.name + ".hdm") - self._ofield.ExportFieldPlot(self.name, False, path) - return path diff --git a/src/ansys/aedt/core/modules/solve_sweeps.py b/src/ansys/aedt/core/modules/solve_sweeps.py index 5795272481f..462b8dabfa9 100644 --- a/src/ansys/aedt/core/modules/solve_sweeps.py +++ b/src/ansys/aedt/core/modules/solve_sweeps.py @@ -176,6 +176,7 @@ def basis_frequencies(self): if "SolutionBlock" in k and "SolutionName" in v and v["SolutionName"] == self.name and "Fields" in v: try: new_list = [float(i) for i in v["Fields"]["IDDblMap"][1::2]] + new_list.sort() new_list = unit_converter( values=new_list, diff --git a/src/ansys/aedt/core/sbrplus/__init__.py b/src/ansys/aedt/core/visualization/__init__.py similarity index 100% rename from src/ansys/aedt/core/sbrplus/__init__.py rename to src/ansys/aedt/core/visualization/__init__.py diff --git a/src/ansys/aedt/core/generic/com_parameters.py b/src/ansys/aedt/core/visualization/advanced/__init__.py similarity index 100% rename from src/ansys/aedt/core/generic/com_parameters.py rename to src/ansys/aedt/core/visualization/advanced/__init__.py diff --git a/src/ansys/aedt/core/generic/farfield_visualization.py b/src/ansys/aedt/core/visualization/advanced/farfield_visualization.py similarity index 71% rename from src/ansys/aedt/core/generic/farfield_visualization.py rename to src/ansys/aedt/core/visualization/advanced/farfield_visualization.py index c1422d1108c..a1800436b9e 100644 --- a/src/ansys/aedt/core/generic/farfield_visualization.py +++ b/src/ansys/aedt/core/visualization/advanced/farfield_visualization.py @@ -27,39 +27,34 @@ import os import shutil import sys -import time from ansys.aedt.core.aedt_logger import pyaedt_logger as logger -from ansys.aedt.core.application.analysis_hf import ScatteringMethods from ansys.aedt.core.application.variables import decompose_variable_value from ansys.aedt.core.generic.constants import AEDT_UNITS from ansys.aedt.core.generic.constants import unit_converter -from ansys.aedt.core.generic.general_methods import check_and_download_folder from ansys.aedt.core.generic.general_methods import conversion_function -from ansys.aedt.core.generic.general_methods import is_ironpython from ansys.aedt.core.generic.general_methods import open_file from ansys.aedt.core.generic.general_methods import pyaedt_function_handler -from ansys.aedt.core.generic.plot import ModelPlotter -from ansys.aedt.core.generic.plot import get_structured_mesh -from ansys.aedt.core.generic.plot import is_notebook -from ansys.aedt.core.generic.plot import plot_2d_chart -from ansys.aedt.core.generic.plot import plot_3d_chart -from ansys.aedt.core.generic.plot import plot_contour -from ansys.aedt.core.generic.plot import plot_polar_chart -from ansys.aedt.core.generic.settings import settings -from ansys.aedt.core.generic.touchstone_parser import read_touchstone +from ansys.aedt.core.visualization.advanced.touchstone_parser import read_touchstone +from ansys.aedt.core.visualization.plot.matplotlib import is_notebook +from ansys.aedt.core.visualization.plot.matplotlib import plot_2d_chart +from ansys.aedt.core.visualization.plot.matplotlib import plot_3d_chart +from ansys.aedt.core.visualization.plot.matplotlib import plot_contour +from ansys.aedt.core.visualization.plot.matplotlib import plot_polar_chart +from ansys.aedt.core.visualization.plot.pyvista import ModelPlotter +from ansys.aedt.core.visualization.plot.pyvista import get_structured_mesh np = None pv = None -if not is_ironpython: - try: - import numpy as np - except ImportError: # pragma: no cover - np = None - try: - import pyvista as pv - except ImportError: # pragma: no cover - pv = None + +try: + import numpy as np +except ImportError: # pragma: no cover + np = None +try: + import pyvista as pv +except ImportError: # pragma: no cover + pv = None class FfdSolutionData(object): @@ -119,7 +114,7 @@ def __init__( if not touchstone_file: touchstone_file = "" - input_file = FfdSolutionDataExporter.export_pyaedt_antenna_metadata( + input_file = export_pyaedt_antenna_metadata( input_file=input_file, output_dir=self.output_dir, variation=variation, @@ -1575,555 +1570,6 @@ def __rotation_to_euler_angles(rotation): # pragma: no cover return np.array([x, y, z]) -class FfdSolutionDataExporter: - """Class to enable export of embedded element pattern data from HFSS. - - An instance of this class is returned from the - :meth:`ansys.aedt.core.Hfss.get_antenna_data` method. This method allows creation of - the embedded - element pattern files for an antenna that have been solved in HFSS. The - ``metadata_file`` properties can then be passed as arguments to - instantiate an instance of the :class:`ansys.aedt.core.generic.farfield_visualization.FfdSolutionData` class for - subsequent analysis and postprocessing of the array data. - - Note that this class is derived from the :class:`FfdSolutionData` class and can be used directly for - far-field postprocessing and array analysis, but it remains a property of the - :class:`ansys.aedt.core.Hfss` application. - - Parameters - ---------- - app : :class:`ansys.aedt.core.Hfss` - HFSS application instance. - sphere_name : str - Infinite sphere to use. - setup_name : str - Name of the setup. Make sure to build a setup string in the form of ``"SetupName : SetupSweep"``. - frequencies : list - Frequency list to export. Specify either a list of strings with units or a list of floats in Hertz units. - For example, ``["9GHz", 9e9]``. - variations : dict, optional - Dictionary of all families including the primary sweep. The default value is ``None``. - overwrite : bool, optional - Whether to overwrite the existing far field solution data. The default is ``True``. - export_touchstone : bool, optional - Whether to export touchstone file. The default is ``False``. Working from 2024 R1. - set_phase_center_per_port : bool, optional - Set phase center per port location. The default is ``True``. - - Examples - -------- - >>> from ansys.aedt.core - >>> app = ansys.aedt.core.Hfss(version="2023.2", design="Antenna") - >>> setup_name = "Setup1 : LastAdaptive" - >>> frequencies = [77e9] - >>> sphere = "3D" - >>> data = app.get_antenna_data(frequencies,setup_name,sphere) - >>> data.plot_3d(quantity_format="dB10") - """ - - def __init__( - self, - app, - sphere_name, - setup_name, - frequencies, - variations=None, - overwrite=True, - export_touchstone=True, - set_phase_center_per_port=True, - ): - # Public - self.sphere_name = sphere_name - self.setup_name = setup_name - - if not variations: - variations = app.available_variations.nominal_w_values_dict_w_dependent - else: - # Set variation to Nominal - for var_name, var_value in variations.items(): - app[var_name] = var_value - - self.variations = variations - self.overwrite = overwrite - self.export_touchstone = export_touchstone - if not isinstance(frequencies, list): - self.frequencies = [frequencies] - else: - self.frequencies = frequencies - - # Private - self.__app = app - self.__model_info = {} - self.__farfield_data = None - self.__metadata_file = "" - - if self.__app.desktop_class.is_grpc_api and set_phase_center_per_port: - self.__app.set_phase_center_per_port() - else: # pragma: no cover - self.__app.logger.warning("Set phase center in port location manually.") - - @property - def model_info(self): - """List of models.""" - return self.__model_info - - @property - def farfield_data(self): - """Farfield data.""" - return self.__farfield_data - - @property - def metadata_file(self): - """Metadata file.""" - return self.__metadata_file - - @pyaedt_function_handler() - def export_farfield(self): - """Export far field solution data of each element.""" - - # Output directory - exported_name_map = "element.txt" - solution_setup_name = self.setup_name.replace(":", "_").replace(" ", "") - full_setup = "{}-{}".format(solution_setup_name, self.sphere_name) - export_path = "{}/{}/".format(self.__app.working_directory, full_setup) - local_path = "{}/{}/".format(settings.remote_rpc_session_temp_folder, full_setup) - export_path = os.path.abspath(check_and_download_folder(local_path, export_path)) - - # 2024.1 - file_path_xml = os.path.join(export_path, self.__app.design_name + ".xml") - # 2023.2 - file_path_txt = os.path.join(export_path, exported_name_map) - - input_file = file_path_xml - if self.__app.desktop_class.aedt_version_id < "2024.1": # pragma: no cover - input_file = file_path_txt - - # Create directory or check if files already exist - if settings.remote_rpc_session: # pragma: no cover - settings.remote_rpc_session.filemanager.makedirs(export_path) - file_exists = settings.remote_rpc_session.filemanager.pathexists(input_file) - elif not os.path.exists(export_path): - os.makedirs(export_path) - file_exists = False - else: - file_exists = os.path.exists(input_file) - - time_before = time.time() - - # Export far field - if self.overwrite or not file_exists: - if self.__app.desktop_class.aedt_version_id < "2024.1": # pragma: no cover - is_exported = self.__app.export_element_pattern( - frequencies=self.frequencies, - setup=self.setup_name, - sphere=self.sphere_name, - variations=self.variations, - output_dir=export_path, - ) - if not is_exported: # pragma: no cover - return False - if self.export_touchstone: - scattering = ScatteringMethods(self.__app) - setup_sweep_parts = self.setup_name.split(":") - - setup_name = setup_sweep_parts[0].strip() - sweep_name = setup_sweep_parts[1].strip() - - touchstone_file = scattering.export_touchstone(setup=setup_name, sweep=sweep_name) - - if touchstone_file: - touchstone_name = os.path.basename(touchstone_file) - output_file = os.path.join(export_path, touchstone_name) - shutil.move(touchstone_file, output_file) - else: - is_exported = self.__app.export_antenna_metadata( - frequencies=self.frequencies, - setup=self.setup_name, - sphere=self.sphere_name, - variations=self.variations, - output_dir=export_path, - export_element_pattern=True, - export_objects=False, - export_touchstone=True, - export_power=True, - ) - if not is_exported: # pragma: no cover - return False - else: - self.__app.logger.info("Using existing element patterns files.") - - # Export geometry - if os.path.isfile(input_file): - geometry_path = os.path.join(export_path, "geometry") - if not os.path.exists(geometry_path): - os.mkdir(geometry_path) - obj_list = self.__create_geometries(geometry_path) - if obj_list: - self.__model_info["object_list"] = obj_list - - if self.__app.component_array: - component_array = self.__app.component_array[self.__app.component_array_names[0]] - self.__model_info["component_objects"] = component_array.get_component_objects() - self.__model_info["cell_position"] = component_array.get_cell_position() - self.__model_info["array_dimension"] = [ - component_array.a_length, - component_array.b_length, - component_array.a_length / component_array.a_size, - component_array.b_length / component_array.b_size, - ] - self.__model_info["lattice_vector"] = component_array.lattice_vector() - - # Create PyAEDT Metadata - var = [] - if self.variations: - for k, v in self.variations.items(): - var.append("{}='{}'".format(k, v)) - variation = " ".join(var) - else: - variation = self.__app.odesign.GetNominalVariation() - - power = {} - - if self.__app.desktop_class.aedt_version_id < "2024.1": - - available_categories = self.__app.post.available_quantities_categories() - excitations = [] - is_power = True - if "Active VSWR" in available_categories: # pragma: no cover - quantities = self.post.available_report_quantities(quantities_category="Active VSWR") - for quantity in quantities: - excitations.append("ElementPatterns:=") - excitations.append(quantity.strip("ActiveVSWR(").strip(")")) - elif "Terminal VSWR" in available_categories: - quantities = self.__app.post.available_report_quantities(quantities_category="Terminal VSWR") - for quantity in quantities: - excitations.append("ElementPatterns:=") - excitations.append(quantity.strip("VSWRt(").strip(")")) - is_power = False - elif "Gamma" in available_categories: - quantities = self.__app.post.available_report_quantities(quantities_category="Gamma") - for quantity in quantities: - excitations.append("ElementPatterns:=") - excitations.append(quantity.strip("Gamma(").strip(")")) - else: # pragma: no cover - for excitation in self.__app.get_all_sources(): - excitations.append("ElementPatterns:=") - excitations.append(excitation) - for excitation_cont1 in range(len(excitations)): - sources = {} - incident_power = {} - accepted_power = {} - radiated_power = {} - unit = "V" - if is_power: - unit = "W" - active_element = excitations[0] - for excitation_cont2, port in enumerate(excitations): - if excitation_cont1 == excitation_cont2: - active_element = port - sources[port] = (f"1{unit}", "0deg") - else: - sources[port] = (f"0{unit}", "0deg") - - power[active_element] = {} - - self.__app.edit_sources(sources) - - report = self.__app.post.reports_by_category.antenna_parameters( - "IncidentPower", self.setup_name, self.sphere_name - ) - data = report.get_solution_data() - incident_powers = data.data_magnitude() - - report = self.__app.post.reports_by_category.antenna_parameters( - "RadiatedPower", self.setup_name, self.sphere_name - ) - data = report.get_solution_data() - radiated_powers = data.data_magnitude() - - report = self.__app.post.reports_by_category.antenna_parameters( - "AcceptedPower", self.setup_name, self.sphere_name - ) - data = report.get_solution_data() - accepted_powers = data.data_magnitude() - - for freq_cont, freq_str in enumerate(self.frequencies): - frequency = freq_str - if isinstance(freq_str, str): - frequency, units = decompose_variable_value(freq_str) - frequency = unit_converter(frequency, "Freq", units, "Hz") - incident_power[frequency] = incident_powers[freq_cont] - radiated_power[frequency] = radiated_powers[freq_cont] - accepted_power[frequency] = accepted_powers[freq_cont] - - power[active_element]["IncidentPower"] = incident_power - power[active_element]["AcceptedPower"] = accepted_power - power[active_element]["RadiatedPower"] = radiated_power - - pyaedt_metadata_file = FfdSolutionDataExporter.export_pyaedt_antenna_metadata( - input_file=input_file, output_dir=export_path, variation=variation, model_info=self.model_info, power=power - ) - if not pyaedt_metadata_file: # pragma: no cover - return False - elapsed_time = time.time() - time_before - self.__app.logger.info("Exporting embedded element patterns.... Done: %s seconds", elapsed_time) - self.__metadata_file = pyaedt_metadata_file - self.__farfield_data = FfdSolutionData(pyaedt_metadata_file) - return pyaedt_metadata_file - - @staticmethod - @pyaedt_function_handler() - def export_pyaedt_antenna_metadata( - input_file, output_dir, variation=None, model_info=None, power=None, touchstone_file=None - ): - """Obtain PyAEDT metadata JSON file from AEDT metadata XML file or embedded element pattern TXT file. - - Parameters - ---------- - input_file : str - Full path to the XML or TXT file. - output_dir : str - Full path to save the file to. - variation : str, optional - Label to identify corresponding variation. - model_info : dict, optional - power : dict, optional - Dictionary with information of the incident power for each frequency. - The default is ``None``, in which case an empty dictionary is applied. - From AEDT 2024.1, this information is available from the XML input file. - For example, the dictionary format for a two element farfield - data = power[1000000000.0]["IncidentPower"] - data = [1, 0.99] - touchstone_file : str, optional - Touchstone file name. The default is ``None``. - - Returns - ------- - str - Metadata JSON file. - """ - from ansys.aedt.core.generic.touchstone_parser import find_touchstone_files - - if not variation: - variation = "Nominal" - - if not power: - power = {} - - if not touchstone_file: - touchstone_file = "" - - pyaedt_metadata_file = os.path.join(output_dir, "pyaedt_antenna_metadata.json") - items = {"variation": variation, "element_pattern": {}, "touchstone_file": touchstone_file} - - if os.path.isfile(input_file) and os.path.basename(input_file).split(".")[1] == "xml": - # Metadata available from 2024.1 - antenna_metadata = FfdSolutionDataExporter.antenna_metadata(input_file) - - # Find all ffd files and move them to main directory - for dir_path, _, filenames in os.walk(output_dir): - ffd_files = [file for file in filenames if file.endswith(".ffd")] - sNp_files = find_touchstone_files(dir_path) - if ffd_files: - # Move ffd files to main directory - for ffd_file in ffd_files: - output_file = os.path.join(output_dir, ffd_file) - pattern_file = os.path.join(dir_path, ffd_file) - shutil.move(pattern_file, output_file) - if sNp_files and not touchstone_file: - # Only one Touchstone allowed - sNp_name, sNp_path = next(iter(sNp_files.items())) - output_file = os.path.join(output_dir, sNp_name) - exported_touchstone_file = os.path.join(sNp_path) - shutil.move(exported_touchstone_file, output_file) - items["touchstone_file"] = sNp_name - - for metadata in antenna_metadata: - - incident_power = {} - for i_freq, i_power_value in metadata["incident_power"].items(): - frequency = i_freq - if isinstance(i_freq, str): - frequency, units = decompose_variable_value(i_freq) - if units: - frequency = unit_converter(frequency, "Freq", units, "Hz") - incident_power[frequency] = float(i_power_value) - - radiated_power = {} - for i_freq, i_power_value in metadata["radiated_power"].items(): - frequency = i_freq - if isinstance(i_freq, str): - frequency, units = decompose_variable_value(i_freq) - if units: - frequency = unit_converter(frequency, "Freq", units, "Hz") - radiated_power[frequency] = float(i_power_value) - - accepted_power = {} - for i_freq, i_power_value in metadata["accepted_power"].items(): - frequency = i_freq - if isinstance(i_freq, str): - frequency, units = decompose_variable_value(i_freq) - if units: - frequency = unit_converter(frequency, "Freq", units, "Hz") - accepted_power[frequency] = float(i_power_value) - - pattern = { - "file_name": metadata["file_name"], - "location": metadata["location"], - "incident_power": incident_power, - "radiated_power": radiated_power, - "accepted_power": accepted_power, - } - - items["element_pattern"][metadata["name"]] = pattern - pattern_file = os.path.join(output_dir, metadata["file_name"]) - if not os.path.isfile(pattern_file): # pragma: no cover - return False - - elif os.path.isfile(input_file) and os.path.basename(input_file).split(".")[1] == "txt": - - # Find all ffd files and move them to main directory - for dir_path, _, _ in os.walk(output_dir): - sNp_files = find_touchstone_files(dir_path) - if sNp_files and not touchstone_file: - # Only one Touchstone allowed - sNp_name, sNp_path = next(iter(sNp_files.items())) - output_file = os.path.join(output_dir, sNp_name) - exported_touchstone_file = os.path.join(sNp_path) - shutil.move(exported_touchstone_file, output_file) - items["touchstone_file"] = sNp_name - break - - with open_file(input_file, "r") as file: - # Skip the first line - file.readline() - # Read and process the remaining lines - for line in file: - antenna_metadata = line.strip().split() - if len(antenna_metadata) == 5: - element_name = antenna_metadata[0] - file_name = antenna_metadata[1] - if ".ffd" not in file_name: - file_name = file_name + ".ffd" - incident_power = None - radiated_power = None - accepted_power = None - if power: - incident_power = power[element_name]["IncidentPower"] - radiated_power = power[element_name]["RadiatedPower"] - accepted_power = power[element_name]["AcceptedPower"] - - pattern = { - "file_name": file_name, - "location": [ - float(antenna_metadata[2]), - float(antenna_metadata[3]), - float(antenna_metadata[4]), - ], - "incident_power": incident_power, - "radiated_power": radiated_power, - "accepted_power": accepted_power, - } - items["element_pattern"][antenna_metadata[0]] = pattern - - items["model_info"] = [] - if model_info: - if "object_list" in model_info: - items["model_info"] = model_info["object_list"] - - required_array_keys = ["array_dimension", "component_objects", "lattice_vector", "cell_position"] - - if all(key in model_info for key in required_array_keys): - items["component_objects"] = model_info["component_objects"] - items["cell_position"] = model_info["cell_position"] - items["array_dimension"] = model_info["array_dimension"] - items["lattice_vector"] = model_info["lattice_vector"] - - with open_file(pyaedt_metadata_file, "w") as f: - json.dump(items, f, indent=2) - return pyaedt_metadata_file - - @staticmethod - @pyaedt_function_handler() - def antenna_metadata(input_file): - """Obtain metadata information from metadata XML file. - - Parameters - ---------- - input_file : str - Full path to the XML file. - - Returns - ------- - dict - Metadata information. - - """ - import xml.etree.ElementTree as ET # nosec - - # Load the XML file - tree = ET.parse(input_file) # nosec - root = tree.getroot() - - element_patterns = root.find("ElementPatterns") - - sources = [] - if element_patterns is None: # pragma: no cover - print("Element Patterns section not found in XML.") - else: - cont = 0 - # Iterate over each Source element - for source in element_patterns.findall("Source"): - source_info = { - "name": source.get("name"), - "file_name": source.find("Filename").text.strip(), - "location": source.find("ReferenceLocation").text.strip().split(","), - } - - # Iterate over Power elements - power_info = source.find("PowerInfo") - if power_info is not None: - source_info["incident_power"] = {} - source_info["accepted_power"] = {} - source_info["radiated_power"] = {} - for power in power_info.findall("Power"): - freq = power.get("Freq") - source_info["incident_power"][freq] = {} - source_info["incident_power"][freq] = power.find("IncidentPower").text.strip() - source_info["accepted_power"][freq] = {} - source_info["accepted_power"][freq] = power.find("AcceptedPower").text.strip() - source_info["radiated_power"][freq] = {} - source_info["radiated_power"][freq] = power.find("RadiatedPower").text.strip() - - sources.append(source_info) - cont += 1 - return sources - - @pyaedt_function_handler() - def __create_geometries(self, export_path): - """Export the geometry in OBJ format.""" - self.__app.logger.info("Exporting geometry...") - model_pv = self.__app.post.get_model_plotter_geometries(plot_air_objects=False) - obj_list = {} - for obj in model_pv.objects: - object_name = os.path.basename(obj.path) - name = os.path.splitext(object_name)[0] - original_path = os.path.dirname(obj.path) - new_path = os.path.join(os.path.abspath(export_path), object_name) - - if not os.path.exists(new_path): - new_path = shutil.move(obj.path, export_path) - if os.path.exists(os.path.join(original_path, name + ".mtl")): # pragma: no cover - shutil.rmtree(os.path.join(original_path, name + ".mtl"), ignore_errors=True) - obj_list[obj.name] = [ - os.path.join(os.path.basename(export_path), object_name), - obj.color, - obj.opacity, - obj.units, - ] - return obj_list - - class UpdateBeamForm: """Provides for updating far field data. @@ -2170,3 +1616,231 @@ def update_theta(self, theta): """Update the Theta value.""" self.__theta = theta self.__update_both() + + +@pyaedt_function_handler() +def export_pyaedt_antenna_metadata( + input_file, output_dir, variation=None, model_info=None, power=None, touchstone_file=None +): + """Obtain PyAEDT metadata JSON file from AEDT metadata XML file or embedded element pattern TXT file. + + Parameters + ---------- + input_file : str + Full path to the XML or TXT file. + output_dir : str + Full path to save the file to. + variation : str, optional + Label to identify corresponding variation. + model_info : dict, optional + power : dict, optional + Dictionary with information of the incident power for each frequency. + The default is ``None``, in which case an empty dictionary is applied. + From AEDT 2024.1, this information is available from the XML input file. + For example, the dictionary format for a two element farfield + data = power[1000000000.0]["IncidentPower"] + data = [1, 0.99] + touchstone_file : str, optional + Touchstone file name. The default is ``None``. + + Returns + ------- + str + Metadata JSON file. + """ + from ansys.aedt.core.visualization.advanced.touchstone_parser import find_touchstone_files + + if not variation: + variation = "Nominal" + + if not power: + power = {} + + if not touchstone_file: + touchstone_file = "" + + pyaedt_metadata_file = os.path.join(output_dir, "pyaedt_antenna_metadata.json") + items = {"variation": variation, "element_pattern": {}, "touchstone_file": touchstone_file} + + if os.path.isfile(input_file) and os.path.basename(input_file).split(".")[1] == "xml": + # Metadata available from 2024.1 + antenna_metadata = antenna_metadata_from_xml(input_file) + + # Find all ffd files and move them to main directory + for dir_path, _, filenames in os.walk(output_dir): + ffd_files = [file for file in filenames if file.endswith(".ffd")] + sNp_files = find_touchstone_files(dir_path) + if ffd_files: + # Move ffd files to main directory + for ffd_file in ffd_files: + output_file = os.path.join(output_dir, ffd_file) + pattern_file = os.path.join(dir_path, ffd_file) + shutil.move(pattern_file, output_file) + if sNp_files and not touchstone_file: + # Only one Touchstone allowed + sNp_name, sNp_path = next(iter(sNp_files.items())) + output_file = os.path.join(output_dir, sNp_name) + exported_touchstone_file = os.path.join(sNp_path) + shutil.move(exported_touchstone_file, output_file) + items["touchstone_file"] = sNp_name + + for metadata in antenna_metadata: + + incident_power = {} + for i_freq, i_power_value in metadata["incident_power"].items(): + frequency = i_freq + if isinstance(i_freq, str): + frequency, units = decompose_variable_value(i_freq) + if units: + frequency = unit_converter(frequency, "Freq", units, "Hz") + incident_power[frequency] = float(i_power_value) + + radiated_power = {} + for i_freq, i_power_value in metadata["radiated_power"].items(): + frequency = i_freq + if isinstance(i_freq, str): + frequency, units = decompose_variable_value(i_freq) + if units: + frequency = unit_converter(frequency, "Freq", units, "Hz") + radiated_power[frequency] = float(i_power_value) + + accepted_power = {} + for i_freq, i_power_value in metadata["accepted_power"].items(): + frequency = i_freq + if isinstance(i_freq, str): + frequency, units = decompose_variable_value(i_freq) + if units: + frequency = unit_converter(frequency, "Freq", units, "Hz") + accepted_power[frequency] = float(i_power_value) + + pattern = { + "file_name": metadata["file_name"], + "location": metadata["location"], + "incident_power": incident_power, + "radiated_power": radiated_power, + "accepted_power": accepted_power, + } + + items["element_pattern"][metadata["name"]] = pattern + pattern_file = os.path.join(output_dir, metadata["file_name"]) + if not os.path.isfile(pattern_file): # pragma: no cover + return False + + elif os.path.isfile(input_file) and os.path.basename(input_file).split(".")[1] == "txt": + + # Find all ffd files and move them to main directory + for dir_path, _, _ in os.walk(output_dir): + sNp_files = find_touchstone_files(dir_path) + if sNp_files and not touchstone_file: + # Only one Touchstone allowed + sNp_name, sNp_path = next(iter(sNp_files.items())) + output_file = os.path.join(output_dir, sNp_name) + exported_touchstone_file = os.path.join(sNp_path) + shutil.move(exported_touchstone_file, output_file) + items["touchstone_file"] = sNp_name + break + + with open_file(input_file, "r") as file: + # Skip the first line + file.readline() + # Read and process the remaining lines + for line in file: + antenna_metadata = line.strip().split() + if len(antenna_metadata) == 5: + element_name = antenna_metadata[0] + file_name = antenna_metadata[1] + if ".ffd" not in file_name: + file_name = file_name + ".ffd" + incident_power = None + radiated_power = None + accepted_power = None + if power: + incident_power = power[element_name]["IncidentPower"] + radiated_power = power[element_name]["RadiatedPower"] + accepted_power = power[element_name]["AcceptedPower"] + + pattern = { + "file_name": file_name, + "location": [ + float(antenna_metadata[2]), + float(antenna_metadata[3]), + float(antenna_metadata[4]), + ], + "incident_power": incident_power, + "radiated_power": radiated_power, + "accepted_power": accepted_power, + } + items["element_pattern"][antenna_metadata[0]] = pattern + + items["model_info"] = [] + if model_info: + if "object_list" in model_info: + items["model_info"] = model_info["object_list"] + + required_array_keys = ["array_dimension", "component_objects", "lattice_vector", "cell_position"] + + if all(key in model_info for key in required_array_keys): + items["component_objects"] = model_info["component_objects"] + items["cell_position"] = model_info["cell_position"] + items["array_dimension"] = model_info["array_dimension"] + items["lattice_vector"] = model_info["lattice_vector"] + + with open_file(pyaedt_metadata_file, "w") as f: + json.dump(items, f, indent=2) + return pyaedt_metadata_file + + +@pyaedt_function_handler() +def antenna_metadata_from_xml(input_file): + """Obtain metadata information from metadata XML file. + + Parameters + ---------- + input_file : str + Full path to the XML file. + + Returns + ------- + dict + Metadata information. + + """ + import xml.etree.ElementTree as ET # nosec + + # Load the XML file + tree = ET.parse(input_file) # nosec + root = tree.getroot() + + element_patterns = root.find("ElementPatterns") + + sources = [] + if element_patterns is None: # pragma: no cover + print("Element Patterns section not found in XML.") + else: + cont = 0 + # Iterate over each Source element + for source in element_patterns.findall("Source"): + source_info = { + "name": source.get("name"), + "file_name": source.find("Filename").text.strip(), + "location": source.find("ReferenceLocation").text.strip().split(","), + } + + # Iterate over Power elements + power_info = source.find("PowerInfo") + if power_info is not None: + source_info["incident_power"] = {} + source_info["accepted_power"] = {} + source_info["radiated_power"] = {} + for power in power_info.findall("Power"): + freq = power.get("Freq") + source_info["incident_power"][freq] = {} + source_info["incident_power"][freq] = power.find("IncidentPower").text.strip() + source_info["accepted_power"][freq] = {} + source_info["accepted_power"][freq] = power.find("AcceptedPower").text.strip() + source_info["radiated_power"][freq] = {} + source_info["radiated_power"][freq] = power.find("RadiatedPower").text.strip() + + sources.append(source_info) + cont += 1 + return sources diff --git a/src/ansys/aedt/core/sbrplus/plot.py b/src/ansys/aedt/core/visualization/advanced/hdm_plot.py similarity index 92% rename from src/ansys/aedt/core/sbrplus/plot.py rename to src/ansys/aedt/core/visualization/advanced/hdm_plot.py index c7c1c20c321..55b6590c4b2 100644 --- a/src/ansys/aedt/core/sbrplus/plot.py +++ b/src/ansys/aedt/core/visualization/advanced/hdm_plot.py @@ -29,10 +29,25 @@ from ansys.aedt.core.generic.constants import AEDT_UNITS from ansys.aedt.core.generic.general_methods import pyaedt_function_handler -from ansys.aedt.core.generic.plot import CommonPlotter -from ansys.aedt.core.generic.plot import ObjClass -import numpy as np -import pyvista as pv +from ansys.aedt.core.visualization.plot.pyvista import CommonPlotter +from ansys.aedt.core.visualization.plot.pyvista import ObjClass + +try: + import numpy as np +except ImportError: + warnings.warn( + "The NumPy module is required to run some functionalities of PostProcess.\n" + "Install with \n\npip install numpy" + ) +try: + import pyvista as pv + + pyvista_available = True +except ImportError: + warnings.warn( + "The PyVista module is required to run some functionalities of PostProcess.\n" + "Install with \n\npip install pyvista\n\nRequires CPython." + ) class HDMPlotter(CommonPlotter): @@ -77,7 +92,7 @@ def add_cad_model(self, filename, cad_color="dodgerblue", opacity=1, units="mm") @pyaedt_function_handler() def add_hdm_bundle_from_file(self, filename, units=None): """Add hdm bundle from file.""" - from ansys.aedt.core.sbrplus.hdm_parser import Parser + from ansys.aedt.core.visualization.advanced.sbrplus.hdm_parser import Parser if os.path.exists(filename): self._bundle = Parser(filename=filename).parse_message() diff --git a/src/ansys/aedt/core/visualization/advanced/misc.py b/src/ansys/aedt/core/visualization/advanced/misc.py new file mode 100644 index 00000000000..1bd2474a65d --- /dev/null +++ b/src/ansys/aedt/core/visualization/advanced/misc.py @@ -0,0 +1,685 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import csv +import logging +import os +import re + +from ansys.aedt.core.generic.constants import CSS4_COLORS +from ansys.aedt.core.generic.constants import SI_UNITS +from ansys.aedt.core.generic.constants import unit_system +from ansys.aedt.core.generic.filesystem import search_files +from ansys.aedt.core.generic.general_methods import open_file +from ansys.aedt.core.generic.general_methods import pyaedt_function_handler +from ansys.aedt.core.generic.load_aedt_file import load_keyword_in_aedt_file +from ansys.aedt.core.modeler.geometry_operators import GeometryOperators + +try: + import pyvista as pv +except ImportError: # pragma: no cover + pv = None + + +class BoxFacePointsAndFields(object): + """Data model class containing field component and coordinates.""" + + def __init__(self): + self.x = [] + self.y = [] + self.z = [] + self.re = {"Ex": [], "Ey": [], "Ez": [], "Hx": [], "Hy": [], "Hz": []} + self.im = {"Ex": [], "Ey": [], "Ez": [], "Hx": [], "Hy": [], "Hz": []} + + def set_xyz_points(self, x, y, z): + self.x = x + self.y = y + self.z = z + + def set_field_component(self, field_component, real, imag, invert): + """Set Field component Real and imaginary parts.""" + if field_component in self.re: + if invert: + self.re[field_component] = [str(-float(i)) for i in real] + self.im[field_component] = [str(-float(i)) for i in imag] + else: + self.re[field_component] = real + self.im[field_component] = imag + else: + print("Error in set_field_component function.") + + def fill_empty_data(self): + for el, val in self.re.items(): + if not val: + zero_field_z_faces = [0] * len(self.x) + self.re[el] = zero_field_z_faces + for el, val in self.im.items(): + if not val: + zero_field_z_faces = [0] * len(self.x) + self.im[el] = zero_field_z_faces + + +@pyaedt_function_handler() +def convert_nearfield_data(dat_folder, frequency=6, invert_phase_for_lower_faces=True, output_folder=None): + """Convert a near field data folder to hfss `nfd` file and link it to `and` file. + + Parameters + ---------- + dat_folder : str + Full path to the folder containing near fields data. + Folder will contain 24 files in the following format: `data_Ex_ymin.dat`. Same for H Fields. + frequency : float, int, str + Frequency in `GHz`. + invert_phase_for_lower_faces : bool + Add 180 deg for all fields at 'negative' faces (xmin, ymin, zmin). + output_folder : str, optional + Output folder where files will be saved. + + Returns + ------- + str + Full path to `.and` file. + """ + file_keys = ["xmin", "xmax", "ymin", "ymax", "zmin", "zmax"] + components = { + "xmin": BoxFacePointsAndFields(), + "ymin": BoxFacePointsAndFields(), + "zmin": BoxFacePointsAndFields(), + "xmax": BoxFacePointsAndFields(), + "ymax": BoxFacePointsAndFields(), + "zmax": BoxFacePointsAndFields(), + } + + file_names = search_files(dat_folder, "*.dat") + for data_file in file_names: + match = re.search(r"data_(\S+)_(\S+).dat", os.path.basename(data_file)) + field_component = match.group(1) + face = match.group(2) + + if not os.path.exists(data_file): + continue + # Read in all data for the current file + x, y, z = [], [], [] + real, imag = [], [] + with open_file(data_file, "r") as f: + for line in f: + line = line.strip().split(" ") + if len(line) == 5: + x.append(line[0]) + y.append(line[1]) + z.append(line[2]) + real.append(line[3]) + imag.append(line[4]) + + assert face in components, "Wrong file name format. Face not found." + if not components[face].x: + components[face].set_xyz_points(x, y, z) + components[face].fill_empty_data() + if "min" in face: + components[face].set_field_component(field_component, real, imag, invert_phase_for_lower_faces) + else: + components[face].set_field_component(field_component, real, imag, False) + + full_data = [] + index = 1 + for el in list(file_keys): + for k in range(index, index + len(components[el].x)): + row = [] + row.append(k) + row.append(components[el].x[k - index]) + row.append(components[el].y[k - index]) + row.append(components[el].z[k - index]) + for field in ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"]: + row.append(components[el].re[field][k - index]) + row.append(components[el].im[field][k - index]) + full_data.append(row) + index += len(components[el].x) + + # WRITE .NFD FILE + #################################################################################################### + # .nfd file needs the following 16 columns where index starts with 1: + # index, x, y, z, re_ex, im_ex, re_ey, im_ey, re_ez, im_ez, re_hx, im_hx, re_hy, im_hy, re_hz, im_hz + if not output_folder: + output_folder = os.path.dirname(dat_folder) + directory_name = os.path.basename(dat_folder) + nfd_name = directory_name + ".nfd" + nfd_full_file = os.path.join(output_folder, nfd_name) + and_full_file = os.path.join(output_folder, directory_name + ".and") + + commented_header_line = "#Index, X, Y, Z, Ex(real, imag), Ey(real, imag), Ez(real, imag), " + commented_header_line += "Hx(real, imag), Hy(real, imag), Hz(real, imag)\n" + + with open_file(nfd_full_file, "w") as file: + writer = csv.writer(file, delimiter=",", lineterminator="\n") + file.write(commented_header_line) + file.write("Frequencies 1\n") + file.write("Frequency " + str(frequency) + "GHz\n") + writer.writerows(full_data) + + print(".nfd file written to %s" % nfd_full_file) # Prints if running ipy64 through external editor + + size_x = float(components["xmax"].x[0]) - float(components["xmin"].x[0]) + size_y = float(components["ymax"].y[0]) - float(components["ymin"].y[0]) + size_z = float(components["zmax"].z[0]) - float(components["zmin"].z[0]) + + center_x = float(components["xmin"].x[0]) + float(size_x / 2.0) + center_y = float(components["ymin"].y[0]) + float(size_y / 2.0) + center_z = float(components["zmin"].z[0]) + float(size_z / 2.0) + + sx_mm = size_x * 1000 + sy_mm = size_y * 1000 + sz_mm = size_z * 1000 + cx_mm = center_x * 1000 + cy_mm = center_y * 1000 + cz_mm = center_z * 1000 + + with open_file(and_full_file, "w") as file: + file.write("$begin 'NearFieldHeader'\n") + file.write(" type='nfd'\n") + file.write(" fields='EH'\n") + file.write(" fsweep='" + str(frequency) + "GHz'\n") + file.write(" geometry='box'\n") + file.write(" center='" + str(cx_mm) + "mm," + str(cy_mm) + "mm," + str(cz_mm) + "mm'\n") + file.write(" size='" + str(sx_mm) + "mm," + str(sy_mm) + "mm," + str(sz_mm) + "mm'\n") + file.write("$end 'NearFieldHeader'\n") + file.write("$begin 'NearFieldData'\n") + file.write(' FreqData("' + str(frequency) + 'GHz","' + nfd_name + '")\n') + file.write("$end 'NearFieldData'\n") + return and_full_file + + +@pyaedt_function_handler() +def parse_rdat_file(file_path): + """ + Parse Ansys report '.rdat' file. + + Returns + ------- + dict + Report data. + """ + report_dict = {} + # data = load_entire_aedt_file(file_path) + data = load_keyword_in_aedt_file(file_path, "ReportsData") + + report_data = data["ReportsData"]["RepMgrRepsData"] + for report_name in report_data: + report_dict[report_name] = {} + for _, trace_data in report_data[report_name]["Traces"].items(): + all_data = trace_data["TraceComponents"]["TraceDataComps"]["0"] + if all_data["TraceDataCol"]["ParameterType"] == "ComplexParam": + all_data_values = all_data["TraceDataCol"]["ColumnValues"] + all_re_values = all_data_values[0::2] + all_im_values = all_data_values[1::2] + all_x_values = trace_data["PrimarySweepInfo"]["PrimarySweepCol"]["ColumnValues"] + si_unit_x = SI_UNITS[unit_system(trace_data["PrimarySweepInfo"]["PrimarySweepCol"]["Units"])] + si_unit_y = SI_UNITS[unit_system(all_data["TraceDataCol"]["Units"])] + report_dict[report_name][trace_data["TraceName"]] = { + "x_name": all_data["TraceCompExpr"], + "x_unit": si_unit_x, + "y_unit": si_unit_y, + "curves": {}, + } + for _, curve_data in trace_data["CurvesInfo"].items(): + report_dict[report_name][trace_data["TraceName"]]["curves"][curve_data[1] + "real"] = { + "x_data": all_x_values[0 : curve_data[0]], + "y_data": all_re_values[0 : curve_data[0]], + } + report_dict[report_name][trace_data["TraceName"]]["curves"][curve_data[1] + "imag"] = { + "x_data": all_x_values[0 : curve_data[0]], + "y_data": all_im_values[0 : curve_data[0]], + } + all_x_values = all_x_values[curve_data[0] :] + all_re_values = all_re_values[curve_data[0] :] + all_im_values = all_im_values[curve_data[0] :] + + else: + y_data = trace_data["TraceComponents"]["TraceDataComps"]["1"] + all_x_values = all_data["TraceDataCol"]["ColumnValues"] + all_y_values = y_data["TraceDataCol"]["ColumnValues"] + si_unit_x = SI_UNITS[unit_system(all_data["TraceDataCol"]["Units"])] + si_unit_y = SI_UNITS[unit_system(y_data["TraceDataCol"]["Units"])] + report_dict[report_name][trace_data["TraceName"]] = { + "x_name": all_data["TraceCompExpr"], + "x_unit": si_unit_x, + "y_unit": si_unit_y, + "curves": {}, + } + for _, curve_data in trace_data["CurvesInfo"].items(): + report_dict[report_name][trace_data["TraceName"]]["curves"][curve_data[1]] = { + "x_data": all_x_values[0 : curve_data[0]], + "y_data": all_y_values[0 : curve_data[0]], + } + all_x_values = all_x_values[curve_data[0] :] + all_y_values = all_y_values[curve_data[0] :] + + return report_dict + + +@pyaedt_function_handler() +def _parse_nastran(file_path): + """Nastran file parser.""" + logger = logging.getLogger("Global") + nas_to_dict = {"Points": [], "PointsId": {}, "Assemblies": {}} + includes = [] + + def parse_lines(input_lines, input_pid=0, in_assembly="Main"): + if in_assembly not in nas_to_dict["Assemblies"]: + nas_to_dict["Assemblies"][in_assembly] = {"Triangles": {}, "Solids": {}, "Lines": {}} + + def get_point(ll, start, length): + n = ll[start : start + length].strip() + if "-" in n[1:] and "e" not in n[1:].lower(): + n = n[0] + n[1:].replace("-", "e-") + return n + + for lk in range(len(input_lines)): + line = input_lines[lk] + line_type = line[:8].strip() + obj_type = "Triangles" + if line.startswith("$") or line.startswith("*"): + continue + elif line_type in ["GRID", "GRID*"]: + num_points = 3 + obj_type = "Grid" + elif line_type in [ + "CTRIA3", + "CTRIA3*", + ]: + num_points = 3 + obj_type = "Triangles" + elif line_type in ["CROD", "CBEAM", "CBAR", "CROD*", "CBEAM*", "CBAR*"]: + num_points = 2 + obj_type = "Lines" + elif line_type in [ + "CQUAD4", + "CQUAD4*", + ]: + num_points = 4 + obj_type = "Triangles" + elif line_type in ["CTETRA", "CTETRA*"]: + num_points = 4 + obj_type = "Solids" + elif line_type in ["CPYRA", "CPYRAM", "CPYRA*", "CPYRAM*"]: + num_points = 5 + obj_type = "Solids" + else: + continue + + points = [] + start_pointer = 8 + word_length = 8 + if line_type.endswith("*"): + word_length = 16 + grid_id = int(line[start_pointer : start_pointer + word_length]) + pp = 0 + start_pointer = start_pointer + word_length + object_id = line[start_pointer : start_pointer + word_length] + if obj_type != "Grid": + object_id = int(object_id) + if object_id not in nas_to_dict["Assemblies"][in_assembly][obj_type]: + nas_to_dict["Assemblies"][in_assembly][obj_type][object_id] = [] + while pp < num_points: + start_pointer = start_pointer + word_length + if start_pointer >= 72: + lk += 1 + line = input_lines[lk] + start_pointer = 8 + points.append(get_point(line, start_pointer, word_length)) + pp += 1 + + if line_type in ["GRID", "GRID*"]: + nas_to_dict["PointsId"][grid_id] = input_pid + nas_to_dict["Points"].append([float(i) for i in points]) + input_pid += 1 + elif line_type in [ + "CTRIA3", + "CTRIA3*", + ]: + tri = [nas_to_dict["PointsId"][int(i)] for i in points] + nas_to_dict["Assemblies"][in_assembly]["Triangles"][object_id].append(tri) + elif line_type in ["CROD", "CBEAM", "CBAR", "CROD*", "CBEAM*", "CBAR*"]: + tri = [nas_to_dict["PointsId"][int(i)] for i in points] + nas_to_dict["Assemblies"][in_assembly]["Lines"][object_id].append(tri) + elif line_type in ["CQUAD4", "CQUAD4*"]: + tri = [ + nas_to_dict["PointsId"][int(points[0])], + nas_to_dict["PointsId"][int(points[1])], + nas_to_dict["PointsId"][int(points[2])], + ] + nas_to_dict["Assemblies"][in_assembly]["Triangles"][object_id].append(tri) + tri = [ + nas_to_dict["PointsId"][int(points[0])], + nas_to_dict["PointsId"][int(points[2])], + nas_to_dict["PointsId"][int(points[3])], + ] + nas_to_dict["Assemblies"][in_assembly]["Triangles"][object_id].append(tri) + else: + from itertools import combinations + + for k in list(combinations(points, 3)): + tri = [ + nas_to_dict["PointsId"][int(k[0])], + nas_to_dict["PointsId"][int(k[1])], + nas_to_dict["PointsId"][int(k[2])], + ] + tri.sort() + tri = tuple(tri) + nas_to_dict["Assemblies"][in_assembly]["Solids"][object_id].append(tri) + + return input_pid + + logger.info("Loading file") + with open_file(file_path, "r") as f: + lines = f.read().splitlines() + for line in lines: + if line.startswith("INCLUDE"): + includes.append(line.split(" ")[1].replace("'", "").strip()) + pid = parse_lines(lines) + for include in includes: + with open_file(os.path.join(os.path.dirname(file_path), include), "r") as f: + lines = f.read().splitlines() + name = include.split(".")[0] + pid = parse_lines(lines, pid, name) + logger.info("File loaded") + for assembly in list(nas_to_dict["Assemblies"].keys())[::]: + if ( + nas_to_dict["Assemblies"][assembly]["Triangles"] + == nas_to_dict["Assemblies"][assembly]["Solids"] + == nas_to_dict["Assemblies"][assembly]["Lines"] + == {} + ): + del nas_to_dict["Assemblies"][assembly] + for _, assembly_object in nas_to_dict["Assemblies"].items(): + + def domino(segments): + + def check_new_connection(s, polylines, exclude_index=-1): + s = s[:] + polylines = [poly[:] for poly in polylines] + attached = False + p_index = None + for i, p in enumerate(polylines): + if i == exclude_index: + continue + if s[0] == p[-1]: + p.extend(s[1:]) # the new segment attaches to the end + attached = True + elif s[-1] == p[0]: + for item in reversed(s[:-1]): + p.insert(0, item) # the new segment attaches to the beginning + attached = True + elif s[0] == p[0]: + for item in s[1:]: + p.insert(0, item) # the new segment attaches to the beginning in reverse order + attached = True + elif s[-1] == p[-1]: + p.extend(s[-2::-1]) # the new segment attaches to the end in reverse order + attached = True + if attached: + p_index = i + break + if not attached: + polylines.append(s) + return polylines, attached, p_index + + polylines = [] + for segment in segments: + polylines, attached_flag, attached_p_index = check_new_connection(segment, polylines) + if attached_flag: + other_polylines = polylines[:attached_p_index] + polylines[attached_p_index + 1 :] + polylines, _, _ = check_new_connection( + polylines[attached_p_index], other_polylines, attached_p_index + ) + + return polylines + + def remove_self_intersections(polylines): + polylines = [poly[:] for poly in polylines] + new_polylines = [] + for p in polylines: + if p[0] in p[1:]: + new_polylines.append([p[0], p[1]]) + p.pop(0) + if p[-1] in p[:-1]: + new_polylines.append([p[-2], p[-1]]) + p.pop(-1) + new_polylines.append(p) + return new_polylines + + if assembly_object["Lines"]: + for lname, lines in assembly_object["Lines"].items(): + new_lines = lines[::] + new_lines = remove_self_intersections(domino(new_lines)) + assembly_object["Lines"][lname] = new_lines + + return nas_to_dict + + +@pyaedt_function_handler() +def _write_stl(nas_to_dict, decimation, working_directory, enable_planar_merge=True): + """Write stl file.""" + logger = logging.getLogger("Global") + + def _write_solid_stl(triangle, pp): + try: + # points = [nas_to_dict["Points"][id] for id in triangle] + points = [pp[i] for i in triangle] + except KeyError: # pragma: no cover + return + fc = GeometryOperators.get_polygon_centroid(points) + v1 = points[0] + v2 = points[1] + cv1 = GeometryOperators.v_points(fc, v1) + cv2 = GeometryOperators.v_points(fc, v2) + if cv2[0] == cv1[0] == 0.0 and cv2[1] == cv1[1] == 0.0: + n = [0, 0, 1] # pragma: no cover + elif cv2[0] == cv1[0] == 0.0 and cv2[2] == cv1[2] == 0.0: + n = [0, 1, 0] # pragma: no cover + elif cv2[1] == cv1[1] == 0.0 and cv2[2] == cv1[2] == 0.0: + n = [1, 0, 0] # pragma: no cover + else: + n = GeometryOperators.v_cross(cv1, cv2) + + normal = GeometryOperators.normalize_vector(n) + if normal: + f.write(" facet normal {} {} {}\n".format(normal[0], normal[1], normal[2])) + f.write(" outer loop\n") + f.write(" vertex {} {} {}\n".format(points[0][0], points[0][1], points[0][2])) + f.write(" vertex {} {} {}\n".format(points[1][0], points[1][1], points[1][2])) + f.write(" vertex {} {} {}\n".format(points[2][0], points[2][1], points[2][2])) + f.write(" endloop\n") + f.write(" endfacet\n") + + logger.info("Creating STL file with detected faces") + enable_stl_merge = False if enable_planar_merge == "False" or enable_planar_merge is False else True + + def decimate(points_in, faces_in): + fin = [[3] + list(i) for i in faces_in] + mesh = pv.PolyData(points_in, faces=fin) + new_mesh = mesh.decimate_pro(decimation, preserve_topology=True, boundary_vertex_deletion=False) + points_out = list(new_mesh.points) + faces_out = [i[1:] for i in new_mesh.faces.reshape(-1, 4) if i[0] == 3] + return points_out, faces_out + + output_stls = [] + for assembly_name, assembly in nas_to_dict["Assemblies"].items(): + output_stl = os.path.join(working_directory, assembly_name + ".stl") + f = open(output_stl, "w") + for tri_id, triangles in assembly["Triangles"].items(): + tri_out = triangles + p_out = nas_to_dict["Points"][::] + if decimation > 0 and len(triangles) > 20: + p_out, tri_out = decimate(nas_to_dict["Points"], tri_out) + f.write("solid Sheet_{}\n".format(tri_id)) + if enable_planar_merge == "Auto" and len(tri_out) > 50000: + enable_stl_merge = False # pragma: no cover + for triangle in tri_out: + _write_solid_stl(triangle, p_out) + f.write("endsolid\n") + for solidid, solid_triangles in assembly["Solids"].items(): + f.write("solid Solid_{}\n".format(solidid)) + import pandas as pd + + df = pd.Series(solid_triangles) + tri_out = df.drop_duplicates(keep=False).to_list() + p_out = nas_to_dict["Points"][::] + if decimation > 0 and len(solid_triangles) > 20: + p_out, tri_out = decimate(nas_to_dict["Points"], tri_out) + if enable_planar_merge == "Auto" and len(tri_out) > 50000: + enable_stl_merge = False # pragma: no cover + for triangle in tri_out: + _write_solid_stl(triangle, p_out) + f.write("endsolid\n") + f.close() + output_stls.append(output_stl) + logger.info("STL file created") + return output_stls, enable_stl_merge + + +@pyaedt_function_handler() +def nastran_to_stl(input_file, output_folder=None, decimation=0, enable_planar_merge="True", preview=False): + """Convert Nastran file into stl.""" + logger = logging.getLogger("Global") + nas_to_dict = _parse_nastran(input_file) + + empty = True + for assembly in nas_to_dict["Assemblies"].values(): + if assembly["Triangles"] or assembly["Solids"] or assembly["Lines"]: + empty = False + break + if empty: # pragma: no cover + logger.error("Failed to import file. Check the model and retry") + return False + if output_folder is None: + output_folder = os.path.dirname(input_file) + output_stls, enable_stl_merge = _write_stl(nas_to_dict, decimation, output_folder, enable_planar_merge) + if preview: + logger.info("Generating preview...") + if decimation > 0: + pl = pv.Plotter(shape=(1, 2)) + else: # pragma: no cover + pl = pv.Plotter() + dargs = dict(show_edges=True) + colors = [] + color_by_assembly = True + if len(nas_to_dict["Assemblies"]) == 1: + color_by_assembly = False + + def preview_pyvista(dict_in): + css4_colors = list(CSS4_COLORS.values()) + k = 0 + p_out = nas_to_dict["Points"][::] + for assembly in dict_in["Assemblies"].values(): + if color_by_assembly: + h = css4_colors[k].lstrip("#") + colors.append(tuple(int(h[i : i + 2], 16) for i in (0, 2, 4))) + k += 1 + + for triangles in assembly["Triangles"].values(): + tri_out = triangles + fin = [[3] + list(i) for i in tri_out] + if not color_by_assembly: + h = css4_colors[k].lstrip("#") + colors.append(tuple(int(h[i : i + 2], 16) for i in (0, 2, 4))) + k = k + 1 if k < len(css4_colors) - 1 else 0 + pl.add_mesh(pv.PolyData(p_out, faces=fin), color=colors[-1], **dargs) + + for triangles in assembly["Solids"].values(): + import pandas as pd + + df = pd.Series(triangles) + tri_out = df.drop_duplicates(keep=False).to_list() + p_out = nas_to_dict["Points"][::] + fin = [[3] + list(i) for i in tri_out] + if not color_by_assembly: + h = css4_colors[k].lstrip("#") + colors.append(tuple(int(h[i : i + 2], 16) for i in (0, 2, 4))) + k = k + 1 if k < len(css4_colors) - 1 else 0 + + pl.add_mesh(pv.PolyData(p_out, faces=fin), color=colors[-1], **dargs) + + preview_pyvista(nas_to_dict) + pl.add_text("Input mesh", font_size=24) + pl.reset_camera() + if decimation > 0 and output_stls: + k = 0 + pl.reset_camera() + pl.subplot(0, 1) + css4_colors = list(CSS4_COLORS.values()) + for output_stl in output_stls: + mesh = pv.read(output_stl) + h = css4_colors[k].lstrip("#") + colors.append(tuple(int(h[i : i + 2], 16) for i in (0, 2, 4))) + pl.add_mesh(mesh, color=colors[-1], **dargs) + k = k + 1 if k < len(css4_colors) - 1 else 0 + pl.add_text("Decimated mesh", font_size=24) + pl.reset_camera() + pl.link_views() + if "PYTEST_CURRENT_TEST" not in os.environ: + pl.show() # pragma: no cover + logger.info("STL files created") + return output_stls, nas_to_dict, enable_stl_merge + + +@pyaedt_function_handler() +def simplify_stl(input_file, output_file=None, decimation=0.5, preview=False): + """Import and simplify a stl file using pyvista and fast-simplification. + + Parameters + ---------- + input_file : str + Input stl file. + output_file : str, optional + Output stl file. + decimation : float, optional + Fraction of the original mesh to remove before creating the stl file. If set to ``0.9``, + this function will try to reduce the data set to 10% of its + original size and will remove 90% of the input triangles. + preview : bool, optional + Whether to preview the model in pyvista or skip it. + Returns + ------- + str + Full path to output stl. + """ + mesh = pv.read(input_file) + if not output_file: + output_file = os.path.splitext(input_file)[0] + "_output.stl" + simple = mesh.decimate_pro(decimation, preserve_topology=True, boundary_vertex_deletion=False) + simple.save(output_file) + if preview: + pl = pv.Plotter(shape=(1, 2)) + dargs = dict(show_edges=True, color=True) + pl.add_mesh(mesh, **dargs) + pl.add_text("Input mesh", font_size=24) + pl.reset_camera() + pl.subplot(0, 1) + pl.add_mesh(simple, **dargs) + pl.add_text("Decimated mesh", font_size=24) + pl.reset_camera() + pl.link_views() + if "PYTEST_CURRENT_TEST" not in os.environ: + pl.show() + return output_file diff --git a/src/ansys/aedt/core/visualization/advanced/sbrplus/__init__.py b/src/ansys/aedt/core/visualization/advanced/sbrplus/__init__.py new file mode 100644 index 00000000000..9c4476773da --- /dev/null +++ b/src/ansys/aedt/core/visualization/advanced/sbrplus/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/src/ansys/aedt/core/sbrplus/hdm_parser.py b/src/ansys/aedt/core/visualization/advanced/sbrplus/hdm_parser.py similarity index 99% rename from src/ansys/aedt/core/sbrplus/hdm_parser.py rename to src/ansys/aedt/core/visualization/advanced/sbrplus/hdm_parser.py index f175e402e00..6bb5121e35f 100644 --- a/src/ansys/aedt/core/sbrplus/hdm_parser.py +++ b/src/ansys/aedt/core/visualization/advanced/sbrplus/hdm_parser.py @@ -36,7 +36,7 @@ except ImportError: warnings.warn( "The NumPy module is required to run some functionalities of PostProcess.\n" - "Install with \n\npip install numpy\n\nRequires CPython." + "Install with \n\npip install numpy" ) from ansys.aedt.core.aedt_logger import pyaedt_logger diff --git a/src/ansys/aedt/core/sbrplus/hdm_utils.py b/src/ansys/aedt/core/visualization/advanced/sbrplus/hdm_utils.py similarity index 97% rename from src/ansys/aedt/core/sbrplus/hdm_utils.py rename to src/ansys/aedt/core/visualization/advanced/sbrplus/hdm_utils.py index 3de0ba527c5..beb52ec1dcf 100644 --- a/src/ansys/aedt/core/sbrplus/hdm_utils.py +++ b/src/ansys/aedt/core/visualization/advanced/sbrplus/hdm_utils.py @@ -27,7 +27,7 @@ def sort_bundle(bundle, monoPW_attrib="sweep_angle_index"): """ In-place sorting utility for hdm ray exports. - Ray exports are not guaranteed to always be in a predermined order, + Ray exports are not guaranteed to always be in a predetermined order, because of the non-deterministic multi-threaded SBR+ solver implementation. SBR+ rays will be sorted by source launch point, UTD bright point if present, diff --git a/src/ansys/aedt/core/sbrplus/matlab/HdmObject.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/HdmObject.m similarity index 100% rename from src/ansys/aedt/core/sbrplus/matlab/HdmObject.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/HdmObject.m diff --git a/src/ansys/aedt/core/sbrplus/matlab/README.md b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/README.md similarity index 100% rename from src/ansys/aedt/core/sbrplus/matlab/README.md rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/README.md diff --git a/src/ansys/aedt/core/sbrplus/matlab/SbrBounceType.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/SbrBounceType.m similarity index 100% rename from src/ansys/aedt/core/sbrplus/matlab/SbrBounceType.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/SbrBounceType.m diff --git a/src/ansys/aedt/core/sbrplus/matlab/StopWatch.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/StopWatch.m similarity index 99% rename from src/ansys/aedt/core/sbrplus/matlab/StopWatch.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/StopWatch.m index 170f944cacf..ef979c3b6d3 100644 --- a/src/ansys/aedt/core/sbrplus/matlab/StopWatch.m +++ b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/StopWatch.m @@ -89,4 +89,3 @@ function reset(obj) %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% end % StopWatch methods end % StopWatch class def - \ No newline at end of file diff --git a/src/ansys/aedt/core/sbrplus/matlab/add_3dlight.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/add_3dlight.m similarity index 100% rename from src/ansys/aedt/core/sbrplus/matlab/add_3dlight.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/add_3dlight.m diff --git a/src/ansys/aedt/core/sbrplus/matlab/amp2db.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/amp2db.m similarity index 100% rename from src/ansys/aedt/core/sbrplus/matlab/amp2db.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/amp2db.m diff --git a/src/ansys/aedt/core/sbrplus/matlab/draw_rays1.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/draw_rays1.m similarity index 99% rename from src/ansys/aedt/core/sbrplus/matlab/draw_rays1.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/draw_rays1.m index 0260b303c2e..f4a5b263884 100644 --- a/src/ansys/aedt/core/sbrplus/matlab/draw_rays1.m +++ b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/draw_rays1.m @@ -34,7 +34,7 @@ % real, imag, mag, power, dB, phase, DEFAULT = dB % % .ifreq (int) frequency index when coloring ray footprints and hit -% points by field levels, DEFAULT = 1, +% points by field levels, DEFAULT = 1, % % .lineWidth (dbl) width of 1D rays, DEFAULT = 1, set to zero (0) to % switch off render-by-line @@ -260,7 +260,7 @@ error(errIdBadInp,errMsg); end end - + else % paramVal not a char array, likely would have tripped up earlier switch % statement. If it's [R G B] numeric, we'll discover that here. @@ -650,7 +650,7 @@ function recurse_track(rb) % draw source point as red, only occurs when rendering points clr = red; return; - end + end if rb.hdmObj.bounce_type == SbrBounceType.UtdEdge % draw UTD diffraction point as orange clr = orange; @@ -675,7 +675,7 @@ function recurse_track(rb) return; end vec_field_func(rb); % gets E, H, J, or M, caches in bncFld external - field_comp_func(); % gets Fx, Fy, Fz, -Fx, -Fy, -Fz, or |F|^2 + field_comp_func(); % gets Fx, Fy, Fz, -Fx, -Fy, -Fz, or |F|^2 cdat = field_clrdat_func(); % real(F), imag(F), |F|, etc. end % fld_clrdat diff --git a/src/ansys/aedt/core/sbrplus/matlab/draw_wfobj.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/draw_wfobj.m similarity index 100% rename from src/ansys/aedt/core/sbrplus/matlab/draw_wfobj.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/draw_wfobj.m diff --git a/src/ansys/aedt/core/sbrplus/matlab/filter_rays1.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/filter_rays1.m similarity index 99% rename from src/ansys/aedt/core/sbrplus/matlab/filter_rays1.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/filter_rays1.m index 58bd4edb351..d1f1d40c56d 100644 --- a/src/ansys/aedt/core/sbrplus/matlab/filter_rays1.m +++ b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/filter_rays1.m @@ -223,7 +223,7 @@ function recurse_track(rb,rbParent,pushFunc,popFunc) if isa(popFunc,'function_handle'); popFunc(rb,rbParent); end - + end % recurse_track %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -430,20 +430,20 @@ function set_draw_branch(rb,rbParent) % This leaf bounce is terminal: either absorbed or reached max num. bounces. drawBranch = rDepth >= fltrCfg.NreflRng(1) && ... - rDepth <= fltrCfg.NreflRng(2); + rDepth <= fltrCfg.NreflRng(2); if ~drawBranch return; end drawBranch = tDepth >= fltrCfg.NtransRng(1) && ... - tDepth <= fltrCfg.NtransRng(2); + tDepth <= fltrCfg.NtransRng(2); if ~drawBranch return; end % leaf bounce survived all filters, so draw its branch drawBranch = true; - + end % draw_branch %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -688,7 +688,7 @@ function set_draw_flags(rb,rbParent) sweepAngIdxMap(idxSweepAng) = theta_phi_deg; end bundle.hdmObj.ray_tracks(itrack).source_bounce = rbSrc; - + else % Ray track already has source bounce. No need to create it. rbSrc = bundle.hdmObj.ray_tracks(itrack).source_bounce; diff --git a/src/ansys/aedt/core/sbrplus/matlab/filtered_tracks.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/filtered_tracks.m similarity index 100% rename from src/ansys/aedt/core/sbrplus/matlab/filtered_tracks.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/filtered_tracks.m diff --git a/src/ansys/aedt/core/sbrplus/matlab/ld_sbrplushdm.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/ld_sbrplushdm.m similarity index 98% rename from src/ansys/aedt/core/sbrplus/matlab/ld_sbrplushdm.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/ld_sbrplushdm.m index 281ffc68124..41f47ef6958 100644 --- a/src/ansys/aedt/core/sbrplus/matlab/ld_sbrplushdm.m +++ b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/ld_sbrplushdm.m @@ -83,7 +83,7 @@ % % All info needed to parse the binary HDM data block is contained in the % header as a Python dictionary. - grammar = struct(); + grammar = struct(); grammar.basic_types = ["int", "float", "enum", "flag", "complex"]; try grammar.header = parseHeader(fid); @@ -129,7 +129,7 @@ how_many = 1; hdm.hdm = parse(fid, grammar, grammar.header.message.type, how_many); end - + catch exception stop_prog_monitor(); fclose(fid); @@ -154,11 +154,11 @@ % parses the header of an already opened hdm export file % % removes content lines starting with '#', parses the header as a -% Python dictionary, and converts it to Matlab format -% +% Python dictionary, and converts it to Matlab format +% % Input Params: % fid (int) file ID from fopen function referring to an hdm file -% +% % Returns: % header (struct) content of the hdm header in struct format @@ -190,12 +190,12 @@ % int -> int64 % float -> double % otherwise -> cell array -% +% % Input Params: % python_data (any) python data format -% +% % Returns: -% matlab_data (any) converted python data +% matlab_data (any) converted python data if isequal(python_data, py.dict) matlab_data = struct(python_data); fields = fieldnames(matlab_data); @@ -225,11 +225,11 @@ % PARSE base parse factory for binary data % % Must call after the header has been parsed already. -% +% % Input Params: % fid (int) file ID from fopen function referring to an hdm file % grammar (struct) data structure describing how to parse the message, -% see parseHeader +% see parseHeader % typename (str) name of a type to parse, must be represented in grammar % how_many (int) number of objects or basic types to parse, ignored when % type associated with typename is vector or list, set to @@ -268,11 +268,11 @@ % typename, recovers its layout from the grammar, and parses all the object % fields. If an object field is optional and not parsed, it is inserted in % the struct as []. -% +% % Input Params: % fid (int) file ID from fopen function referring to an hdm file % grammar (struct) data structure describing how to parse the message, -% see parseHeader +% see parseHeader % typename (str) name of a type to parse, must be represented in grammar % how_many (int) number of object instances to parse % @@ -342,11 +342,11 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function content = parse_array(fid, grammar, typename, base, size) % parse method to parse a vector or list (repeated message) from binary data. -% +% % Input Params: % fid (int) file ID from fopen function referring to an hdm file % grammar (struct) data structure describing how to parse the message, -% see parseHeader +% see parseHeader % typename (str) vector or list. % base (str) underlying type for the repeated message % size (str) number of repeated messages @@ -385,11 +385,11 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function content = parse_basic(fid, grammar, typename, how_many) % parse method to parse a basic type (int, float, enum, flag, complex) -% +% % Input Params: % fid (int) file ID from fopen function referring to an hdm file % grammar (struct) data structure describing how to parse the message, -% see parseHeader +% see parseHeader % typename (str) int, enum, flag, float or complex. % how_many (int) number of basic types to read from binary data % @@ -473,12 +473,12 @@ function start_prog_monitor(showProg,fname,progPeriod,minFileSize) error('ld_sbrplushdm:badInput', ... 'Unrecognized string input to showProg argument: %s',showProg); end - + elseif islogical(showProg) % show progress in graphical progress bar LD_PROG_MON.showProg = showProg; LD_PROG_MON.doProgBar = true; -end +end if ~LD_PROG_MON.showProg % no progress monitoring requested diff --git a/src/ansys/aedt/core/sbrplus/matlab/ld_wfobj.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/ld_wfobj.m similarity index 99% rename from src/ansys/aedt/core/sbrplus/matlab/ld_wfobj.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/ld_wfobj.m index 6f95415ba04..c0e6105975e 100644 --- a/src/ansys/aedt/core/sbrplus/matlab/ld_wfobj.m +++ b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/ld_wfobj.m @@ -392,7 +392,7 @@ function next_part_block() end end fclose(fid); - + end % ld_mtlfile @@ -423,12 +423,12 @@ function next_part_block() buff = fgets(fid); continue; end - + idx = find(buff == '#',1); if isempty(idx) return; % return with active line end - + % remove content after '#' if idx == 1 % effectively a blank line, get the next one diff --git a/src/ansys/aedt/core/sbrplus/matlab/pwr2db.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/pwr2db.m similarity index 100% rename from src/ansys/aedt/core/sbrplus/matlab/pwr2db.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/pwr2db.m diff --git a/src/ansys/aedt/core/sbrplus/matlab/validate_sfields.m b/src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/validate_sfields.m similarity index 100% rename from src/ansys/aedt/core/sbrplus/matlab/validate_sfields.m rename to src/ansys/aedt/core/visualization/advanced/sbrplus/matlab/validate_sfields.m diff --git a/src/ansys/aedt/core/generic/touchstone_parser.py b/src/ansys/aedt/core/visualization/advanced/touchstone_parser.py similarity index 93% rename from src/ansys/aedt/core/generic/touchstone_parser.py rename to src/ansys/aedt/core/visualization/advanced/touchstone_parser.py index 52ec2db60be..93136f8f670 100644 --- a/src/ansys/aedt/core/generic/touchstone_parser.py +++ b/src/ansys/aedt/core/visualization/advanced/touchstone_parser.py @@ -28,15 +28,11 @@ import re import subprocess -from ansys.aedt.core.generic.general_methods import is_ironpython -from ansys.aedt.core.misc.misc import installed_versions - -if not is_ironpython: - import matplotlib.pyplot as plt - import numpy as np - import skrf as rf - from ansys.aedt.core.generic.general_methods import pyaedt_function_handler +from ansys.aedt.core.misc.misc import installed_versions +import matplotlib.pyplot as plt +import numpy as np +import skrf as rf REAL_IMAG = "RI" MAG_ANGLE = "MA" @@ -203,37 +199,13 @@ def plot_return_losses(self): # pragma: no cover def get_mixed_mode_touchstone_data(self, num_of_diff_ports=None, port_ordering="1234"): """Transform network from single ended parameters to generalized mixed mode parameters. - Example 1, an N-port single-ended network with port order 1234 is converted to mixed-mode - parameters. - - A B - +------------+ +-----------+ - 0-|s0========s2|-2 0-|d0=======d1|-1 - 1-|s1========s3|-3 2-|d2=======d3|-3 - ... ... =se2gmm=> ... ... - 2N-4-|s2N-4==s2N-2|-2N-2 2N-4-|cN-4===cN-3|-2N-3 - 2N-3-|s2N-3==s2N-1|-2N-1 2N-2-|cN-2===cN-1|-2N-1 - +------------+ +-----------+ - - Example 2, an N-port single-ended network with port order 1324 is converted to mixed-mode - parameters. - - A B - +------------+ +-----------+ - 0-|s0========s2|-1 0-|d0=======d1|-1 - 2-|s1========s3|-3 2-|d2=======d3|-3 - ... ... =se2gmm=> ... ... - 2N-4-|s2N-4==s2N-2|-2N-3 2N-4-|cN-4===cN-3|-2N-3 - 2N-2-|s2N-3==s2N-1|-2N-1 2N-2-|cN-2===cN-1|-2N-1 - +------------+ +-----------+ - Parameters ---------- num_of_diff_ports : int, optional The number of differential ports. port_ordering : str, optional The current port ordering. Options are ``"1234"``, ``"1324"``. The default - is ``1234`` + is ``1234``. Returns ------- diff --git a/src/ansys/aedt/core/visualization/plot/__init__.py b/src/ansys/aedt/core/visualization/plot/__init__.py new file mode 100644 index 00000000000..9c4476773da --- /dev/null +++ b/src/ansys/aedt/core/visualization/plot/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/src/ansys/aedt/core/visualization/plot/matplotlib.py b/src/ansys/aedt/core/visualization/plot/matplotlib.py new file mode 100644 index 00000000000..21011d5a120 --- /dev/null +++ b/src/ansys/aedt/core/visualization/plot/matplotlib.py @@ -0,0 +1,471 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import ast +import warnings + +from ansys.aedt.core.aedt_logger import pyaedt_logger +from ansys.aedt.core.generic.general_methods import pyaedt_function_handler + +try: + import numpy as np +except ImportError: + warnings.warn( + "The NumPy module is required to run some functionalities of PostProcess.\n" + "Install with \n\npip install numpy" + ) + +try: + from matplotlib.patches import PathPatch + from matplotlib.path import Path + import matplotlib.pyplot as plt + + rc_params = { + "axes.titlesize": 26, # Use these default settings for Matplotlb axes. + "axes.labelsize": 20, # Apply the settings only in this module. + "xtick.labelsize": 18, + "ytick.labelsize": 18, + } + + plt.ioff() + default_rc_params = plt.rcParams.copy() + plt.rcParams.update(rc_params) + +except ImportError: + warnings.warn( + "The Matplotlib module is required to run some functionalities of PostProcess.\n" + "Install with \n\npip install matplotlib\n\nRequires CPython." + ) +except Exception: + warnings.warn("Unknown error occurred while attempting to import Matplotlib.") + + +def is_notebook(): + """Check if pyaedt is running in Jupyter or not. + + Returns + ------- + bool + """ + try: + shell = get_ipython().__class__.__name__ + if shell == "ZMQInteractiveShell": + return True # Jupyter notebook or qtconsole + else: + return False + except NameError: + return False # Probably standard Python interpreter + + +@pyaedt_function_handler() +def plot_polar_chart( + plot_data, size=(2000, 1000), show_legend=True, xlabel="", ylabel="", title="", snapshot_path=None, show=True +): + """Create a Matplotlib polar plot based on a list of data. + + Parameters + ---------- + plot_data : list of list + List of plot data. Every item has to be in the following format + ``[x points, y points, label]``. + size : tuple, optional + Image size in pixel (width, height). + show_legend : bool + Either to show legend or not. + xlabel : str + Plot X label. + ylabel : str + Plot Y label. + title : str + Plot title label. + snapshot_path : str + Full path to the image file if a snapshot is needed. + show : bool, optional + Whether to render the figure. The default is ``True``. If ``False``, the + figure is not drawn. + + Returns + ------- + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. + """ + dpi = 100.0 + fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) + + label_id = 1 + legend = [] + for plot_object in plot_data: + if len(plot_object) == 3: + label = plot_object[2] + else: + label = "Trace " + str(label_id) + theta = np.array(plot_object[0]) + r = np.array(plot_object[1]) + ax.plot(theta, r) + ax.grid(True) + ax.set_theta_zero_location("N") + ax.set_theta_direction(-1) + legend.append(label) + label_id += 1 + + ax.set(xlabel=xlabel, ylabel=ylabel, title=title) + if show_legend: + ax.legend(legend) + + # fig = plt.gcf() + fig.set_size_inches(size[0] / dpi, size[1] / dpi) + if snapshot_path: + fig.savefig(snapshot_path) + if show: # pragma: no cover + fig.show() + plt.rcParams.update(default_rc_params) + return fig + + +@pyaedt_function_handler() +def plot_3d_chart(plot_data, size=(2000, 1000), xlabel="", ylabel="", title="", snapshot_path=None, show=True): + """Create a Matplotlib 3D plot based on a list of data. + + Parameters + ---------- + plot_data : list of list + List of plot data. Every item has to be in the following format + ``[x points, y points, z points, label]``. + size : tuple, optional + Image size in pixel (width, height). + xlabel : str, optional + Plot X label. + ylabel : str, optional + Plot Y label. + title : str, optional + Plot Title label. + snapshot_path : str, optional + Full path to image file if a snapshot is needed. + show : bool, optional + Whether to show the plot or return the matplotlib object. Default is `True`. + + Returns + ------- + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. + """ + dpi = 100.0 + fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) + fig.set_size_inches(size[0] / dpi, size[1] / dpi) + if isinstance(plot_data[0], np.ndarray): + x = plot_data[0] + y = plot_data[1] + z = plot_data[2] + else: + theta_grid, phi_grid = np.meshgrid(plot_data[0], plot_data[1]) + if isinstance(plot_data[2], list): + r = np.array(plot_data[2]) + else: + r = plot_data[2] + x = r * np.sin(theta_grid) * np.cos(phi_grid) + y = r * np.sin(theta_grid) * np.sin(phi_grid) + z = r * np.cos(theta_grid) + + ax.set_xlabel(xlabel, labelpad=20) + ax.set_ylabel(ylabel, labelpad=20) + ax.set_title(title) + ax.plot_surface(x, y, z, rstride=1, cstride=1, cmap=plt.get_cmap("jet"), linewidth=0, antialiased=True, alpha=0.8) + + if snapshot_path: + fig.savefig(snapshot_path) + if show: # pragma: no cover + fig.show() + plt.rcParams.update(default_rc_params) + return fig + + +@pyaedt_function_handler() +def plot_2d_chart( + plot_data, size=(2000, 1000), show_legend=True, xlabel="", ylabel="", title="", snapshot_path=None, show=True +): + """Create a Matplotlib figure based on a list of data. + + Parameters + ---------- + plot_data : list of list + List of plot data. Every item has to be in the following format + `[x points, y points, label]`. + size : tuple, optional + Image size in pixel (width, height). The default is `(2000,1600)`. + show_legend : bool, optional + Either to show legend or not. The default value is ``True``. + xlabel : str, optional + Plot X label. The default value is ``""``. + ylabel : str, optional + Plot Y label. The default value is ``""``. + title : str, optional + Plot Title label. The default value is ``""``. + snapshot_path : str, optional + Full path to image file if a snapshot is needed. + The default value is ``None``. + show : bool, optional + Whether to show the plot or return the matplotlib object. Default is `True`. + + Returns + ------- + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. + """ + dpi = 100.0 + fig, ax = plt.subplots() + fig.set_size_inches(size[0] / dpi, size[1] / dpi) + label_id = 1 + for plo_obj in plot_data: + if isinstance(plo_obj[0], np.ndarray): + x = plo_obj[0] + y = plo_obj[1] + else: + x = np.array([i for i, j in zip(plo_obj[0], plo_obj[1]) if j]) + y = np.array([i for i in plo_obj[1] if i]) + label = "Plot {}".format(str(label_id)) + if len(plo_obj) > 2: + label = plo_obj[2] + ax.plot(x, y, label=label) + label_id += 1 + + ax.set(xlabel=xlabel, ylabel=ylabel, title=title) + if show_legend: + ax.legend() + + if snapshot_path: + fig.savefig(snapshot_path) + elif show and not is_notebook(): # pragma: no cover + fig.show() + plt.rcParams.update(default_rc_params) + return fig + + +@pyaedt_function_handler() +def plot_matplotlib( + plot_data, + size=(2000, 1000), + show_legend=True, + xlabel="", + ylabel="", + title="", + snapshot_path=None, + x_limits=None, + y_limits=None, + axis_equal=False, + annotations=None, + show=True, +): # pragma: no cover + """Create a matplotlib plot based on a list of data. + + Parameters + ---------- + plot_data : list of list + List of plot data. Every item has to be in the following format + For type ``fill``: `[x points, y points, color, label, alpha, type=="fill"]`. + For type ``path``: `[vertices, codes, color, label, alpha, type=="path"]`. + For type ``contour``: `[vertices, codes, color, label, alpha, line_width, type=="contour"]`. + size : tuple, optional + Image size in pixel (width, height). Default is `(2000, 1000)`. + show_legend : bool, optional + Either to show legend or not. Default is `True`. + xlabel : str, optional + Plot X label. Default is `""`. + ylabel : str, optional + Plot Y label. Default is `""`. + title : str, optional + Plot Title label. Default is `""`. + snapshot_path : str, optional + Full path to image file if a snapshot is needed. Default is `None`. + x_limits : list, optional + List of x limits (left and right). Default is `None`. + y_limits : list, optional + List of y limits (bottom and top). Default is `None`. + axis_equal : bool, optional + Whether to show the same scale on both axis or have a different scale based on plot size. + Default is `False`. + annotations : list, optional + List of annotations to add to the plot. The format is [x, y, string, dictionary of font options]. + Default is `None`. + show : bool, optional + Whether to show the plot or return the matplotlib object. Default is `True`. + + Returns + ------- + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. + """ + dpi = 100.0 + fig, ax = plt.subplots() + fig.set_size_inches(size[0] / dpi, size[1] / dpi) + + if isinstance(plot_data, str): + plot_data = ast.literal_eval(plot_data) + for points in plot_data: + if points[-1] == "fill": + plt.fill(points[0], points[1], c=points[2], label=points[3], alpha=points[4]) + elif points[-1] == "path": + path = Path(points[0], points[1]) + patch = PathPatch(path, color=points[2], alpha=points[4], label=points[3]) + ax.add_patch(patch) + elif points[-1] == "contour": + path = Path(points[0], points[1]) + patch = PathPatch(path, color=points[2], alpha=points[4], label=points[3], fill=False, linewidth=points[5]) + ax.add_patch(patch) + + ax.set(xlabel=xlabel, ylabel=ylabel, title=title) + if show_legend: + ax.legend(loc="upper right") + + # evaluating the limits + xmin = ymin = 1e30 + xmax = ymax = -1e30 + for points in plot_data: + if points[-1] == "fill": + xmin = min(xmin, min(points[0])) + xmax = max(xmax, max(points[0])) + ymin = min(ymin, min(points[1])) + ymax = max(ymax, max(points[1])) + else: + for p in points[0]: + xmin = min(xmin, p[0]) + xmax = max(xmax, p[0]) + ymin = min(ymin, p[1]) + ymax = max(ymax, p[1]) + if x_limits: + ax.set_xlim(x_limits) + else: + ax.set_xlim([xmin, xmax]) + if y_limits: + ax.set_ylim(y_limits) + else: + ax.set_ylim([ymin, ymax]) + + if axis_equal: + ax.axis("equal") + + if annotations: + for annotation in annotations: + plt.text(annotation[0], annotation[1], annotation[2], **annotation[3]) + + if snapshot_path: + plt.savefig(snapshot_path) + if show: # pragma: no cover + plt.show() + plt.rcParams.update(default_rc_params) + return fig + + +@pyaedt_function_handler() +def plot_contour( + plot_data, + size=(2000, 1600), + xlabel="", + ylabel="", + title="", + polar=False, + levels=64, + max_theta=180, + color_bar=None, + snapshot_path=None, + show=True, +): + """Create a Matplotlib figure contour based on a list of data. + + Parameters + ---------- + plot_data : list of np.ndarray + List of plot data. Each item of the list a numpy array. The list has the following format: + ``[data, x points, y points]``. + size : tuple, list, optional + Image size in pixel (width, height). The default is ``(2000,1600)``. + xlabel : str, optional + Plot X label. The default value is ``""``. + ylabel : str, optional + Plot Y label. The default value is ``""``. + title : str, optional + Plot Title label. The default value is ``""``. + polar : bool, optional + Generate the plot in polar coordinates. The default is ``True``. If ``False``, the plot + generated is rectangular. + levels : int, optional + Color map levels. The default is ``64``. + max_theta : float or int, optional + Maximum theta angle for plotting. It applies only for polar plots. + The default is ``180``, which plots the data for all angles. + Setting ``max_theta`` to 90 limits the displayed data to the upper + hemisphere, that is (0 < theta < 90). + color_bar : str, optional + Color bar title. The default is ``None`` in which case the color bar is not included. + snapshot_path : str, optional + Full path to image file if a snapshot is needed. + The default value is ``None``. + show : bool, optional + Whether to show the plot or return the matplotlib object. Default is ``True``. + + Returns + ------- + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. + """ + + dpi = 100.0 + figsize = (size[0] / dpi, size[1] / dpi) + + projection = "polar" if polar else "rectilinear" + fig, ax = plt.subplots(figsize=figsize, subplot_kw={"projection": projection}) + + ax.set_xlabel(xlabel) + if polar: + ax.set_rticks(np.linspace(0, max_theta, 3)) + else: + ax.set_ylabel(ylabel) + + ax.set(xlabel=xlabel, ylabel=ylabel, title=title) + if len(plot_data) != 3: # pragma: no cover + pyaedt_logger.error("Input should contain 3 numpy arrays.") + return False + ph = plot_data[2] + th = plot_data[1] + data_to_plot = plot_data[0] + plt.contourf( + ph, + th, + data_to_plot, + levels=levels, + cmap="jet", + ) + + if color_bar: + cbar = plt.colorbar() + cbar.set_label(color_bar, rotation=270, labelpad=20) + + ax = plt.gca() + ax.yaxis.set_label_coords(-0.1, 0.5) + + if snapshot_path: + fig.savefig(snapshot_path) + if show: # pragma: no cover + fig.show() + plt.rcParams.update(default_rc_params) + return fig diff --git a/src/ansys/aedt/core/generic/pdf.py b/src/ansys/aedt/core/visualization/plot/pdf.py similarity index 98% rename from src/ansys/aedt/core/generic/pdf.py rename to src/ansys/aedt/core/visualization/plot/pdf.py index 196929e72a5..a16a4c6b3f8 100644 --- a/src/ansys/aedt/core/generic/pdf.py +++ b/src/ansys/aedt/core/visualization/plot/pdf.py @@ -41,9 +41,9 @@ class ReportSpec: document_prefix: str = "ANSS" ansys_version: str = "2023R2" revision: str = "Rev 1.0" - logo_name: str = os.path.join(os.path.dirname(__file__), "Ansys.png") + logo_name: str = os.path.join(os.path.dirname(__file__), "../../generic/Ansys.png") company_name: str = "Ansys Inc." - template_name: str = os.path.join(os.path.dirname(__file__), "AnsysTemplate.json") + template_name: str = os.path.join(os.path.dirname(__file__), "../../generic/AnsysTemplate.json") design_name: str = "Design1" project_name: str = "Project1" pyaedt_version: str = __version__ @@ -93,7 +93,7 @@ def __init__(self, version="2023R2", design_name="design1", project_name="AnsysP self.alias_nb_pages() def read_template(self, template_file): - """Reade pdf template + """Reade pdf template. template_file : str Path to the json template file. @@ -137,6 +137,7 @@ def __add_cover_page(self): ) def header(self): + """Header.""" from datetime import date def add_field(field_name, field_value): @@ -205,6 +206,7 @@ def add_field(field_name, field_value): # Page footer def footer(self): + """Footer.""" # Position at 1.5 cm from bottom self.set_y(-15) self.set_x(self.l_margin) @@ -220,7 +222,7 @@ def create(self, add_cover_page=True, add_new_section_after=True): Parameters ---------- - add_cover_page :bool, optional + add_cover_page : bool, optional Whether to add cover page or not. Default is ``True``. add_new_section_after : bool, optional Whether if add a new section after the cover page or not. @@ -237,6 +239,7 @@ def create(self, add_cover_page=True, add_new_section_after=True): def add_project_info(self, design): """ + Add project information. Parameters ---------- @@ -491,11 +494,7 @@ def add_empty_line(self, num_lines=1): self.ln(num_lines * self.font_size) def add_page_break(self): - """Add a new page break line. - - Parameters - ---------- - """ + """Add a new page break line.""" self.add_page("P" if self.use_portrait else "L") def add_table( @@ -580,8 +579,10 @@ def add_text(self, content, bold=False, italic=False): ) def add_toc(self): + """Add toc.""" + def p(section, **kwargs): - "Inserts a paragraph" + # Inserts a paragraph self.cell(w=self.epw, h=self.font_size, text=section, new_x="LMARGIN", new_y="NEXT", **kwargs) self.add_page("P" if self.use_portrait else "L") diff --git a/src/ansys/aedt/core/generic/plot.py b/src/ansys/aedt/core/visualization/plot/pyvista.py similarity index 79% rename from src/ansys/aedt/core/generic/plot.py rename to src/ansys/aedt/core/visualization/plot/pyvista.py index 0997abf294d..d942f724a14 100644 --- a/src/ansys/aedt/core/generic/plot.py +++ b/src/ansys/aedt/core/visualization/plot/pyvista.py @@ -22,7 +22,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import ast from collections import defaultdict import csv from datetime import datetime @@ -35,67 +34,26 @@ from ansys.aedt.core.aedt_logger import pyaedt_logger from ansys.aedt.core.generic.constants import AEDT_UNITS from ansys.aedt.core.generic.constants import CSS4_COLORS -from ansys.aedt.core.generic.general_methods import is_ironpython from ansys.aedt.core.generic.general_methods import open_file from ansys.aedt.core.generic.general_methods import pyaedt_function_handler -if not is_ironpython: - try: - import numpy as np - except ImportError: - warnings.warn( - "The NumPy module is required to run some functionalities of PostProcess.\n" - "Install with \n\npip install numpy\n\nRequires CPython." - ) - - try: - import pyvista as pv - - pyvista_available = True - except ImportError: - warnings.warn( - "The PyVista module is required to run some functionalities of PostProcess.\n" - "Install with \n\npip install pyvista\n\nRequires CPython." - ) - - try: - from matplotlib.patches import PathPatch - from matplotlib.path import Path - import matplotlib.pyplot as plt - - rc_params = { - "axes.titlesize": 26, # Use these default settings for Matplotlb axes. - "axes.labelsize": 20, # Apply the settings only in this module. - "xtick.labelsize": 18, - "ytick.labelsize": 18, - } - - except ImportError: - warnings.warn( - "The Matplotlib module is required to run some functionalities of PostProcess.\n" - "Install with \n\npip install matplotlib\n\nRequires CPython." - ) - except Exception: - warnings.warn("Unknown error occurred while attempting to import Matplotlib.") - - -# Override default settings for matplotlib -def update_plot_settings(func, *args, **kwargs): - if callable(func): +try: + import numpy as np +except ImportError: + warnings.warn( + "The NumPy module is required to run some functionalities of PostProcess.\n" + "Install with \n\npip install numpy" + ) - def wrapper(*args, **kwargs): - # Turn off interactive mode - plt.ioff() - default_rc_params = plt.rcParams.copy() - plt.rcParams.update(rc_params) # Apply new settings. - out = func(*args, **kwargs) - plt.rcParams.update(default_rc_params) - return out +try: + import pyvista as pv - else: - wrapper = None - raise TypeError("First argument must be callable.") - return wrapper + pyvista_available = True +except ImportError: + warnings.warn( + "The PyVista module is required to run some functionalities of PostProcess.\n" + "Install with \n\npip install pyvista" + ) @pyaedt_function_handler() @@ -344,399 +302,6 @@ def _parse_streamline(filepath): return streamlines -@pyaedt_function_handler() -@update_plot_settings -def plot_polar_chart( - plot_data, size=(2000, 1000), show_legend=True, xlabel="", ylabel="", title="", snapshot_path=None, show=True -): - """Create a Matplotlib polar plot based on a list of data. - - Parameters - ---------- - plot_data : list of list - List of plot data. Every item has to be in the following format - `[x points, y points, label]`. - size : tuple, optional - Image size in pixel (width, height). - show_legend : bool - Either to show legend or not. - xlabel : str - Plot X label. - ylabel : str - Plot Y label. - title : str - Plot title label. - snapshot_path : str - Full path to the image file if a snapshot is needed. - show : bool, optional - Whether to render the figure. The default is ``True``. If ``False``, the - figure is not drawn. - - Returns - ------- - :class:`matplotlib.pyplot.Figure` - Matplotlib figure object. - """ - dpi = 100.0 - fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) - - label_id = 1 - legend = [] - for plot_object in plot_data: - if len(plot_object) == 3: - label = plot_object[2] - else: - label = "Trace " + str(label_id) - theta = np.array(plot_object[0]) - r = np.array(plot_object[1]) - ax.plot(theta, r) - ax.grid(True) - ax.set_theta_zero_location("N") - ax.set_theta_direction(-1) - legend.append(label) - label_id += 1 - - ax.set(xlabel=xlabel, ylabel=ylabel, title=title) - if show_legend: - ax.legend(legend) - - # fig = plt.gcf() - fig.set_size_inches(size[0] / dpi, size[1] / dpi) - if snapshot_path: - fig.savefig(snapshot_path) - if show: # pragma: no cover - fig.show() - return fig - - -@pyaedt_function_handler() -@update_plot_settings -def plot_3d_chart(plot_data, size=(2000, 1000), xlabel="", ylabel="", title="", snapshot_path=None, show=True): - """Create a Matplotlib 3D plot based on a list of data. - - Parameters - ---------- - plot_data : list of list - List of plot data. Every item has to be in the following format - `[x points, y points, z points, label]`. - size : tuple, optional - Image size in pixel (width, height). - xlabel : str, optional - Plot X label. - ylabel : str, optional - Plot Y label. - title : str, optional - Plot Title label. - snapshot_path : str, optional - Full path to image file if a snapshot is needed. - show : bool, optional - Whether to show the plot or return the matplotlib object. Default is `True`. - - Returns - ------- - :class:`matplotlib.pyplot.Figure` - Matplotlib figure object. - """ - dpi = 100.0 - fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) - fig.set_size_inches(size[0] / dpi, size[1] / dpi) - if isinstance(plot_data[0], np.ndarray): - x = plot_data[0] - y = plot_data[1] - z = plot_data[2] - else: - theta_grid, phi_grid = np.meshgrid(plot_data[0], plot_data[1]) - if isinstance(plot_data[2], list): - r = np.array(plot_data[2]) - else: - r = plot_data[2] - x = r * np.sin(theta_grid) * np.cos(phi_grid) - y = r * np.sin(theta_grid) * np.sin(phi_grid) - z = r * np.cos(theta_grid) - - ax.set_xlabel(xlabel, labelpad=20) - ax.set_ylabel(ylabel, labelpad=20) - ax.set_title(title) - ax.plot_surface(x, y, z, rstride=1, cstride=1, cmap=plt.get_cmap("jet"), linewidth=0, antialiased=True, alpha=0.8) - - if snapshot_path: - fig.savefig(snapshot_path) - if show: # pragma: no cover - fig.show() - return fig - - -@pyaedt_function_handler() -@update_plot_settings -def plot_2d_chart( - plot_data, size=(2000, 1000), show_legend=True, xlabel="", ylabel="", title="", snapshot_path=None, show=True -): - """Create a Matplotlib figure based on a list of data. - - Parameters - ---------- - plot_data : list of list - List of plot data. Every item has to be in the following format - `[x points, y points, label]`. - size : tuple, optional - Image size in pixel (width, height). The default is `(2000,1600)`. - show_legend : bool, optional - Either to show legend or not. The default value is ``True``. - xlabel : str, optional - Plot X label. The default value is ``""``. - ylabel : str, optional - Plot Y label. The default value is ``""``. - title : str, optional - Plot Title label. The default value is ``""``. - snapshot_path : str, optional - Full path to image file if a snapshot is needed. - The default value is ``None``. - show : bool, optional - Whether to show the plot or return the matplotlib object. Default is `True`. - - Returns - ------- - :class:`matplotlib.pyplot.Figure` - Matplotlib figure object. - """ - dpi = 100.0 - fig, ax = plt.subplots() - fig.set_size_inches(size[0] / dpi, size[1] / dpi) - label_id = 1 - for plo_obj in plot_data: - if isinstance(plo_obj[0], np.ndarray): - x = plo_obj[0] - y = plo_obj[1] - else: - x = np.array([i for i, j in zip(plo_obj[0], plo_obj[1]) if j]) - y = np.array([i for i in plo_obj[1] if i]) - label = "Plot {}".format(str(label_id)) - if len(plo_obj) > 2: - label = plo_obj[2] - ax.plot(x, y, label=label) - label_id += 1 - - ax.set(xlabel=xlabel, ylabel=ylabel, title=title) - if show_legend: - ax.legend() - - if snapshot_path: - fig.savefig(snapshot_path) - elif show and not is_notebook(): # pragma: no cover - fig.show() - return fig - - -@pyaedt_function_handler() -@update_plot_settings -def plot_matplotlib( - plot_data, - size=(2000, 1000), - show_legend=True, - xlabel="", - ylabel="", - title="", - snapshot_path=None, - x_limits=None, - y_limits=None, - axis_equal=False, - annotations=None, - show=True, -): # pragma: no cover - """Create a matplotlib plot based on a list of data. - - Parameters - ---------- - plot_data : list of list - List of plot data. Every item has to be in the following format - For type ``fill``: `[x points, y points, color, label, alpha, type=="fill"]`. - For type ``path``: `[vertices, codes, color, label, alpha, type=="path"]`. - For type ``contour``: `[vertices, codes, color, label, alpha, line_width, type=="contour"]`. - size : tuple, optional - Image size in pixel (width, height). Default is `(2000, 1000)`. - show_legend : bool, optional - Either to show legend or not. Default is `True`. - xlabel : str, optional - Plot X label. Default is `""`. - ylabel : str, optional - Plot Y label. Default is `""`. - title : str, optional - Plot Title label. Default is `""`. - snapshot_path : str, optional - Full path to image file if a snapshot is needed. Default is `None`. - x_limits : list, optional - List of x limits (left and right). Default is `None`. - y_limits : list, optional - List of y limits (bottom and top). Default is `None`. - axis_equal : bool, optional - Whether to show the same scale on both axis or have a different scale based on plot size. - Default is `False`. - annotations : list, optional - List of annotations to add to the plot. The format is [x, y, string, dictionary of font options]. - Default is `None`. - show : bool, optional - Whether to show the plot or return the matplotlib object. Default is `True`. - - Returns - ------- - :class:`matplotlib.pyplot.Figure` - Matplotlib figure object. - """ - dpi = 100.0 - fig, ax = plt.subplots() - fig.set_size_inches(size[0] / dpi, size[1] / dpi) - - if isinstance(plot_data, str): - plot_data = ast.literal_eval(plot_data) - for points in plot_data: - if points[-1] == "fill": - plt.fill(points[0], points[1], c=points[2], label=points[3], alpha=points[4]) - elif points[-1] == "path": - path = Path(points[0], points[1]) - patch = PathPatch(path, color=points[2], alpha=points[4], label=points[3]) - ax.add_patch(patch) - elif points[-1] == "contour": - path = Path(points[0], points[1]) - patch = PathPatch(path, color=points[2], alpha=points[4], label=points[3], fill=False, linewidth=points[5]) - ax.add_patch(patch) - - ax.set(xlabel=xlabel, ylabel=ylabel, title=title) - if show_legend: - ax.legend(loc="upper right") - - # evaluating the limits - xmin = ymin = 1e30 - xmax = ymax = -1e30 - for points in plot_data: - if points[-1] == "fill": - xmin = min(xmin, min(points[0])) - xmax = max(xmax, max(points[0])) - ymin = min(ymin, min(points[1])) - ymax = max(ymax, max(points[1])) - else: - for p in points[0]: - xmin = min(xmin, p[0]) - xmax = max(xmax, p[0]) - ymin = min(ymin, p[1]) - ymax = max(ymax, p[1]) - if x_limits: - ax.set_xlim(x_limits) - else: - ax.set_xlim([xmin, xmax]) - if y_limits: - ax.set_ylim(y_limits) - else: - ax.set_ylim([ymin, ymax]) - - if axis_equal: - ax.axis("equal") - - if annotations: - for annotation in annotations: - plt.text(annotation[0], annotation[1], annotation[2], **annotation[3]) - - if snapshot_path: - plt.savefig(snapshot_path) - if show: # pragma: no cover - plt.show() - return fig - - -@pyaedt_function_handler() -@update_plot_settings -def plot_contour( - plot_data, - size=(2000, 1600), - xlabel="", - ylabel="", - title="", - polar=False, - levels=64, - max_theta=180, - color_bar=None, - snapshot_path=None, - show=True, -): - """Create a Matplotlib figure contour based on a list of data. - - Parameters - ---------- - plot_data : list of np.ndarray - List of plot data. Each item of the list a numpy array. The list has the following format: - `[data, x points, y points]`. - size : tuple, list, optional - Image size in pixel (width, height). The default is `(2000,1600)`. - xlabel : str, optional - Plot X label. The default value is ``""``. - ylabel : str, optional - Plot Y label. The default value is ``""``. - title : str, optional - Plot Title label. The default value is ``""``. - polar : bool, optional - Generate the plot in polar coordinates. The default is ``True``. If ``False``, the plot - generated is rectangular. - levels : int, optional - Color map levels. The default is ``64``. - max_theta : float or int, optional - Maximum theta angle for plotting. It applies only for polar plots. - The default is ``180``, which plots the data for all angles. - Setting ``max_theta`` to 90 limits the displayed data to the upper - hemisphere, that is (0 < theta < 90). - color_bar : str, optional - Color bar title. The default is ``None`` in which case the color bar is not included. - snapshot_path : str, optional - Full path to image file if a snapshot is needed. - The default value is ``None``. - show : bool, optional - Whether to show the plot or return the matplotlib object. Default is `True`. - - Returns - ------- - :class:`matplotlib.pyplot.Figure` - Matplotlib figure object. - """ - - dpi = 100.0 - figsize = (size[0] / dpi, size[1] / dpi) - - projection = "polar" if polar else "rectilinear" - fig, ax = plt.subplots(figsize=figsize, subplot_kw={"projection": projection}) - - ax.set_xlabel(xlabel) - if polar: - ax.set_rticks(np.linspace(0, max_theta, 3)) - else: - ax.set_ylabel(ylabel) - - ax.set(xlabel=xlabel, ylabel=ylabel, title=title) - if len(plot_data) != 3: # pragma: no cover - pyaedt_logger.error("Input should contain 3 numpy arrays.") - return False - ph = plot_data[2] - th = plot_data[1] - data_to_plot = plot_data[0] - plt.contourf( - ph, - th, - data_to_plot, - levels=levels, - cmap="jet", - ) - - if color_bar: - cbar = plt.colorbar() - cbar.set_label(color_bar, rotation=270, labelpad=20) - - ax = plt.gca() - ax.yaxis.set_label_coords(-0.1, 0.5) - - if snapshot_path: - fig.savefig(snapshot_path) - if show: # pragma: no cover - fig.show() - return fig - - class ObjClass(object): """Manages mesh files to be plotted in pyvista. @@ -765,6 +330,7 @@ def __init__(self, path, color, opacity, units): @property def color(self): + """Color.""" return self._color @color.setter diff --git a/src/ansys/aedt/core/visualization/post/__init__.py b/src/ansys/aedt/core/visualization/post/__init__.py new file mode 100644 index 00000000000..d8e30fb6848 --- /dev/null +++ b/src/ansys/aedt/core/visualization/post/__init__.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +def post_processor(app=None, project=None, design=None, version=None): + """PostProcessor. + + Returns + ------- + :class:`ansys.aedt.core.visualization.post.post_common_3d.PostProcessor3D` or + :class:`ansys.aedt.core.visualization.post.post_icepak.PostProcessorIcepak` or + :class:`ansys.aedt.core.visualization.post.post_circuit.PostProcessorCircuit` + PostProcessor object. + """ + if not app: + from ansys.aedt.core.generic.design_types import get_pyaedt_app + from ansys.aedt.core.generic.desktop_sessions import _desktop_sessions + + if not _desktop_sessions: + from ansys.aedt.core.desktop import Desktop + + d = Desktop(version=version, non_graphical=True) + else: + d = _desktop_sessions.values()[0] + app = get_pyaedt_app(project_name=project, design_name=design, desktop=d) + app.logger.reset_timer() + PostProcessor = None + if app: + if app.design_type == "Icepak": + from ansys.aedt.core.visualization.post.post_icepak import PostProcessorIcepak as PostProcessor + elif app.design_type in ["Twin Builder", "RMxprt", "RMxprtSolution", "Circuit Design", "Circuit Netlist"]: + from ansys.aedt.core.visualization.post.post_circuit import PostProcessorCircuit as PostProcessor + else: + from ansys.aedt.core.visualization.post.post_common_3d import PostProcessor3D as PostProcessor + if PostProcessor: + _post = PostProcessor(app) + app.logger.info_timer("Post class has been initialized!") + return _post + app.logger.error("Failed to initialize Post Processor!") + return None diff --git a/src/ansys/aedt/core/visualization/post/common.py b/src/ansys/aedt/core/visualization/post/common.py new file mode 100644 index 00000000000..374fef3f469 --- /dev/null +++ b/src/ansys/aedt/core/visualization/post/common.py @@ -0,0 +1,2411 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains this class: `PostProcessorCommon`. + +This module provides all functionalities for common AEDT post processing. + +""" + +from __future__ import absolute_import # noreorder + +import os +import re + +from ansys.aedt.core.generic.data_handlers import _dict_items_to_list_items +from ansys.aedt.core.generic.general_methods import generate_unique_name +from ansys.aedt.core.generic.general_methods import pyaedt_function_handler +from ansys.aedt.core.generic.general_methods import read_configuration_file +from ansys.aedt.core.visualization.post.solution_data import SolutionData +from ansys.aedt.core.visualization.report.constants import TEMPLATES_BY_DESIGN +import ansys.aedt.core.visualization.report.emi +import ansys.aedt.core.visualization.report.eye +import ansys.aedt.core.visualization.report.field +import ansys.aedt.core.visualization.report.standard + +TEMPLATES_BY_NAME = { + "Standard": ansys.aedt.core.visualization.report.standard.Standard, + "EddyCurrent": ansys.aedt.core.visualization.report.standard.Standard, + "Modal Solution Data": ansys.aedt.core.visualization.report.standard.Standard, + "Terminal Solution Data": ansys.aedt.core.visualization.report.standard.Standard, + "Fields": ansys.aedt.core.visualization.report.field.Fields, + "CG Fields": ansys.aedt.core.visualization.report.field.Fields, + "DC R/L Fields": ansys.aedt.core.visualization.report.field.Fields, + "AC R/L Fields": ansys.aedt.core.visualization.report.field.Fields, + "Matrix": ansys.aedt.core.visualization.report.standard.Standard, + "Monitor": ansys.aedt.core.visualization.report.standard.Standard, + "Far Fields": ansys.aedt.core.visualization.report.field.FarField, + "Near Fields": ansys.aedt.core.visualization.report.field.NearField, + "Eye Diagram": ansys.aedt.core.visualization.report.eye.EyeDiagram, + "Statistical Eye": ansys.aedt.core.visualization.report.eye.AMIEyeDiagram, + "AMI Contour": ansys.aedt.core.visualization.report.eye.AMIConturEyeDiagram, + "Eigenmode Parameters": ansys.aedt.core.visualization.report.standard.Standard, + "Spectrum": ansys.aedt.core.visualization.report.standard.Spectral, + "EMIReceiver": ansys.aedt.core.visualization.report.emi.EMIReceiver, +} + + +class PostProcessorCommon(object): + """Manages the main AEDT postprocessing functions. + + This class is inherited in the caller application and is accessible through the post variable( eg. ``hfss.post`` or + ``q3d.post``). + + .. note:: + Some functionalities are available only when AEDT is running in + the graphical mode. + + Parameters + ---------- + app : :class:`ansys.aedt.core.application.analysis_3d.FieldAnalysis3D` + Inherited parent object. The parent object must provide the members + ``_modeler``, ``_desktop``, ``_odesign``, and ``logger``. + + Examples + -------- + >>> from ansys.aedt.core import Q3d + >>> q3d = Q3d() + >>> q.post.get_solution_data(domain="Original") + """ + + def __init__(self, app): + self._app = app + self.oeditor = None + if self.modeler: + self.oeditor = self.modeler.oeditor + self._scratch = self._app.working_directory + self.plots = self._get_plot_inputs() + self.reports_by_category = Reports(self, self._app.design_type) + + @property + def available_report_types(self): + """Report types. + + References + ---------- + + >>> oModule.GetAvailableReportTypes + """ + return list(self.oreportsetup.GetAvailableReportTypes()) + + @property + def update_report_dynamically(self): + """Get/Set the boolean to automatically update reports on edits. + + Returns + ------- + bool + """ + return ( + True + if self._app.odesktop.GetRegistryInt( + f"Desktop/Settings/ProjectOptions/{self._app.design_type}/UpdateReportsDynamicallyOnEdits" + ) + == 1 + else False + ) + + @update_report_dynamically.setter + def update_report_dynamically(self, value): + if value: + self._app.odesktop.SetRegistryInt( + f"Desktop/Settings/ProjectOptions/{self._app.design_type}/UpdateReportsDynamicallyOnEdits", 1 + ) + else: # pragma: no cover + self._app.odesktop.SetRegistryInt( + f"Desktop/Settings/ProjectOptions/{self._app.design_type}/UpdateReportsDynamicallyOnEdits", 0 + ) + + @pyaedt_function_handler() + def available_display_types(self, report_category=None) -> list: + """Retrieve display types for a report categories. + + Parameters + ---------- + report_category : str, optional + Type of the report. The default value is ``None``. + + Returns + ------- + list + List of available report categories. + + References + ---------- + >>> oModule.GetAvailableDisplayTypes + """ + if not report_category: + report_category = self.available_report_types[0] + if report_category: + return list(self.oreportsetup.GetAvailableDisplayTypes(report_category)) + return [] # pragma: no cover + + @pyaedt_function_handler() + def available_quantities_categories( + self, report_category=None, display_type=None, solution=None, context=None, is_siwave_dc=False + ): + """Compute the list of all available report categories. + + Parameters + ---------- + report_category : str, optional + Report category. The default is ``None``, in which case the first default category is used. + display_type : str, optional + Report display type. The default is ``None``, in which case the first default type + is used. In most cases, this default type is ``"Rectangular Plot"``. + solution : str, optional + Report setup. The default is ``None``, in which case the first + nominal adaptive solution is used. + context : str, dict, optional + Report category. The default is ``None``, in which case the first default context + is used. For Maxwell 2D/3D eddy current solution types, the report category + can be provided as a dictionary, where the key is the matrix name and the value + the reduced matrix. + is_siwave_dc : bool, optional + Whether the setup is Siwave DCIR. The default is ``False``. + + Returns + ------- + list + + References + ---------- + >>> oModule.GetAllCategories + """ + if not report_category: + report_category = self.available_report_types[0] + if not display_type: + display_type = self.available_display_types(report_category)[0] + if not solution and hasattr(self._app, "nominal_adaptive"): + solution = self._app.nominal_adaptive + if is_siwave_dc: # pragma: no cover + id_ = "0" + if context: + id_ = str( + [ + "RL", + "Sources", + "Vias", + "Bondwires", + "Probes", + ].index(context) + ) + context = [ + "NAME:Context", + "SimValueContext:=", + [37010, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0, "DCIRID", False, id_, "IDIID", False, "1"], + ] + elif self._app.design_type in ["Maxwell 2D", "Maxwell 3D"] and self._app.solution_type == "EddyCurrent": + if isinstance(context, dict): + for k, v in context.items(): + context = ["Context:=", k, "Matrix:=", v] + elif context and isinstance(context, str): + context = ["Context:=", context] + elif not context: + context = "" + elif not context: # pragma: no cover + context = "" + + if solution and report_category and display_type: + return list(self.oreportsetup.GetAllCategories(report_category, display_type, solution, context)) + return [] # pragma: no cover + + @pyaedt_function_handler() + def available_report_quantities( + self, + report_category=None, + display_type=None, + solution=None, + quantities_category=None, + context=None, + is_siwave_dc=False, + differential_pairs=False, + ): + """Compute the list of all available report quantities of a given report quantity category. + + Parameters + ---------- + report_category : str, optional + Report Category. The default is ``None``, in which case the default category is used. + display_type : str, optional + Report Display Type. + The default is ``None``, in which case the default type is used. + In most of the cases the default type is "Rectangular Plot". + solution : str, optional + Report Setup. + The default is ``None``, in which case the first nominal adaptive solution is used. + quantities_category : str, optional + The category that the quantities belong to. + It must be one of the ``available_quantities_categories`` method. + The default is ``None``, in which case the first default quantity is used. + context : str, dict, optional + Report Context. + The default is ``None``, in which case the default context is used. + For Maxwell 2D/3D Eddy Current solution types this can be provided as a dictionary + where the key is the matrix name and value the reduced matrix. + is_siwave_dc : bool, optional + Whether if the setup is Siwave DCIR or not. Default is ``False``. + differential_pairs : bool, optional + Whether if return differential pairs traces or not. Default is ``False``. + + Returns + ------- + list + + References + ---------- + >>> oModule.GetAllQuantities + + Examples + -------- + The example shows how to get report expressions for a Maxwell design with Eddy current solution. + The context has to be provided as a dictionary where the key is the name of the original matrix + and the value is the name of the reduced matrix. + >>> from ansys.aedt.core import Maxwell3d + >>> m3d = Maxwell3d(solution_type="EddyCurrent") + >>> rectangle1 = m3d.modeler.create_rectangle(0, [0.5, 1.5, 0], [2.5, 5], name="Sheet1") + >>> rectangle2 = m3d.modeler.create_rectangle(0, [9, 1.5, 0], [2.5, 5], name="Sheet2") + >>> rectangle3 = m3d.modeler.create_rectangle(0, [16.5, 1.5, 0], [2.5, 5], name="Sheet3") + >>> m3d.assign_current(rectangle1.faces[0], amplitude=1, name="Cur1") + >>> m3d.assign_current(rectangle2.faces[0], amplitude=1, name="Cur2") + >>> m3d.assign_current(rectangle3.faces[0], amplitude=1, name="Cur3") + >>> L = m3d.assign_matrix(assignment=["Cur1", "Cur2", "Cur3"], matrix_name="Matrix1") + >>> out = L.join_series(sources=["Cur1", "Cur2"], matrix_name="ReducedMatrix1") + >>> expressions = m3d.post.available_report_quantities(report_category="EddyCurrent", + ... display_type="Data Table", + ... context={"Matrix1": "ReducedMatrix1"}) + >>> m3d.release_desktop(False, False) + """ + if not report_category: + report_category = self.available_report_types[0] + if not display_type: + display_type = self.available_display_types(report_category)[0] + if not solution and hasattr(self._app, "nominal_adaptive"): + solution = self._app.nominal_adaptive + if is_siwave_dc: + context_id = "0" + if context: + context_id = str( + [ + "RL", + "Sources", + "Vias", + "Bondwires", + "Probes", + ].index(context) + ) + context = [ + "NAME:Context", + "SimValueContext:=", + [ + 37010, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "DCIRID", + False, + context_id, + "IDIID", + False, + "1", + ], + ] + elif differential_pairs: + if self.post_solution_type in ["HFSS3DLayout"]: + context = [ + "NAME:Context", + "SimValueContext:=", + [ + 3, + 0, + 2, + 3, + True, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "EnsDiffPairKey", + False, + "1", + "IDIID", + False, + "1", + ], + ] + elif self.post_solution_type in ["NexximLNA", "NexximTransient"]: + context = [ + "NAME:Context", + "SimValueContext:=", + [ + 3, + 0, + 2, + 3, + True, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "USE_DIFF_PAIRS", + False, + "1", + "IDIID", + False, + "1", + ], + ] + else: + context = ["Diff:=", "differential_pairs", "Domain:=", "Sweep"] + elif self._app.design_type in ["Maxwell 2D", "Maxwell 3D"] and self._app.solution_type == "EddyCurrent": + if isinstance(context, dict): + for k, v in context.items(): + context = ["Context:=", k, "Matrix:=", v] + elif context and isinstance(context, str): + context = ["Context:=", context] + elif not context: + context = "" + elif not context: + context = "" + if not quantities_category: + categories = self.available_quantities_categories(report_category, display_type, solution, context) + if categories and display_type and report_category and solution: + for el in categories: + res = list(self.oreportsetup.GetAllQuantities(report_category, display_type, solution, context, el)) + if res: + return res + if quantities_category and display_type and report_category and solution: + return list( + self.oreportsetup.GetAllQuantities( + report_category, display_type, solution, context, quantities_category + ) + ) + return [] # pragma: no cover + + @pyaedt_function_handler() + def get_all_report_quantities( + self, + solution=None, + context=None, + is_siwave_dc=False, + ): + """Return all the possible report categories organized by report types, solution and categories. + + Parameters + ---------- + solution : str optional + Solution to get the report quantities. + The default is ``None``, in which case the all solutions are used. + context : str, dict, optional + Report Context. + The default is ``None``, in which case the default context is used. + For Maxwell 2D/3D Eddy Current solution types this can be provided as a dictionary + where the key is the matrix name and value the reduced matrix. + is_siwave_dc : bool, optional + Whether if the setup is Siwave DCIR or not. Default is ``False``. + + Returns + ------- + dict + A dictionary with primary key the report type, secondary key the solution type and + third key the report categories. + """ + rep_quantities = {} + for rep in self.available_report_types: + rep_quantities[rep] = {} + solutions = [solution] if isinstance(solution, str) else self.available_report_solutions(rep) + for solution in solutions: + rep_quantities[rep][solution] = {} + for quant in self.available_quantities_categories( + rep, context=context, solution=solution, is_siwave_dc=is_siwave_dc + ): + rep_quantities[rep][solution][quant] = self.available_report_quantities( + rep, quantities_category=quant, context=context, solution=solution, is_siwave_dc=is_siwave_dc + ) + + return rep_quantities + + @pyaedt_function_handler() + def available_report_solutions(self, report_category=None): + """Get the list of available solutions that can be used for the reports. + This list differs from the one obtained with ``app.existing_analysis_sweeps``, + because it includes additional elements like "AdaptivePass". + + Parameters + ---------- + report_category : str, optional + Report Category. Default is ``None`` which takes default category. + + Returns + ------- + list + + References + ---------- + >>> oModule.GetAvailableSolutions + """ + if not report_category: + report_category = self.available_report_types[0] + if report_category: + return list(self.oreportsetup.GetAvailableSolutions(report_category)) + return None # pragma: no cover + + @pyaedt_function_handler() + def _get_plot_inputs(self): + names = self._app.get_oo_name(self.oreportsetup) + plots = [] + skip_plot = False + if self._app.design_type == "Circuit Netlist" and self._app.desktop_class.non_graphical: + skip_plot = True + if names and not skip_plot: + for name in names: + obj = self._app.get_oo_object(self.oreportsetup, name) + report_type = obj.GetPropValue("Report Type") + + report = TEMPLATES_BY_NAME.get(report_type, TEMPLATES_BY_NAME["Standard"]) + + plots.append(report(self, report_type, None)) + plots[-1]._props["plot_name"] = name + plots[-1]._is_created = True + plots[-1].report_type = obj.GetPropValue("Display Type") + return plots + + @property + def oreportsetup(self): + """Report setup. + + Returns + ------- + :attr:`ansys.aedt.core.modules.post_general.PostProcessor.oreportsetup` + + References + ---------- + + >>> oDesign.GetModule("ReportSetup") + """ + return self._app.oreportsetup + + @property + def logger(self): + """Logger.""" + return self._app.logger + + @property + def _desktop(self): + """Desktop.""" + return self._app._desktop + + @property + def _odesign(self): + """Design.""" + return self._app._odesign + + @property + def _oproject(self): + """Project.""" + return self._app._oproject + + @property + def modeler(self): + """Modeler.""" + return self._app.modeler + + @property + def post_solution_type(self): + """Design solution type. + + Returns + ------- + type + Design solution type. + """ + return self._app.solution_type + + @property + def all_report_names(self): + """List of all report names. + + Returns + ------- + list + + References + ---------- + + >>> oModule.GetAllReportNames() + """ + return list(self.oreportsetup.GetAllReportNames()) + + @pyaedt_function_handler(PlotName="plot_name") + def copy_report_data(self, plot_name, paste=True): + """Copy report data as static data. + + Parameters + ---------- + plot_name : str + Name of the report. + paste : bool, optional + Whether to paste the report. The default is ``True``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + References + ---------- + + >>> oModule.CopyReportsData + >>> oModule.PasteReports + """ + self.oreportsetup.CopyReportsData([plot_name]) + if paste: + self.paste_report_data() + return True + + @pyaedt_function_handler() + def paste_report_data(self): + """Paste report data as static data. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + References + ---------- + >>> oModule.PasteReports + """ + self.oreportsetup.PasteReports() + return True + + @pyaedt_function_handler() + def delete_report(self, plot_name=None): + """Delete all reports or specific report. + + Parameters + ---------- + plot_name : str, optional + Name of the plot to delete. The default value is ``None`` and in this case, all reports are deleted. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + References + ---------- + + >>> oModule.DeleteReports + """ + try: + if plot_name: + self.oreportsetup.DeleteReports([plot_name]) + for plot in self.plots: + if plot.plot_name == plot_name: + self.plots.remove(plot) + else: + self.oreportsetup.DeleteAllReports() + self.plots.clear() + return True + except Exception: # pragma: no cover + return False + + @pyaedt_function_handler() + def rename_report(self, plot_name, new_name): + """Rename a plot. + + Parameters + ---------- + plot_name : str + Name of the plot. + new_name : str + New name of the plot. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + References + ---------- + + >>> oModule.RenameReport + """ + try: + self.oreportsetup.RenameReport(plot_name, new_name) + for plot in self.plots: + if plot.plot_name == plot_name: + plot.plot_name = self.oreportsetup.GetChildObject(new_name).GetPropValue("Name") + return True + except Exception: + return False + + @pyaedt_function_handler(soltype="solution_type", ctxt="context", expression="expressions") + def get_solution_data_per_variation( + self, solution_type="Far Fields", setup_sweep_name="", context=None, sweeps=None, expressions="" + ): + """Retrieve solution data for each variation. + + Parameters + ---------- + solution_type : str, optional + Type of the solution. For example, ``"Far Fields"`` or ``"Modal Solution Data"``. The default + is ``"Far Fields"``. + setup_sweep_name : str, optional + Name of the setup for computing the report. The default is ``""``, + in which case ``"nominal adaptive"`` is used. + context : list, optional + List of context variables. The default is ``None``. + sweeps : dict, optional + Dictionary of variables and values. The default is ``None``, + in which case this list is used: + ``{'Theta': 'All', 'Phi': 'All', 'Freq': 'All'}``. + expressions : str or list, optional + One or more traces to include. The default is ``""``. + + Returns + ------- + from ansys.aedt.core.modules.solutions.SolutionData + + + References + ---------- + + >>> oModule.GetSolutionDataPerVariation + """ + if sweeps is None: + sweeps = {"Theta": "All", "Phi": "All", "Freq": "All"} + if not context: + context = [] + if not isinstance(expressions, list): + expressions = [expressions] + if not setup_sweep_name: + setup_sweep_name = self._app.nominal_adaptive + sweep_list = self.__convert_dict_to_report_sel(sweeps) + try: + data = list( + self.oreportsetup.GetSolutionDataPerVariation( + solution_type, setup_sweep_name, context, sweep_list, expressions + ) + ) + self.logger.info("Solution Data Correctly Loaded.") + return SolutionData(data) + except Exception: + self.logger.warning("Solution Data failed to load. Check solution, context or expression.") + return None + + @pyaedt_function_handler() + def steal_focus_oneditor(self): + """Remove the selection of an object that would prevent the image from exporting correctly. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + References + ---------- + + >>> oDesktop.RestoreWindow + """ + self._desktop.RestoreWindow() + param = ["NAME:SphereParameters", "XCenter:=", "0mm", "YCenter:=", "0mm", "ZCenter:=", "0mm", "Radius:=", "1mm"] + attr = ["NAME:Attributes", "Name:=", "DUMMYSPHERE1", "Flags:=", "NonModel#"] + self.oeditor.CreateSphere(param, attr) + self.oeditor.Delete(["NAME:Selections", "Selections:=", "DUMMYSPHERE1"]) + return True + + @pyaedt_function_handler() + def export_report_to_file( + self, + output_dir, + plot_name, + extension, + unique_file=False, + uniform=False, + start=None, + end=None, + step=None, + use_trace_number_format=False, + ): + """Export a 2D Plot data to a file. + + This method leaves the data in the plot (as data) as a reference + for the Plot after the loops. + + Parameters + ---------- + output_dir : str + Path to the directory of exported report + plot_name : str + Name of the plot to export. + extension : str + Extension of export , one of + * (CSV) .csv + * (Tab delimited) .tab + * (Post processor format) .txt + * (Ensight XY data) .exy + * (Anosft Plot Data) .dat + * (Ansoft Report Data Files) .rdat + unique_file : bool + If set to True, generates unique file in output_dit + uniform : bool, optional + Whether the export uniform points to the file. The + default is ``False``. + start : str, optional + Start range with units for the sweep if the ``uniform`` parameter + is set to ``True``. + end : str, optional + End range with units for the sweep if the ``uniform`` parameter + is set to ``True``. + step : str, optional + Step range with units for the sweep if the ``uniform`` parameter is + set to ``True``. + use_trace_number_format : bool, optional + Whether to use trace number formats and use separate columns for curve. The default is ``False``. + + Returns + ------- + str + Path of exported file. + + References + ---------- + + >>> oModule.ExportReportDataToFile + >>> oModule.ExportUniformPointsToFile + >>> oModule.ExportToFile + + Examples + -------- + >>> from ansys.aedt.core import Circuit + >>> cir = Circuit("my_project.aedt") + >>> report = cir.post.create_report("MyScattering") + >>> cir.post.export_report_to_file("C:\\temp", "MyTestScattering", ".csv") + """ + npath = output_dir + + if "." not in extension: # pragma: no cover + extension = "." + extension + + supported_ext = [".csv", ".tab", ".txt", ".exy", ".dat", ".rdat"] + if extension not in supported_ext: # pragma: no cover + msg = f"Extension {extension} is not supported. Use one of {', '.join(supported_ext)}" + raise ValueError(msg) + + file_path = os.path.join(npath, plot_name + extension) + if unique_file: # pragma: no cover + while os.path.exists(file_path): + file_name = generate_unique_name(plot_name) + file_path = os.path.join(npath, file_name + extension) + + if extension == ".rdat": + self.oreportsetup.ExportReportDataToFile(plot_name, file_path) + elif uniform: + self.oreportsetup.ExportUniformPointsToFile(plot_name, file_path, start, end, step, use_trace_number_format) + else: + self.oreportsetup.ExportToFile(plot_name, file_path, use_trace_number_format) + return file_path + + @pyaedt_function_handler() + def export_report_to_csv( + self, project_dir, plot_name, uniform=False, start=None, end=None, step=None, use_trace_number_format=False + ): + """Export the 2D Plot data to a CSV file. + + This method leaves the data in the plot (as data) as a reference + for the Plot after the loops. + + Parameters + ---------- + project_dir : str + Path to the project directory. The CSV file is plot_name.csv. + plot_name : str + Name of the plot to export. + uniform : bool, optional + Whether the export uniform points to the file. The + default is ``False``. + start : str, optional + Start range with units for the sweep if the ``uniform`` parameter + is set to ``True``. + end : str, optional + End range with units for the sweep if the ``uniform`` parameter + is set to ``True``. + step : str, optional + Step range with units for the sweep if the ``uniform`` parameter is + set to ``True``. + use_trace_number_format : bool, optional + Whether to use trace number formats. The default is ``False``. + + Returns + ------- + str + Path of exported file. + + References + ---------- + + >>> oModule.ExportReportDataToFile + >>> oModule.ExportToFile + >>> oModule.ExportUniformPointsToFile + """ + return self.export_report_to_file( + project_dir, + plot_name, + extension=".csv", + uniform=uniform, + start=start, + end=end, + step=step, + use_trace_number_format=use_trace_number_format, + ) + + @pyaedt_function_handler(project_dir="project_path") + def export_report_to_jpg(self, project_path, plot_name, width=0, height=0, image_format="jpg"): + """Export plot to an image file. + + Parameters + ---------- + project_path : str + Path to the project directory. + plot_name : str + Name of the plot to export. + width : int, optional + Image width. Default is ``0`` which takes Desktop size or 1980 pixel in case of non-graphical mode. + height : int, optional + Image height. Default is ``0`` which takes Desktop size or 1020 pixel in case of non-graphical mode. + image_format : str, optional + Format of the image file. The default is ``"jpg"``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + References + ---------- + + >>> oModule.ExportImageToFile + """ + file_name = os.path.join(project_path, plot_name + "." + image_format) # name of the image file + if self._app.desktop_class.non_graphical: # pragma: no cover + if width == 0: + width = 1980 + if height == 0: + height = 1020 + self.oreportsetup.ExportImageToFile(plot_name, file_name, width, height) + return True + + @pyaedt_function_handler(plotname="plot_name") + def _get_report_inputs( + self, + expressions, + setup_sweep_name=None, + domain="Sweep", + variations=None, + primary_sweep_variable=None, + secondary_sweep_variable=None, + report_category=None, + plot_type="Rectangular Plot", + context=None, + subdesign_id=None, + polyline_points=0, + plot_name=None, + only_get_method=False, + ): + ctxt = [] + if not setup_sweep_name: + setup_sweep_name = self._app.nominal_sweep + elif setup_sweep_name not in self._app.existing_analysis_sweeps: + self.logger.error("Sweep not Available.") + return False + families_input = {} + did = 3 + if domain == "Sweep" and not primary_sweep_variable: + primary_sweep_variable = "Freq" + elif not primary_sweep_variable: + primary_sweep_variable = "Time" + did = 1 + if not variations or primary_sweep_variable not in variations: + families_input[primary_sweep_variable] = ["All"] + elif isinstance(variations[primary_sweep_variable], list): + families_input[primary_sweep_variable] = variations[primary_sweep_variable] + else: + families_input[primary_sweep_variable] = [variations[primary_sweep_variable]] + if not variations: + variations = self._app.available_variations.nominal_w_values_dict + for el in list(variations.keys()): + if el == primary_sweep_variable: + continue + if isinstance(variations[el], list): + families_input[el] = variations[el] + else: + families_input[el] = [variations[el]] + if only_get_method and domain == "Sweep": + if "Phi" not in families_input: + families_input["Phi"] = ["All"] + if "Theta" not in families_input: + families_input["Theta"] = ["All"] + + if self.post_solution_type in ["TR", "AC", "DC"]: + ctxt = [ + "NAME:Context", + "SimValueContext:=", + [did, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0], + ] + setup_sweep_name = self.post_solution_type + elif self.post_solution_type in ["HFSS3DLayout"]: + if context == "Differential Pairs": + ctxt = [ + "NAME:Context", + "SimValueContext:=", + [ + did, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "EnsDiffPairKey", + False, + "1", + "IDIID", + False, + "1", + ], + ] + else: + ctxt = [ + "NAME:Context", + "SimValueContext:=", + [did, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0, "IDIID", False, "1"], + ] + elif self.post_solution_type in ["NexximLNA", "NexximTransient"]: + ctxt = ["NAME:Context", "SimValueContext:=", [did, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0]] + if subdesign_id: + ctxt_temp = ["NUMLEVELS", False, "1", "SUBDESIGNID", False, str(subdesign_id)] + for el in ctxt_temp: + ctxt[2].append(el) + if context == "Differential Pairs": + ctxt_temp = ["USE_DIFF_PAIRS", False, "1"] + for el in ctxt_temp: + ctxt[2].append(el) + elif context == "Differential Pairs": + ctxt = ["Diff:=", "Differential Pairs", "Domain:=", domain] + elif self.post_solution_type in ["Q3D Extractor", "2D Extractor"]: + if not context: + ctxt = ["Context:=", "Original"] + else: + ctxt = ["Context:=", context] + elif context: + ctxt = ["Context:=", context] + if context in self.modeler.line_names: + ctxt.append("PointCount:=") + ctxt.append(polyline_points) + + if not isinstance(expressions, list): + expressions = [expressions] + + if not report_category and not self._app.design_solutions.report_type: + self.logger.error("Solution not supported") + return False + if not report_category: + modal_data = self._app.design_solutions.report_type + else: + modal_data = report_category + if not plot_name: + plot_name = generate_unique_name("Plot") + + arg = ["X Component:=", primary_sweep_variable, "Y Component:=", expressions] + if plot_type in ["3D Polar Plot", "3D Spherical Plot"]: + if not primary_sweep_variable: + primary_sweep_variable = "Phi" + if not secondary_sweep_variable: + secondary_sweep_variable = "Theta" + arg = [ + "Phi Component:=", + primary_sweep_variable, + "Theta Component:=", + secondary_sweep_variable, + "Mag Component:=", + expressions, + ] + elif plot_type == "Radiation Pattern": + if not primary_sweep_variable: + primary_sweep_variable = "Phi" + arg = ["Ang Component:=", primary_sweep_variable, "Mag Component:=", expressions] + elif plot_type in ["Smith Chart", "Polar Plot"]: + arg = ["Polar Component:=", expressions] + elif plot_type == "Rectangular Contour Plot": + arg = [ + "X Component:=", + primary_sweep_variable, + "Y Component:=", + secondary_sweep_variable, + "Z Component:=", + expressions, + ] + return [plot_name, modal_data, plot_type, setup_sweep_name, ctxt, families_input, arg] + + @pyaedt_function_handler(plotname="plot_name") + def create_report( + self, + expressions=None, + setup_sweep_name=None, + domain="Sweep", + variations=None, + primary_sweep_variable=None, + secondary_sweep_variable=None, + report_category=None, + plot_type="Rectangular Plot", + context=None, + subdesign_id=None, + polyline_points=1001, + plot_name=None, + ): + """Create a report in AEDT. It can be a 2D plot, 3D plot, polar plot, or a data table. + + Parameters + ---------- + expressions : str or list, optional + One or more formulas to add to the report. Example is value = ``"dB(S(1,1))"``. + setup_sweep_name : str, optional + Setup name with the sweep. The default is ``""``. + domain : str, optional + Plot Domain. Options are "Sweep", "Time", "DCIR". + variations : dict, optional + Dictionary of all families including the primary sweep. The default is ``{"Freq": ["All"]}``. + primary_sweep_variable : str, optional + Name of the primary sweep. The default is ``"Freq"``. + secondary_sweep_variable : str, optional + Name of the secondary sweep variable in 3D Plots. + report_category : str, optional + Category of the Report to be created. If `None` default data Report is used. + The Report Category can be one of the types available for creating a report depend on the simulation setup. + For example for a Far Field Plot in HFSS the UI shows the report category as "Create Far Fields Report". + The report category is "Far Fields" in this case. + Depending on the setup different categories are available. + If ``None`` default category is used (the first item in the Results drop down menu in AEDT). + plot_type : str, optional + The format of Data Visualization. Default is ``Rectangular Plot``. + context : str, dict, optional + The default is ``None``. + - For HFSS 3D Layout, options are ``"Bondwires"``, ``"Differential Pairs"``, + ``None``, ``"Probes"``, ``"RL"``, ``"Sources"``, and ``"Vias"``. + - For Q2D or Q3D, specify the name of a reduced matrix. + - For a far fields plot, specify the name of an infinite sphere. + - For Maxwell 2D/3D Eddy Current solution types this can be provided as a dictionary + where the key is the matrix name and value the reduced matrix. + plot_name : str, optional + Name of the plot. The default is ``None``. + polyline_points : int, optional, + Number of points to create the report for plots on polylines on. + subdesign_id : int, optional + Specify a subdesign ID to export a Touchstone file of this subdesign. Valid for Circuit Only. + The default value is ``None``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Standard` + ``True`` when successful, ``False`` when failed. + + + References + ---------- + + >>> oModule.CreateReport + + Examples + -------- + >>> from ansys.aedt.core import Hfss + >>> hfss = Hfss() + >>> hfss.post.create_report("dB(S(1,1))") + >>> variations = hfss.available_variations.nominal_w_values_dict + >>> variations["Theta"] = ["All"] + >>> variations["Phi"] = ["All"] + >>> variations["Freq"] = ["30GHz"] + >>> hfss.post.create_report(expressions="db(GainTotal)", + ... setup_sweep_name=hfss.nominal_adaptive, + ... variations=variations, + ... primary_sweep_variable="Phi", + ... secondary_sweep_variable="Theta", + ... report_category="Far Fields", + ... plot_type="3D Polar Plot", + ... context="3D") + >>> hfss.post.create_report("S(1,1)",hfss.nominal_sweep,variations=variations,plot_type="Smith Chart") + >>> hfss.release_desktop(False, False) + + >>> from ansys.aedt.core import Maxwell2d + >>> m2d = Maxwell2d() + >>> m2d.post.create_report(expressions="InputCurrent(PHA)", + ... domain="Time", + ... primary_sweep_variable="Time", + ... plot_name="Winding Plot 1") + >>> m2d.release_desktop(False, False) + + >>> from ansys.aedt.core import Maxwell3d + >>> m3d = Maxwell3d(solution_type="EddyCurrent") + >>> rectangle1 = m3d.modeler.create_rectangle(0, [0.5, 1.5, 0], [2.5, 5], name="Sheet1") + >>> rectangle2 = m3d.modeler.create_rectangle(0, [9, 1.5, 0], [2.5, 5], name="Sheet2") + >>> rectangle3 = m3d.modeler.create_rectangle(0, [16.5, 1.5, 0], [2.5, 5], name="Sheet3") + >>> m3d.assign_current(rectangle1.faces[0], amplitude=1, name="Cur1") + >>> m3d.assign_current(rectangle2.faces[0], amplitude=1, name="Cur2") + >>> m3d.assign_current(rectangle3.faces[0], amplitude=1, name="Cur3") + >>> L = m3d.assign_matrix(assignment=["Cur1", "Cur2", "Cur3"], matrix_name="Matrix1") + >>> out = L.join_series(sources=["Cur1", "Cur2"], matrix_name="ReducedMatrix1") + >>> expressions = m3d.post.available_report_quantities(report_category="EddyCurrent", + ... display_type="Data Table", + ... context={"Matrix1": "ReducedMatrix1"}) + >>> report = m3d.post.create_report( + ... expressions=expressions, + ... context={"Matrix1": "ReducedMatrix1"}, + ... plot_type="Data Table", + ... plot_name="reduced_matrix") + >>> m3d.release_desktop(False, False) + """ + if not setup_sweep_name: + setup_sweep_name = self._app.nominal_sweep + if not domain: + domain = "Sweep" + setup_name = setup_sweep_name.split(":")[0] + if setup_name: + for setup in self._app.setups: + if setup.name == setup_name and "Time" in setup.default_intrinsics: + domain = "Time" + if domain in ["Spectral", "Spectrum"]: + report_category = "Spectrum" + elif not report_category and not self._app.design_solutions.report_type: + self.logger.error("Solution not supported") + return False + elif not report_category: + report_category = self._app.design_solutions.report_type + if report_category in TEMPLATES_BY_NAME: + report_class = TEMPLATES_BY_NAME[report_category] + elif "Fields" in report_category: + report_class = TEMPLATES_BY_NAME["Fields"] + else: + report_class = TEMPLATES_BY_NAME["Standard"] + + report = report_class(self, report_category, setup_sweep_name) + if not expressions: + expressions = [ + i for i in self.available_report_quantities(report_category=report_category, context=context) + ] + report.expressions = expressions + report.domain = domain + if not variations and domain == "Sweep": + variations = self._app.available_variations.nominal_w_values_dict + if variations: + variations["Freq"] = "All" + else: + variations = {"Freq": ["All"]} + elif not variations and domain != "Sweep": + variations = self._app.available_variations.nominal_w_values_dict + report.variations = variations + if primary_sweep_variable: + report.primary_sweep = primary_sweep_variable + elif domain == "DCIR": # pragma: no cover + report.primary_sweep = "Index" + if variations: + variations["Index"] = ["All"] + else: # pragma: no cover + variations = {"Index": "All"} + if secondary_sweep_variable: + report.secondary_sweep = secondary_sweep_variable + + report.variations = variations + report.report_type = plot_type + report.sub_design_id = subdesign_id + report.point_number = polyline_points + if context == "Differential Pairs": + report.differential_pairs = True + elif context in [ + "RL", + "Sources", + "Vias", + "Bondwires", + "Probes", + ]: + report.siwave_dc_category = [ + "RL", + "Sources", + "Vias", + "Bondwires", + "Probes", + ].index(context) + elif self._app.design_type in ["Q3D Extractor", "2D Extractor"] and context: + report.matrix = context + elif ( + self._app.design_type in ["Maxwell 2D", "Maxwell 3D"] + and self._app.solution_type == "EddyCurrent" + and context + ): + if isinstance(context, dict): + for k, v in context.items(): + report.matrix = k + report.reduced_matrix = v + elif context in self.modeler.line_names or context in self.modeler.point_names: + report.polyline = context + else: + report.matrix = context + elif report_category == "Far Fields": + if not context and self._app._field_setups: + report.far_field_sphere = self._app.field_setups[0].name + else: + if isinstance(context, dict): + if "Context" in context.keys() and "SourceContext" in context.keys(): + report.far_field_sphere = context["Context"] + report.source_context = context["SourceContext"] + if "Context" in context.keys() and "Source Group" in context.keys(): + report.far_field_sphere = context["Context"] + report.source_group = context["Source Group"] + else: + report.far_field_sphere = context + elif report_category == "Near Fields": + report.near_field = context + elif context: + if context in self.modeler.line_names or context in self.modeler.point_names: + report.polyline = context + + result = report.create(plot_name) + if result: + return report + return False + + @pyaedt_function_handler() + def get_solution_data( + self, + expressions=None, + setup_sweep_name=None, + domain=None, + variations=None, + primary_sweep_variable=None, + report_category=None, + context=None, + subdesign_id=None, + polyline_points=1001, + math_formula=None, + ): + """Get a simulation result from a solved setup and cast it in a ``SolutionData`` object. + Data to be retrieved from Electronics Desktop are any simulation results available in that + specific simulation context. + Most of the argument have some defaults which works for most of the ``Standard`` report quantities. + + Parameters + ---------- + expressions : str or list, optional + One or more formulas to add to the report. Example is value ``"dB(S(1,1))"`` or a list of values. + Default is ``None`` which returns all traces. + setup_sweep_name : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + domain : str, optional + Plot Domain. Options are "Sweep" for frequency domain related results and "Time" for transient related data. + variations : dict, optional + Dictionary of all families including the primary sweep. + The default is ``None`` which uses the nominal variations of the setup. + primary_sweep_variable : str, optional + Name of the primary sweep. The default is ``"None"`` which, depending on the context, + internally assigns the primary sweep to: + 1. ``Freq`` for frequency domain results, + 2. ``Time`` for transient results, + 3. ``Theta`` for radiation patterns, + 4. ``distance`` for field plot over a polyline. + report_category : str, optional + Category of the Report to be created. If ``None`` default data Report is used. + The Report Category can be one of the types available for creating a report depend on the simulation setup. + For example for a Far Field Plot in HFSS the UI shows the report category as "Create Far Fields Report". + The report category is "Far Fields" in this case. + Depending on the setup different categories are available. + If ``None`` default category is used (the first item in the Results drop down menu in AEDT). + To get the list of available categories user can use method ``available_report_types``. + context : str, dict, optional + This is the context of the report. + The default is ``None``. It can be: + 1. `None` + 2. ``"Differential Pairs"`` + 3. Reduce Matrix Name for Q2d/Q3d solution + 4. Infinite Sphere name for Far Fields Plot. + 5. Dictionary. If dictionary is passed, key is the report property name and value is property value. + 6. For Maxwell 2D/3D eddy current solution types, this can be provided as a dictionary, + where the key is the matrix name and value the reduced matrix. + subdesign_id : int, optional + Subdesign ID for exporting a Touchstone file of this subdesign. + This parameter is valid for ``Circuit`` only. + The default value is ``None``. + polyline_points : int, optional + Number of points on which to create the report for plots on polylines. + This parameter is valid for ``Fields`` plot only. + math_formula : str, optional + One of the available AEDT mathematical formulas to apply. For example, ``abs, dB``. + + + Returns + ------- + :class:`ansys.aedt.core.modules.solutions.SolutionData` + Solution Data object. + + References + ---------- + + >>> oModule.GetSolutionDataPerVariation + + Examples + -------- + >>> from ansys.aedt.core import Hfss + >>> hfss = Hfss() + >>> hfss.post.create_report("dB(S(1,1))") + >>> variations = hfss.available_variations.nominal_w_values_dict + >>> variations["Theta"] = ["All"] + >>> variations["Phi"] = ["All"] + >>> variations["Freq"] = ["30GHz"] + >>> data1 = hfss.post.get_solution_data( + ... "GainTotal", + ... hfss.nominal_adaptive, + ... variations=variations, + ... primary_sweep_variable="Phi", + ... secondary_sweep_variable="Theta", + ... context="3D", + ... report_category="Far Fields", + ...) + + >>> data2 =hfss.post.get_solution_data( + ... "S(1,1)", + ... hfss.nominal_sweep, + ... variations=variations, + ...) + >>> data2.plot() + >>> hfss.release_desktop(False, False) + + >>> from ansys.aedt.core import Maxwell2d + >>> m2d = Maxwell2d() + >>> data3 = m2d.post.get_solution_data( + ... "InputCurrent(PHA)", domain="Time", primary_sweep_variable="Time", + ... ) + >>> data3.plot("InputCurrent(PHA)") + >>> m2d.release_desktop(False, False) + + >>> from ansys.aedt.core import Circuit + >>> circuit = Circuit() + >>> context = {"algorithm": "FFT", "max_frequency": "100MHz", "time_stop": "2.5us", "time_start": "0ps"} + >>> spectralPlotData = circuit.post.get_solution_data(expressions="V(Vprobe1)", domain="Spectral", + ... primary_sweep_variable="Spectrum", context=context) + >>> circuit.release_desktop(False, False) + + >>> from ansys.aedt.core import Maxwell3d + >>> m3d = Maxwell3d(solution_type="EddyCurrent") + >>> rectangle1 = m3d.modeler.create_rectangle(0, [0.5, 1.5, 0], [2.5, 5], name="Sheet1") + >>> rectangle2 = m3d.modeler.create_rectangle(0, [9, 1.5, 0], [2.5, 5], name="Sheet2") + >>> rectangle3 = m3d.modeler.create_rectangle(0, [16.5, 1.5, 0], [2.5, 5], name="Sheet3") + >>> m3d.assign_current(rectangle1.faces[0], amplitude=1, name="Cur1") + >>> m3d.assign_current(rectangle2.faces[0], amplitude=1, name="Cur2") + >>> m3d.assign_current(rectangle3.faces[0], amplitude=1, name="Cur3") + >>> L = m3d.assign_matrix(assignment=["Cur1", "Cur2", "Cur3"], matrix_name="Matrix1") + >>> out = L.join_series(sources=["Cur1", "Cur2"], matrix_name="ReducedMatrix1") + >>> expressions = m3d.post.available_report_quantities(report_category="EddyCurrent", + ... display_type="Data Table", + ... context={"Matrix1": "ReducedMatrix1"}) + >>> data = m2d.post.get_solution_data(expressions=expressions, context={"Matrix1": "ReducedMatrix1"}) + >>> m3d.release_desktop(False, False) + """ + expressions = [expressions] if isinstance(expressions, str) else expressions + if not setup_sweep_name: + setup_sweep_name = self._app.nominal_sweep + if not domain: + domain = "Sweep" + setup_name = setup_sweep_name.split(":")[0] + if setup_name: + for setup in self._app.setups: + if setup.name == setup_name and "Time" in setup.default_intrinsics: + domain = "Time" + if domain in ["Spectral", "Spectrum"]: + report_category = "Spectrum" + if not report_category and not self._app.design_solutions.report_type: + self.logger.error("Solution not supported") + return False + elif not report_category: + report_category = self._app.design_solutions.report_type + if report_category in TEMPLATES_BY_NAME: + report_class = TEMPLATES_BY_NAME[report_category] + elif "Fields" in report_category: + report_class = TEMPLATES_BY_NAME["Fields"] + else: + report_class = TEMPLATES_BY_NAME["Standard"] + + report = report_class(self, report_category, setup_sweep_name) + if not expressions: + expressions = [ + i for i in self.available_report_quantities(report_category=report_category, context=context) + ] + if math_formula: + expressions = [f"{math_formula}({i})" for i in expressions] + report.expressions = expressions + report.domain = domain + if primary_sweep_variable: + report.primary_sweep = primary_sweep_variable + if not variations and domain == "Sweep": + variations = self._app.available_variations.nominal_w_values_dict + if variations: + variations["Freq"] = "All" + else: + variations = {"Freq": ["All"]} + elif not variations and domain != "Sweep": + variations = self._app.available_variations.nominal_w_values_dict + report.variations = variations + report.sub_design_id = subdesign_id + report.point_number = polyline_points + if context == "Differential Pairs": + report.differential_pairs = True + elif self._app.design_type in ["Q3D Extractor", "2D Extractor"] and context: + report.matrix = context + elif ( + self._app.design_type in ["Maxwell 2D", "Maxwell 3D"] + and self._app.solution_type == "EddyCurrent" + and context + ): + if isinstance(context, dict): + for k, v in context.items(): + report.matrix = k + report.reduced_matrix = v + elif ( + hasattr(self.modeler, "line_names") + and hasattr(self.modeler, "point_names") + and context in self.modeler.point_names + self.modeler.line_names + ): + report.polyline = context + else: + report.matrix = context + elif report_category == "Far Fields": + if not context and self._app.field_setups: + report.far_field_sphere = self._app.field_setups[0].name + else: + if isinstance(context, dict): + if "Context" in context.keys() and "SourceContext" in context.keys(): + report.far_field_sphere = context["Context"] + report.source_context = context["SourceContext"] + else: + report.far_field_sphere = context + elif report_category == "Near Fields": + report.near_field = context + elif context and isinstance(context, dict): + for attribute in context: + if hasattr(report, attribute): + report.__setattr__(attribute, context[attribute]) + else: + self.logger.warning(f"Parameter {attribute} is not available, check syntax.") + elif context: + if ( + hasattr(self.modeler, "line_names") + and hasattr(self.modeler, "point_names") + and context in self.modeler.point_names + self.modeler.line_names + ): + report.polyline = context + elif context in [ + "RL", + "Sources", + "Vias", + "Bondwires", + "Probes", + ]: + report.siwave_dc_category = [ + "RL", + "Sources", + "Vias", + "Bondwires", + "Probes", + ].index(context) + solution_data = report.get_solution_data() + return solution_data + + @pyaedt_function_handler(input_dict="report_settings") + def create_report_from_configuration(self, input_file=None, report_settings=None, solution_name=None, name=None): + """Create a report based on a JSON file, TOML file, RPT file, or dictionary of properties. + + Parameters + ---------- + input_file : str, optional + Path to the JSON, TOML, or RPT file containing report settings. + report_settings : dict, optional + Dictionary containing report settings. + solution_name : str, optional + Setup name to use. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Standard` + Report object if succeeded. + + Examples + -------- + + Create report from JSON file. + >>> from ansys.aedt.core import Hfss + >>> hfss = Hfss() + >>> hfss.post.create_report_from_configuration(r'C:\\temp\\my_report.json', + ... solution_name="Setup1 : LastAdpative") + + Create report from RPT file. + >>> from ansys.aedt.core import Hfss + >>> hfss = Hfss() + >>> hfss.post.create_report_from_configuration(r'C:\\temp\\my_report.rpt') + + Create report from dictionary. + >>> from ansys.aedt.core import Hfss + >>> from ansys.aedt.core.generic.general_methods import read_json + >>> hfss = Hfss() + >>> dict_vals = read_json("Report_Simple.json") + >>> hfss.post.create_report_from_configuration(report_settings=dict_vals) + """ + if not report_settings and not input_file: # pragma: no cover + self.logger.error("Either a file or a dictionary must be passed as input.") + return False + props = {} + if input_file: + _, file_extension = os.path.splitext(input_file) + if file_extension == ".rpt": + old_expressions = self.all_report_names + self.oreportsetup.CreateReportFromTemplate(input_file) + new_expressions = [item for item in self.all_report_names if item not in old_expressions] + if new_expressions: + report_name = new_expressions[0] + self.plots = self._get_plot_inputs() + report = None + for plot in self.plots: + if plot.plot_name == report_name: + report = plot + break + return report + else: + props = read_configuration_file(input_file) + if report_settings: + props = self.__apply_settings(props, report_settings) + + else: + props = report_settings + + if ( + isinstance(props.get("expressions", {}), list) + and props["expressions"] + and isinstance(props["expressions"][0], str) + ): # pragma: no cover + props["expressions"] = {i: {} for i in props["expressions"]} + elif isinstance(props.get("expressions", {}), str): # pragma: no cover + props["expressions"] = {props["expressions"]: {}} + _dict_items_to_list_items(props, "expressions") + if not solution_name: + if "Fields" in props.get("report_category", ""): + solution_name = self._app.nominal_sweep + else: + solution_name = self._app.nominal_adaptive + if props.get("report_category", None) and props["report_category"] in TEMPLATES_BY_NAME: + if props.get("context", {"context": {}}).get("domain", "") == "Spectral": + report_temp = TEMPLATES_BY_NAME["Spectrum"] + elif ( + "AMIAnalysis" in self._app.get_setup(solution_name.split(":")[0].strip()).props + and props["report_category"] == "Standard" + ): + report_temp = TEMPLATES_BY_NAME["AMI Contour"] + elif "AMIAnalysis" in self._app.get_setup(solution_name.split(":")[0].strip()).props: + report_temp = TEMPLATES_BY_NAME["Statistical Eye"] + else: + report_temp = TEMPLATES_BY_NAME[props["report_category"]] + report = report_temp(self, props["report_category"], solution_name) + for k, v in props.items(): + report._props[k] = v + for el, k in self._app.available_variations.nominal_w_values_dict.items(): + if ( + report._props.get("context", None) + and report._props["context"].get("variations", None) + and el not in report._props["context"]["variations"] + ): + report._props["context"]["variations"][el] = k + _ = report.expressions + report.create(name) + if report.report_type != "Data Table": + report._update_traces() + self.oreportsetup.UpdateReports(report.plot_name) + self.logger.info(f"Report {report.plot_name} created successfully.") + return report + self.logger.error("Failed to create report.") + return False # pragma: no cover + + @staticmethod + @pyaedt_function_handler() + def __convert_dict_to_report_sel(sweeps): + if isinstance(sweeps, list): + return sweeps + sweep_list = [] + for el in sweeps: + sweep_list.append(el + ":=") + if isinstance(sweeps[el], list): + sweep_list.append(sweeps[el]) + else: + sweep_list.append([sweeps[el]]) + return sweep_list + + @staticmethod + @pyaedt_function_handler() + def __apply_settings(props, report_settings): + for k, v in report_settings.items(): + if k in props: + if isinstance(v, dict): + props[k] = apply_settings(props[k], v) + else: + props[k] = v + else: + props[k] = v + return props + + +class Reports(object): + """Provides the names of default solution types.""" + + def __init__(self, post_app, design_type): + self._post_app = post_app + self._design_type = design_type + self._templates = TEMPLATES_BY_DESIGN.get(self._design_type, None) + + @pyaedt_function_handler() + def _retrieve_default_expressions(self, expressions, report, setup_sweep_name): + if expressions: + return expressions + setup_only_name = setup_sweep_name.split(":")[0].strip() + get_setup = self._post_app._app.get_setup(setup_only_name) + is_siwave_dc = False + if ( + "SolveSetupType" in get_setup.props and get_setup.props["SolveSetupType"] == "SiwaveDCIR" + ): # pragma: no cover + is_siwave_dc = True + return self._post_app.available_report_quantities( + solution=setup_sweep_name, context=report._context, is_siwave_dc=is_siwave_dc + ) + + @pyaedt_function_handler(setup_name="setup") + def standard(self, expressions=None, setup=None): + """Create a standard or default report object. + + Parameters + ---------- + expressions : str or list + Expression List to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Standard` + + Examples + -------- + + >>> from ansys.aedt.core import Circuit + >>> cir = Circuit(my_project) + >>> report = cir.post.reports_by_category.standard("dB(S(1,1))","LNA") + >>> report.create() + >>> solutions = report.get_solution_data() + >>> report2 = cir.post.reports_by_category.standard(["dB(S(2,1))", "dB(S(2,2))"],"LNA") + + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "Standard" in self._templates: + rep = ansys.aedt.core.visualization.report.standard.Standard(self._post_app, "Standard", setup) + + elif self._post_app._app.design_solutions.report_type: + rep = ansys.aedt.core.visualization.report.standard.Standard( + self._post_app, self._post_app._app.design_solutions.report_type, setup + ) + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler(setup_name="setup") + def monitor(self, expressions=None, setup=None): + """Create an Icepak Monitor Report object. + + Parameters + ---------- + expressions : str or list + One or more expressions to add to the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Standard` + + Examples + -------- + + >>> from ansys.aedt.core import Icepak + >>> ipk = Icepak(my_project) + >>> report = ipk.post.reports_by_category.monitor(["monitor_surf.Temperature","monitor_point.Temperature"]) + >>> report = report.create() + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "Monitor" in self._templates: + rep = ansys.aedt.core.visualization.report.standard.Standard(self._post_app, "Monitor", setup) + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler(setup_name="setup") + def fields(self, expressions=None, setup=None, polyline=None): + """Create a Field Report object. + + Parameters + ---------- + expressions : str or list + One or more expressions to add to the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + polyline : str, optional + Name of the polyline to plot the field on. + If a name is not provided, the report might be incorrect. + The default value is ``None``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Fields` + + Examples + -------- + + >>> from ansys.aedt.core import Hfss + >>> hfss = Hfss(my_project) + >>> report = hfss.post.reports_by_category.fields("Mag_E", "Setup : LastAdaptive", "Polyline1") + >>> report.create() + >>> solutions = report.get_solution_data() + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "Fields" in self._templates: + rep = ansys.aedt.core.visualization.report.field.Fields(self._post_app, "Fields", setup) + rep.polyline = polyline + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler(setup_name="setup") + def cg_fields(self, expressions=None, setup=None, polyline=None): + """Create a CG Field Report object in Q3D and Q2D. + + Parameters + ---------- + expressions : str or list + Expression List to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + polyline : str, optional + Name of the polyline to plot the field on. + If a name is not provided, the report might be incorrect. + The default value is ``None``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Fields` + + Examples + -------- + + >>> from ansys.aedt.core import Q3d + >>> q3d = Q3d(my_project) + >>> report = q3d.post.reports_by_category.cg_fields("SmoothQ", "Setup : LastAdaptive", "Polyline1") + >>> report.create() + >>> solutions = report.get_solution_data() + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "CG Fields" in self._templates: + rep = ansys.aedt.core.visualization.report.field.Fields(self._post_app, "CG Fields", setup) + rep.polyline = polyline + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler(setup_name="setup") + def dc_fields(self, expressions=None, setup=None, polyline=None): + """Create a DC Field Report object in Q3D. + + Parameters + ---------- + expressions : str or list + Expression List to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + polyline : str, optional + Name of the polyline to plot the field on. + If a name is not provided, the report might be incorrect. + The default value is ``None``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Fields` + + Examples + -------- + + >>> from ansys.aedt.core import Q3d + >>> q3d = Q3d(my_project) + >>> report = q3d.post.reports_by_category.dc_fields("Mag_VolumeJdc", "Setup : LastAdaptive", "Polyline1") + >>> report.create() + >>> solutions = report.get_solution_data() + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "DC R/L Fields" in self._templates: + rep = ansys.aedt.core.visualization.report.field.Fields(self._post_app, "DC R/L Fields", setup) + rep.polyline = polyline + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler(setup_name="setup") + def rl_fields(self, expressions=None, setup=None, polyline=None): + """Create an AC RL Field Report object in Q3D and Q2D. + + Parameters + ---------- + expressions : str or list + Expression List to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + polyline : str, optional + Name of the polyline to plot the field on. + If a name is not provided, the report might be incorrect. + The default value is ``None``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Fields` + + Examples + -------- + + >>> from ansys.aedt.core import Q3d + >>> q3d = Q3d(my_project) + >>> report = q3d.post.reports_by_category.rl_fields("Mag_SurfaceJac", "Setup : LastAdaptive", "Polyline1") + >>> report.create() + >>> solutions = report.get_solution_data() + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "AC R/L Fields" in self._templates or "RL Fields" in self._templates: + if self._post_app._app.design_type == "Q3D Extractor": + rep = ansys.aedt.core.visualization.report.field.Fields(self._post_app, "AC R/L Fields", setup) + else: + rep = ansys.aedt.core.visualization.report.field.Fields(self._post_app, "RL Fields", setup) + rep.polyline = polyline + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler(setup_name="setup") + def far_field(self, expressions=None, setup=None, sphere_name=None, source_context=None): + """Create a Far Field Report object. + + Parameters + ---------- + expressions : str or list + Expression List to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + sphere_name : str, optional + Name of the sphere to create the far field on. + source_context : str, optional + Name of the active source to create the far field on. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.FarField` + + Examples + -------- + + >>> from ansys.aedt.core import Hfss + >>> hfss = Hfss(my_project) + >>> report = hfss.post.reports_by_category.far_field("GainTotal", "Setup : LastAdaptive", "3D_Sphere") + >>> report.primary_sweep = "Phi" + >>> report.create() + >>> solutions = report.get_solution_data() + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "Far Fields" in self._templates: + rep = ansys.aedt.core.visualization.report.field.FarField(self._post_app, "Far Fields", setup) + rep.far_field_sphere = sphere_name + rep.source_context = source_context + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler(setup_name="setup", sphere_name="infinite_sphere") + def antenna_parameters(self, expressions=None, setup=None, infinite_sphere=None): + """Create an Antenna Parameters Report object. + + Parameters + ---------- + expressions : str or list + Expression List to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + infinite_sphere : str, optional + Name of the sphere to compute antenna parameters on. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.AntennaParameters` + + Examples + -------- + + >>> from ansys.aedt.core import Hfss + >>> hfss = Hfss(my_project) + >>> report = hfss.post.reports_by_category.antenna_parameters("GainTotal", "Setup : LastAdaptive", "3D_Sphere") + >>> report.create() + >>> solutions = report.get_solution_data() + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "Antenna Parameters" in self._templates: + rep = ansys.aedt.core.visualization.report.field.AntennaParameters( + self._post_app, "Antenna Parameters", setup, infinite_sphere + ) + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler(setup_name="setup") + def near_field(self, expressions=None, setup=None): + """Create a Field Report object. + + Parameters + ---------- + expressions : str or list + Expression List to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.NearField` + + Examples + -------- + + >>> from ansys.aedt.core import Hfss + >>> hfss = Hfss(my_project) + >>> report = hfss.post.reports_by_category.near_field("GainTotal", "Setup : LastAdaptive", "NF_1") + >>> report.primary_sweep = "Phi" + >>> report.create() + >>> solutions = report.get_solution_data() + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "Near Fields" in self._templates: + rep = ansys.aedt.core.visualization.report.field.NearField(self._post_app, "Near Fields", setup) + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler(setup_name="setup") + def modal_solution(self, expressions=None, setup=None): + """Create a Standard or Default Report object. + + Parameters + ---------- + expressions : str or list + Expression List to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Standard` + + Examples + -------- + + >>> from ansys.aedt.core import Hfss + >>> hfss = Hfss(my_project) + >>> report = hfss.post.reports_by_category.modal_solution("dB(S(1,1))") + >>> report.create() + >>> solutions = report.get_solution_data() + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "Modal Solution Data" in self._templates: + rep = ansys.aedt.core.visualization.report.standard.Standard(self._post_app, "Modal Solution Data", setup) + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler(setup_name="setup") + def terminal_solution(self, expressions=None, setup=None): + """Create a Standard or Default Report object. + + Parameters + ---------- + expressions : str or list + Expression List to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Standard` + + Examples + -------- + + >>> from ansys.aedt.core import Hfss + >>> hfss = Hfss(my_project) + >>> report = hfss.post.reports_by_category.terminal_solution("dB(S(1,1))") + >>> report.create() + >>> solutions = report.get_solution_data() + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "Terminal Solution Data" in self._templates: + rep = ansys.aedt.core.visualization.report.standard.Standard( + self._post_app, "Terminal Solution Data", setup + ) + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler(setup_name="setup") + def eigenmode(self, expressions=None, setup=None): + """Create a Standard or Default Report object. + + Parameters + ---------- + expressions : str or list + Expression List to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Standard` + + Examples + -------- + + >>> from ansys.aedt.core import Hfss + >>> hfss = Hfss(my_project) + >>> report = hfss.post.reports_by_category.eigenmode("dB(S(1,1))") + >>> report.create() + >>> solutions = report.get_solution_data() + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "Eigenmode Parameters" in self._templates: + rep = ansys.aedt.core.visualization.report.standard.Standard(self._post_app, "Eigenmode Parameters", setup) + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler(setup_name="setup") + def statistical_eye_contour(self, expressions=None, setup=None, quantity_type=3): + """Create a standard statistical AMI contour plot. + + Parameters + ---------- + expressions : str + Expression to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is either the sweep + name to use in the export or ``LastAdaptive``. + quantity_type : int, optional + For AMI analysis only, the quantity type. The default is ``3``. Options are: + + - ``0`` for Initial Wave + - ``1`` for Wave after Source + - ``2`` for Wave after Channel + - ``3`` for Wave after Probe. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.AMIConturEyeDiagram` + + Examples + -------- + + >>> from ansys.aedt.core import Circuit + >>> cir= Circuit() + >>> new_eye = cir.post.reports_by_category.statistical_eye_contour("V(Vout)") + >>> new_eye.unit_interval = "1e-9s" + >>> new_eye.time_stop = "100ns" + >>> new_eye.create() + + """ + if not setup: + for setup in self._post_app._app.setups: + if "AMIAnalysis" in setup.props: + setup = setup.name + if not setup: + self._post_app._app.logger.error("AMI analysis is needed to create this report.") + return False + + if isinstance(expressions, list): + expressions = expressions[0] + report_cat = "Standard" + rep = ansys.aedt.core.visualization.report.eye.AMIConturEyeDiagram(self._post_app, report_cat, setup) + rep.quantity_type = quantity_type + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + + return rep + return + + @pyaedt_function_handler(setup_name="setup") + def eye_diagram( + self, expressions=None, setup=None, quantity_type=3, statistical_analysis=True, unit_interval="1ns" + ): + """Create a Standard or Default Report object. + + Parameters + ---------- + expressions : str + Expression to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + quantity_type : int, optional + For AMI Analysis only, specify the quantity type. Options are: 0 for Initial Wave, + 1 for Wave after Source, 2 for Wave after Channel and 3 for Wave after Probe. Default is 3. + statistical_analysis : bool, optional + For AMI Analysis only, whether to plot the statistical eye plot or transient eye plot. + The default is ``True``. + unit_interval : str, optional + Unit interval for the eye diagram. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Standard` + + Examples + -------- + + >>> from ansys.aedt.core import Circuit + >>> cir= Circuit() + >>> new_eye = cir.post.reports_by_category.eye_diagram("V(Vout)") + >>> new_eye.unit_interval = "1e-9s" + >>> new_eye.time_stop = "100ns" + >>> new_eye.create() + + """ + if not setup: + setup = self._post_app._app.nominal_sweep + if "Eye Diagram" in self._templates: + if "AMIAnalysis" in self._post_app._app.get_setup(setup).props: + + report_cat = "Eye Diagram" + if statistical_analysis: + report_cat = "Statistical Eye" + rep = ansys.aedt.core.visualization.report.eye.AMIEyeDiagram(self._post_app, report_cat, setup) + rep.quantity_type = quantity_type + expressions = self._retrieve_default_expressions(expressions, rep, setup) + if isinstance(expressions, list): + rep.expressions = expressions[0] + return rep + + else: + rep = ansys.aedt.core.visualization.report.eye.EyeDiagram(self._post_app, "Eye Diagram", setup) + rep.unit_interval = unit_interval + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + return + + @pyaedt_function_handler(setup_name="setup") + def spectral(self, expressions=None, setup=None): + """Create a Spectral Report object. + + Parameters + ---------- + expressions : str or list, optional + Expression List to add into the report. The expression can be any of the available formula + you can enter into the Electronics Desktop Report Editor. + setup : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to + use in the export or ``LastAdaptive``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.Spectrum` + + Examples + -------- + + >>> from ansys.aedt.core import Circuit + >>> cir= Circuit() + >>> new_eye = cir.post.reports_by_category.spectral("V(Vout)") + >>> new_eye.create() + + """ + if not setup: + setup = self._post_app._app.nominal_sweep + rep = None + if "Spectrum" in self._templates: + rep = ansys.aedt.core.visualization.report.standard.Spectral(self._post_app, "Spectrum", setup) + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) + return rep + + @pyaedt_function_handler() + def emi_receiver(self, expressions=None, setup_name=None): + """Create an EMI receiver report. + + Parameters + ---------- + expressions : str or list, optional + One or more expressions to add into the report. An expression can be any of the formulas that + can be entered into the Electronics Desktop Report Editor. + setup_name : str, optional + Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` + setup is used. Be sure to build a setup string in the form of + ``"SetupName : SetupSweep"``, where ``SetupSweep`` is either the sweep name + to use in the export or ``LastAdaptive``. + + Returns + ------- + :class:`ansys.aedt.core.modules.report_templates.EMIReceiver` + + Examples + -------- + + >>> from ansys.aedt.core import Circuit + >>> cir= Circuit() + >>> new_eye = cir.post.emi_receiver() + >>> new_eye.create() + + """ + if not setup_name: + setup_name = self._post_app._app.nominal_sweep + rep = None + if "EMIReceiver" in self._templates and self._post_app._app.desktop_class.aedt_version_id > "2023.2": + rep = ansys.aedt.core.visualization.report.emi.EMIReceiver(self._post_app, setup_name) + if not expressions: + expressions = f"Average[{rep.net}]" + else: + if not isinstance(expressions, list): + expressions = [expressions] + pattern = r"\w+\[(.*?)\]" + for expression in expressions: + match = re.search(pattern, expression) + if match: + net_name = match.group(1) + rep.net = net_name + rep.expressions = self._retrieve_default_expressions(expressions, rep, setup_name) + + return rep diff --git a/src/ansys/aedt/core/generic/compliance.py b/src/ansys/aedt/core/visualization/post/compliance.py similarity index 98% rename from src/ansys/aedt/core/generic/compliance.py rename to src/ansys/aedt/core/visualization/post/compliance.py index 9d30c6b0872..53cfb0707cf 100644 --- a/src/ansys/aedt/core/generic/compliance.py +++ b/src/ansys/aedt/core/visualization/post/compliance.py @@ -33,9 +33,9 @@ from ansys.aedt.core.generic.general_methods import read_configuration_file from ansys.aedt.core.generic.general_methods import read_csv from ansys.aedt.core.generic.general_methods import write_csv -from ansys.aedt.core.generic.pdf import AnsysReport -from ansys.aedt.core.generic.spisim import SpiSim from ansys.aedt.core.modeler.geometry_operators import GeometryOperators +from ansys.aedt.core.visualization.plot.pdf import AnsysReport +from ansys.aedt.core.visualization.post.spisim import SpiSim default_keys = [ "file", @@ -283,6 +283,7 @@ def parameters(self, val): @property def add_project_info(self): + """Add project information.""" return self._add_project_info @add_project_info.setter @@ -291,6 +292,7 @@ def add_project_info(self, val): @property def add_specs_info(self): + """Add specification information.""" return self._add_specs_info @add_specs_info.setter @@ -299,6 +301,7 @@ def add_specs_info(self, val): @property def specs_folder(self): + """Add specification folder.""" return self._specs_folder @specs_folder.setter @@ -309,6 +312,7 @@ def specs_folder(self, val): @property def template_name(self): + """Template name.""" return self._template_name @template_name.setter @@ -317,6 +321,7 @@ def template_name(self, val): @property def project_file(self): + """Project file.""" return self._project_file @project_file.setter @@ -325,10 +330,12 @@ def project_file(self, val): @property def project_name(self): + """Project name.""" return Path(self.project_file).stem @property def use_portrait(self): + """Use portrait.""" return self._use_portrait @use_portrait.setter @@ -809,7 +816,7 @@ def _add_contour_eye_diagram_violations(self, report, pdf_report, image_name, lo if not mag_data: result_value = "FAILED. No BER obtained" for point in mag_data: - if GeometryOperators.point_in_polygon(point, points_to_check) >= 0: + if GeometryOperators.point_in_polygon(point[:2], points_to_check) >= 0: result_value = "FAILED. Mask Violation" break font_table.append([None, [255, 0, 0]] if "FAIL" in result_value else ["", None]) diff --git a/src/ansys/aedt/core/visualization/post/farfield_exporter.py b/src/ansys/aedt/core/visualization/post/farfield_exporter.py new file mode 100644 index 00000000000..bfbcbc3af4a --- /dev/null +++ b/src/ansys/aedt/core/visualization/post/farfield_exporter.py @@ -0,0 +1,357 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import shutil +import time + +from ansys.aedt.core.application.analysis_hf import ScatteringMethods +from ansys.aedt.core.application.variables import decompose_variable_value +from ansys.aedt.core.generic.constants import unit_converter +from ansys.aedt.core.generic.general_methods import check_and_download_folder +from ansys.aedt.core.generic.general_methods import pyaedt_function_handler +from ansys.aedt.core.generic.settings import settings +from ansys.aedt.core.visualization.advanced.farfield_visualization import FfdSolutionData +from ansys.aedt.core.visualization.advanced.farfield_visualization import export_pyaedt_antenna_metadata + + +class FfdSolutionDataExporter: + """Class to enable export of embedded element pattern data from HFSS. + + An instance of this class is returned from the + :meth:`ansys.aedt.core.Hfss.get_antenna_data` method. This method allows creation of + the embedded + element pattern files for an antenna that have been solved in HFSS. The + ``metadata_file`` properties can then be passed as arguments to + instantiate an instance of the :class:`ansys.aedt.core.generic.farfield_visualization.FfdSolutionData` class for + subsequent analysis and postprocessing of the array data. + + Note that this class is derived from the :class:`FfdSolutionData` class and can be used directly for + far-field postprocessing and array analysis, but it remains a property of the + :class:`ansys.aedt.core.Hfss` application. + + Parameters + ---------- + app : :class:`ansys.aedt.core.Hfss` + HFSS application instance. + sphere_name : str + Infinite sphere to use. + setup_name : str + Name of the setup. Make sure to build a setup string in the form of ``"SetupName : SetupSweep"``. + frequencies : list + Frequency list to export. Specify either a list of strings with units or a list of floats in Hertz units. + For example, ``["9GHz", 9e9]``. + variations : dict, optional + Dictionary of all families including the primary sweep. The default value is ``None``. + overwrite : bool, optional + Whether to overwrite the existing far field solution data. The default is ``True``. + export_touchstone : bool, optional + Whether to export touchstone file. The default is ``False``. Working from 2024 R1. + set_phase_center_per_port : bool, optional + Set phase center per port location. The default is ``True``. + + Examples + -------- + >>> from ansys.aedt.core + >>> app = ansys.aedt.core.Hfss(version="2023.2", design="Antenna") + >>> setup_name = "Setup1 : LastAdaptive" + >>> frequencies = [77e9] + >>> sphere = "3D" + >>> data = app.get_antenna_data(frequencies,setup_name,sphere) + >>> data.plot_3d(quantity_format="dB10") + """ + + def __init__( + self, + app, + sphere_name, + setup_name, + frequencies, + variations=None, + overwrite=True, + export_touchstone=True, + set_phase_center_per_port=True, + ): + # Public + self.sphere_name = sphere_name + self.setup_name = setup_name + + if not variations: + variations = app.available_variations.nominal_w_values_dict_w_dependent + else: + # Set variation to Nominal + for var_name, var_value in variations.items(): + app[var_name] = var_value + + self.variations = variations + self.overwrite = overwrite + self.export_touchstone = export_touchstone + if not isinstance(frequencies, list): + self.frequencies = [frequencies] + else: + self.frequencies = frequencies + + # Private + self.__app = app + self.__model_info = {} + self.__farfield_data = None + self.__metadata_file = "" + + if self.__app.desktop_class.is_grpc_api and set_phase_center_per_port: + self.__app.set_phase_center_per_port() + else: # pragma: no cover + self.__app.logger.warning("Set phase center in port location manually.") + + @property + def model_info(self): + """List of models.""" + return self.__model_info + + @property + def farfield_data(self): + """Farfield data.""" + return self.__farfield_data + + @property + def metadata_file(self): + """Metadata file.""" + return self.__metadata_file + + @pyaedt_function_handler() + def export_farfield(self): + """Export far field solution data of each element.""" + + # Output directory + exported_name_map = "element.txt" + solution_setup_name = self.setup_name.replace(":", "_").replace(" ", "") + full_setup = "{}-{}".format(solution_setup_name, self.sphere_name) + export_path = "{}/{}/".format(self.__app.working_directory, full_setup) + local_path = "{}/{}/".format(settings.remote_rpc_session_temp_folder, full_setup) + export_path = os.path.abspath(check_and_download_folder(local_path, export_path)) + + # 2024.1 + file_path_xml = os.path.join(export_path, self.__app.design_name + ".xml") + # 2023.2 + file_path_txt = os.path.join(export_path, exported_name_map) + + input_file = file_path_xml + if self.__app.desktop_class.aedt_version_id < "2024.1": # pragma: no cover + input_file = file_path_txt + + # Create directory or check if files already exist + if settings.remote_rpc_session: # pragma: no cover + settings.remote_rpc_session.filemanager.makedirs(export_path) + file_exists = settings.remote_rpc_session.filemanager.pathexists(input_file) + elif not os.path.exists(export_path): + os.makedirs(export_path) + file_exists = False + else: + file_exists = os.path.exists(input_file) + + time_before = time.time() + + # Export far field + if self.overwrite or not file_exists: + if self.__app.desktop_class.aedt_version_id < "2024.1": # pragma: no cover + is_exported = self.__app.export_element_pattern( + frequencies=self.frequencies, + setup=self.setup_name, + sphere=self.sphere_name, + variations=self.variations, + output_dir=export_path, + ) + if not is_exported: # pragma: no cover + return False + if self.export_touchstone: + scattering = ScatteringMethods(self.__app) + setup_sweep_parts = self.setup_name.split(":") + + setup_name = setup_sweep_parts[0].strip() + sweep_name = setup_sweep_parts[1].strip() + + touchstone_file = scattering.export_touchstone(setup=setup_name, sweep=sweep_name) + + if touchstone_file: + touchstone_name = os.path.basename(touchstone_file) + output_file = os.path.join(export_path, touchstone_name) + shutil.move(touchstone_file, output_file) + else: + is_exported = self.__app.export_antenna_metadata( + frequencies=self.frequencies, + setup=self.setup_name, + sphere=self.sphere_name, + variations=self.variations, + output_dir=export_path, + export_element_pattern=True, + export_objects=False, + export_touchstone=True, + export_power=True, + ) + if not is_exported: # pragma: no cover + return False + else: + self.__app.logger.info("Using existing element patterns files.") + + # Export geometry + if os.path.isfile(input_file): + geometry_path = os.path.join(export_path, "geometry") + if not os.path.exists(geometry_path): + os.mkdir(geometry_path) + obj_list = self.__create_geometries(geometry_path) + if obj_list: + self.__model_info["object_list"] = obj_list + + if self.__app.component_array: + component_array = self.__app.component_array[self.__app.component_array_names[0]] + self.__model_info["component_objects"] = component_array.get_component_objects() + self.__model_info["cell_position"] = component_array.get_cell_position() + self.__model_info["array_dimension"] = [ + component_array.a_length, + component_array.b_length, + component_array.a_length / component_array.a_size, + component_array.b_length / component_array.b_size, + ] + self.__model_info["lattice_vector"] = component_array.lattice_vector() + + # Create PyAEDT Metadata + var = [] + if self.variations: + for k, v in self.variations.items(): + var.append("{}='{}'".format(k, v)) + variation = " ".join(var) + else: + variation = self.__app.odesign.GetNominalVariation() + + power = {} + + if self.__app.desktop_class.aedt_version_id < "2024.1": + + available_categories = self.__app.post.available_quantities_categories() + excitations = [] + is_power = True + if "Active VSWR" in available_categories: # pragma: no cover + quantities = self.post.available_report_quantities(quantities_category="Active VSWR") + for quantity in quantities: + excitations.append("ElementPatterns:=") + excitations.append(quantity.strip("ActiveVSWR(").strip(")")) + elif "Terminal VSWR" in available_categories: + quantities = self.__app.post.available_report_quantities(quantities_category="Terminal VSWR") + for quantity in quantities: + excitations.append("ElementPatterns:=") + excitations.append(quantity.strip("VSWRt(").strip(")")) + is_power = False + elif "Gamma" in available_categories: + quantities = self.__app.post.available_report_quantities(quantities_category="Gamma") + for quantity in quantities: + excitations.append("ElementPatterns:=") + excitations.append(quantity.strip("Gamma(").strip(")")) + else: # pragma: no cover + for excitation in self.__app.get_all_sources(): + excitations.append("ElementPatterns:=") + excitations.append(excitation) + for excitation_cont1 in range(len(excitations)): + sources = {} + incident_power = {} + accepted_power = {} + radiated_power = {} + unit = "V" + if is_power: + unit = "W" + active_element = excitations[0] + for excitation_cont2, port in enumerate(excitations): + if excitation_cont1 == excitation_cont2: + active_element = port + sources[port] = (f"1{unit}", "0deg") + else: + sources[port] = (f"0{unit}", "0deg") + + power[active_element] = {} + + self.__app.edit_sources(sources) + + report = self.__app.post.reports_by_category.antenna_parameters( + "IncidentPower", self.setup_name, self.sphere_name + ) + data = report.get_solution_data() + incident_powers = data.data_magnitude() + + report = self.__app.post.reports_by_category.antenna_parameters( + "RadiatedPower", self.setup_name, self.sphere_name + ) + data = report.get_solution_data() + radiated_powers = data.data_magnitude() + + report = self.__app.post.reports_by_category.antenna_parameters( + "AcceptedPower", self.setup_name, self.sphere_name + ) + data = report.get_solution_data() + accepted_powers = data.data_magnitude() + + for freq_cont, freq_str in enumerate(self.frequencies): + frequency = freq_str + if isinstance(freq_str, str): + frequency, units = decompose_variable_value(freq_str) + frequency = unit_converter(frequency, "Freq", units, "Hz") + incident_power[frequency] = incident_powers[freq_cont] + radiated_power[frequency] = radiated_powers[freq_cont] + accepted_power[frequency] = accepted_powers[freq_cont] + + power[active_element]["IncidentPower"] = incident_power + power[active_element]["AcceptedPower"] = accepted_power + power[active_element]["RadiatedPower"] = radiated_power + + pyaedt_metadata_file = export_pyaedt_antenna_metadata( + input_file=input_file, output_dir=export_path, variation=variation, model_info=self.model_info, power=power + ) + if not pyaedt_metadata_file: # pragma: no cover + return False + elapsed_time = time.time() - time_before + self.__app.logger.info("Exporting embedded element patterns.... Done: %s seconds", elapsed_time) + self.__metadata_file = pyaedt_metadata_file + self.__farfield_data = FfdSolutionData(pyaedt_metadata_file) + return pyaedt_metadata_file + + @pyaedt_function_handler() + def __create_geometries(self, export_path): + """Export the geometry in OBJ format.""" + self.__app.logger.info("Exporting geometry...") + model_pv = self.__app.post.get_model_plotter_geometries(plot_air_objects=False) + obj_list = {} + for obj in model_pv.objects: + object_name = os.path.basename(obj.path) + name = os.path.splitext(object_name)[0] + original_path = os.path.dirname(obj.path) + new_path = os.path.join(os.path.abspath(export_path), object_name) + + if not os.path.exists(new_path): + new_path = shutil.move(obj.path, export_path) + if os.path.exists(os.path.join(original_path, name + ".mtl")): # pragma: no cover + shutil.rmtree(os.path.join(original_path, name + ".mtl"), ignore_errors=True) + obj_list[obj.name] = [ + os.path.join(os.path.basename(export_path), object_name), + obj.color, + obj.opacity, + obj.units, + ] + return obj_list diff --git a/src/ansys/aedt/core/visualization/post/field_data.py b/src/ansys/aedt/core/visualization/post/field_data.py new file mode 100644 index 00000000000..70d7dbb85eb --- /dev/null +++ b/src/ansys/aedt/core/visualization/post/field_data.py @@ -0,0 +1,1771 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from abc import abstractmethod +from collections import defaultdict +import csv +import os +import shutil +import sys +import tempfile + +from ansys.aedt.core.generic.constants import AllowedMarkers +from ansys.aedt.core.generic.constants import EnumUnits +from ansys.aedt.core.generic.data_handlers import _dict2arg +from ansys.aedt.core.generic.general_methods import GrpcApiError +from ansys.aedt.core.generic.general_methods import check_and_download_file +from ansys.aedt.core.generic.general_methods import open_file +from ansys.aedt.core.generic.general_methods import pyaedt_function_handler +from ansys.aedt.core.generic.load_aedt_file import load_keyword_in_aedt_file +from ansys.aedt.core.modeler.cad.elements_3d import FacePrimitive + +pd = None + +try: + import pandas as pd +except ImportError: + pd = None + + +class BaseFolderPlot: + @abstractmethod + def to_dict(self): + """Convert the settings to a dictionary. + + Returns + ------- + dict + A dictionary containing settings. + """ + + @abstractmethod + def from_dict(self, dictionary): + """Initialize the settings from a dictionary. + + Parameters + ---------- + dictionary : dict + Dictionary containing the configuration settings. + Dictionary syntax must be the same of the AEDT file. + """ + + +class ColorMapSettings(BaseFolderPlot): + """Provides methods and variables for editing color map folder settings. + + Parameters + ---------- + map_type : str, optional + The type of colormap to use. Must be one of the allowed types + (`"Spectrum"`, `"Ramp"`, `"Uniform"`). + Default is `"Spectrum"`. + color : str or list[float], optional + Color to use. If "Spectrum" color map, a string is expected. + Else a list of 3 values (R,G,B). Default is `"Rainbow"`. + """ + + def __init__(self, map_type="Spectrum", color="Rainbow"): + self._map_type = None + self.map_type = map_type + + # Default color settings + self._color_spectrum = "Rainbow" + self._color_ramp = [255, 127, 127] + self._color_uniform = [127, 255, 255] + + # User-provided color settings + self.color = color + + @property + def map_type(self): + """Get the color map type for the field plot.""" + return self._map_type + + @map_type.setter + def map_type(self, value): + """Set the type of color mapping for the field plot. + + Parameters + ---------- + value : str + The type of mapping to set. Must be one of 'Spectrum', 'Ramp', or 'Uniform'. + + Raises + ------ + ValueError + If the provided `value` is not valid, raises a ``ValueError`` with an appropriate message. + """ + if value not in ["Spectrum", "Ramp", "Uniform"]: + raise ValueError(f"{value} is not valid. Only 'Spectrum', 'Ramp', and 'Uniform' are accepted.") + self._map_type = value + + @property + def color(self): + """Get the color based on the map type. + + Returns: + str or list of float: The color scheme based on the map type. + """ + if self.map_type == "Spectrum": + return self._color_spectrum + elif self.map_type == "Ramp": + return self._color_ramp + elif self.map_type == "Uniform": + return self._color_uniform + + @color.setter + def color(self, v): + """Set the colormap based on the map type. + + Parameters: + ----------- + v : str or list[float] + The color value to be set. If a string, it should represent a valid color + spectrum specification (`"Magenta"`, `"Rainbow"`, `"Temperature"` or `"Gray"`). + If a tuple, it should contain three elements representing RGB values. + + Raises: + ------- + ValueError: If the provided color value is not valid for the specified map type. + """ + if self.map_type == "Spectrum": + self._validate_color_spectrum(v) + self._color_spectrum = v + else: + self._validate_color(v) + if self.map_type == "Ramp": + self._color_ramp = v + else: + self._color_uniform = v + + @staticmethod + def _validate_color_spectrum(value): + if value not in ["Magenta", "Rainbow", "Temperature", "Gray"]: + raise ValueError( + f"{value} is not valid. Only 'Magenta', 'Rainbow', 'Temperature', and 'Gray' are accepted." + ) + + @staticmethod + def _validate_color(value): + if not isinstance(value, list) or len(value) != 3: + raise ValueError(f"{value} is not valid. Three values (R, G, B) must be passed.") + + def __repr__(self): + color_repr = self.color + return f"ColorMapSettings(map_type='{self.map_type}', color={color_repr})" + + def to_dict(self): + """Convert the color map settings to a dictionary. + + Returns + ------- + dict + A dictionary containing all the color map settings + for the folder field plot settings. + """ + return { + "ColorMapSettings": { + "ColorMapType": self.map_type, + {"Spectrum": "SpectrumType", "Uniform": "UniformColor", "Ramp": "RampColor"}[self.map_type]: self.color, + } + } + + def from_dict(self, settings): + """Initialize the number format settings of the colormap settings from a dictionary. + + Parameters + ---------- + dictionary : dict + Dictionary containing the configuration for colormap settings. + Dictionary syntax must be the same of relevant portion of the AEDT file. + """ + self._map_type = settings["ColorMapType"] + self._color_spectrum = settings["SpectrumType"] + self._color_ramp = settings["RampColor"] + self._color_uniform = settings["UniformColor"] + + +class AutoScale(BaseFolderPlot): + """Provides methods and variables for editing automatic scale folder settings. + + Parameters + ---------- + n_levels : int, optional + Number of color levels of the scale. Default is `10`. + limit_precision_digits : bool, optional + Whether to limit precision digits. Default is `False`. + precision_digits : int, optional + Precision digits. Default is `3`. + use_current_scale_for_animation : bool, optional + Whether to use the scale for the animation. Default is `False`. + """ + + def __init__( + self, n_levels=10, limit_precision_digits=False, precision_digits=3, use_current_scale_for_animation=False + ): + self.n_levels = n_levels + self.limit_precision_digits = limit_precision_digits + self.precision_digits = precision_digits + self.use_current_scale_for_animation = use_current_scale_for_animation + + def __repr__(self): + return ( + f"AutoScale(n_levels={self.n_levels}, " + f"limit_precision_digits={self.limit_precision_digits}, " + f"precision_digits={self.precision_digits}, " + f"use_current_scale_for_animation={self.use_current_scale_for_animation})" + ) + + def to_dict(self): + """Convert the auto-scale settings to a dictionary. + + Returns + ------- + dict + A dictionary containing all the auto-scale settings + for the folder field plot settings. + """ + return { + "m_nLevels": self.n_levels, + "LimitFieldValuePrecision": self.limit_precision_digits, + "FieldValuePrecisionDigits": self.precision_digits, + "AnimationStaticScale": self.use_current_scale_for_animation, + } + + def from_dict(self, dictionary): + """Initialize the auto-scale settings from a dictionary. + + Parameters + ---------- + dictionary : dict + Dictionary containing the configuration for auto-scale settings. + Dictionary syntax must be the same of relevant portion of the AEDT file. + """ + self.n_levels = dictionary["m_nLevels"] + self.limit_precision_digits = dictionary["LimitFieldValuePrecision"] + self.precision_digits = dictionary["FieldValuePrecisionDigits"] + self.use_current_scale_for_animation = dictionary["AnimationStaticScale"] + + +class MinMaxScale(BaseFolderPlot): + """Provides methods and variables for editing min-max scale folder settings. + + Parameters + ---------- + n_levels : int, optional + Number of color levels of the scale. Default is `10`. + min_value : float, optional + Minimum value of the scale. Default is `0`. + max_value : float, optional + Maximum value of the scale. Default is `1`. + """ + + def __init__(self, n_levels=10, min_value=0, max_value=1): + self.n_levels = n_levels + self.min_value = min_value + self.max_value = max_value + + def __repr__(self): + return f"MinMaxScale(n_levels={self.n_levels}, min_value={self.min_value}, max_value={self.max_value})" + + def to_dict(self): + """Convert the min-max scale settings to a dictionary. + + Returns + ------- + dict + A dictionary containing all the min-max scale settings + for the folder field plot settings. + """ + return {"minvalue": self.min_value, "maxvalue": self.max_value, "m_nLevels": self.n_levels} + + def from_dict(self, dictionary): + """Initialize the min-max scale settings from a dictionary. + + Parameters + ---------- + dictionary : dict + Dictionary containing the configuration for min-max scale settings. + Dictionary syntax must be the same of relevant portion of the AEDT file. + """ + self.min_value = dictionary["minvalue"] + self.max_value = dictionary["maxvalue"] + self.n_levels = dictionary["m_nLevels"] + + +class SpecifiedScale: + """Provides methods and variables for editing min-max scale folder settings. + + Parameters + ---------- + scale_values : int, optional + Scale levels. Default is `None`. + """ + + def __init__(self, scale_values=None): + if scale_values is None: + scale_values = [] + if not isinstance(scale_values, list): + raise ValueError("scale_values must be a list.") + self.scale_values = scale_values + + def __repr__(self): + return f"SpecifiedScale(scale_values={self.scale_values})" + + def to_dict(self): + """Convert the specified scale settings to a dictionary. + + Returns + ------- + dict + A dictionary containing all the specified scale settings + for the folder field plot settings. + """ + return {"UserSpecifyValues": [len(self.scale_values)] + self.scale_values} + + def from_dict(self, dictionary): + """Initialize the specified scale settings from a dictionary. + + Parameters + ---------- + dictionary : dict + Dictionary containing the configuration for specified scale settings. + Dictionary syntax must be the same of relevant portion of the AEDT file. + """ + self.scale_values = dictionary["UserSpecifyValues"][:-1] + + +class NumberFormat(BaseFolderPlot): + """Provides methods and variables for editing number format folder settings. + + Parameters + ---------- + format_type : int, optional + Scale levels. Default is `None`. + width : int, optional + Width of the numbers space. Default is `4`. + precision : int, optional + Precision of the numbers. Default is `4`. + """ + + def __init__(self, format_type="Automatic", width=4, precision=4): + self._format_type = format_type + self.width = width + self.precision = precision + self._accepted = ["Automatic", "Scientific", "Decimal"] + + @property + def format_type(self): + """Get the current number format type.""" + return self._format_type + + @format_type.setter + def format_type(self, v): + """Set the numeric format type of the scale. + + Parameters: + ----------- + v (str): The new format type to be set. Must be one of the accepted values + ("Automatic", "Scientific" or "Decimal"). + + Raises: + ------- + ValueError: If the provided value is not in the list of accepted values. + """ + if v is not None and v in self._accepted: + self._format_type = v + else: + raise ValueError(f"{v} is not valid. Accepted values are {', '.join(self._accepted)}.") + + def __repr__(self): + return f"NumberFormat(format_type={self.format_type}, width={self.width}, precision={self.precision})" + + def to_dict(self): + """Convert the number format settings to a dictionary. + + Returns + ------- + dict + A dictionary containing all the number format settings + for the folder field plot settings. + """ + return { + "ValueNumberFormatTypeAuto": self._accepted.index(self.format_type), + "ValueNumberFormatTypeScientific": self.format_type == "Scientific", + "ValueNumberFormatWidth": self.width, + "ValueNumberFormatPrecision": self.precision, + } + + def from_dict(self, dictionary): + """Initialize the number format settings of the field plot settings from a dictionary. + + Parameters + ---------- + dictionary : dict + Dictionary containing the configuration for number format settings. + Dictionary syntax must be the same of relevant portion of the AEDT file. + """ + self._format_type = self._accepted[dictionary["ValueNumberFormatTypeAuto"]] + self.width = dictionary["ValueNumberFormatWidth"] + self.precision = dictionary["ValueNumberFormatPrecision"] + + +class Scale3DSettings(BaseFolderPlot): + """Provides methods and variables for editing scale folder settings. + + Parameters + ---------- + scale_type : str, optional + Scale type. Default is `"Auto"`. + scale_settings : :class:`ansys.aedt.core.modules.post_general.AutoScale`, + :class:`ansys.aedt.core.modules.post_general.MinMaxScale` or + :class:`ansys.aedt.core.modules.post_general.SpecifiedScale`, optional + Scale settings. Default is `AutoScale()`. + log : bool, optional + Whether to use a log scale. Default is `False`. + db : bool, optional + Whether to use dB scale. Default is `False`. + unit : int, optional + Unit to use in the scale. Default is `None`. + number_format : :class:`ansys.aedt.core.modules.post_general.NumberFormat`, optional + Number format settings. Default is `NumberFormat()`. + """ + + def __init__( + self, + scale_type="Auto", + scale_settings=AutoScale(), + log=False, + db=False, + unit=None, + number_format=NumberFormat(), + ): + self._scale_type = None # Initialize with None to use the setter for validation + self._scale_settings = None + self._unit = None + self._auto_scale = AutoScale() + self._minmax_scale = MinMaxScale() + self._specified_scale = SpecifiedScale() + self._accepted = ["Auto", "MinMax", "Specified"] + self.number_format = number_format + self.log = log + self.db = db + self.unit = unit + self.scale_type = scale_type # This will trigger the setter and validate the scale_type + self.scale_settings = scale_settings + + @property + def unit(self): + """Get unit used in the plot.""" + return EnumUnits(self._unit).name + + @unit.setter + def unit(self, v): + """Set unit used in the plot. + + Parameters + ---------- + v: str + Unit to be set. + """ + if v is not None: + try: + self._unit = EnumUnits[v].value + except KeyError: + raise KeyError(f"{v} is not a valid unit.") + + @property + def scale_type(self): + """Get type of scale used for the field plot.""" + return self._scale_type + + @scale_type.setter + def scale_type(self, value): + """Set the scale type used for the field plot. + + Parameters: + ----------- + value (str): The type of scaling to set. + Must be one of the accepted values ("Auto", "MinMax" or "Specified"). + + Raises: + ------- + ValueError: If the provided value is not in the list of accepted values. + """ + if value is not None and value not in self._accepted: + raise ValueError(f"{value} is not valid. Accepted values are {', '.join(self._accepted)}.") + self._scale_type = value + # Automatically adjust scale_settings based on scale_type + if value == "Auto": + self._scale_settings = self._auto_scale + elif value == "MinMax": + self._scale_settings = self._minmax_scale + elif value == "Specified": + self._scale_settings = self._specified_scale + + @property + def scale_settings(self): + """Get the current scale settings based on the scale type.""" + self.scale_type = self.scale_type # update correct scale settings + return self._scale_settings + + @scale_settings.setter + def scale_settings(self, value): + """Set the current scale settings based on the scale type.""" + if self.scale_type == "Auto": + if isinstance(value, AutoScale): + self._scale_settings = value + return + elif self.scale_type == "MinMax": + if isinstance(value, MinMaxScale): + self._scale_settings = value + return + elif self.scale_type == "Specified": + if isinstance(value, SpecifiedScale): + self._scale_settings = value + return + raise ValueError("Invalid scale settings for current scale type.") + + def __repr__(self): + return ( + f"Scale3DSettings(scale_type='{self.scale_type}', scale_settings={self.scale_settings}, " + f"log={self.log}, db={self.db})" + ) + + def to_dict(self): + """Convert the scale settings to a dictionary. + + Returns + ------- + dict + A dictionary containing all scale settings + for the folder field plot settings. + """ + arg_out = { + "Scale3DSettings": { + "unit": self._unit, + "ScaleType": self._accepted.index(self.scale_type), + "log": self.log, + "dB": self.db, + } + } + arg_out["Scale3DSettings"].update(self.number_format.to_dict()) + arg_out["Scale3DSettings"].update(self.scale_settings.to_dict()) + return arg_out + + def from_dict(self, dictionary): + """Initialize the scale settings of the field plot settings from a dictionary. + + Parameters + ---------- + dictionary : dict + Dictionary containing the configuration for scale settings. + Dictionary syntax must be the same of relevant portion of the AEDT file. + """ + self._scale_type = self._accepted[dictionary["ScaleType"]] + self.number_format = NumberFormat() + self.number_format.from_dict(dictionary) + self.log = dictionary["log"] + self.db = dictionary["dB"] + self.unit = EnumUnits(int(dictionary["unit"])).name + self._auto_scale = AutoScale() + self._auto_scale.from_dict(dictionary) + self._minmax_scale = MinMaxScale() + self._minmax_scale.from_dict(dictionary) + self._specified_scale = SpecifiedScale() + self._specified_scale.from_dict(dictionary) + + +class MarkerSettings(BaseFolderPlot): + """Provides methods and variables for editing marker folder settings. + + Parameters + ---------- + marker_type : str, optional + The type of maker to use. Must be one of the allowed types + (`"Octahedron"`, `"Tetrahedron"`, `"Sphere"`, `"Box"`, `"Arrow"`). + Default is `"Box"`. + marker_size : float, optional + Size of the marker. Default is `0.005`. + map_size : bool, optional + Whether to map the field magnitude to the arrow type. Default is `False`. + map_color : bool, optional + Whether to map the field magnitude to the arrow color. Default is `True`. + """ + + def __init__(self, marker_type="Box", map_size=False, map_color=True, marker_size=0.005): + self._marker_type = None + self.marker_type = marker_type + self.map_size = map_size + self.map_color = map_color + self.marker_size = marker_size + + @property + def marker_type(self): + """Get the type of maker to use.""" + return AllowedMarkers(self._marker_type).name + + @marker_type.setter + def marker_type(self, v): + """Set the type of maker to use. + + Parameters: + ---------- + v : str + Marker type. Must be one of the allowed types + (`"Octahedron"`, `"Tetrahedron"`, `"Sphere"`, `"Box"`, `"Arrow"`). + """ + try: + self._marker_type = AllowedMarkers[v].value + except KeyError: + raise KeyError(f"{v} is not a valid marker type.") + + def __repr__(self): + return ( + f"MarkerSettings(marker_type='{self.marker_type}', map_size={self.map_size}, " + f"map_color={self.map_color}, marker_size={self.marker_size})" + ) + + def to_dict(self): + """Convert the marker settings to a dictionary. + + Returns + ------- + dict + A dictionary containing all the marker settings + for the folder field plot settings. + """ + return { + "Marker3DSettings": { + "MarkerType": self._marker_type, + "MarkerMapSize": self.map_size, + "MarkerMapColor": self.map_color, + "MarkerSize": self.marker_size, + } + } + + def from_dict(self, dictionary): + """Initialize the marker settings of the field plot settings from a dictionary. + + Parameters + ---------- + dictionary : dict + Dictionary containing the configuration for marker settings. + Dictionary syntax must be the same of relevant portion of the AEDT file. + """ + self.marker_type = AllowedMarkers(int(dictionary["MarkerType"])).name + self.map_size = dictionary["MarkerMapSize"] + self.map_color = dictionary["MarkerMapColor"] + self.marker_size = dictionary["MarkerSize"] + + +class ArrowSettings(BaseFolderPlot): + """Provides methods and variables for editing arrow folder settings. + + Parameters + ---------- + arrow_type : str, optional + The type of arrows to use. Must be one of the allowed types + (`"Line"`, `"Cylinder"`, `"Umbrella"`). Default is `"Line"`. + arrow_size : float, optional + Size of the arrow. Default is `0.005`. + map_size : bool, optional + Whether to map the field magnitude to the arrow type. Default is `False`. + map_color : bool, optional + Whether to map the field magnitude to the arrow color. Default is `True`. + show_arrow_tail : bool, optional + Whether to show the arrow tail. Default is `False`. + magnitude_filtering : bool, optional + Whether to filter the field magnitude for plotting vectors. Default is `False`. + magnitude_threshold : bool, optional + Threshold value for plotting vectors. Default is `0`. + min_magnitude : bool, optional + Minimum value for plotting vectors. Default is `0`. + max_magnitude : bool, optional + Maximum value for plotting vectors. Default is `0.5`. + """ + + def __init__( + self, + arrow_type="Line", + arrow_size=0.005, + map_size=False, + map_color=True, + show_arrow_tail=False, + magnitude_filtering=False, + magnitude_threshold=0, + min_magnitude=0, + max_magnitude=0.5, + ): + self._arrow_type = None + self._allowed_arrow_types = ["Line", "Cylinder", "Umbrella"] + self.arrow_type = arrow_type + self.arrow_size = arrow_size + self.map_size = map_size + self.map_color = map_color + self.show_arrow_tail = show_arrow_tail + self.magnitude_filtering = magnitude_filtering + self.magnitude_threshold = magnitude_threshold + self.min_magnitude = min_magnitude + self.max_magnitude = max_magnitude + + @property + def arrow_type(self): + """Get the type of arrows used in the field plot.""" + return self._arrow_type + + @arrow_type.setter + def arrow_type(self, v): + """Set the type of arrows for the field plot. + + Parameters: + ----------- + v (str): The type of arrows to use. Must be one of the allowed types ("Line", "Cylinder", "Umbrella"). + + Raises: + ------- + ValueError: If the provided value is not in the list of allowed arrow types. + """ + if v in self._allowed_arrow_types: + self._arrow_type = v + else: + raise ValueError(f"{v} is not valid. Accepted values are {','.join(self._allowed_arrow_types)}.") + + def __repr__(self): + return ( + f"Arrow3DSettings(arrow_type='{self.arrow_type}', arrow_size={self.arrow_size}, " + f"map_size={self.map_size}, map_color={self.map_color}, " + f"show_arrow_tail={self.show_arrow_tail}, magnitude_filtering={self.magnitude_filtering}, " + f"magnitude_threshold={self.magnitude_threshold}, min_magnitude={self.min_magnitude}, " + f"max_magnitude={self.max_magnitude})" + ) + + def to_dict(self): + """Convert the arrow settings to a dictionary. + + Returns + ------- + dict + A dictionary containing all the arrow settings + for the folder field plot settings. + """ + return { + "Arrow3DSettings": { + "ArrowType": self._allowed_arrow_types.index(self.arrow_type), + "ArrowMapSize": self.map_size, + "ArrowMapColor": self.map_color, # Missing option in ui + "ShowArrowTail": self.show_arrow_tail, + "ArrowSize": self.arrow_size, + "ArrowMinMagnitude": self.min_magnitude, + "ArrowMaxMagnitude": self.max_magnitude, + "ArrowMagnitudeThreshold": self.magnitude_threshold, + "ArrowMagnitudeFilteringFlag": self.magnitude_filtering, + } + } + + def from_dict(self, dictionary): + """Initialize the arrow settings of the field plot settings from a dictionary. + + Parameters + ---------- + dictionary : dict + Dictionary containing the configuration for arrow settings. + Dictionary syntax must be the same of relevant portion of the AEDT file. + """ + self.arrow_type = self._allowed_arrow_types[dictionary["ArrowType"]] + self.arrow_size = dictionary["ArrowType"] + self.map_size = dictionary["ArrowMapSize"] + self.map_color = dictionary["ArrowMapColor"] + self.show_arrow_tail = dictionary["ShowArrowTail"] + self.magnitude_filtering = dictionary["ArrowMagnitudeFilteringFlag"] + self.magnitude_threshold = dictionary["ArrowMagnitudeThreshold"] + self.min_magnitude = dictionary["ArrowMinMagnitude"] + self.max_magnitude = dictionary["ArrowMaxMagnitude"] + + +class FolderPlotSettings(BaseFolderPlot): + """Provides methods and variables for editing field plots folder settings. + + Parameters + ---------- + postprocessor : :class:`ansys.aedt.core.modules.post_general.PostProcessor` + folder_name : str + Name of the plot field folder. + arrow_settings : :class:`ansys.aedt.core.modules.solution.ArrowSettings`, optional + Arrow settings. Default is `None`. + marker_settings : :class:`ansys.aedt.core.modules.solution.MarkerSettings`, optional + Marker settings. Default is `None`. + scale_settings : :class:`ansys.aedt.core.modules.solution.Scale3DSettings`, optional + Scale settings. Default is `None`. + color_map_settings : :class:`ansys.aedt.core.modules.solution.ColorMapSettings`, optional + Colormap settings. Default is `None`. + """ + + def __init__( + self, + postprocessor, + folder_name, + arrow_settings=None, + marker_settings=None, + scale_settings=None, + color_map_settings=None, + ): + self.arrow_settings = arrow_settings + self.marker_settings = marker_settings + self.scale_settings = scale_settings + self.color_map_settings = color_map_settings + self._postprocessor = postprocessor + self._folder_name = folder_name + + def update(self): + """ + Update folder plot settings. + """ + out = [] + _dict2arg(self.to_dict(), out) + self._postprocessor.ofieldsreporter.SetPlotFolderSettings(self._folder_name, out[0]) + + def to_dict(self): + """Convert the field plot settings to a dictionary. + + Returns + ------- + dict + A dictionary containing all the settings for the field plot, + including arrow settings, marker settings, + scale settings, and color map settings. + """ + out = {} + out.update(self.arrow_settings.to_dict()) + out.update(self.marker_settings.to_dict()) + out.update(self.scale_settings.to_dict()) + out.update(self.color_map_settings.to_dict()) + return {"FieldsPlotSettings": out} + + def from_dict(self, dictionary): + """Initialize the field plot settings from a dictionary. + + Parameters + ---------- + dictionary : dict + Dictionary containing the configuration for the color map, + scale, arrow, and marker settings. Dictionary syntax must + be the same of the AEDT file. + """ + cmap = ColorMapSettings() + cmap.from_dict(dictionary["ColorMapSettings"]) + self.color_map_settings = cmap + scale = Scale3DSettings() + scale.from_dict(dictionary["Scale3DSettings"]) + self.scale_settings = scale + arrow = ArrowSettings() + arrow.from_dict(dictionary["Arrow3DSettings"]) + marker = MarkerSettings() + marker.from_dict(dictionary["Marker3DSettings"]) + self.arrow_settings = arrow + self.marker_settings = marker + + +class FieldPlot: + """Provides for creating and editing field plots. + + Parameters + ---------- + postprocessor : :class:`ansys.aedt.core.modules.post_general.PostProcessor` + objects : list + List of objects. + solution : str + Name of the solution. + quantity : str + Name of the plot or the name of the object. + intrinsics : dict, optional + Name of the intrinsic dictionary. The default is ``{}``. + + """ + + @pyaedt_function_handler( + objlist="objects", + surfacelist="surfaces", + linelist="lines", + cutplanelist="cutplanes", + solutionName="solution", + quantityName="quantity", + IntrinsincList="intrinsics", + seedingFaces="seeding_faces", + layers_nets="layer_nets", + layers_plot_type="layer_plot_type", + ) + def __init__( + self, + postprocessor, + objects=None, + surfaces=None, + lines=None, + cutplanes=None, + solution="", + quantity="", + intrinsics=None, + seeding_faces=None, + layer_nets=None, + layer_plot_type="LayerNetsExtFace", + ): + self._postprocessor = postprocessor + self.oField = postprocessor.ofieldsreporter + self.volumes = [] if objects is None else objects + self.surfaces = [] if surfaces is None else surfaces + self.lines = [] if lines is None else lines + self.cutplanes = [] if cutplanes is None else cutplanes + self.layer_nets = [] if layer_nets is None else layer_nets + self.layer_plot_type = layer_plot_type + self.seeding_faces = [] if seeding_faces is None else seeding_faces + self.solution = solution + self.quantity = quantity + self.intrinsics = {} if intrinsics is None else intrinsics + self.name = "Field_Plot" + self.plot_folder = "Field_Plot" + self.Filled = False + self.IsoVal = "Fringe" + self.SmoothShade = True + self.AddGrid = False + self.MapTransparency = True + self.Refinement = 0 + self.Transparency = 0 + self.SmoothingLevel = 0 + self.ArrowUniform = True + self.ArrowSpacing = 0 + self.MinArrowSpacing = 0 + self.MaxArrowSpacing = 0 + self.GridColor = [255, 255, 255] + self.PlotIsoSurface = True + self.PointSize = 1 + self.CloudSpacing = 0.5 + self.CloudMinSpacing = -1 + self.CloudMaxSpacing = -1 + self.LineWidth = 4 + self.LineStyle = "Cylinder" + self.IsoValType = "Tone" + self.NumofPoints = 100 + self.TraceStepLength = "0.001mm" + self.UseAdaptiveStep = True + self.SeedingSamplingOption = True + self.SeedingPointsNumber = 15 + self.FractionOfMaximum = 0.8 + self._filter_boxes = [] + self.field_type = None + self._folder_settings = None + + def _parse_folder_settings(self): + """Parse the folder settings for the field plot from the AEDT file. + + Returns: + FolderPlotSettings or None: An instance of FolderPlotSettings if found, otherwise None. + """ + folder_settings_data = load_keyword_in_aedt_file( + self._postprocessor._app.project_file, + "FieldsPlotManagerID", + design_name=self._postprocessor._app.design_name, + ) + relevant_settings = [ + d + for d in folder_settings_data["FieldsPlotManagerID"].values() + if isinstance(d, dict) and d.get("PlotFolder", False) and d["PlotFolder"] == self.plot_folder + ] + + if not relevant_settings: + self._postprocessor._app.logger.error( + "Could not find settings data in the design properties." + " Define the `FolderPlotSettings` class from scratch or save the project file and try again." + ) + return None + else: + fps = FolderPlotSettings(self._postprocessor, self.plot_folder) + fps.from_dict(relevant_settings[0]) + return fps + + @property + def folder_settings(self): + """Get the folder settings.""" + if self._folder_settings is None: + self._folder_settings = self._parse_folder_settings() + return self._folder_settings + + @folder_settings.setter + def folder_settings(self, v): + """Set the fieldplot folder settings. + + Parameters + ---------- + v : FolderPlotSettings + The new folder plot settings to be set. + + Raises + ------ + ValueError + If the provided value is not an instance of `FolderPlotSettings`. + """ + if isinstance(v, FolderPlotSettings): + self._folder_settings = v + else: + raise ValueError("Invalid type for `folder_settings`, use `FolderPlotSettings` class.") + + @property + def filter_boxes(self): + """Volumes on which filter the plot.""" + return self._filter_boxes + + @filter_boxes.setter + def filter_boxes(self, val): + if isinstance(val, str): + val = [val] + self._filter_boxes = val + + @property + def plotGeomInfo(self): + """Plot geometry information.""" + idx = 0 + if self.volumes: + idx += 1 + if self.surfaces: + idx += 1 + if self.cutplanes: + idx += 1 + if self.lines: + idx += 1 + if self.layer_nets: + idx += 1 + + info = [idx] + if self.volumes: + info.append("Volume") + info.append("ObjList") + info.append(len(self.volumes)) + for index in self.volumes: + info.append(str(index)) + if self.surfaces: + model_faces = [] + nonmodel_faces = [] + if self._postprocessor._app.design_type == "HFSS 3D Layout Design": + model_faces = [str(i) for i in self.surfaces] + else: + models = self._postprocessor.modeler.model_objects + for index in self.surfaces: + try: + if isinstance(index, FacePrimitive): + index = index.id + oname = self._postprocessor.modeler.oeditor.GetObjectNameByFaceID(index) + if oname in models: + model_faces.append(str(index)) + else: + nonmodel_faces.append(str(index)) + except Exception: + self._postprocessor.logger.debug( + "Something went wrong while processing surface {}.".format(index) + ) + info.append("Surface") + if model_faces: + info.append("FacesList") + info.append(len(model_faces)) + for index in model_faces: + info.append(index) + if nonmodel_faces: + info.append("NonModelFaceList") + info.append(len(nonmodel_faces)) + for index in nonmodel_faces: + info.append(index) + if self.cutplanes: + info.append("Surface") + info.append("CutPlane") + info.append(len(self.cutplanes)) + for index in self.cutplanes: + info.append(str(index)) + if self.lines: + info.append("Line") + info.append(len(self.lines)) + for index in self.lines: + info.append(str(index)) + if self.layer_nets: + if self.layer_plot_type == "LayerNets": + info.append("Volume") + info.append("LayerNets") + else: + info.append("Surface") + info.append("LayerNetsExtFace") + info.append(len(self.layer_nets)) + for index in self.layer_nets: + info.append(index[0]) + info.append(len(index[1:])) + info.extend(index[1:]) + return info + + @property + def intrinsicVar(self): + """Intrinsic variable. + + Returns + ------- + list or dict + Variables for the field plot. + """ + var = "" + for a in self.intrinsics: + var += a + "='" + str(self.intrinsics[a]) + "' " + return var + + @property + def plotsettings(self): + """Plot settings. + + Returns + ------- + list + List of plot settings. + """ + if self.surfaces or self.cutplanes or (self.layer_nets and self.layer_plot_type == "LayerNetsExtFace"): + arg = [ + "NAME:PlotOnSurfaceSettings", + "Filled:=", + self.Filled, + "IsoValType:=", + self.IsoVal, + "SmoothShade:=", + self.SmoothShade, + "AddGrid:=", + self.AddGrid, + "MapTransparency:=", + self.MapTransparency, + "Refinement:=", + self.Refinement, + "Transparency:=", + self.Transparency, + "SmoothingLevel:=", + self.SmoothingLevel, + [ + "NAME:Arrow3DSpacingSettings", + "ArrowUniform:=", + self.ArrowUniform, + "ArrowSpacing:=", + self.ArrowSpacing, + "MinArrowSpacing:=", + self.MinArrowSpacing, + "MaxArrowSpacing:=", + self.MaxArrowSpacing, + ], + "GridColor:=", + self.GridColor, + ] + elif self.lines: + arg = [ + "NAME:PlotOnLineSettings", + ["NAME:LineSettingsID", "Width:=", self.LineWidth, "Style:=", self.LineStyle], + "IsoValType:=", + self.IsoValType, + "ArrowUniform:=", + self.ArrowUniform, + "NumofArrow:=", + self.NumofPoints, + "Refinement:=", + self.Refinement, + ] + else: + arg = [ + "NAME:PlotOnVolumeSettings", + "PlotIsoSurface:=", + self.PlotIsoSurface, + "PointSize:=", + self.PointSize, + "Refinement:=", + self.Refinement, + "CloudSpacing:=", + self.CloudSpacing, + "CloudMinSpacing:=", + self.CloudMinSpacing, + "CloudMaxSpacing:=", + self.CloudMaxSpacing, + [ + "NAME:Arrow3DSpacingSettings", + "ArrowUniform:=", + self.ArrowUniform, + "ArrowSpacing:=", + self.ArrowSpacing, + "MinArrowSpacing:=", + self.MinArrowSpacing, + "MaxArrowSpacing:=", + self.MaxArrowSpacing, + ], + ] + return arg + + @pyaedt_function_handler() + def get_points_value(self, points, filename=None, visibility=False): # pragma: no cover + """ + Get points data from field plot. + + .. note:: + This method is working only if the associated field plot is currently visible. + + .. note:: + This method does not work in non-graphical mode. + + Parameters + ---------- + points : list, list of lists or dict + List with [x,y,z] coordinates of a point or list of lists of points or + dictionary with keys containing point names and for each key the point + coordinates [x,y,z]. + filename : str, optional + Full path or relative path with filename. + Default is ``None`` in which case no file is exported. + visibility : bool, optional + Whether to keep the markers visible in the UI. Default is ``False``. + + Returns + ------- + dict or pd.DataFrame + Dict containing 5 keys: point names, x,y,z coordinates and the quantity probed. + Each key is associated with a list with the same length of the argument points. + If pandas is installed, the output is a pandas DataFrame with point names as + index and coordinates and quantity as columns. + """ + self.oField.ClearAllMarkers() + + # Clean inputs + if isinstance(points, dict): + points_name, points_value = list(points.keys()), list(points.values()) + elif isinstance(points, list): + points_name = None + if not isinstance(points[0], list): + points_value = [points] + else: + points_value = points + else: + raise AttributeError("``points`` argument is invalid.") + if filename is not None: + if not os.path.isdir(os.path.dirname(filename)): + raise AttributeError("Specified path ({}) does not exist".format(filename)) + + # Create markers + u = self._postprocessor._app.modeler.model_units + added_points_name = [] + for pt_name_idx, pt in enumerate(points_value): + try: + pt = [c if isinstance(c, str) else "{}{}".format(c, u) for c in pt] + self.oField.AddMarkerToPlot(pt, self.name) + if points_name is not None: + added_points_name.append(points_name[pt_name_idx]) + except (GrpcApiError, SystemExit) as e: # pragma: no cover + self._postprocessor.logger.error( + "Point {} not added. Check if it lies inside the plot.".format(str(pt)) + ) + raise e + + # Export data + temp_file = tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".csv") + temp_file.close() + self.oField.ExportMarkerTable(temp_file.name) + with open_file(temp_file.name, "r") as f: + reader = csv.DictReader(f) + out_dict = defaultdict(list) + for row in reader: + for key in row.keys(): + if key == "Name": + val = row[key] + else: + val = float(row[key].lstrip()) + out_dict[key.lstrip()].append(val) + + # Modify data if needed + if points_name is not None: + out_dict["Name"] = added_points_name + # Export data + if filename is not None: + with open(filename, mode="w") as outfile: + writer = csv.DictWriter(outfile, fieldnames=out_dict.keys()) + writer.writeheader() + for i in range(len(out_dict["Name"])): + row = {field: out_dict[field][i] for field in out_dict} + writer.writerow(row) + elif filename is not None: + # Export data + shutil.copy2(temp_file.name, filename) + os.remove(temp_file.name) + + if not visibility: + self.oField.ClearAllMarkers() + + # Convert to pandas + if pd is not None: + df = pd.DataFrame(out_dict, columns=out_dict.keys()) + df = df.set_index("Name") + return df + else: + return out_dict + + @property + def surfacePlotInstruction(self): + """Surface plot settings. + + Returns + ------- + list + List of surface plot settings. + """ + out = [ + "NAME:" + self.name, + "SolutionName:=", + self.solution, + "QuantityName:=", + self.quantity, + "PlotFolder:=", + self.plot_folder, + ] + if self.field_type: + out.extend(["FieldType:=", self.field_type]) + out.extend( + [ + "UserSpecifyName:=", + 1, + "UserSpecifyFolder:=", + 1, + "StreamlinePlot:=", + False, + "AdjacentSidePlot:=", + False, + "FullModelPlot:=", + False, + "IntrinsicVar:=", + self.intrinsicVar, + "PlotGeomInfo:=", + self.plotGeomInfo, + "FilterBoxes:=", + [len(self.filter_boxes)] + self.filter_boxes, + self.plotsettings, + "EnableGaussianSmoothing:=", + False, + "SurfaceOnly:=", + True if self.surfaces or self.cutplanes else False, + ] + ) + return out + + @property + def surfacePlotInstructionLineTraces(self): + """Surface plot settings for field line traces. + + ..note:: + ``Specify seeding points on selections`` is by default set to ``by sampling``. + + Returns + ------- + list + List of plot settings for line traces. + """ + out = [ + "NAME:" + self.name, + "SolutionName:=", + self.solution, + "UserSpecifyName:=", + 0, + "UserSpecifyFolder:=", + 0, + "QuantityName:=", + "QuantityName_FieldLineTrace", + "PlotFolder:=", + self.plot_folder, + ] + if self.field_type: + out.extend(["FieldType:=", self.field_type]) + out.extend( + [ + "IntrinsicVar:=", + self.intrinsicVar, + "Trace Step Length:=", + self.TraceStepLength, + "Use Adaptive Step:=", + self.UseAdaptiveStep, + "Seeding Faces:=", + self.seeding_faces, + "Seeding Markers:=", + [0], + "Surface Tracing Objects:=", + self.surfaces, + "Volume Tracing Objects:=", + self.volumes, + "Seeding Sampling Option:=", + self.SeedingSamplingOption, + "Seeding Points Number:=", + self.SeedingPointsNumber, + "Fractional of Maximal:=", + self.FractionOfMaximum, + "Discrete Seeds Option:=", + "Marker Point", + [ + "NAME:InceptionEvaluationSettings", + "Gas Type:=", + 0, + "Gas Pressure:=", + 1, + "Use Inception:=", + True, + "Potential U0:=", + 0, + "Potential K:=", + 0, + "Potential A:=", + 1, + ], + self.field_line_trace_plot_settings, + ] + ) + return out + + @property + def field_plot_settings(self): + """Field Plot Settings. + + Returns + ------- + list + Field Plot Settings. + """ + return [ + "NAME:FieldsPlotItemSettings", + [ + "NAME:PlotOnSurfaceSettings", + "Filled:=", + self.Filled, + "IsoValType:=", + self.IsoVal, + "AddGrid:=", + self.AddGrid, + "MapTransparency:=", + self.MapTransparency, + "Refinement:=", + self.Refinement, + "Transparency:=", + self.Transparency, + "SmoothingLevel:=", + self.SmoothingLevel, + "ShadingType:=", + self.SmoothShade, + [ + "NAME:Arrow3DSpacingSettings", + "ArrowUniform:=", + self.ArrowUniform, + "ArrowSpacing:=", + self.ArrowSpacing, + "MinArrowSpacing:=", + self.MinArrowSpacing, + "MaxArrowSpacing:=", + self.MaxArrowSpacing, + ], + "GridColor:=", + self.GridColor, + ], + ] + + @property + def field_line_trace_plot_settings(self): + """Settings for the field line traces in the plot. + + Returns + ------- + list + List of settings for the field line traces in the plot. + """ + return [ + "NAME:FieldLineTracePlotSettings", + ["NAME:LineSettingsID", "Width:=", self.LineWidth, "Style:=", self.LineStyle], + "IsoValType:=", + self.IsoValType, + ] + + @pyaedt_function_handler() + def create(self): + """Create a field plot. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + try: + if self.seeding_faces: + self.oField.CreateFieldPlot(self.surfacePlotInstructionLineTraces, "FieldLineTrace") + else: + self.oField.CreateFieldPlot(self.surfacePlotInstruction, "Field") + if ( + "Maxwell" in self._postprocessor._app.design_type + and "Transient" in self._postprocessor.post_solution_type + ): + self._postprocessor.ofieldsreporter.SetPlotsViewSolutionContext( + [self.name], self.solution, "Time:" + self.intrinsics["Time"] + ) + self._postprocessor.field_plots[self.name] = self + return True + except Exception: + return False + + @pyaedt_function_handler() + def update(self): + """Update the field plot. + + .. note:: + This method works on any plot created inside PyAEDT. + For Plot already existing in AEDT Design it may produce incorrect results. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + try: + if self.seeding_faces: + if self.seeding_faces[0] != len(self.seeding_faces) - 1: + for face in self.seeding_faces[1:]: + if not isinstance(face, int): + self._postprocessor.logger.error("Provide valid object id for seeding faces.") + return False + else: + if face not in list(self._postprocessor._app.modeler.objects.keys()): + self._postprocessor.logger.error("Invalid object id.") + self.seeding_faces.remove(face) + return False + self.seeding_faces[0] = len(self.seeding_faces) - 1 + if self.volumes[0] != len(self.volumes) - 1: + for obj in self.volumes[1:]: + if not isinstance(obj, int): + self._postprocessor.logger.error("Provide valid object id for in-volume object.") + return False + else: + if obj not in list(self._postprocessor._app.modeler.objects.keys()): + self._postprocessor.logger.error("Invalid object id.") + self.volumes.remove(obj) + return False + self.volumes[0] = len(self.volumes) - 1 + if self.surfaces[0] != len(self.surfaces) - 1: + for obj in self.surfaces[1:]: + if not isinstance(obj, int): + self._postprocessor.logger.error("Provide valid object id for surface object.") + return False + else: + if obj not in list(self._postprocessor._app.modeler.objects.keys()): + self._postprocessor.logger.error("Invalid object id.") + self.surfaces.remove(obj) + return False + self.surfaces[0] = len(self.surfaces) - 1 + self.oField.ModifyFieldPlot(self.name, self.surfacePlotInstructionLineTraces) + else: + self.oField.ModifyFieldPlot(self.name, self.surfacePlotInstruction) + return True + except Exception: + return False + + @pyaedt_function_handler() + def update_field_plot_settings(self): + """Modify the field plot settings. + + .. note:: + This method is not available for field plot line traces. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + self.oField.SetFieldPlotSettings(self.name, ["NAME:FieldsPlotItemSettings", self.plotsettings]) + return True + + @pyaedt_function_handler() + def delete(self): + """Delete the field plot.""" + self.oField.DeleteFieldPlot([self.name]) + self._postprocessor.field_plots.pop(self.name, None) + + @pyaedt_function_handler() + def change_plot_scale(self, minimum_value, maximum_value, is_log=False, is_db=False, scale_levels=None): + """Change Field Plot Scale. + + .. deprecated:: 0.10.1 + Use :class:`FieldPlot.folder_settings` methods instead. + + Parameters + ---------- + minimum_value : str, float + Minimum value of the scale. + maximum_value : str, float + Maximum value of the scale. + is_log : bool, optional + Set to ``True`` if Log Scale is setup. + is_db : bool, optional + Set to ``True`` if dB Scale is setup. + scale_levels : int, optional + Set number of color levels. The default is ``None``, in which case the + setting is not changed. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + References + ---------- + >>> oModule.SetPlotFolderSettings + """ + return self._postprocessor.change_field_plot_scale( + self.plot_folder, minimum_value, maximum_value, is_log, is_db, scale_levels + ) + + @pyaedt_function_handler() + def export_image( + self, + full_path=None, + width=1920, + height=1080, + orientation="isometric", + display_wireframe=True, + selections=None, + show_region=True, + show_axis=True, + show_grid=True, + show_ruler=True, + ): + """Export the active plot to an image file. + + .. note:: + There are some limitations on HFSS 3D Layout plots. + + 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 + Plot height. + orientation : str, optional + View of the exported plot. Options are ``isometric``, + ``top``, ``bottom``, ``right``, ``left``, ``front``, + ``back``, and any custom orientation. + display_wireframe : bool, optional + Whether the objects has to be put in wireframe mode. Default is ``True``. + selections : str or List[str], optional + Objects to fit for the zoom on the exported image. + Default is None in which case all the objects in the design will be shown. + One important note is that, if the fieldplot extension is larger than the + selection extension, the fieldplot extension will be the one considered + for the zoom of the exported image. + show_region : bool, optional + Whether to include the air region in the exported image. Default is ``True``. + show_grid : bool, optional + Whether to display the background grid in the exported image. + Default is ``True``. + show_axis : bool, optional + Whether to display the axis triad in the exported image. Default is ``True``. + show_ruler : bool, optional + Whether to display the ruler in the exported image. Default is ``True``. + + Returns + ------- + str + Full path to exported file if successful. + + References + ---------- + >>> oModule.ExportPlotImageToFile + >>> oModule.ExportModelImageToFile + >>> oModule.ExportPlotImageWithViewToFile + """ + self.oField.UpdateQuantityFieldsPlots(self.plot_folder) + if not full_path: + full_path = os.path.join(self._postprocessor._app.working_directory, self.name + ".png") + status = self._postprocessor.export_field_jpg( + full_path, + self.name, + self.plot_folder, + orientation=orientation, + width=width, + height=height, + display_wireframe=display_wireframe, + selections=selections, + show_region=show_region, + show_axis=show_axis, + show_grid=show_grid, + show_ruler=show_ruler, + ) + full_path = check_and_download_file(full_path) + if status: + return full_path + else: + return False + + @pyaedt_function_handler() + def export_image_from_aedtplt( + self, export_path=None, view="isometric", plot_mesh=False, scale_min=None, scale_max=None + ): + """Save an image of the active plot using PyVista. + + .. note:: + This method only works if the CPython with PyVista module is installed. + + Parameters + ---------- + export_path : str, optional + Path where image will be saved. + The default is ``None`` which export file in working_directory. + view : str, optional + View to export. Options are ``"isometric"``, ``"xy"``, ``"xz"``, ``"yz"``. + plot_mesh : bool, optional + Plot mesh. + scale_min : float, optional + Scale output min. + scale_max : float, optional + Scale output max. + + Returns + ------- + str + Full path to exported file if successful. + + References + ---------- + >>> oModule.UpdateAllFieldsPlots + >>> oModule.UpdateQuantityFieldsPlots + >>> oModule.ExportFieldPlot + """ + if not export_path: + export_path = self._postprocessor._app.working_directory + if sys.version_info.major > 2: + return self._postprocessor.plot_field_from_fieldplot( + self.name, + project_path=export_path, + meshplot=plot_mesh, + imageformat="jpg", + view=view, + plot_label=self.quantity, + show=False, + scale_min=scale_min, + scale_max=scale_max, + ) + else: + self._postprocessor.logger.info("This method works only on CPython with PyVista") + return False diff --git a/src/ansys/aedt/core/visualization/post/field_summary.py b/src/ansys/aedt/core/visualization/post/field_summary.py new file mode 100644 index 00000000000..e971dd69695 --- /dev/null +++ b/src/ansys/aedt/core/visualization/post/field_summary.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains these classes: `FieldPlot`, `PostProcessor`, and `SolutionData`. + +This module provides all functionalities for creating and editing plots in the 3D tools. + +""" +from __future__ import absolute_import + +from collections import defaultdict +import csv +import os +import tempfile + +from ansys.aedt.core import pyaedt_function_handler +from ansys.aedt.core.generic.general_methods import open_file + +pd = None + +try: + import pandas as pd +except ImportError: + pd = None + +TOTAL_QUANTITIES = [ + "HeatFlowRate", + "RadiationFlow", + "ConductionHeatFlow", + "ConvectiveHeatFlow", + "MassFlowRate", + "VolumeFlowRate", + "SurfJouleHeatingDensity", +] +AVAILABLE_QUANTITIES = [ + "Temperature", + "SurfTemperature", + "HeatFlowRate", + "RadiationFlow", + "ConductionHeatFlow", + "ConvectiveHeatFlow", + "HeatTransCoeff", + "HeatFlux", + "RadiationFlux", + "Speed", + "Ux", + "Uy", + "Uz", + "SurfUx", + "SurfUy", + "SurfUz", + "Pressure", + "SurfPressure", + "MassFlowRate", + "VolumeFlowRate", + "MassFlux", + "ViscosityRatio", + "WallYPlus", + "TKE", + "Epsilon", + "Kx", + "Ky", + "Kz", + "SurfElectricPotential", + "ElectricPotential", + "SurfCurrentDensity", + "CurrentDensity", + "SurfCurrentDensityX", + "SurfCurrentDensityY", + "SurfCurrentDensityZ", + "CurrentDensityX", + "CurrentDensityY", + "CurrentDensityZ", + "SurfJouleHeatingDensity", + "JouleHeatingDensity", +] + + +class FieldSummary: + """Provides Icepak field summary methods.""" + + def __init__(self, app): + self._app = app + self.calculations = [] + + @pyaedt_function_handler() + def add_calculation( + self, + entity, + geometry, + geometry_name, + quantity, + normal="", + side="Default", + mesh="All", + ref_temperature="AmbientTemp", + time="0s", + ): + """ + Add an entry in the field summary calculation requests. + + Parameters + ---------- + entity : str + Type of entity to perform the calculation on. Options are + ``"Boundary"``, ``"Monitor``", and ``"Object"``. + (``"Monitor"`` is available in AEDT 2024 R1 and later.) + geometry : str + Location to perform the calculation on. Options are + ``"Surface"`` and ``"Volume"``. + geometry_name : str or list of str + Objects to perform the calculation on. If a list is provided, + the calculation is performed on the combination of those + objects. + quantity : str + Quantity to compute. + normal : list of floats + Coordinate values for direction relative to normal. The default is ``""``, + in which case the normal to the face is used. + side : str, optional + String containing which side of the face to use. The default is + ``"Default"``. Options are ``"Adjacent"``, ``"Combined"``, and + `"Default"``. + mesh : str, optional + Surface meshes to use. The default is ``"All"``. Options are ``"All"`` and + ``"Reduced"``. + ref_temperature : str, optional + Reference temperature to use in the calculation of the heat transfer + coefficient. The default is ``"AmbientTemp"``. + time : str, optional + Timestep to get the data from. Default is ``"0s"``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if quantity not in AVAILABLE_QUANTITIES: + raise AttributeError( + "Quantity {} is not supported. Available quantities are:\n{}".format( + quantity, ", ".join(AVAILABLE_QUANTITIES) + ) + ) + if isinstance(normal, list): + if not isinstance(normal[0], str): + normal = [str(i) for i in normal] + normal = ",".join(normal) + if isinstance(geometry_name, str): + geometry_name = [geometry_name] + calc_args = [ + entity, + geometry, + ",".join(geometry_name), + quantity, + normal, + side, + mesh, + ref_temperature, + False, + ] # TODO : last argument not documented + if self._app.solution_type == "Transient": + calc_args = [time] + calc_args + self.calculations.append(calc_args) + return True + + @pyaedt_function_handler(IntrinsincDict="intrinsics", setup_name="setup", design_variation="variation") + def get_field_summary_data(self, setup=None, variation=None, intrinsics="", pandas_output=False): + """ + Get field summary output computation. + + Parameters + ---------- + setup : str, optional + Setup name to use for the computation. The + default is ``None``, in which case the nominal variation is used. + variation : dict, optional + Dictionary containing the design variation to use for the computation. + The default is ``{}``, in which case nominal variation is used. + intrinsics : str, optional + Intrinsic values to use for the computation. The default is ``""``, + which is suitable when no frequency needs to be selected. + pandas_output : bool, optional + Whether to use pandas output. The default is ``False``, in + which case the dictionary output is used. + + Returns + ------- + dict or pandas.DataFrame + Output type depending on the Boolean ``pandas_output`` parameter. + The output consists of information exported from the field summary. + """ + if variation is None: + variation = {} + with tempfile.NamedTemporaryFile(mode="w+", delete=False) as temp_file: + temp_file.close() + self.export_csv(temp_file.name, setup, variation, intrinsics) + with open_file(temp_file.name, "r") as f: + for _ in range(4): + _ = next(f) + reader = csv.DictReader(f) + out_dict = defaultdict(list) + for row in reader: + for key in row.keys(): + out_dict[key].append(row[key]) + os.remove(temp_file.name) + if pandas_output: + if pd is None: + raise ImportError("pandas package is needed.") + df = pd.DataFrame.from_dict(out_dict) + for col in ["Min", "Max", "Mean", "Stdev", "Total"]: + if col in df.columns: + df[col] = df[col].astype(float) + return df + return out_dict + + @pyaedt_function_handler(filename="output_file", design_variation="variations", setup_name="setup") + def export_csv(self, output_file, setup=None, variations=None, intrinsics=""): + """ + Get the field summary output computation. + + Parameters + ---------- + output_file : str + Path and filename to write the output file to. + setup : str, optional + Setup name to use for the computation. The + default is ``None``, in which case the nominal variation is used. + variations : dict, optional + Dictionary containing the design variation to use for the computation. + The default is ``{}``, in which case the nominal variation is used. + intrinsics : str, optional + Intrinsic values to use for the computation. The default is ``""``, + which is suitable when no frequency needs to be selected. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if variations is None: + variations = {} + if not setup: + setup = self._app.nominal_sweep + dv_string = "" + for el in variations: + dv_string += el + "='" + variations[el] + "' " + self._create_field_summary(setup, dv_string) + self._app.osolution.ExportFieldsSummary( + [ + "SolutionName:=", + setup, + "DesignVariationKey:=", + dv_string, + "ExportFileName:=", + output_file, + "IntrinsicValue:=", + intrinsics, + ] + ) + return True + + @pyaedt_function_handler() + def _create_field_summary(self, setup, variation): + arg = ["SolutionName:=", setup, "Variation:=", variation] + for i in self.calculations: + arg.append("Calculation:=") + arg.append(i) + self._app.osolution.EditFieldsSummarySetting(arg) diff --git a/src/ansys/aedt/core/modules/fields_calculator.py b/src/ansys/aedt/core/visualization/post/fields_calculator.py similarity index 92% rename from src/ansys/aedt/core/modules/fields_calculator.py rename to src/ansys/aedt/core/visualization/post/fields_calculator.py index 021fee5bcfe..92b49e5c16d 100644 --- a/src/ansys/aedt/core/modules/fields_calculator.py +++ b/src/ansys/aedt/core/visualization/post/fields_calculator.py @@ -26,14 +26,11 @@ import ansys.aedt.core from ansys.aedt.core.generic.general_methods import generate_unique_project_name -from ansys.aedt.core.generic.general_methods import is_ironpython from ansys.aedt.core.generic.general_methods import open_file from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.generic.general_methods import read_configuration_file - -if not is_ironpython: - from jsonschema import exceptions - from jsonschema import validate +from jsonschema import exceptions +from jsonschema import validate class FieldsCalculator: @@ -50,36 +47,40 @@ class FieldsCalculator: Examples -------- Custom expressions can be added as dictionary on-the-fly: + >>> from ansys.aedt.core import Hfss >>> hfss = Hfss() >>> poly = hfss.modeler.create_polyline([[0, 0, 0], [1, 0, 1]], name="Polyline1") >>> my_expression = { - ... "name": "test", - ... "description": "Voltage drop along a line", - ... "design_type": ["HFSS", "Q3D Extractor"], - ... "fields_type": ["Fields", "CG Fields"], - ... "solution_type": "", - ... "primary_sweep": "Freq", - ... "assignment": "", - ... "assignment_type": ["Line"], - ... "operations": ["Fundamental_Quantity('E')", - ... "Operation('Real')", - ... "Operation('Tangent')", - ... "Operation('Dot')", - ... "EnterLine('assignment')", - ... "Operation('LineValue')", - ... "Operation('Integrate')", - ... "Operation('CmplxR')"], - ... "report": ["Data Table", "Rectangular Plot"], - ... } + ... "name": "test", + ... "description": "Voltage drop along a line", + ... "design_type": ["HFSS", "Q3D Extractor"], + ... "fields_type": ["Fields", "CG Fields"], + ... "solution_type": "", + ... "primary_sweep": "Freq", + ... "assignment": "", + ... "assignment_type": ["Line"], + ... "operations": ["Fundamental_Quantity('E')", + ... "Operation('Real')", + ... "Operation('Tangent')", + ... "Operation('Dot')", + ... "EnterLine('assignment')", + ... "Operation('LineValue')", + ... "Operation('Integrate')", + ... "Operation('CmplxR')"], + ... "report": ["Data Table", "Rectangular Plot"], + ... } >>> expr_name = hfss.post.fields_calculator.add_expression(my_expression, "Polyline1") >>> hfss.release_desktop(False, False) + or they can be added from the ``expression_catalog.toml``: + >>> from ansys.aedt.core import Hfss >>> hfss = Hfss() >>> poly = hfss.modeler.create_polyline([[0, 0, 0], [1, 0, 1]], name="Polyline1") >>> expr_name = hfss.post.fields_calculator.add_expression("voltage_line", "Polyline1") >>> hfss.release_desktop(False, False) + """ def __init__(self, app): @@ -490,20 +491,16 @@ def validate_expression(self, expression): self.__app.logger.error("Incorrect data type.") return False - if is_ironpython: # pragma: no cover - self.__app.logger.warning("Iron Python: Unable to validate json Schema.") - else: - try: - validate(instance=expression, schema=self.expression_schema) - for prop_name, sub_schema in self.expression_schema["properties"].items(): - if "default" in sub_schema and prop_name not in expression: - expression[prop_name] = sub_schema["default"] - return expression - except exceptions.ValidationError as e: - self.__app.logger.warning("Configuration is invalid.") - self.__app.logger.warning("Validation error:" + e.message) - return False - return True + try: + validate(instance=expression, schema=self.expression_schema) + for prop_name, sub_schema in self.expression_schema["properties"].items(): + if "default" in sub_schema and prop_name not in expression: + expression[prop_name] = sub_schema["default"] + return expression + except exceptions.ValidationError as e: + self.__app.logger.warning("Configuration is invalid.") + self.__app.logger.warning("Validation error:" + e.message) + return False @pyaedt_function_handler() def calculator_write(self, expression, output_file, setup=None, intrinsics=None): @@ -525,9 +522,9 @@ def calculator_write(self, expression, output_file, setup=None, intrinsics=None) Key is the variable name and value is the variable value. These are typically: frequency, time and phase. If it is a dictionary, keys depend on the solution type and can be expressed as: - - ``"Freq"`` - - ``"Time"`` - - ``"Phase"`` + - ``"Freq"``. + - ``"Time"``. + - ``"Phase"``. The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. Returns diff --git a/src/ansys/aedt/core/modules/monitor_icepak.py b/src/ansys/aedt/core/visualization/post/monitor_icepak.py similarity index 100% rename from src/ansys/aedt/core/modules/monitor_icepak.py rename to src/ansys/aedt/core/visualization/post/monitor_icepak.py diff --git a/src/ansys/aedt/core/visualization/post/post_circuit.py b/src/ansys/aedt/core/visualization/post/post_circuit.py new file mode 100644 index 00000000000..e38acf1d280 --- /dev/null +++ b/src/ansys/aedt/core/visualization/post/post_circuit.py @@ -0,0 +1,581 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains these classes: `FieldPlot`, `PostProcessor`, and `SolutionData`. + +This module provides all functionalities for creating and editing plots in the 3D tools. + +""" +from __future__ import absolute_import + +import warnings + +from ansys.aedt.core import generate_unique_name +from ansys.aedt.core import pyaedt_function_handler +from ansys.aedt.core.generic.constants import unit_converter +from ansys.aedt.core.visualization.post.common import PostProcessorCommon + +try: + import pandas as pd +except ImportError: + pd = None + warnings.warn( + "The Pandas module is required to run some functionalities of PostProcess.\n" + "Install with \n\npip install pandas" + ) + + +class PostProcessorCircuit(PostProcessorCommon): + """Manages the main schematic postprocessing functions. + + .. note:: + Some functionalities are available only when AEDT is running in the graphical mode. + + Parameters + ---------- + app : :class:`ansys.aedt.core.application.analysis_nexxim.FieldAnalysisCircuit` + Inherited parent object. The parent object must provide the members + `_modeler`, `_desktop`, `_odesign`, and `logger`. + + """ + + def __init__(self, app): + PostProcessorCommon.__init__(self, app) + + @pyaedt_function_handler(setupname="setup", plotname="plot_name") + def create_ami_initial_response_plot( + self, + setup, + ami_name, + variation_list_w_value, + plot_type="Rectangular Plot", + plot_initial_response=True, + plot_intermediate_response=False, + plot_final_response=False, + plot_name=None, + ): + """Create an AMI initial response plot. + + Parameters + ---------- + setup : str + Name of the setup. + ami_name : str + AMI probe name to use. + variation_list_w_value : list + List of variations with relative values. + plot_type : str + String containing the report type. Default is ``"Rectangular Plot"``. It can be ``"Data Table"``, + ``"Rectangular Stacked Plot"``or any of the other valid AEDT Report types. + The default is ``"Rectangular Plot"``. + plot_initial_response : bool, optional + Set either to plot the initial input response. Default is ``True``. + plot_intermediate_response : bool, optional + Set whether to plot the intermediate input response. Default is ``False``. + plot_final_response : bool, optional + Set whether to plot the final input response. Default is ``False``. + plot_name : str, optional + Plot name. The default is ``None``, in which case + a unique name is automatically assigned. + + Returns + ------- + str + Name of the plot. + """ + if not plot_name: + plot_name = generate_unique_name("AMIAnalysis") + variations = ["__InitialTime:=", ["All"]] + i = 0 + for a in variation_list_w_value: + if (i % 2) == 0: + if ":=" in a: + variations.append(a) + else: + variations.append(a + ":=") + else: + if isinstance(a, list): + variations.append(a) + else: + variations.append([a]) + i += 1 + ycomponents = [] + if plot_initial_response: + ycomponents.append("InitialImpulseResponse<{}.int_ami_rx>".format(ami_name)) + if plot_intermediate_response: + ycomponents.append("IntermediateImpulseResponse<{}.int_ami_rx>".format(ami_name)) + if plot_final_response: + ycomponents.append("FinalImpulseResponse<{}.int_ami_rx>".format(ami_name)) + self.oreportsetup.CreateReport( + plot_name, + "Standard", + plot_type, + setup, + [ + "NAME:Context", + "SimValueContext:=", + [ + 55824, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "NUMLEVELS", + False, + "1", + "PCID", + False, + "-1", + "PID", + False, + "1", + "SCID", + False, + "-1", + "SID", + False, + "0", + ], + ], + variations, + ["X Component:=", "__InitialTime", "Y Component:=", ycomponents], + ) + return plot_name + + @pyaedt_function_handler(setupname="setup", plotname="plot_name") + def create_ami_statistical_eye_plot( + self, setup, ami_name, variation_list_w_value, ami_plot_type="InitialEye", plot_name=None + ): + """Create an AMI statistical eye plot. + + Parameters + ---------- + setup : str + Name of the setup. + ami_name : str + AMI probe name to use. + variation_list_w_value : list + Variations with relative values. + ami_plot_type : str, optional + String containing the report AMI type. The default is ``"InitialEye"``. + Options are ``"EyeAfterChannel"``, ``"EyeAfterProbe"````"EyeAfterSource"``, + and ``"InitialEye"``.. + plot_name : str, optional + Plot name. The default is ``None``, in which case + a unique name starting with ``"Plot"`` is automatically assigned. + + Returns + ------- + str + The name of the plot. + + References + ---------- + + >>> oModule.CreateReport + """ + if not plot_name: + plot_name = generate_unique_name("AMYAanalysis") + variations = [ + "__UnitInterval:=", + ["All"], + "__Amplitude:=", + ["All"], + ] + i = 0 + for a in variation_list_w_value: + if (i % 2) == 0: + if ":=" in a: + variations.append(a) + else: + variations.append(a + ":=") + else: + if isinstance(a, list): + variations.append(a) + else: + variations.append([a]) + i += 1 + ycomponents = [] + if ami_plot_type == "InitialEye" or ami_plot_type == "EyeAfterSource": + ibs_type = "tx" + else: + ibs_type = "rx" + ycomponents.append("{}<{}.int_ami_{}>".format(ami_plot_type, ami_name, ibs_type)) + + ami_id = "0" + if ami_plot_type == "EyeAfterSource": + ami_id = "1" + elif ami_plot_type == "EyeAfterChannel": + ami_id = "2" + elif ami_plot_type == "EyeAfterProbe": + ami_id = "3" + self.oreportsetup.CreateReport( + plot_name, + "Statistical Eye", + "Statistical Eye Plot", + setup, + [ + "NAME:Context", + "SimValueContext:=", + [ + 55819, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "NUMLEVELS", + False, + "1", + "QTID", + False, + ami_id, + "SCID", + False, + "-1", + "SID", + False, + "0", + ], + ], + variations, + ["X Component:=", "__UnitInterval", "Y Component:=", "__Amplitude", "Eye Diagram Component:=", ycomponents], + ) + return plot_name + + @pyaedt_function_handler(setupname="setup", plotname="plot_name") + def create_statistical_eye_plot(self, setup, probe_names, variation_list_w_value, plot_name=None): + """Create a statistical QuickEye, VerifEye, and/or Statistical Eye plot. + + Parameters + ---------- + setup : str + Name of the setup. + probe_names : str or list + One or more names of the probes to plot in the eye diagram. + variation_list_w_value : list + List of variations with relative values. + plot_name : str, optional + Plot name. The default is ``None``, in which case a name is automatically assigned. + + Returns + ------- + str + The name of the plot. + + References + ---------- + + >>> oModule.CreateReport + """ + if not plot_name: + plot_name = generate_unique_name("AMIAanalysis") + variations = [ + "__UnitInterval:=", + ["All"], + "__Amplitude:=", + ["All"], + ] + i = 0 + for a in variation_list_w_value: + if (i % 2) == 0: + if ":=" in a: + variations.append(a) + else: + variations.append(a + ":=") + else: + if isinstance(a, list): + variations.append(a) + else: + variations.append([a]) + i += 1 + if isinstance(probe_names, list): + ycomponents = probe_names + else: + ycomponents = [probe_names] + + self.oreportsetup.CreateReport( + plot_name, + "Statistical Eye", + "Statistical Eye Plot", + setup, + [ + "NAME:Context", + "SimValueContext:=", + [ + 55819, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "NUMLEVELS", + False, + "1", + "QTID", + False, + "1", + "SCID", + False, + "-1", + "SID", + False, + "0", + ], + ], + variations, + ["X Component:=", "__UnitInterval", "Y Component:=", "__Amplitude", "Eye Diagram Component:=", ycomponents], + ) + return plot_name + + @pyaedt_function_handler() + def sample_waveform( + self, + waveform_data, + waveform_sweep, + waveform_unit="V", + waveform_sweep_unit="s", + unit_interval=1e-9, + clock_tics=None, + pandas_enabled=False, + ): + """Sampling a waveform at clock times plus half unit interval. + + Parameters + ---------- + waveform_data : list + Waveform data. + waveform_sweep : list + Waveform sweep data. + waveform_unit : str, optional + Waveform units. The default values is ``V``. + waveform_sweep_unit : str, optional + Time units. The default value is ``s``. + unit_interval : float, optional + Unit interval in seconds. The default is ``1e-9``. + clock_tics : list, optional + List with clock tics. The default is ``None``, in which case the clock tics from + the AMI receiver are used. + pandas_enabled : bool, optional + Whether to enable the Pandas data format. The default is ``False``. + + Returns + ------- + list or :class:`pandas.Series` + Sampled waveform in ``Volts`` at different times in ``seconds``. + + Examples + -------- + >>> from ansys.aedt.core import Circuit + >>> circuit = Circuit() + >>> circuit.post.sample_ami_waveform(name,probe_name,source_name,circuit.available_variations.nominal) + + """ + + new_tic = [] + for tic in clock_tics: + new_tic.append(unit_converter(tic, unit_system="Time", input_units="s", output_units=waveform_sweep_unit)) + new_ui = unit_converter(unit_interval, unit_system="Time", input_units="s", output_units=waveform_sweep_unit) + + zipped_lists = zip(new_tic, [new_ui / 2] * len(new_tic)) + extraction_tic = [x + y for (x, y) in zipped_lists] + + if pandas_enabled: + sweep_filtered = waveform_sweep.values + filtered_tic = list(filter(lambda num: num >= waveform_sweep.values[0], extraction_tic)) + else: + sweep_filtered = waveform_sweep + filtered_tic = list(filter(lambda num: num >= waveform_sweep[0], extraction_tic)) + + outputdata = [] + new_voltage = [] + tic_in_s = [] + for tic in filtered_tic: + if tic >= sweep_filtered[0]: + sweep_filtered = list(filter(lambda num: num >= tic, sweep_filtered)) + if sweep_filtered: + if pandas_enabled: + waveform_index = waveform_sweep[waveform_sweep.values == sweep_filtered[0]].index.values + else: + waveform_index = waveform_sweep.index(sweep_filtered[0]) + if not isinstance(waveform_data[waveform_index], float): + voltage = waveform_data[waveform_index].values[0] + else: + voltage = waveform_data[waveform_index] + new_voltage.append( + unit_converter(voltage, unit_system="Voltage", input_units=waveform_unit, output_units="V") + ) + tic_in_s.append( + unit_converter(tic, unit_system="Time", input_units=waveform_sweep_unit, output_units="s") + ) + if not pandas_enabled: + outputdata.append([tic_in_s[-1:][0], new_voltage[-1:][0]]) + del sweep_filtered[0] + else: + break + if pandas_enabled: + return pd.Series(new_voltage, index=tic_in_s) + return outputdata + + @pyaedt_function_handler(setupname="setup", probe_name="probe", source_name="source") + def sample_ami_waveform( + self, + setup, + probe, + source, + variation_list_w_value, + unit_interval=1e-9, + ignore_bits=0, + plot_type=None, + clock_tics=None, + ): + """Sampling a waveform at clock times plus half unit interval. + + Parameters + ---------- + setup : str + Name of the setup. + probe : str + Name of the AMI probe. + source : str + Name of the AMI source. + variation_list_w_value : list + Variations with relative values. + unit_interval : float, optional + Unit interval in seconds. The default is ``1e-9``. + ignore_bits : int, optional + Number of initial bits to ignore. The default is ``0``. + plot_type : str, optional + Report type. The default is ``None``, in which case all report types are generated. + Options for a specific report type are ``"InitialWave"``, ``"WaveAfterSource"``, + ``"WaveAfterChannel"``, and ``"WaveAfterProbe"``. + clock_tics : list, optional + List with clock tics. The default is ``None``, in which case the clock tics from + the AMI receiver are used. + + Returns + ------- + list + Sampled waveform in ``Volts`` at different times in ``seconds``. + + Examples + -------- + >>> circuit = Circuit() + >>> circuit.post.sample_ami_waveform(setupname,probe_name,source_name,circuit.available_variations.nominal) + + """ + initial_solution_type = self.post_solution_type + self._app.solution_type = "NexximAMI" + + if plot_type == "InitialWave" or plot_type == "WaveAfterSource": + plot_expression = [plot_type + "<" + source + ".int_ami_tx>"] + elif plot_type == "WaveAfterChannel" or plot_type == "WaveAfterProbe": + plot_expression = [plot_type + "<" + probe + ".int_ami_rx>"] + else: + plot_expression = [ + "InitialWave<" + source + ".int_ami_tx>", + "WaveAfterSource<" + source + ".int_ami_tx>", + "WaveAfterChannel<" + probe + ".int_ami_rx>", + "WaveAfterProbe<" + probe + ".int_ami_rx>", + ] + waveform = [] + waveform_sweep = [] + waveform_unit = [] + waveform_sweep_unit = [] + waveform_data = None + for exp in plot_expression: + waveform_data = self.get_solution_data( + expressions=exp, setup_sweep_name=setup, domain="Time", variations=variation_list_w_value + ) + samples_per_bit = 0 + for sample in waveform_data.primary_sweep_values: + sample_seconds = unit_converter( + sample, unit_system="Time", input_units=waveform_data.units_sweeps["Time"], output_units="s" + ) + if sample_seconds > unit_interval: + samples_per_bit -= 1 + break + else: + samples_per_bit += 1 + if samples_per_bit * ignore_bits > len(waveform_data.data_real()): + self._app.solution_type = initial_solution_type + self.logger.warning("Ignored bits are greater than generated bits.") + return None + waveform.append(waveform_data.data_real()[samples_per_bit * ignore_bits :]) + waveform_sweep.append(waveform_data.primary_sweep_values[samples_per_bit * ignore_bits :]) + waveform_unit.append(waveform_data.units_data[exp]) + waveform_sweep_unit.append(waveform_data.units_sweeps["Time"]) + + tics = clock_tics + if not clock_tics: + clock_expression = "ClockTics<" + probe + ".int_ami_rx>" + clock_tic = self.get_solution_data( + expressions=clock_expression, + setup_sweep_name=setup, + domain="Clock Times", + variations=variation_list_w_value, + ) + tics = clock_tic.data_real() + + outputdata = [[] for i in range(len(waveform))] + is_pandas_enabled = False + if waveform_data: + is_pandas_enabled = waveform_data.enable_pandas_output + for waveform_cont, waveform_real in enumerate(waveform): + outputdata[waveform_cont] = self.sample_waveform( + waveform_data=waveform_real, + waveform_sweep=waveform_sweep[waveform_cont], + waveform_unit=waveform_unit[waveform_cont], + waveform_sweep_unit=waveform_sweep_unit[waveform_cont], + unit_interval=unit_interval, + clock_tics=tics, + pandas_enabled=is_pandas_enabled, + ) + return outputdata diff --git a/src/ansys/aedt/core/modules/post_processor.py b/src/ansys/aedt/core/visualization/post/post_common_3d.py similarity index 50% rename from src/ansys/aedt/core/modules/post_processor.py rename to src/ansys/aedt/core/visualization/post/post_common_3d.py index 9ce93d343fe..48b60aadb53 100644 --- a/src/ansys/aedt/core/modules/post_processor.py +++ b/src/ansys/aedt/core/visualization/post/post_common_3d.py @@ -23,2358 +23,46 @@ # SOFTWARE. """ -This module contains these classes: `FieldPlot`, `PostProcessor`, and `SolutionData`. +This module contains this class: `PostProcessor3D`. This module provides all functionalities for creating and editing plots in the 3D tools. """ - -from __future__ import absolute_import # noreorder +from __future__ import absolute_import import ast -from collections import defaultdict -import csv import os import random -import re -import string -import tempfile - -from ansys.aedt.core.application.variables import decompose_variable_value -from ansys.aedt.core.generic.constants import unit_converter -from ansys.aedt.core.generic.data_handlers import _dict_items_to_list_items -from ansys.aedt.core.generic.general_methods import check_and_download_file -from ansys.aedt.core.generic.general_methods import generate_unique_name -from ansys.aedt.core.generic.general_methods import is_ironpython -from ansys.aedt.core.generic.general_methods import open_file -from ansys.aedt.core.generic.general_methods import pyaedt_function_handler -from ansys.aedt.core.generic.general_methods import read_configuration_file -from ansys.aedt.core.generic.settings import settings -from ansys.aedt.core.modeler.cad.elements_3d import FacePrimitive -import ansys.aedt.core.modules.report_templates as rt -from ansys.aedt.core.modules.solutions import FieldPlot -from ansys.aedt.core.modules.solutions import SolutionData -from ansys.aedt.core.modules.solutions import VRTFieldPlot - -if not is_ironpython: - try: - from enum import Enum - - import pandas as pd - except ImportError: # pragma: no cover - pd = None - Enum = None -else: # pragma: no cover - Enum = object - -TEMPLATES_BY_DESIGN = { - "HFSS": [ - "Modal Solution Data", - "Terminal Solution Data", - "Eigenmode Parameters", - "Fields", - "Far Fields", - "Emissions", - "Near Fields", - "Antenna Parameters", - ], - "Maxwell 3D": [ - "Transient", - "EddyCurrent", - "Magnetostatic", - "Electrostatic", - "DCConduction", - "ElectroDCConduction", - "ElectricTransient", - "Fields", - "Spectrum", - ], - "Maxwell 2D": [ - "Transient", - "EddyCurrent", - "Magnetostatic", - "Electrostatic", - "ElectricTransient", - "ElectroDCConduction", - "Fields", - "Spectrum", - ], - "Icepak": ["Monitor", "Fields"], - "Circuit Design": ["Standard", "Eye Diagram", "Statistical Eye", "Spectrum", "EMIReceiver"], - "HFSS 3D Layout": ["Standard", "Fields", "Spectrum"], - "HFSS 3D Layout Design": ["Standard", "Fields", "Spectrum"], - "Mechanical": ["Standard", "Fields"], - "Q3D Extractor": ["Matrix", "CG Fields", "DC R/L Fields", "AC R/L Fields"], - "2D Extractor": ["Matrix", "CG Fields", "RL Fields"], - "Twin Builder": ["Standard", "Spectrum"], -} -TEMPLATES_BY_NAME = { - "Standard": rt.Standard, - "Modal Solution Data": rt.Standard, - "Terminal Solution Data": rt.Standard, - "Fields": rt.Fields, - "CG Fields": rt.Fields, - "DC R/L Fields": rt.Fields, - "AC R/L Fields": rt.Fields, - "Matrix": rt.Standard, - "Monitor": rt.Standard, - "Far Fields": rt.FarField, - "Near Fields": rt.NearField, - "Eye Diagram": rt.EyeDiagram, - "Statistical Eye": rt.AMIEyeDiagram, - "AMI Contour": rt.AMIConturEyeDiagram, - "Eigenmode Parameters": rt.Standard, - "Spectrum": rt.Spectral, - "EMIReceiver": rt.EMIReceiver, -} - - -class Reports(object): - """Provides the names of default solution types.""" - - def __init__(self, post_app, design_type): - self._post_app = post_app - self._design_type = design_type - self._templates = TEMPLATES_BY_DESIGN.get(self._design_type, None) - - @pyaedt_function_handler() - def _retrieve_default_expressions(self, expressions, report, setup_sweep_name): - if expressions: - return expressions - setup_only_name = setup_sweep_name.split(":")[0].strip() - get_setup = self._post_app._app.get_setup(setup_only_name) - is_siwave_dc = False - if ( - "SolveSetupType" in get_setup.props and get_setup.props["SolveSetupType"] == "SiwaveDCIR" - ): # pragma: no cover - is_siwave_dc = True - return self._post_app.available_report_quantities( - solution=setup_sweep_name, context=report._context, is_siwave_dc=is_siwave_dc - ) - - @pyaedt_function_handler(setup_name="setup") - def standard(self, expressions=None, setup=None): - """Create a standard or default report object. - - Parameters - ---------- - expressions : str or list - Expression List to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Standard` - - Examples - -------- - - >>> from ansys.aedt.core import Circuit - >>> cir = Circuit(my_project) - >>> report = cir.post.reports_by_category.standard("dB(S(1,1))","LNA") - >>> report.create() - >>> solutions = report.get_solution_data() - >>> report2 = cir.post.reports_by_category.standard(["dB(S(2,1))", "dB(S(2,2))"],"LNA") - - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "Standard" in self._templates: - rep = rt.Standard(self._post_app, "Standard", setup) - - elif self._post_app._app.design_solutions.report_type: - rep = rt.Standard(self._post_app, self._post_app._app.design_solutions.report_type, setup) - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler(setup_name="setup") - def monitor(self, expressions=None, setup=None): - """Create an Icepak Monitor Report object. - - Parameters - ---------- - expressions : str or list - One or more expressions to add to the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Standard` - - Examples - -------- - - >>> from ansys.aedt.core import Icepak - >>> ipk = Icepak(my_project) - >>> report = ipk.post.reports_by_category.monitor(["monitor_surf.Temperature","monitor_point.Temperature"]) - >>> report = report.create() - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "Monitor" in self._templates: - rep = rt.Standard(self._post_app, "Monitor", setup) - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler(setup_name="setup") - def fields(self, expressions=None, setup=None, polyline=None): - """Create a Field Report object. - - Parameters - ---------- - expressions : str or list - One or more expressions to add to the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - polyline : str, optional - Name of the polyline to plot the field on. - If a name is not provided, the report might be incorrect. - The default value is ``None``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Fields` - - Examples - -------- - - >>> from ansys.aedt.core import Hfss - >>> hfss = Hfss(my_project) - >>> report = hfss.post.reports_by_category.fields("Mag_E", "Setup : LastAdaptive", "Polyline1") - >>> report.create() - >>> solutions = report.get_solution_data() - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "Fields" in self._templates: - rep = rt.Fields(self._post_app, "Fields", setup) - rep.polyline = polyline - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler(setup_name="setup") - def cg_fields(self, expressions=None, setup=None, polyline=None): - """Create a CG Field Report object in Q3D and Q2D. - - Parameters - ---------- - expressions : str or list - Expression List to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - polyline : str, optional - Name of the polyline to plot the field on. - If a name is not provided, the report might be incorrect. - The default value is ``None``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Fields` - - Examples - -------- - - >>> from ansys.aedt.core import Q3d - >>> q3d = Q3d(my_project) - >>> report = q3d.post.reports_by_category.cg_fields("SmoothQ", "Setup : LastAdaptive", "Polyline1") - >>> report.create() - >>> solutions = report.get_solution_data() - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "CG Fields" in self._templates: - rep = rt.Fields(self._post_app, "CG Fields", setup) - rep.polyline = polyline - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler(setup_name="setup") - def dc_fields(self, expressions=None, setup=None, polyline=None): - """Create a DC Field Report object in Q3D. - - Parameters - ---------- - expressions : str or list - Expression List to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - polyline : str, optional - Name of the polyline to plot the field on. - If a name is not provided, the report might be incorrect. - The default value is ``None``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Fields` - - Examples - -------- - - >>> from ansys.aedt.core import Q3d - >>> q3d = Q3d(my_project) - >>> report = q3d.post.reports_by_category.dc_fields("Mag_VolumeJdc", "Setup : LastAdaptive", "Polyline1") - >>> report.create() - >>> solutions = report.get_solution_data() - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "DC R/L Fields" in self._templates: - rep = rt.Fields(self._post_app, "DC R/L Fields", setup) - rep.polyline = polyline - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler(setup_name="setup") - def rl_fields(self, expressions=None, setup=None, polyline=None): - """Create an AC RL Field Report object in Q3D and Q2D. - - Parameters - ---------- - expressions : str or list - Expression List to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - polyline : str, optional - Name of the polyline to plot the field on. - If a name is not provided, the report might be incorrect. - The default value is ``None``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Fields` - - Examples - -------- - - >>> from ansys.aedt.core import Q3d - >>> q3d = Q3d(my_project) - >>> report = q3d.post.reports_by_category.rl_fields("Mag_SurfaceJac", "Setup : LastAdaptive", "Polyline1") - >>> report.create() - >>> solutions = report.get_solution_data() - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "AC R/L Fields" in self._templates or "RL Fields" in self._templates: - if self._post_app._app.design_type == "Q3D Extractor": - rep = rt.Fields(self._post_app, "AC R/L Fields", setup) - else: - rep = rt.Fields(self._post_app, "RL Fields", setup) - rep.polyline = polyline - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler(setup_name="setup") - def far_field(self, expressions=None, setup=None, sphere_name=None, source_context=None): - """Create a Far Field Report object. - - Parameters - ---------- - expressions : str or list - Expression List to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - sphere_name : str, optional - Name of the sphere to create the far field on. - source_context : str, optional - Name of the active source to create the far field on. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.FarField` - - Examples - -------- - - >>> from ansys.aedt.core import Hfss - >>> hfss = Hfss(my_project) - >>> report = hfss.post.reports_by_category.far_field("GainTotal", "Setup : LastAdaptive", "3D_Sphere") - >>> report.primary_sweep = "Phi" - >>> report.create() - >>> solutions = report.get_solution_data() - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "Far Fields" in self._templates: - rep = rt.FarField(self._post_app, "Far Fields", setup) - rep.far_field_sphere = sphere_name - rep.source_context = source_context - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler(setup_name="setup", sphere_name="infinite_sphere") - def antenna_parameters(self, expressions=None, setup=None, infinite_sphere=None): - """Create an Antenna Parameters Report object. - - Parameters - ---------- - expressions : str or list - Expression List to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - infinite_sphere : str, optional - Name of the sphere to compute antenna parameters on. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.AntennaParameters` - - Examples - -------- - - >>> from ansys.aedt.core import Hfss - >>> hfss = Hfss(my_project) - >>> report = hfss.post.reports_by_category.antenna_parameters("GainTotal", "Setup : LastAdaptive", "3D_Sphere") - >>> report.create() - >>> solutions = report.get_solution_data() - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "Antenna Parameters" in self._templates: - rep = rt.AntennaParameters(self._post_app, "Antenna Parameters", setup, infinite_sphere) - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler(setup_name="setup") - def near_field(self, expressions=None, setup=None): - """Create a Field Report object. - - Parameters - ---------- - expressions : str or list - Expression List to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.NearField` - - Examples - -------- - - >>> from ansys.aedt.core import Hfss - >>> hfss = Hfss(my_project) - >>> report = hfss.post.reports_by_category.near_field("GainTotal", "Setup : LastAdaptive", "NF_1") - >>> report.primary_sweep = "Phi" - >>> report.create() - >>> solutions = report.get_solution_data() - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "Near Fields" in self._templates: - rep = rt.NearField(self._post_app, "Near Fields", setup) - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler(setup_name="setup") - def modal_solution(self, expressions=None, setup=None): - """Create a Standard or Default Report object. - - Parameters - ---------- - expressions : str or list - Expression List to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Standard` - - Examples - -------- - - >>> from ansys.aedt.core import Hfss - >>> hfss = Hfss(my_project) - >>> report = hfss.post.reports_by_category.modal_solution("dB(S(1,1))") - >>> report.create() - >>> solutions = report.get_solution_data() - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "Modal Solution Data" in self._templates: - rep = rt.Standard(self._post_app, "Modal Solution Data", setup) - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler(setup_name="setup") - def terminal_solution(self, expressions=None, setup=None): - """Create a Standard or Default Report object. - - Parameters - ---------- - expressions : str or list - Expression List to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Standard` - - Examples - -------- - - >>> from ansys.aedt.core import Hfss - >>> hfss = Hfss(my_project) - >>> report = hfss.post.reports_by_category.terminal_solution("dB(S(1,1))") - >>> report.create() - >>> solutions = report.get_solution_data() - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "Terminal Solution Data" in self._templates: - rep = rt.Standard(self._post_app, "Terminal Solution Data", setup) - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler(setup_name="setup") - def eigenmode(self, expressions=None, setup=None): - """Create a Standard or Default Report object. - - Parameters - ---------- - expressions : str or list - Expression List to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Standard` - - Examples - -------- - - >>> from ansys.aedt.core import Hfss - >>> hfss = Hfss(my_project) - >>> report = hfss.post.reports_by_category.eigenmode("dB(S(1,1))") - >>> report.create() - >>> solutions = report.get_solution_data() - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "Eigenmode Parameters" in self._templates: - rep = rt.Standard(self._post_app, "Eigenmode Parameters", setup) - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler(setup_name="setup") - def statistical_eye_contour(self, expressions=None, setup=None, quantity_type=3): - """Create a standard statistical AMI contour plot. - - Parameters - ---------- - expressions : str - Expression to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is either the sweep - name to use in the export or ``LastAdaptive``. - quantity_type : int, optional - For AMI analysis only, the quantity type. The default is ``3``. Options are: - - - ``0`` for Initial Wave - - ``1`` for Wave after Source - - ``2`` for Wave after Channel - - ``3`` for Wave after Probe. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.AMIConturEyeDiagram` - - Examples - -------- - - >>> from ansys.aedt.core import Circuit - >>> cir= Circuit() - >>> new_eye = cir.post.reports_by_category.statistical_eye_contour("V(Vout)") - >>> new_eye.unit_interval = "1e-9s" - >>> new_eye.time_stop = "100ns" - >>> new_eye.create() - - """ - if not setup: - for setup in self._post_app._app.setups: - if "AMIAnalysis" in setup.props: - setup = setup.name - if not setup: - self._post_app._app.logger.error("AMI analysis is needed to create this report.") - return False - - if isinstance(expressions, list): - expressions = expressions[0] - report_cat = "Standard" - rep = rt.AMIConturEyeDiagram(self._post_app, report_cat, setup) - rep.quantity_type = quantity_type - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - - return rep - return - - @pyaedt_function_handler(setup_name="setup") - def eye_diagram( - self, expressions=None, setup=None, quantity_type=3, statistical_analysis=True, unit_interval="1ns" - ): - """Create a Standard or Default Report object. - - Parameters - ---------- - expressions : str - Expression to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - quantity_type : int, optional - For AMI Analysis only, specify the quantity type. Options are: 0 for Initial Wave, - 1 for Wave after Source, 2 for Wave after Channel and 3 for Wave after Probe. Default is 3. - statistical_analysis : bool, optional - For AMI Analysis only, whether to plot the statistical eye plot or transient eye plot. - The default is ``True``. - unit_interval : str, optional - Unit interval for the eye diagram. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Standard` - - Examples - -------- - - >>> from ansys.aedt.core import Circuit - >>> cir= Circuit() - >>> new_eye = cir.post.reports_by_category.eye_diagram("V(Vout)") - >>> new_eye.unit_interval = "1e-9s" - >>> new_eye.time_stop = "100ns" - >>> new_eye.create() - - """ - if not setup: - setup = self._post_app._app.nominal_sweep - if "Eye Diagram" in self._templates: - if "AMIAnalysis" in self._post_app._app.get_setup(setup).props: - - report_cat = "Eye Diagram" - if statistical_analysis: - report_cat = "Statistical Eye" - rep = rt.AMIEyeDiagram(self._post_app, report_cat, setup) - rep.quantity_type = quantity_type - expressions = self._retrieve_default_expressions(expressions, rep, setup) - if isinstance(expressions, list): - rep.expressions = expressions[0] - return rep - - else: - rep = rt.EyeDiagram(self._post_app, "Eye Diagram", setup) - rep.unit_interval = unit_interval - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - return - - @pyaedt_function_handler(setup_name="setup") - def spectral(self, expressions=None, setup=None): - """Create a Spectral Report object. - - Parameters - ---------- - expressions : str or list, optional - Expression List to add into the report. The expression can be any of the available formula - you can enter into the Electronics Desktop Report Editor. - setup : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Spectrum` - - Examples - -------- - - >>> from ansys.aedt.core import Circuit - >>> cir= Circuit() - >>> new_eye = cir.post.reports_by_category.spectral("V(Vout)") - >>> new_eye.create() - - """ - if not setup: - setup = self._post_app._app.nominal_sweep - rep = None - if "Spectrum" in self._templates: - rep = rt.Spectral(self._post_app, "Spectrum", setup) - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup) - return rep - - @pyaedt_function_handler() - def emi_receiver(self, expressions=None, setup_name=None): - """Create an EMI receiver report. - - Parameters - ---------- - expressions : str or list, optional - One or more expressions to add into the report. An expression can be any of the formulas that - can be entered into the Electronics Desktop Report Editor. - setup_name : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is either the sweep name - to use in the export or ``LastAdaptive``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.EMIReceiver` - - Examples - -------- - - >>> from ansys.aedt.core import Circuit - >>> cir= Circuit() - >>> new_eye = cir.post.emi_receiver() - >>> new_eye.create() - - """ - if not setup_name: - setup_name = self._post_app._app.nominal_sweep - rep = None - if "EMIReceiver" in self._templates and self._post_app._app.desktop_class.aedt_version_id > "2023.2": - rep = rt.EMIReceiver(self._post_app, setup_name) - if not expressions: - expressions = "Average[{}]".format(rep.net) - else: - if not isinstance(expressions, list): - expressions = [expressions] - pattern = r"\w+\[(.*?)\]" - for expression in expressions: - match = re.search(pattern, expression) - if match: - net_name = match.group(1) - rep.net = net_name - rep.expressions = self._retrieve_default_expressions(expressions, rep, setup_name) - - return rep - - -orientation_to_view = { - "isometric": "iso", - "top": "XY", - "bottom": "XY", - "right": "XZ", - "left": "XZ", - "front": "YZ", - "back": "YZ", -} - - -@pyaedt_function_handler() -def _convert_dict_to_report_sel(sweeps): - if isinstance(sweeps, list): - return sweeps - sweep_list = [] - for el in sweeps: - sweep_list.append(el + ":=") - if isinstance(sweeps[el], list): - sweep_list.append(sweeps[el]) - else: - sweep_list.append([sweeps[el]]) - return sweep_list - - -class PostProcessorCommon(object): - """Manages the main AEDT postprocessing functions. - - This class is inherited in the caller application and is accessible through the post variable( eg. ``hfss.post`` or - ``q3d.post``). - - .. note:: - Some functionalities are available only when AEDT is running in - the graphical mode. - - Parameters - ---------- - app : :class:`ansys.aedt.core.application.analysis_3d.FieldAnalysis3D` - Inherited parent object. The parent object must provide the members - ``_modeler``, ``_desktop``, ``_odesign``, and ``logger``. - - Examples - -------- - >>> from ansys.aedt.core import Q3d - >>> q3d = Q3d() - >>> q3d = q.post.get_solution_data(domain="Original") - """ - - def __init__(self, app): - self._app = app - self.oeditor = None - if self.modeler: - self.oeditor = self.modeler.oeditor - self._scratch = self._app.working_directory - self.plots = self._get_plot_inputs() - self.reports_by_category = Reports(self, self._app.design_type) - - @property - def available_report_types(self): - """Report types. - - References - ---------- - - >>> oModule.GetAvailableReportTypes - """ - return list(self.oreportsetup.GetAvailableReportTypes()) - - @property - def update_report_dynamically(self): - """Get/Set the boolean to automatically update reports on edits. - - Returns - ------- - bool - """ - return ( - True - if self._app.odesktop.GetRegistryInt( - "Desktop/Settings/ProjectOptions/{}/UpdateReportsDynamicallyOnEdits".format(self._app.design_type) - ) - == 1 - else False - ) - - @update_report_dynamically.setter - def update_report_dynamically(self, value): - if value: - self._app.odesktop.SetRegistryInt( - "Desktop/Settings/ProjectOptions/{}/UpdateReportsDynamicallyOnEdits".format(self._app.design_type), 1 - ) - else: # pragma: no cover - self._app.odesktop.SetRegistryInt( - "Desktop/Settings/ProjectOptions/{}/UpdateReportsDynamicallyOnEdits".format(self._app.design_type), 0 - ) - - @pyaedt_function_handler() - def available_display_types(self, report_category=None): - """Retrieve display types for a report categories. - - Parameters - ---------- - report_category : str, optional - Type of the report. The default value is ``None``. - - Returns - ------- - list - List of available report categories. - - References - ---------- - >>> oModule.GetAvailableDisplayTypes - """ - if not report_category: - report_category = self.available_report_types[0] - if report_category: - return list(self.oreportsetup.GetAvailableDisplayTypes(report_category)) - return [] # pragma: no cover - - @pyaedt_function_handler() - def available_quantities_categories( - self, report_category=None, display_type=None, solution=None, context=None, is_siwave_dc=False - ): - """Compute the list of all available report categories. - - Parameters - ---------- - report_category : str, optional - Report category. The default is ``None``, in which case the first default category is used. - display_type : str, optional - Report display type. The default is ``None``, in which case the first default type - is used. In most cases, this default type is ``"Rectangular Plot"``. - solution : str, optional - Report setup. The default is ``None``, in which case the first - nominal adaptive solution is used. - context : str, dict, optional - Report category. The default is ``None``, in which case the first default context - is used. For Maxwell 2D/3D eddy current solution types, the report category - can be provided as a dictionary, where the key is the matrix name and the value - the reduced matrix. - is_siwave_dc : bool, optional - Whether the setup is Siwave DCIR. The default is ``False``. - - Returns - ------- - list - - References - ---------- - >>> oModule.GetAllCategories - """ - if not report_category: - report_category = self.available_report_types[0] - if not display_type: - display_type = self.available_display_types(report_category)[0] - if not solution and hasattr(self._app, "nominal_adaptive"): - solution = self._app.nominal_adaptive - if is_siwave_dc: # pragma: no cover - id_ = "0" - if context: - id_ = str( - [ - "RL", - "Sources", - "Vias", - "Bondwires", - "Probes", - ].index(context) - ) - context = [ - "NAME:Context", - "SimValueContext:=", - [37010, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0, "DCIRID", False, id_, "IDIID", False, "1"], - ] - elif self._app.design_type in ["Maxwell 2D", "Maxwell 3D"] and self._app.solution_type == "EddyCurrent": - if isinstance(context, dict): - for k, v in context.items(): - context = ["Context:=", k, "Matrix:=", v] - elif context and isinstance(context, str): - context = ["Context:=", context] - elif not context: - context = "" - elif not context: # pragma: no cover - context = "" - - if solution and report_category and display_type: - return list(self.oreportsetup.GetAllCategories(report_category, display_type, solution, context)) - return [] # pragma: no cover - - @pyaedt_function_handler() - def available_report_quantities( - self, - report_category=None, - display_type=None, - solution=None, - quantities_category=None, - context=None, - is_siwave_dc=False, - ): - """Compute the list of all available report quantities of a given report quantity category. - - Parameters - ---------- - report_category : str, optional - Report Category. The default is ``None``, in which case the default category is used. - display_type : str, optional - Report Display Type. - The default is ``None``, in which case the default type is used. - In most of the cases the default type is "Rectangular Plot". - solution : str, optional - Report Setup. - The default is ``None``, in which case the first nominal adaptive solution is used. - quantities_category : str, optional - The category that the quantities belong to. - It must be one of the ``available_quantities_categories`` method. - The default is ``None``, in which case the first default quantity is used. - context : str, dict, optional - Report Context. - The default is ``None``, in which case the default context is used. - For Maxwell 2D/3D Eddy Current solution types this can be provided as a dictionary - where the key is the matrix name and value the reduced matrix. - is_siwave_dc : bool, optional - Whether if the setup is Siwave DCIR or not. Default is ``False``. - - Returns - ------- - list - - References - ---------- - >>> oModule.GetAllQuantities - - Examples - -------- - The example shows how to get report expressions for a Maxwell design with Eddy current solution. - The context has to be provided as a dictionary where the key is the name of the original matrix - and the value is the name of the reduced matrix. - >>> from ansys.aedt.core import Maxwell3d - >>> m3d = Maxwell3d(solution_type="EddyCurrent") - >>> rectangle1 = m3d.modeler.create_rectangle(0, [0.5, 1.5, 0], [2.5, 5], name="Sheet1") - >>> rectangle2 = m3d.modeler.create_rectangle(0, [9, 1.5, 0], [2.5, 5], name="Sheet2") - >>> rectangle3 = m3d.modeler.create_rectangle(0, [16.5, 1.5, 0], [2.5, 5], name="Sheet3") - >>> m3d.assign_current(rectangle1.faces[0], amplitude=1, name="Cur1") - >>> m3d.assign_current(rectangle2.faces[0], amplitude=1, name="Cur2") - >>> m3d.assign_current(rectangle3.faces[0], amplitude=1, name="Cur3") - >>> L = m3d.assign_matrix(assignment=["Cur1", "Cur2", "Cur3"], matrix_name="Matrix1") - >>> out = L.join_series(sources=["Cur1", "Cur2"], matrix_name="ReducedMatrix1") - >>> expressions = m3d.post.available_report_quantities(report_category="EddyCurrent", - ... display_type="Data Table", - ... context={"Matrix1": "ReducedMatrix1"}) - >>> m3d.release_desktop(False, False) - """ - if not report_category: - report_category = self.available_report_types[0] - if not display_type: - display_type = self.available_display_types(report_category)[0] - if not solution and hasattr(self._app, "nominal_adaptive"): - solution = self._app.nominal_adaptive - if is_siwave_dc: - id = "0" - if context: - id = str( - [ - "RL", - "Sources", - "Vias", - "Bondwires", - "Probes", - ].index(context) - ) - context = [ - "NAME:Context", - "SimValueContext:=", - [37010, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0, "DCIRID", False, id, "IDIID", False, "1"], - ] - elif self._app.design_type in ["Maxwell 2D", "Maxwell 3D"] and self._app.solution_type == "EddyCurrent": - if isinstance(context, dict): - for k, v in context.items(): - context = ["Context:=", k, "Matrix:=", v] - elif context and isinstance(context, str): - context = ["Context:=", context] - elif not context: - context = "" - elif not context: - context = "" - if not quantities_category: - categories = self.available_quantities_categories(report_category, display_type, solution, context) - quantities_category = "" - if categories: - quantities_category = "All" if "All" in categories else categories[0] - if quantities_category and display_type and report_category and solution: - return list( - self.oreportsetup.GetAllQuantities( - report_category, display_type, solution, context, quantities_category - ) - ) - return [] # pragma: no cover - - @pyaedt_function_handler() - def get_all_report_quantities( - self, - solution=None, - context=None, - is_siwave_dc=False, - ): - """Return all the possible report categories organized by report types, solution and categories. - - Parameters - ---------- - solution : str optional - Solution to get the report quantities. - The default is ``None``, in which case the all solutions are used. - context : str, dict, optional - Report Context. - The default is ``None``, in which case the default context is used. - For Maxwell 2D/3D Eddy Current solution types this can be provided as a dictionary - where the key is the matrix name and value the reduced matrix. - is_siwave_dc : bool, optional - Whether if the setup is Siwave DCIR or not. Default is ``False``. - - Returns - ------- - dict - A dictionary with primary key the report type, secondary key the solution type and - third key the report categories. - """ - rep_quantities = {} - for rep in self.available_report_types: - rep_quantities[rep] = {} - solutions = [solution] if isinstance(solution, str) else self.available_report_solutions(rep) - for solution in solutions: - rep_quantities[rep][solution] = {} - for quant in self.available_quantities_categories( - rep, context=context, solution=solution, is_siwave_dc=is_siwave_dc - ): - rep_quantities[rep][solution][quant] = self.available_report_quantities( - rep, quantities_category=quant, context=context, solution=solution, is_siwave_dc=is_siwave_dc - ) - - return rep_quantities - - @pyaedt_function_handler() - def available_report_solutions(self, report_category=None): - """Get the list of available solutions that can be used for the reports. - This list differs from the one obtained with ``app.existing_analysis_sweeps``, - because it includes additional elements like "AdaptivePass". - - Parameters - ---------- - report_category : str, optional - Report Category. Default is ``None`` which takes default category. - - Returns - ------- - list - - References - ---------- - >>> oModule.GetAvailableSolutions - """ - if not report_category: - report_category = self.available_report_types[0] - if report_category: - return list(self.oreportsetup.GetAvailableSolutions(report_category)) - return None # pragma: no cover - - @pyaedt_function_handler() - def _get_plot_inputs(self): - names = self._app.get_oo_name(self.oreportsetup) - plots = [] - skip_plot = False - if self._app.design_type == "Circuit Netlist" and self._app.desktop_class.non_graphical: - skip_plot = True - if names and not skip_plot: - for name in names: - obj = self._app.get_oo_object(self.oreportsetup, name) - report_type = obj.GetPropValue("Report Type") - - if report_type in TEMPLATES_BY_NAME: - report = TEMPLATES_BY_NAME[report_type] - else: - report = rt.Standard - plots.append(report(self, report_type, None)) - plots[-1].props["plot_name"] = name - plots[-1]._is_created = True - plots[-1].report_type = obj.GetPropValue("Display Type") - return plots - - @property - def oreportsetup(self): - """Report setup. - - Returns - ------- - :attr:`ansys.aedt.core.modules.post_processor.PostProcessor.oreportsetup` - - References - ---------- - - >>> oDesign.GetModule("ReportSetup") - """ - return self._app.oreportsetup - - @property - def logger(self): - """Logger.""" - return self._app.logger - - @property - def _desktop(self): - """Desktop.""" - return self._app._desktop - - @property - def _odesign(self): - """Design.""" - return self._app._odesign - - @property - def _oproject(self): - """Project.""" - return self._app._oproject - - @property - def modeler(self): - """Modeler.""" - return self._app.modeler - - @property - def post_solution_type(self): - """Design solution type. - - Returns - ------- - type - Design solution type. - """ - return self._app.solution_type - - @property - def all_report_names(self): - """List of all report names. - - Returns - ------- - list - - References - ---------- - - >>> oModule.GetAllReportNames() - """ - return list(self.oreportsetup.GetAllReportNames()) - - @pyaedt_function_handler(PlotName="plot_name") - def copy_report_data(self, plot_name, paste=True): - """Copy report data as static data. - - Parameters - ---------- - plot_name : str - Name of the report. - paste : bool, optional - Whether to paste the report. The default is ``True``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - - References - ---------- - - >>> oModule.CopyReportsData - >>> oModule.PasteReports - """ - self.oreportsetup.CopyReportsData([plot_name]) - if paste: - self.paste_report_data() - return True - - @pyaedt_function_handler() - def paste_report_data(self): - """Paste report data as static data. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - - References - ---------- - >>> oModule.PasteReports - """ - self.oreportsetup.PasteReports() - return True - - @pyaedt_function_handler() - def delete_report(self, plot_name=None): - """Delete all reports or specific report. - - Parameters - ---------- - plot_name : str, optional - Name of the plot to delete. The default value is ``None`` and in this case, all reports are deleted. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - - References - ---------- - - >>> oModule.DeleteReports - """ - try: - if plot_name: - self.oreportsetup.DeleteReports([plot_name]) - for plot in self.plots: - if plot.plot_name == plot_name: - self.plots.remove(plot) - else: - self.oreportsetup.DeleteAllReports() - if is_ironpython: # pragma: no cover - del self.plots[:] - else: - self.plots.clear() - return True - except Exception: # pragma: no cover - return False - - @pyaedt_function_handler() - def rename_report(self, plot_name, new_name): - """Rename a plot. - - Parameters - ---------- - plot_name : str - Name of the plot. - new_name : str - New name of the plot. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - - References - ---------- - - >>> oModule.RenameReport - """ - try: - self.oreportsetup.RenameReport(plot_name, new_name) - for plot in self.plots: - if plot.plot_name == plot_name: - plot.plot_name = self.oreportsetup.GetChildObject(new_name).GetPropValue("Name") - return True - except Exception: - return False - - @pyaedt_function_handler(soltype="solution_type", ctxt="context", expression="expressions") - def get_solution_data_per_variation( - self, solution_type="Far Fields", setup_sweep_name="", context=None, sweeps=None, expressions="" - ): - """Retrieve solution data for each variation. - - Parameters - ---------- - solution_type : str, optional - Type of the solution. For example, ``"Far Fields"`` or ``"Modal Solution Data"``. The default - is ``"Far Fields"``. - setup_sweep_name : str, optional - Name of the setup for computing the report. The default is ``""``, - in which case ``"nominal adaptive"`` is used. - context : list, optional - List of context variables. The default is ``None``. - sweeps : dict, optional - Dictionary of variables and values. The default is ``None``, - in which case this list is used: - ``{'Theta': 'All', 'Phi': 'All', 'Freq': 'All'}``. - expressions : str or list, optional - One or more traces to include. The default is ``""``. - - Returns - ------- - from ansys.aedt.core.modules.solutions.SolutionData - - - References - ---------- - - >>> oModule.GetSolutionDataPerVariation - """ - if sweeps is None: - sweeps = {"Theta": "All", "Phi": "All", "Freq": "All"} - if not context: - context = [] - if not isinstance(expressions, list): - expressions = [expressions] - if not setup_sweep_name: - setup_sweep_name = self._app.nominal_adaptive - sweep_list = _convert_dict_to_report_sel(sweeps) - try: - data = list( - self.oreportsetup.GetSolutionDataPerVariation( - solution_type, setup_sweep_name, context, sweep_list, expressions - ) - ) - self.logger.info("Solution Data Correctly Loaded.") - return SolutionData(data) - except Exception: - self.logger.warning("Solution Data failed to load. Check solution, context or expression.") - return None - - @pyaedt_function_handler() - def steal_focus_oneditor(self): - """Remove the selection of an object that would prevent the image from exporting correctly. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - - References - ---------- - - >>> oDesktop.RestoreWindow - """ - self._desktop.RestoreWindow() - param = ["NAME:SphereParameters", "XCenter:=", "0mm", "YCenter:=", "0mm", "ZCenter:=", "0mm", "Radius:=", "1mm"] - attr = ["NAME:Attributes", "Name:=", "DUMMYSPHERE1", "Flags:=", "NonModel#"] - self.oeditor.CreateSphere(param, attr) - self.oeditor.Delete(["NAME:Selections", "Selections:=", "DUMMYSPHERE1"]) - return True - - @pyaedt_function_handler() - def export_report_to_file( - self, - output_dir, - plot_name, - extension, - unique_file=False, - uniform=False, - start=None, - end=None, - step=None, - use_trace_number_format=False, - ): - """Export a 2D Plot data to a file. - - This method leaves the data in the plot (as data) as a reference - for the Plot after the loops. - - Parameters - ---------- - output_dir : str - Path to the directory of exported report - plot_name : str - Name of the plot to export. - extension : str - Extension of export , one of - * (CSV) .csv - * (Tab delimited) .tab - * (Post processor format) .txt - * (Ensight XY data) .exy - * (Anosft Plot Data) .dat - * (Ansoft Report Data Files) .rdat - unique_file : bool - If set to True, generates unique file in output_dit - uniform : bool, optional - Whether the export uniform points to the file. The - default is ``False``. - start : str, optional - Start range with units for the sweep if the ``uniform`` parameter - is set to ``True``. - end : str, optional - End range with units for the sweep if the ``uniform`` parameter - is set to ``True``. - step : str, optional - Step range with units for the sweep if the ``uniform`` parameter is - set to ``True``. - use_trace_number_format : bool, optional - Whether to use trace number formats and use separate columns for curve. The default is ``False``. - - Returns - ------- - str - Path of exported file. - - References - ---------- - - >>> oModule.ExportReportDataToFile - >>> oModule.ExportUniformPointsToFile - >>> oModule.ExportToFile - - Examples - -------- - >>> from ansys.aedt.core import Circuit - >>> cir = Circuit("my_project.aedt") - >>> report = cir.post.create_report("MyScattering") - >>> cir.post.export_report_to_file("C:\\temp", "MyTestScattering", ".csv") - """ - npath = output_dir - - if "." not in extension: # pragma: no cover - extension = "." + extension - - supported_ext = [".csv", ".tab", ".txt", ".exy", ".dat", ".rdat"] - if extension not in supported_ext: # pragma: no cover - msg = "Extension {} is not supported. Use one of {}".format(extension, ", ".join(supported_ext)) - raise ValueError(msg) - - file_path = os.path.join(npath, plot_name + extension) - if unique_file: # pragma: no cover - while os.path.exists(file_path): - file_name = generate_unique_name(plot_name) - file_path = os.path.join(npath, file_name + extension) - - if extension == ".rdat": - self.oreportsetup.ExportReportDataToFile(plot_name, file_path) - elif uniform: - self.oreportsetup.ExportUniformPointsToFile(plot_name, file_path, start, end, step, use_trace_number_format) - else: - self.oreportsetup.ExportToFile(plot_name, file_path, use_trace_number_format) - return file_path - - @pyaedt_function_handler() - def export_report_to_csv( - self, project_dir, plot_name, uniform=False, start=None, end=None, step=None, use_trace_number_format=False - ): - """Export the 2D Plot data to a CSV file. - - This method leaves the data in the plot (as data) as a reference - for the Plot after the loops. - - Parameters - ---------- - project_dir : str - Path to the project directory. The CSV file is plot_name.csv. - plot_name : str - Name of the plot to export. - uniform : bool, optional - Whether the export uniform points to the file. The - default is ``False``. - start : str, optional - Start range with units for the sweep if the ``uniform`` parameter - is set to ``True``. - end : str, optional - End range with units for the sweep if the ``uniform`` parameter - is set to ``True``. - step : str, optional - Step range with units for the sweep if the ``uniform`` parameter is - set to ``True``. - use_trace_number_format : bool, optional - Whether to use trace number formats. The default is ``False``. - - Returns - ------- - str - Path of exported file. - - References - ---------- - - >>> oModule.ExportReportDataToFile - >>> oModule.ExportToFile - >>> oModule.ExportUniformPointsToFile - """ - return self.export_report_to_file( - project_dir, - plot_name, - extension=".csv", - uniform=uniform, - start=start, - end=end, - step=step, - use_trace_number_format=use_trace_number_format, - ) - - @pyaedt_function_handler(project_dir="project_path") - def export_report_to_jpg(self, project_path, plot_name, width=0, height=0, image_format="jpg"): - """Export plot to an image file. - - Parameters - ---------- - project_path : str - Path to the project directory. - plot_name : str - Name of the plot to export. - width : int, optional - Image width. Default is ``0`` which takes Desktop size or 1980 pixel in case of non-graphical mode. - height : int, optional - Image height. Default is ``0`` which takes Desktop size or 1020 pixel in case of non-graphical mode. - image_format : str, optional - Format of the image file. The default is ``"jpg"``. - - Returns - ------- - bool - ``True`` when successful, ``False`` when failed. - - References - ---------- - - >>> oModule.ExportImageToFile - """ - file_name = os.path.join(project_path, plot_name + "." + image_format) # name of the image file - if self._app.desktop_class.non_graphical: # pragma: no cover - if width == 0: - width = 1980 - if height == 0: - height = 1020 - self.oreportsetup.ExportImageToFile(plot_name, file_name, width, height) - return True - - @pyaedt_function_handler(plotname="plot_name") - def _get_report_inputs( - self, - expressions, - setup_sweep_name=None, - domain="Sweep", - variations=None, - primary_sweep_variable=None, - secondary_sweep_variable=None, - report_category=None, - plot_type="Rectangular Plot", - context=None, - subdesign_id=None, - polyline_points=0, - plot_name=None, - only_get_method=False, - ): - ctxt = [] - if not setup_sweep_name: - setup_sweep_name = self._app.nominal_sweep - elif setup_sweep_name not in self._app.existing_analysis_sweeps: - self.logger.error("Sweep not Available.") - return False - families_input = {} - did = 3 - if domain == "Sweep" and not primary_sweep_variable: - primary_sweep_variable = "Freq" - elif not primary_sweep_variable: - primary_sweep_variable = "Time" - did = 1 - if not variations or primary_sweep_variable not in variations: - families_input[primary_sweep_variable] = ["All"] - elif isinstance(variations[primary_sweep_variable], list): - families_input[primary_sweep_variable] = variations[primary_sweep_variable] - else: - families_input[primary_sweep_variable] = [variations[primary_sweep_variable]] - if not variations: - variations = self._app.available_variations.nominal_w_values_dict - for el in list(variations.keys()): - if el == primary_sweep_variable: - continue - if isinstance(variations[el], list): - families_input[el] = variations[el] - else: - families_input[el] = [variations[el]] - if only_get_method and domain == "Sweep": - if "Phi" not in families_input: - families_input["Phi"] = ["All"] - if "Theta" not in families_input: - families_input["Theta"] = ["All"] - - if self.post_solution_type in ["TR", "AC", "DC"]: - ctxt = [ - "NAME:Context", - "SimValueContext:=", - [did, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0], - ] - setup_sweep_name = self.post_solution_type - elif self.post_solution_type in ["HFSS3DLayout"]: - if context == "Differential Pairs": - ctxt = [ - "NAME:Context", - "SimValueContext:=", - [ - did, - 0, - 2, - 0, - False, - False, - -1, - 1, - 0, - 1, - 1, - "", - 0, - 0, - "EnsDiffPairKey", - False, - "1", - "IDIID", - False, - "1", - ], - ] - else: - ctxt = [ - "NAME:Context", - "SimValueContext:=", - [did, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0, "IDIID", False, "1"], - ] - elif self.post_solution_type in ["NexximLNA", "NexximTransient"]: - ctxt = ["NAME:Context", "SimValueContext:=", [did, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0]] - if subdesign_id: - ctxt_temp = ["NUMLEVELS", False, "1", "SUBDESIGNID", False, str(subdesign_id)] - for el in ctxt_temp: - ctxt[2].append(el) - if context == "Differential Pairs": - ctxt_temp = ["USE_DIFF_PAIRS", False, "1"] - for el in ctxt_temp: - ctxt[2].append(el) - elif context == "Differential Pairs": - ctxt = ["Diff:=", "Differential Pairs", "Domain:=", domain] - elif self.post_solution_type in ["Q3D Extractor", "2D Extractor"]: - if not context: - ctxt = ["Context:=", "Original"] - else: - ctxt = ["Context:=", context] - elif context: - ctxt = ["Context:=", context] - if context in self.modeler.line_names: - ctxt.append("PointCount:=") - ctxt.append(polyline_points) - - if not isinstance(expressions, list): - expressions = [expressions] - - if not report_category and not self._app.design_solutions.report_type: - self.logger.error("Solution not supported") - return False - if not report_category: - modal_data = self._app.design_solutions.report_type - else: - modal_data = report_category - if not plot_name: - plot_name = generate_unique_name("Plot") - - arg = ["X Component:=", primary_sweep_variable, "Y Component:=", expressions] - if plot_type in ["3D Polar Plot", "3D Spherical Plot"]: - if not primary_sweep_variable: - primary_sweep_variable = "Phi" - if not secondary_sweep_variable: - secondary_sweep_variable = "Theta" - arg = [ - "Phi Component:=", - primary_sweep_variable, - "Theta Component:=", - secondary_sweep_variable, - "Mag Component:=", - expressions, - ] - elif plot_type == "Radiation Pattern": - if not primary_sweep_variable: - primary_sweep_variable = "Phi" - arg = ["Ang Component:=", primary_sweep_variable, "Mag Component:=", expressions] - elif plot_type in ["Smith Chart", "Polar Plot"]: - arg = ["Polar Component:=", expressions] - elif plot_type == "Rectangular Contour Plot": - arg = [ - "X Component:=", - primary_sweep_variable, - "Y Component:=", - secondary_sweep_variable, - "Z Component:=", - expressions, - ] - return [plot_name, modal_data, plot_type, setup_sweep_name, ctxt, families_input, arg] - - @pyaedt_function_handler(plotname="plot_name") - def create_report( - self, - expressions=None, - setup_sweep_name=None, - domain="Sweep", - variations=None, - primary_sweep_variable=None, - secondary_sweep_variable=None, - report_category=None, - plot_type="Rectangular Plot", - context=None, - subdesign_id=None, - polyline_points=1001, - plot_name=None, - ): - """Create a report in AEDT. It can be a 2D plot, 3D plot, polar plot, or a data table. - - Parameters - ---------- - expressions : str or list, optional - One or more formulas to add to the report. Example is value = ``"dB(S(1,1))"``. - setup_sweep_name : str, optional - Setup name with the sweep. The default is ``""``. - domain : str, optional - Plot Domain. Options are "Sweep", "Time", "DCIR". - variations : dict, optional - Dictionary of all families including the primary sweep. The default is ``{"Freq": ["All"]}``. - primary_sweep_variable : str, optional - Name of the primary sweep. The default is ``"Freq"``. - secondary_sweep_variable : str, optional - Name of the secondary sweep variable in 3D Plots. - report_category : str, optional - Category of the Report to be created. If `None` default data Report is used. - The Report Category can be one of the types available for creating a report depend on the simulation setup. - For example for a Far Field Plot in HFSS the UI shows the report category as "Create Far Fields Report". - The report category is "Far Fields" in this case. - Depending on the setup different categories are available. - If ``None`` default category is used (the first item in the Results drop down menu in AEDT). - plot_type : str, optional - The format of Data Visualization. Default is ``Rectangular Plot``. - context : str, dict, optional - The default is ``None``. - - For HFSS 3D Layout, options are ``"Bondwires"``, ``"Differential Pairs"``, - ``None``, ``"Probes"``, ``"RL"``, ``"Sources"``, and ``"Vias"``. - - For Q2D or Q3D, specify the name of a reduced matrix. - - For a far fields plot, specify the name of an infinite sphere. - - For Maxwell 2D/3D Eddy Current solution types this can be provided as a dictionary - where the key is the matrix name and value the reduced matrix. - plot_name : str, optional - Name of the plot. The default is ``None``. - polyline_points : int, optional, - Number of points to create the report for plots on polylines on. - subdesign_id : int, optional - Specify a subdesign ID to export a Touchstone file of this subdesign. Valid for Circuit Only. - The default value is ``None``. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Standard` - ``True`` when successful, ``False`` when failed. - - - References - ---------- - - >>> oModule.CreateReport - - Examples - -------- - >>> from ansys.aedt.core import Hfss - >>> hfss = Hfss() - >>> hfss.post.create_report("dB(S(1,1))") - >>> variations = hfss.available_variations.nominal_w_values_dict - >>> variations["Theta"] = ["All"] - >>> variations["Phi"] = ["All"] - >>> variations["Freq"] = ["30GHz"] - >>> hfss.post.create_report(expressions="db(GainTotal)", - ... setup_sweep_name=hfss.nominal_adaptive, - ... variations=variations, - ... primary_sweep_variable="Phi", - ... secondary_sweep_variable="Theta", - ... report_category="Far Fields", - ... plot_type="3D Polar Plot", - ... context="3D") - >>> hfss.post.create_report("S(1,1)",hfss.nominal_sweep,variations=variations,plot_type="Smith Chart") - >>> hfss.release_desktop(False, False) - - >>> from ansys.aedt.core import Maxwell2d - >>> m2d = Maxwell2d() - >>> m2d.post.create_report(expressions="InputCurrent(PHA)", - ... domain="Time", - ... primary_sweep_variable="Time", - ... plot_name="Winding Plot 1") - >>> m2d.release_desktop(False, False) - - >>> from ansys.aedt.core import Maxwell3d - >>> m3d = Maxwell3d(solution_type="EddyCurrent") - >>> rectangle1 = m3d.modeler.create_rectangle(0, [0.5, 1.5, 0], [2.5, 5], name="Sheet1") - >>> rectangle2 = m3d.modeler.create_rectangle(0, [9, 1.5, 0], [2.5, 5], name="Sheet2") - >>> rectangle3 = m3d.modeler.create_rectangle(0, [16.5, 1.5, 0], [2.5, 5], name="Sheet3") - >>> m3d.assign_current(rectangle1.faces[0], amplitude=1, name="Cur1") - >>> m3d.assign_current(rectangle2.faces[0], amplitude=1, name="Cur2") - >>> m3d.assign_current(rectangle3.faces[0], amplitude=1, name="Cur3") - >>> L = m3d.assign_matrix(assignment=["Cur1", "Cur2", "Cur3"], matrix_name="Matrix1") - >>> out = L.join_series(sources=["Cur1", "Cur2"], matrix_name="ReducedMatrix1") - >>> expressions = m3d.post.available_report_quantities(report_category="EddyCurrent", - ... display_type="Data Table", - ... context={"Matrix1": "ReducedMatrix1"}) - >>> report = m3d.post.create_report( - ... expressions=expressions, - ... context={"Matrix1": "ReducedMatrix1"}, - ... plot_type="Data Table", - ... plot_name="reduced_matrix") - >>> m3d.release_desktop(False, False) - """ - if not setup_sweep_name: - setup_sweep_name = self._app.nominal_sweep - if not domain: - domain = "Sweep" - setup_name = setup_sweep_name.split(":")[0] - if setup_name: - for setup in self._app.setups: - if setup.name == setup_name and "Time" in setup.default_intrinsics: - domain = "Time" - if domain in ["Spectral", "Spectrum"]: - report_category = "Spectrum" - elif not report_category and not self._app.design_solutions.report_type: - self.logger.error("Solution not supported") - return False - elif not report_category: - report_category = self._app.design_solutions.report_type - if report_category in TEMPLATES_BY_NAME: - report_class = TEMPLATES_BY_NAME[report_category] - elif "Fields" in report_category: - report_class = TEMPLATES_BY_NAME["Fields"] - else: - report_class = TEMPLATES_BY_NAME["Standard"] - - report = report_class(self, report_category, setup_sweep_name) - if not expressions: - expressions = [ - i for i in self.available_report_quantities(report_category=report_category, context=context) - ] - report.expressions = expressions - report.domain = domain - if not variations and domain == "Sweep": - variations = self._app.available_variations.nominal_w_values_dict - if variations: - variations["Freq"] = "All" - else: - variations = {"Freq": ["All"]} - elif not variations and domain != "Sweep": - variations = self._app.available_variations.nominal_w_values_dict - report.variations = variations - if primary_sweep_variable: - report.primary_sweep = primary_sweep_variable - elif domain == "DCIR": # pragma: no cover - report.primary_sweep = "Index" - if variations: - variations["Index"] = ["All"] - else: # pragma: no cover - variations = {"Index": "All"} - if secondary_sweep_variable: - report.secondary_sweep = secondary_sweep_variable - - report.variations = variations - report.report_type = plot_type - report.sub_design_id = subdesign_id - report.point_number = polyline_points - if context == "Differential Pairs": - report.differential_pairs = True - elif context in [ - "RL", - "Sources", - "Vias", - "Bondwires", - "Probes", - ]: - report.siwave_dc_category = [ - "RL", - "Sources", - "Vias", - "Bondwires", - "Probes", - ].index(context) - elif self._app.design_type in ["Q3D Extractor", "2D Extractor"] and context: - report.matrix = context - elif ( - self._app.design_type in ["Maxwell 2D", "Maxwell 3D"] - and self._app.solution_type == "EddyCurrent" - and context - ): - if isinstance(context, dict): - for k, v in context.items(): - report.matrix = k - report.reduced_matrix = v - elif context in self.modeler.line_names or context in self.modeler.point_names: - report.polyline = context - else: - report.matrix = context - elif report_category == "Far Fields": - if not context and self._app._field_setups: - report.far_field_sphere = self._app.field_setups[0].name - else: - if isinstance(context, dict): - if "Context" in context.keys() and "SourceContext" in context.keys(): - report.far_field_sphere = context["Context"] - report.source_context = context["SourceContext"] - if "Context" in context.keys() and "Source Group" in context.keys(): - report.far_field_sphere = context["Context"] - report.source_group = context["Source Group"] - else: - report.far_field_sphere = context - elif report_category == "Near Fields": - report.near_field = context - elif context: - if context in self.modeler.line_names or context in self.modeler.point_names: - report.polyline = context - - result = report.create(plot_name) - if result: - return report - return False - - @pyaedt_function_handler() - def get_solution_data( - self, - expressions=None, - setup_sweep_name=None, - domain=None, - variations=None, - primary_sweep_variable=None, - report_category=None, - context=None, - subdesign_id=None, - polyline_points=1001, - math_formula=None, - ): - """Get a simulation result from a solved setup and cast it in a ``SolutionData`` object. - Data to be retrieved from Electronics Desktop are any simulation results available in that - specific simulation context. - Most of the argument have some defaults which works for most of the ``Standard`` report quantities. - - Parameters - ---------- - expressions : str or list, optional - One or more formulas to add to the report. Example is value ``"dB(S(1,1))"`` or a list of values. - Default is ``None`` which returns all traces. - setup_sweep_name : str, optional - Name of the setup. The default is ``None``, in which case the ``nominal_adaptive`` - setup is used. Be sure to build a setup string in the form of - ``"SetupName : SetupSweep"``, where ``SetupSweep`` is the sweep name to - use in the export or ``LastAdaptive``. - domain : str, optional - Plot Domain. Options are "Sweep" for frequency domain related results and "Time" for transient related data. - variations : dict, optional - Dictionary of all families including the primary sweep. - The default is ``None`` which uses the nominal variations of the setup. - primary_sweep_variable : str, optional - Name of the primary sweep. The default is ``"None"`` which, depending on the context, - internally assigns the primary sweep to: - 1. ``Freq`` for frequency domain results, - 2. ``Time`` for transient results, - 3. ``Theta`` for radiation patterns, - 4. ``distance`` for field plot over a polyline. - report_category : str, optional - Category of the Report to be created. If ``None`` default data Report is used. - The Report Category can be one of the types available for creating a report depend on the simulation setup. - For example for a Far Field Plot in HFSS the UI shows the report category as "Create Far Fields Report". - The report category is "Far Fields" in this case. - Depending on the setup different categories are available. - If ``None`` default category is used (the first item in the Results drop down menu in AEDT). - To get the list of available categories user can use method ``available_report_types``. - context : str, dict, optional - This is the context of the report. - The default is ``None``. It can be: - 1. `None` - 2. ``"Differential Pairs"`` - 3. Reduce Matrix Name for Q2d/Q3d solution - 4. Infinite Sphere name for Far Fields Plot. - 5. Dictionary. If dictionary is passed, key is the report property name and value is property value. - 6. For Maxwell 2D/3D eddy current solution types, this can be provided as a dictionary, - where the key is the matrix name and value the reduced matrix. - subdesign_id : int, optional - Subdesign ID for exporting a Touchstone file of this subdesign. - This parameter is valid for ``Circuit`` only. - The default value is ``None``. - polyline_points : int, optional - Number of points on which to create the report for plots on polylines. - This parameter is valid for ``Fields`` plot only. - math_formula : str, optional - One of the available AEDT mathematical formulas to apply. For example, ``abs, dB``. - - - Returns - ------- - :class:`ansys.aedt.core.modules.solutions.SolutionData` - Solution Data object. - - References - ---------- - - >>> oModule.GetSolutionDataPerVariation - - Examples - -------- - >>> from ansys.aedt.core import Hfss - >>> hfss = Hfss() - >>> hfss.post.create_report("dB(S(1,1))") - >>> variations = hfss.available_variations.nominal_w_values_dict - >>> variations["Theta"] = ["All"] - >>> variations["Phi"] = ["All"] - >>> variations["Freq"] = ["30GHz"] - >>> data1 = hfss.post.get_solution_data( - ... "GainTotal", - ... hfss.nominal_adaptive, - ... variations=variations, - ... primary_sweep_variable="Phi", - ... secondary_sweep_variable="Theta", - ... context="3D", - ... report_category="Far Fields", - ...) - - >>> data2 =hfss.post.get_solution_data( - ... "S(1,1)", - ... hfss.nominal_sweep, - ... variations=variations, - ...) - >>> data2.plot() - >>> hfss.release_desktop(False, False) - - >>> from ansys.aedt.core import Maxwell2d - >>> m2d = Maxwell2d() - >>> data3 = m2d.post.get_solution_data( - ... "InputCurrent(PHA)", domain="Time", primary_sweep_variable="Time", - ... ) - >>> data3.plot("InputCurrent(PHA)") - >>> m2d.release_desktop(False, False) - - >>> from ansys.aedt.core import Circuit - >>> circuit = Circuit() - >>> context = {"algorithm": "FFT", "max_frequency": "100MHz", "time_stop": "2.5us", "time_start": "0ps"} - >>> spectralPlotData = circuit.post.get_solution_data(expressions="V(Vprobe1)", domain="Spectral", - ... primary_sweep_variable="Spectrum", context=context) - >>> circuit.release_desktop(False, False) - - >>> from ansys.aedt.core import Maxwell3d - >>> m3d = Maxwell3d(solution_type="EddyCurrent") - >>> rectangle1 = m3d.modeler.create_rectangle(0, [0.5, 1.5, 0], [2.5, 5], name="Sheet1") - >>> rectangle2 = m3d.modeler.create_rectangle(0, [9, 1.5, 0], [2.5, 5], name="Sheet2") - >>> rectangle3 = m3d.modeler.create_rectangle(0, [16.5, 1.5, 0], [2.5, 5], name="Sheet3") - >>> m3d.assign_current(rectangle1.faces[0], amplitude=1, name="Cur1") - >>> m3d.assign_current(rectangle2.faces[0], amplitude=1, name="Cur2") - >>> m3d.assign_current(rectangle3.faces[0], amplitude=1, name="Cur3") - >>> L = m3d.assign_matrix(assignment=["Cur1", "Cur2", "Cur3"], matrix_name="Matrix1") - >>> out = L.join_series(sources=["Cur1", "Cur2"], matrix_name="ReducedMatrix1") - >>> expressions = m3d.post.available_report_quantities(report_category="EddyCurrent", - ... display_type="Data Table", - ... context={"Matrix1": "ReducedMatrix1"}) - >>> data = m2d.post.get_solution_data(expressions=expressions, context={"Matrix1": "ReducedMatrix1"}) - >>> m3d.release_desktop(False, False) - """ - expressions = [expressions] if isinstance(expressions, str) else expressions - if not setup_sweep_name: - setup_sweep_name = self._app.nominal_sweep - if not domain: - domain = "Sweep" - setup_name = setup_sweep_name.split(":")[0] - if setup_name: - for setup in self._app.setups: - if setup.name == setup_name and "Time" in setup.default_intrinsics: - domain = "Time" - if domain in ["Spectral", "Spectrum"]: - report_category = "Spectrum" - if not report_category and not self._app.design_solutions.report_type: - self.logger.error("Solution not supported") - return False - elif not report_category: - report_category = self._app.design_solutions.report_type - if report_category in TEMPLATES_BY_NAME: - report_class = TEMPLATES_BY_NAME[report_category] - elif "Fields" in report_category: - report_class = TEMPLATES_BY_NAME["Fields"] - else: - report_class = TEMPLATES_BY_NAME["Standard"] - - report = report_class(self, report_category, setup_sweep_name) - if not expressions: - expressions = [ - i for i in self.available_report_quantities(report_category=report_category, context=context) - ] - if math_formula: - expressions = ["{}({})".format(math_formula, i) for i in expressions] - report.expressions = expressions - report.domain = domain - if primary_sweep_variable: - report.primary_sweep = primary_sweep_variable - if not variations and domain == "Sweep": - variations = self._app.available_variations.nominal_w_values_dict - if variations: - variations["Freq"] = "All" - else: - variations = {"Freq": ["All"]} - elif not variations and domain != "Sweep": - variations = self._app.available_variations.nominal_w_values_dict - report.variations = variations - report.sub_design_id = subdesign_id - report.point_number = polyline_points - if context == "Differential Pairs": - report.differential_pairs = True - elif self._app.design_type in ["Q3D Extractor", "2D Extractor"] and context: - report.matrix = context - elif ( - self._app.design_type in ["Maxwell 2D", "Maxwell 3D"] - and self._app.solution_type == "EddyCurrent" - and context - ): - if isinstance(context, dict): - for k, v in context.items(): - report.matrix = k - report.reduced_matrix = v - elif ( - hasattr(self.modeler, "line_names") - and hasattr(self.modeler, "point_names") - and context in self.modeler.point_names + self.modeler.line_names - ): - report.polyline = context - else: - report.matrix = context - elif report_category == "Far Fields": - if not context and self._app.field_setups: - report.far_field_sphere = self._app.field_setups[0].name - else: - if isinstance(context, dict): - if "Context" in context.keys() and "SourceContext" in context.keys(): - report.far_field_sphere = context["Context"] - report.source_context = context["SourceContext"] - else: - report.far_field_sphere = context - elif report_category == "Near Fields": - report.near_field = context - elif context and isinstance(context, dict): - for attribute in context: - if hasattr(report, attribute): - report.__setattr__(attribute, context[attribute]) - else: - self.logger.warning("Parameter " + attribute + " is not available, check syntax.") - elif context: - if ( - hasattr(self.modeler, "line_names") - and hasattr(self.modeler, "point_names") - and context in self.modeler.point_names + self.modeler.line_names - ): - report.polyline = context - elif context in [ - "RL", - "Sources", - "Vias", - "Bondwires", - "Probes", - ]: - report.siwave_dc_category = [ - "RL", - "Sources", - "Vias", - "Bondwires", - "Probes", - ].index(context) - solution_data = report.get_solution_data() - return solution_data - - @pyaedt_function_handler(input_dict="report_settings") - def create_report_from_configuration(self, input_file=None, report_settings=None, solution_name=None): - """Create a report based on a JSON file, TOML file, RPT file, or dictionary of properties. - - Parameters - ---------- - input_file : str, optional - Path to the JSON, TOML, or RPT file containing report settings. - report_settings : dict, optional - Dictionary containing report settings. - solution_name : str, optional - Setup name to use. - - Returns - ------- - :class:`ansys.aedt.core.modules.report_templates.Standard` - Report object if succeeded. - - Examples - -------- - - Create report from JSON file. - >>> from ansys.aedt.core import Hfss - >>> hfss = Hfss() - >>> hfss.post.create_report_from_configuration(r'C:\\temp\\my_report.json', - ... solution_name="Setup1 : LastAdpative") - - Create report from RPT file. - >>> from ansys.aedt.core import Hfss - >>> hfss = Hfss() - >>> hfss.post.create_report_from_configuration(r'C:\\temp\\my_report.rpt') +import string +import warnings - Create report from dictionary. - >>> from ansys.aedt.core import Hfss - >>> from ansys.aedt.core.generic.general_methods import read_json - >>> hfss = Hfss() - >>> dict_vals = read_json("Report_Simple.json") - >>> hfss.post.create_report_from_configuration(report_settings=dict_vals) - """ - if not report_settings and not input_file: # pragma: no cover - self.logger.error("Either a file or a dictionary must be passed as input.") - return False - if input_file: - _, file_extension = os.path.splitext(input_file) - if file_extension == ".rpt": - old_expressions = self.all_report_names - self.oreportsetup.CreateReportFromTemplate(input_file) - new_expressions = [item for item in self.all_report_names if item not in old_expressions] - if new_expressions: - report_name = new_expressions[0] - self.plots = self._get_plot_inputs() - report = None - for plot in self.plots: - if plot.plot_name == report_name: - report = plot - break - return report - else: - props = read_configuration_file(input_file) - else: - props = report_settings +from ansys.aedt.core import generate_unique_name +from ansys.aedt.core import pyaedt_function_handler +from ansys.aedt.core import settings +from ansys.aedt.core.application.variables import decompose_variable_value +from ansys.aedt.core.generic.constants import unit_converter +from ansys.aedt.core.generic.general_methods import check_and_download_file +from ansys.aedt.core.generic.general_methods import open_file +from ansys.aedt.core.modeler.cad.elements_3d import FacePrimitive +from ansys.aedt.core.visualization.plot.pyvista import ModelPlotter +from ansys.aedt.core.visualization.post.common import PostProcessorCommon +from ansys.aedt.core.visualization.post.fields_calculator import FieldsCalculator + +try: + import numpy as np +except ImportError: + np = None + warnings.warn( + "The NumPy module is required to run some functionalities of PostProcess.\n" + "Install with \n\npip install numpy" + ) - if ( - isinstance(props.get("expressions", {}), list) - and props["expressions"] - and isinstance(props["expressions"][0], str) - ): # pragma: no cover - props["expressions"] = {i: {} for i in props["expressions"]} - elif isinstance(props.get("expressions", {}), str): # pragma: no cover - props["expressions"] = {props["expressions"]: {}} - _dict_items_to_list_items(props, "expressions") - if not solution_name: - solution_name = self._app.nominal_sweep - if props.get("report_category", None) and props["report_category"] in TEMPLATES_BY_NAME: - if ( - "AMIAnalysis" in self._app.get_setup(solution_name.split(":")[0].strip()).props - and props["report_category"] == "Standard" - ): - report_temp = TEMPLATES_BY_NAME["AMI Contour"] - elif "AMIAnalysis" in self._app.get_setup(solution_name.split(":")[0].strip()).props: - report_temp = TEMPLATES_BY_NAME["Statistical Eye"] - else: - report_temp = TEMPLATES_BY_NAME[props["report_category"]] - report = report_temp(self, props["report_category"], solution_name) - for k, v in props.items(): - report.props[k] = v - for el, k in self._app.available_variations.nominal_w_values_dict.items(): - if ( - report.props.get("context", None) - and report.props["context"].get("variations", None) - and el not in report.props["context"]["variations"] - ): - report.props["context"]["variations"][el] = k - report.expressions - report.create() - report._update_traces() - return report - return False # pragma: no cover +from ansys.aedt.core.visualization.post.field_data import FieldPlot +from ansys.aedt.core.visualization.post.vrt_data import VRTFieldPlot +from ansys.aedt.core.visualization.report.constants import ORIENTATION_TO_VIEW -class PostProcessor(PostProcessorCommon, object): +class PostProcessor3D(PostProcessorCommon): """Manages the main AEDT postprocessing functions. The inherited ``AEDTConfig`` class contains all ``_desktop`` @@ -2407,6 +95,7 @@ def __init__(self, app): self._post_osolution = self._app.osolution self.field_plots = self._get_fields_plot() PostProcessorCommon.__init__(self, app) + self.fields_calculator = FieldsCalculator(app) app.logger.info_timer("PostProcessor class has been initialized!") @property @@ -2452,7 +141,7 @@ def ofieldsreporter(self): Returns ------- - :attr:`ansys.aedt.core.modules.post_processor.PostProcessor.ofieldsreporter` + :attr:`ansys.aedt.core.modules.post_general.PostProcessor.ofieldsreporter` References ---------- @@ -2712,11 +401,12 @@ def get_scalar_field_value( Intrinsic variables required to compute the field before the export. These are typically: frequency, time and phase. It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed as: - - ``"Freq"`` or ``"Frequency"`` - - ``"Time"`` - - ``"Phase"`` - in lower or camel case. + + If it is a dictionary, keys depend on the solution type and can be expressed as: + - ``"Freq"`` or ``"Frequency"``. + - ``"Time"``. + - ``"Phase"``. + If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. phase : str, optional @@ -2883,11 +573,10 @@ def export_field_file_on_grid( Intrinsic variables required to compute the field before the export. These are typically: frequency, time and phase. It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed as: - - ``"Freq"`` or ``"Frequency"`` - - ``"Time"`` - - ``"Phase"`` - in lower or camel case. + If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: + - ``"Freq"`` or ``"Frequency"``. + - ``"Time"``. + - ``"Phase"``. If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. phase : str, optional @@ -3067,11 +756,10 @@ def export_field_file( Intrinsic variables required to compute the field before the export. These are typically: frequency, time and phase. It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed as: - - ``"Freq"`` or ``"Frequency"`` - - ``"Time"`` - - ``"Phase"`` - in lower or camel case. + If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: + - ``"Freq"`` or ``"Frequency"`` + - ``"Time"`` + - ``"Phase"`` If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. phase : str, optional @@ -3463,10 +1151,10 @@ def create_fieldplot_line( Intrinsic variables required to compute the field before the export. These are typically: frequency, time and phase. It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: - - ``"Freq"`` or ``"Frequency"``. - - ``"Time"``. - - ``"Phase"``. + If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: + - ``"Freq"`` or ``"Frequency"``. + - ``"Time"``. + - ``"Phase"``. If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. plot_name : str, optional @@ -3537,10 +1225,10 @@ def create_fieldplot_line_traces( Intrinsic variables required to compute the field before the export. These are typically: frequency, time and phase. It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: - - ``"Freq"`` or ``"Frequency"``. - - ``"Time"``. - - ``"Phase"``. + If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: + - ``"Freq"`` or ``"Frequency"``. + - ``"Time"``. + - ``"Phase"``. If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. plot_name : str, optional @@ -3718,10 +1406,12 @@ def create_fieldplot_layers( Intrinsic variables required to compute the field before the export. These are typically: frequency, time and phase. It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: - - ``"Freq"`` or ``"Frequency"``. - - ``"Time"``. - - ``"Phase"``. + If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: + + - ``"Freq"`` or ``"Frequency"``. + - ``"Time"``. + - ``"Phase"``. + If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. name : str, optional @@ -3809,10 +1499,12 @@ def create_fieldplot_layers_nets( Intrinsic variables required to compute the field before the export. These are typically: frequency, time and phase. It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: - - ``"Freq"`` or ``"Frequency"``. - - ``"Time"``. - - ``"Phase"``. + If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: + + - ``"Freq"`` or ``"Frequency"``. + - ``"Time"``. + - ``"Phase"``. + If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. plot_on_surface : bool, optional @@ -3896,11 +1588,10 @@ def create_fieldplot_surface( Intrinsic variables required to compute the field before the export. These are typically: frequency, time and phase. It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed as: - - ``"Freq"`` or ``"Frequency"`` - - ``"Time"`` - - ``"Phase"`` - in lower or camel case. + If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: + - ``"Freq"`` or ``"Frequency"``. + - ``"Time"``. + - ``"Phase"``. If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. plot_name : str, optional @@ -3963,10 +1654,12 @@ def create_fieldplot_cutplane( Intrinsic variables required to compute the field before the export. These are typically: frequency, time and phase. It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: - - ``"Freq"`` or ``"Frequency"``. - - ``"Time"``. - - ``"Phase"``. + If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: + + - ``"Freq"`` or ``"Frequency"``. + - ``"Time"``. + - ``"Phase"``. + If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. plot_name : str, optional @@ -4046,10 +1739,10 @@ def create_fieldplot_volume( Intrinsic variables required to compute the field before the export. These are typically: frequency, time and phase. It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: - - ``"Freq"`` or ``"Frequency"``. - - ``"Time"``. - - ``"Phase"``. + If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: + - ``"Freq"`` or ``"Frequency"``. + - ``"Time"``. + - ``"Phase"``. If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. plot_name : str, optional @@ -4150,7 +1843,7 @@ def export_field_jpg( (float(bound[1]) + float(bound[4])) / 2, (float(bound[2]) + float(bound[5])) / 2, ] - view = orientation_to_view.get(orientation, "iso") + view = ORIENTATION_TO_VIEW.get(orientation, "iso") cs = self.modeler.create_coordinate_system(origin=center, mode="view", view=view) self.ofieldsreporter.ExportPlotImageToFile(file_name, folder_name, plot_name, cs.name) cs.delete() @@ -4451,10 +2144,10 @@ def export_mesh_obj(self, setup=None, intrinsics=None, export_air_objects=False, Intrinsic variables required to compute the field before the export. These are typically: frequency, time and phase. It can be provided either as a dictionary or as a string. - If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: - - ``"Freq"`` or ``"Frequency"``. - - ``"Time"``. - - ``"Phase"``. + If it is a dictionary, keys depend on the solution type and can be expressed in lower or camel case as: + - ``"Freq"`` or ``"Frequency"``. + - ``"Time"``. + - ``"Phase"``. If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. export_air_objects : bool, optional @@ -5080,7 +2773,7 @@ def create_sbr_point_visual_ray_tracing( shoot_utd : bool, optional Whether if enable or UTD Rays shooting or not. Default is ``False``. custom_location : list, optional - List of x, y,z position of point source. Default is ``None`. + List of x, y,z position of point source. Default is ``None``. shoot_filter_type : str, optional Shooter Type. Default is ``"All Rays"``. Options are ``Rays by index``, ``Rays in box``. ray_index_start : int, optional @@ -5095,6 +2788,7 @@ def create_sbr_point_visual_ray_tracing( Returns ------- :class:` from ansys.aedt.core.modules.solutions.VRTFieldPlot` + """ if custom_location is None: custom_location = [0, 0, 0] @@ -5136,776 +2830,906 @@ def set_tuning_offset(self, setup, offsets): self.logger.error("Tuning offset applies only to solved setup with derivatives enabled.") return False + @pyaedt_function_handler() + def nb_display(self, show_axis=True, show_grid=True, show_ruler=True): + """Show the Jupyter Notebook display. -class CircuitPostProcessor(PostProcessorCommon, object): - """Manages the main AEDT Nexxim postprocessing functions. + .. 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``. - .. note:: - Some functionalities are available only when AEDT is running in the graphical mode. + Returns + ------- + :class:`IPython.core.display.Image` + Jupyter notebook image. - Parameters - ---------- - app : :class:`ansys.aedt.core.application.analysis_nexxim.FieldAnalysisCircuit` - Inherited parent object. The parent object must provide the members - `_modeler`, `_desktop`, `_odesign`, and `logger`. + """ + try: + from IPython.display import Image + + ipython_available = True + except ImportError: + ipython_available = False + Image = None + if ipython_available: + file_name = self.export_model_picture(show_axis=show_axis, show_grid=show_grid, show_ruler=show_ruler) + return Image(file_name, width=500) + else: + warnings.warn("The Ipython package is missing and must be installed.") - """ + @pyaedt_function_handler() + def get_efields_data(self, setup_sweep_name="", ff_setup="Infinite Sphere1", freq="All"): + """Compute Etheta and EPhi. - def __init__(self, app): - PostProcessorCommon.__init__(self, app) + .. warning:: + This method requires NumPy to be installed on your machine. - @pyaedt_function_handler(setupname="setup", plotname="plot_name") - def create_ami_initial_response_plot( - self, - setup, - ami_name, - variation_list_w_value, - plot_type="Rectangular Plot", - plot_initial_response=True, - plot_intermediate_response=False, - plot_final_response=False, - plot_name=None, - ): - """Create an AMI initial response plot. Parameters ---------- - setup : str - Name of the setup. - ami_name : str - AMI probe name to use. - variation_list_w_value : list - List of variations with relative values. - plot_type : str - String containing the report type. Default is ``"Rectangular Plot"``. It can be ``"Data Table"``, - ``"Rectangular Stacked Plot"``or any of the other valid AEDT Report types. - The default is ``"Rectangular Plot"``. - plot_initial_response : bool, optional - Set either to plot the initial input response. Default is ``True``. - plot_intermediate_response : bool, optional - Set whether to plot the intermediate input response. Default is ``False``. - plot_final_response : bool, optional - Set whether to plot the final input response. Default is ``False``. - plot_name : str, optional - Plot name. The default is ``None``, in which case - a unique name is automatically assigned. + 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 ------- - str - Name of the plot. + np.ndarray + Numpy array containing ``[theta_range, phi_range, Etheta, Ephi]``. """ - if not plot_name: - plot_name = generate_unique_name("AMIAnalysis") - variations = ["__InitialTime:=", ["All"]] - i = 0 - for a in variation_list_w_value: - if (i % 2) == 0: - if ":=" in a: - variations.append(a) - else: - variations.append(a + ":=") - else: - if isinstance(a, list): - variations.append(a) + 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: - variations.append([a]) - i += 1 - ycomponents = [] - if plot_initial_response: - ycomponents.append("InitialImpulseResponse<{}.int_ami_rx>".format(ami_name)) - if plot_intermediate_response: - ycomponents.append("IntermediateImpulseResponse<{}.int_ami_rx>".format(ami_name)) - if plot_final_response: - ycomponents.append("FinalImpulseResponse<{}.int_ami_rx>".format(ami_name)) - self.oreportsetup.CreateReport( - plot_name, - "Standard", - plot_type, - setup, - [ - "NAME:Context", - "SimValueContext:=", - [ - 55824, - 0, - 2, - 0, - False, - False, - -1, - 1, - 0, - 1, - 1, - "", - 0, - 0, - "NUMLEVELS", - False, - "1", - "PCID", - False, - "-1", - "PID", - False, - "1", - "SCID", - False, - "-1", - "SID", - False, - "0", - ], - ], - variations, - ["X Component:=", "__InitialTime", "Y Component:=", ycomponents], - ) - return plot_name + 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) + + trace_name = "rETheta" + solnData = self.get_far_field_data( + expressions=trace_name, setup_sweep_name=setup_sweep_name, domain=ff_setup + ) + + 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( + expressions=trace_name, setup_sweep_name=setup_sweep_name, domain=ff_setup + ) + data = solnData.nominal_variation + + real_phi = np.array(data.GetRealDataValues(trace_name)) + imag_phi = np.array(data.GetImagDataValues(trace_name)) - @pyaedt_function_handler(setupname="setup", plotname="plot_name") - def create_ami_statistical_eye_plot( - self, setup, ami_name, variation_list_w_value, ami_plot_type="InitialEye", plot_name=None + 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 + + @pyaedt_function_handler() + def get_model_plotter_geometries( + self, + objects=None, + plot_as_separate_objects=True, + plot_air_objects=False, + force_opacity_value=None, + array_coordinates=None, + generate_mesh=True, + get_objects_from_aedt=True, ): - """Create an AMI statistical eye plot. + """Initialize the Model Plotter object with actual modeler objects and return it. Parameters ---------- - setup : str - Name of the setup. - ami_name : str - AMI probe name to use. - variation_list_w_value : list - Variations with relative values. - ami_plot_type : str, optional - String containing the report AMI type. The default is ``"InitialEye"``. - Options are ``"EyeAfterChannel"``, ``"EyeAfterProbe"````"EyeAfterSource"``, - and ``"InitialEye"``.. - plot_name : str, optional - Plot name. The default is ``None``, in which case - a unique name starting with ``"Plot"`` is automatically assigned. + objects : list, optional + Optional list of objects to plot. If `None` all objects will be exported. + 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. + array_coordinates : list of list + List of array element centers. The modeler objects will be duplicated and translated. + List of [[x1,y1,z1], [x2,y2,z2]...]. + generate_mesh : bool, optional + Whether to generate the mesh after importing objects. The default is ``True``. + get_objects_from_aedt : bool, optional + Whether to export objects from AEDT and initialize them. The default is ``True``. Returns ------- - str - The name of the plot. + :class:`ansys.aedt.core.generic.plot.ModelPlotter` + Model Object. + """ - References - ---------- + if self._app._aedt_version < "2021.2": + raise RuntimeError("Object is supported from AEDT 2021 R2.") # pragma: no cover - >>> oModule.CreateReport - """ - if not plot_name: - plot_name = generate_unique_name("AMYAanalysis") - variations = [ - "__UnitInterval:=", - ["All"], - "__Amplitude:=", - ["All"], - ] - i = 0 - for a in variation_list_w_value: - if (i % 2) == 0: - if ":=" in a: - variations.append(a) - else: - variations.append(a + ":=") + files = [] + if get_objects_from_aedt and self._app.solution_type not in ["HFSS3DLayout", "HFSS 3D Layout Design"]: + files = self.export_model_obj( + assignment=objects, export_as_single_objects=plot_as_separate_objects, air_objects=plot_air_objects + ) + + model = ModelPlotter() + model.off_screen = True + units = self.modeler.model_units + for file in files: + if force_opacity_value: + model.add_object(file[0], file[1], force_opacity_value, units) else: - if isinstance(a, list): - variations.append(a) - else: - variations.append([a]) - i += 1 - ycomponents = [] - if ami_plot_type == "InitialEye" or ami_plot_type == "EyeAfterSource": - ibs_type = "tx" - else: - ibs_type = "rx" - ycomponents.append("{}<{}.int_ami_{}>".format(ami_plot_type, ami_name, ibs_type)) - - ami_id = "0" - if ami_plot_type == "EyeAfterSource": - ami_id = "1" - elif ami_plot_type == "EyeAfterChannel": - ami_id = "2" - elif ami_plot_type == "EyeAfterProbe": - ami_id = "3" - self.oreportsetup.CreateReport( - plot_name, - "Statistical Eye", - "Statistical Eye Plot", - setup, - [ - "NAME:Context", - "SimValueContext:=", - [ - 55819, - 0, - 2, - 0, - False, - False, - -1, - 1, - 0, - 1, - 1, - "", - 0, - 0, - "NUMLEVELS", - False, - "1", - "QTID", - False, - ami_id, - "SCID", - False, - "-1", - "SID", - False, - "0", - ], - ], - variations, - ["X Component:=", "__UnitInterval", "Y Component:=", "__Amplitude", "Eye Diagram Component:=", ycomponents], - ) - return plot_name + model.add_object(file[0], file[1], file[2], units) + model.array_coordinates = array_coordinates + if generate_mesh: + model.generate_geometry_mesh() + return model - @pyaedt_function_handler(setupname="setup", plotname="plot_name") - def create_statistical_eye_plot(self, setup, probe_names, variation_list_w_value, plot_name=None): - """Create a statistical QuickEye, VerifEye, and/or Statistical Eye plot. + @pyaedt_function_handler() + def plot_model_obj( + self, + objects=None, + show=True, + export_path=None, + plot_as_separate_objects=True, + plot_air_objects=False, + force_opacity_value=None, + clean_files=False, + array_coordinates=None, + view="isometric", + show_legend=True, + dark_mode=False, + show_bounding=False, + show_grid=False, + ): + """Plot the model or a substet of objects. Parameters ---------- - setup : str - Name of the setup. - probe_names : str or list - One or more names of the probes to plot in the eye diagram. - variation_list_w_value : list - List of variations with relative values. - plot_name : str, optional - Plot name. The default is ``None``, in which case a name is automatically assigned. + 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. + array_coordinates : list of list + List of array element centers. The modeler objects will be duplicated and translated. + List of [[x1,y1,z1], [x2,y2,z2]...]. + view : str, optional + View to export. Options are ``"isometric"``, ``"xy"``, ``"xz"``, ``"yz"``. + The default is ``"isometric"``. + show_legend : bool, optional + Whether to display the legend or not. The default is ``True``. + dark_mode : bool, optional + Whether to display the model in dark mode or not. The default is ``False``. + show_grid : bool, optional + Whether to display the axes grid or not. The default is ``False``. + show_bounding : bool, optional + Whether to display the axes bounding box or not. The default is ``False``. Returns ------- - str - The name of the plot. - - References - ---------- - - >>> oModule.CreateReport + :class:`ansys.aedt.core.generic.plot.ModelPlotter` + Model Object. """ - if not plot_name: - plot_name = generate_unique_name("AMIAanalysis") - variations = [ - "__UnitInterval:=", - ["All"], - "__Amplitude:=", - ["All"], - ] - i = 0 - for a in variation_list_w_value: - if (i % 2) == 0: - if ":=" in a: - variations.append(a) - else: - variations.append(a + ":=") - else: - if isinstance(a, list): - variations.append(a) - else: - variations.append([a]) - i += 1 - if isinstance(probe_names, list): - ycomponents = probe_names - else: - ycomponents = [probe_names] - - self.oreportsetup.CreateReport( - plot_name, - "Statistical Eye", - "Statistical Eye Plot", - setup, - [ - "NAME:Context", - "SimValueContext:=", - [ - 55819, - 0, - 2, - 0, - False, - False, - -1, - 1, - 0, - 1, - 1, - "", - 0, - 0, - "NUMLEVELS", - False, - "1", - "QTID", - False, - "1", - "SCID", - False, - "-1", - "SID", - False, - "0", - ], - ], - variations, - ["X Component:=", "__UnitInterval", "Y Component:=", "__Amplitude", "Eye Diagram Component:=", ycomponents], + model = self.get_model_plotter_geometries( + objects=objects, + plot_as_separate_objects=plot_as_separate_objects, + plot_air_objects=plot_air_objects, + force_opacity_value=force_opacity_value, + array_coordinates=array_coordinates, + generate_mesh=False, ) - return plot_name - @pyaedt_function_handler() - def sample_waveform( + model.show_legend = show_legend + model.off_screen = not show + if dark_mode: + model.background_color = [40, 40, 40] + model.bounding_box = show_bounding + model.show_grid = show_grid + if view != "isometric" and view in ["xy", "xz", "yz"]: + model.camera_position = view + elif view != "isometric": + self.logger.warning("Wrong view setup. It has to be one of xy, xz, yz, isometric.") + if export_path: + model.plot(export_path) + elif show: + model.plot() + if clean_files: + model.clean_cache_and_files(clean_cache=False) + return model + + @pyaedt_function_handler(plotname="plot_name", meshplot="mesh_plot", imageformat="image_format") + def plot_field_from_fieldplot( self, - waveform_data, - waveform_sweep, - waveform_unit="V", - waveform_sweep_unit="s", - unit_interval=1e-9, - clock_tics=None, - pandas_enabled=False, + plot_name, + project_path="", + mesh_plot=False, + image_format="jpg", + view="isometric", + plot_label="Temperature", + plot_folder=None, + show=True, + scale_min=None, + scale_max=None, + plot_cad_objs=True, + log_scale=True, + dark_mode=False, + show_grid=False, + show_bounding=False, + show_legend=True, + plot_as_separate_objects=True, + file_format="case", ): - """Sampling a waveform at clock times plus half unit interval. + """Export a field plot to an image file (JPG or PNG) using Python PyVista. + + This method does not support streamlines plot. + + .. note:: + The PyVista module rebuilds the mesh and the overlap fields on the mesh. Parameters ---------- - waveform_data : list - Waveform data. - waveform_sweep : list - Waveform sweep data. - waveform_unit : str, optional - Waveform units. The default values is ``V``. - waveform_sweep_unit : str, optional - Time units. The default value is ``s``. - unit_interval : float, optional - Unit interval in seconds. The default is ``1e-9``. - clock_tics : list, optional - List with clock tics. The default is ``None``, in which case the clock tics from - the AMI receiver are used. - pandas_enabled : bool, optional - Whether to enable the Pandas data format. The default is ``False``. + plot_name : str + Name of the field plot to export. + project_path : str, optional + Path for saving the image file. The default is ``""``. + mesh_plot : bool, optional + Whether to create and plot the mesh over the fields. The default is ``False``. + image_format : 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 ``"isometric"``, ``"xy"``, ``"xz"``, ``"yz"``. + plot_label : str, optional + Type of the plot. The default is ``"Temperature"``. + plot_folder : str, optional + Plot folder to update before exporting the field. + The default is ``None``, in which case all plot folders are updated. + show : bool, optional + Export Image without plotting on UI. + scale_min : float, optional + Fix the Scale Minimum value. + scale_max : float, optional + Fix the Scale Maximum value. + plot_cad_objs : bool, optional + Whether to include objects in the plot. The default is ``True``. + log_scale : bool, optional + Whether to plot fields in log scale. The default is ``True``. + dark_mode : bool, optional + Whether to display the model in dark mode or not. The default is ``False``. + show_grid : bool, optional + Whether to display the axes grid or not. The default is ``False``. + show_bounding : bool, optional + Whether to display the axes bounding box or not. The default is ``False``. + show_legend : bool, optional + Whether to display the legend. The default is ``True``. + plot_as_separate_objects : bool, optional + Whether to plot each object separately, which can require + more time to export from AEDT. The default is ``True``. + file_format : str, optional + File format to export the plot to. The default is ``"case". + Options are ``"aedtplt"`` and ``"case"``. + If the active design is a Q3D design, the file format is automatically + set to ``"fldplt"``. Returns ------- - list or :class:`pandas.Series` - Sampled waveform in ``Volts`` at different times in ``seconds``. - - Examples - -------- - >>> from ansys.aedt.core import Circuit - >>> circuit = Circuit() - >>> circuit.post.sample_ami_waveform(name,probe_name,source_name,circuit.available_variations.nominal) - + :class:`ansys.aedt.core.generic.plot.ModelPlotter` + Model Object. """ - - new_tic = [] - for tic in clock_tics: - new_tic.append(unit_converter(tic, unit_system="Time", input_units="s", output_units=waveform_sweep_unit)) - new_ui = unit_converter(unit_interval, unit_system="Time", input_units="s", output_units=waveform_sweep_unit) - - zipped_lists = zip(new_tic, [new_ui / 2] * len(new_tic)) - extraction_tic = [x + y for (x, y) in zipped_lists] - - if pandas_enabled: - sweep_filtered = waveform_sweep.values - filtered_tic = list(filter(lambda num: num >= waveform_sweep.values[0], extraction_tic)) + is_pcb = False + if self._app.solution_type in ["HFSS3DLayout", "HFSS 3D Layout Design"]: + is_pcb = True + if not plot_folder: + self.ofieldsreporter.UpdateAllFieldsPlots() else: - sweep_filtered = waveform_sweep - filtered_tic = list(filter(lambda num: num >= waveform_sweep[0], extraction_tic)) - - outputdata = [] - new_voltage = [] - tic_in_s = [] - for tic in filtered_tic: - if tic >= sweep_filtered[0]: - sweep_filtered = list(filter(lambda num: num >= tic, sweep_filtered)) - if sweep_filtered: - if pandas_enabled: - waveform_index = waveform_sweep[waveform_sweep.values == sweep_filtered[0]].index.values - else: - waveform_index = waveform_sweep.index(sweep_filtered[0]) - if not isinstance(waveform_data[waveform_index], float): - voltage = waveform_data[waveform_index].values[0] - else: - voltage = waveform_data[waveform_index] - new_voltage.append( - unit_converter(voltage, unit_system="Voltage", input_units=waveform_unit, output_units="V") - ) - tic_in_s.append( - unit_converter(tic, unit_system="Time", input_units=waveform_sweep_unit, output_units="s") - ) - if not pandas_enabled: - outputdata.append([tic_in_s[-1:][0], new_voltage[-1:][0]]) - del sweep_filtered[0] - else: - break - if pandas_enabled: - return pd.Series(new_voltage, index=tic_in_s) - return outputdata + self.ofieldsreporter.UpdateQuantityFieldsPlots(plot_folder) - @pyaedt_function_handler(setupname="setup", probe_name="probe", source_name="source") - def sample_ami_waveform( + file_to_add = self.export_field_plot(plot_name, self._app.working_directory, file_format=file_format) + model = self.get_model_plotter_geometries( + generate_mesh=False, + get_objects_from_aedt=plot_cad_objs, + plot_as_separate_objects=plot_as_separate_objects, + ) + model.show_legend = show_legend + model.off_screen = not show + if dark_mode: + model.background_color = [40, 40, 40] + model.bounding_box = show_bounding + model.show_grid = show_grid + if file_to_add: + model.add_field_from_file( + file_to_add, + coordinate_units=self.modeler.model_units, + show_edges=mesh_plot, + log_scale=log_scale, + ) + if plot_label: + model.fields[0].label = plot_label + + if view != "isometric" and view in ["xy", "xz", "yz"]: + model.camera_position = view + elif view != "isometric": + self.logger.warning("Wrong view setup. It has to be one of xy, xz, yz, isometric.") + if is_pcb: + model.z_scale = 5 + + if scale_min is not None and scale_max is None or scale_min is None and scale_max is not None: + self.logger.warning("Invalid scale values: both values must be None or different from None.") + elif scale_min is not None and scale_max is not None and not 0 <= scale_min < scale_max: + self.logger.warning("Invalid scale values: scale_min must be greater than zero and less than scale_max.") + elif log_scale and scale_min == 0: + self.logger.warning("Invalid scale minimum value for logarithm scale.") + else: + model.range_min = scale_min + model.range_max = scale_max + if project_path: + model.plot(os.path.join(project_path, plot_name + "." + image_format)) + elif show: + model.plot() + return model + + @pyaedt_function_handler(object_list="assignment", imageformat="image_format", setup_name="setup") + def plot_field( self, - setup, - probe, - source, - variation_list_w_value, - unit_interval=1e-9, - ignore_bits=0, - plot_type=None, - clock_tics=None, + quantity, + assignment, + plot_type="Surface", + setup=None, + intrinsics=None, + mesh_on_fields=False, + view="isometric", + plot_label=None, + show=True, + scale_min=None, + scale_max=None, + plot_cad_objs=True, + log_scale=False, + export_path="", + image_format="jpg", + keep_plot_after_generation=False, + dark_mode=False, + show_bounding=False, + show_grid=False, + show_legend=True, + filter_objects=None, + plot_as_separate_objects=True, ): - """Sampling a waveform at clock times plus half unit interval. + """Create a field plot using Python PyVista and export to an image file (JPG or PNG). + + .. note:: + The PyVista module rebuilds the mesh and the overlap fields on the mesh. Parameters ---------- - setup : str - Name of the setup. - probe : str - Name of the AMI probe. - source : str - Name of the AMI source. - variation_list_w_value : list - Variations with relative values. - unit_interval : float, optional - Unit interval in seconds. The default is ``1e-9``. - ignore_bits : int, optional - Number of initial bits to ignore. The default is ``0``. - plot_type : str, optional - Report type. The default is ``None``, in which case all report types are generated. - Options for a specific report type are ``"InitialWave"``, ``"WaveAfterSource"``, - ``"WaveAfterChannel"``, and ``"WaveAfterProbe"``. - clock_tics : list, optional - List with clock tics. The default is ``None``, in which case the clock tics from - the AMI receiver are used. + quantity : str + Quantity to plot. For example, ``"Mag_E"``. + assignment : str, list + One or more objects or faces to apply the field plot to. + plot_type : str, optional + Plot type. The default is ``Surface``. Options are + ``"CutPlane"``, ``"Surface"``, and ``"Volume"``. + setup : str, optional + Setup and sweep name on which create the field plot. Default is None for nominal setup usage. + intrinsics : dict, str, optional + Intrinsic variables required to compute the field before the export. + These are typically: frequency, time and phase. + It can be provided either as a dictionary or as a string. + If it is a dictionary, keys depend on the solution type and can be expressed as: - Returns - ------- - list - Sampled waveform in ``Volts`` at different times in ``seconds``. + - ``"Freq"`` or ``"Frequency"``. + - ``"Time"``. + - ``"Phase"``. - Examples - -------- - >>> circuit = Circuit() - >>> circuit.post.sample_ami_waveform(setupname,probe_name,source_name,circuit.available_variations.nominal) + If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. + The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. + mesh_on_fields : bool, optional + Whether to create and plot the mesh over the fields. The + default is ``False``. + view : str, optional + View to export. Options are ``"isometric"``, ``"xy"``, ``"xz"``, ``"yz"``. + plot_label : str, optional + Type of the plot. The default is ``"Temperature"``. + show : bool, optional + Export Image without plotting on UI. + scale_min : float, optional + Fix the Scale Minimum value. + scale_max : float, optional + Fix the Scale Maximum value. + plot_cad_objs : bool, optional + Whether to include objects in the plot. The default is ``True``. + log_scale : bool, optional + Whether to plot fields in log scale. The default is ``False``. + export_path : str, optional + Image export path. Default is ``None`` to not export the image. + image_format : str, optional + Format of the image file. Options are ``"jpg"``, ``"png"``, ``"svg"``, and ``"webp"``. + The default is ``"jpg"``. + keep_plot_after_generation : bool, optional + Either to keep the Field Plot in AEDT after the generation is completed. Default is ``False``. + dark_mode : bool, optional + Whether to display the model in dark mode or not. The default is ``False``. + show_grid : bool, optional + Whether to display the axes grid or not. The default is ``False``. + show_bounding : bool, optional + Whether to display the axes bounding box or not. The default is ``False``. + show_legend : bool, optional + Whether to display the legend or not. The default is ``True``. + filter_objects : list, optional + Objects list for filtering the ``CutPlane`` plots. + plot_as_separate_objects : bool, optional + Plot each object separately. It may require more time to export from AEDT. + Returns + ------- + :class:`ansys.aedt.core.generic.plot.ModelPlotter` + Model Object. """ - initial_solution_type = self.post_solution_type - self._app.solution_type = "NexximAMI" + intrinsics = self._app._check_intrinsics(intrinsics, setup=setup) + if filter_objects is None: + filter_objects = [] + if os.getenv("PYAEDT_DOC_GENERATION", "False").lower() in ("true", "1", "t"): # pragma: no cover + show = False + if not setup: + setup = self._app.existing_analysis_sweeps[0] - if plot_type == "InitialWave" or plot_type == "WaveAfterSource": - plot_expression = [plot_type + "<" + source + ".int_ami_tx>"] - elif plot_type == "WaveAfterChannel" or plot_type == "WaveAfterProbe": - plot_expression = [plot_type + "<" + probe + ".int_ami_rx>"] + # file_to_add = [] + if plot_type == "Surface": + plotf = self.create_fieldplot_surface(assignment, quantity, setup, intrinsics) + elif plot_type == "Volume": + plotf = self.create_fieldplot_volume(assignment, quantity, setup, intrinsics) else: - plot_expression = [ - "InitialWave<" + source + ".int_ami_tx>", - "WaveAfterSource<" + source + ".int_ami_tx>", - "WaveAfterChannel<" + probe + ".int_ami_rx>", - "WaveAfterProbe<" + probe + ".int_ami_rx>", - ] - waveform = [] - waveform_sweep = [] - waveform_unit = [] - waveform_sweep_unit = [] - for exp in plot_expression: - waveform_data = self.get_solution_data( - expressions=exp, setup_sweep_name=setup, domain="Time", variations=variation_list_w_value - ) - samples_per_bit = 0 - for sample in waveform_data.primary_sweep_values: - sample_seconds = unit_converter( - sample, unit_system="Time", input_units=waveform_data.units_sweeps["Time"], output_units="s" - ) - if sample_seconds > unit_interval: - samples_per_bit -= 1 - break - else: - samples_per_bit += 1 - if samples_per_bit * ignore_bits > len(waveform_data.data_real()): - self._app.solution_type = initial_solution_type - self.logger.warning("Ignored bits are greater than generated bits.") - return None - waveform.append(waveform_data.data_real()[samples_per_bit * ignore_bits :]) - waveform_sweep.append(waveform_data.primary_sweep_values[samples_per_bit * ignore_bits :]) - waveform_unit.append(waveform_data.units_data[exp]) - waveform_sweep_unit.append(waveform_data.units_sweeps["Time"]) - - tics = clock_tics - if not clock_tics: - clock_expression = "ClockTics<" + probe + ".int_ami_rx>" - clock_tic = self.get_solution_data( - expressions=clock_expression, - setup_sweep_name=setup, - domain="Clock Times", - variations=variation_list_w_value, + plotf = self.create_fieldplot_cutplane( + assignment, quantity, setup, intrinsics, filter_objects=filter_objects ) - tics = clock_tic.data_real() - - outputdata = [[] for i in range(len(waveform))] - for w in range(0, len(waveform)): - outputdata[w] = self.sample_waveform( - waveform_data=waveform[w], - waveform_sweep=waveform_sweep[w], - waveform_unit=waveform_unit[w], - waveform_sweep_unit=waveform_sweep_unit[w], - unit_interval=unit_interval, - clock_tics=tics, - pandas_enabled=waveform_data.enable_pandas_output, - ) - return outputdata - - -TOTAL_QUANTITIES = [ - "HeatFlowRate", - "RadiationFlow", - "ConductionHeatFlow", - "ConvectiveHeatFlow", - "MassFlowRate", - "VolumeFlowRate", - "SurfJouleHeatingDensity", -] -AVAILABLE_QUANTITIES = [ - "Temperature", - "SurfTemperature", - "HeatFlowRate", - "RadiationFlow", - "ConductionHeatFlow", - "ConvectiveHeatFlow", - "HeatTransCoeff", - "HeatFlux", - "RadiationFlux", - "Speed", - "Ux", - "Uy", - "Uz", - "SurfUx", - "SurfUy", - "SurfUz", - "Pressure", - "SurfPressure", - "MassFlowRate", - "VolumeFlowRate", - "MassFlux", - "ViscosityRatio", - "WallYPlus", - "TKE", - "Epsilon", - "Kx", - "Ky", - "Kz", - "SurfElectricPotential", - "ElectricPotential", - "SurfCurrentDensity", - "CurrentDensity", - "SurfCurrentDensityX", - "SurfCurrentDensityY", - "SurfCurrentDensityZ", - "CurrentDensityX", - "CurrentDensityY", - "CurrentDensityZ", - "SurfJouleHeatingDensity", - "JouleHeatingDensity", -] - - -class FieldSummary: - def __init__(self, app): - self._app = app - self.calculations = [] + # if plotf: + # file_to_add = self.export_field_plot(plotf.name, self._app.working_directory, plotf.name) + + model = self.plot_field_from_fieldplot( + plotf.name, + export_path, + mesh_on_fields, + image_format, + view, + plot_label if plot_label else quantity, + None, + show, + scale_min, + scale_max, + plot_cad_objs, + log_scale, + dark_mode=dark_mode, + show_grid=show_grid, + show_bounding=show_bounding, + show_legend=show_legend, + plot_as_separate_objects=plot_as_separate_objects, + ) + if not keep_plot_after_generation: + plotf.delete() + return model - @pyaedt_function_handler() - def add_calculation( + @pyaedt_function_handler(object_list="assignment", variation_list="variations", setup_name="setup") + def plot_animated_field( self, - entity, - geometry, - geometry_name, quantity, - normal="", - side="Default", - mesh="All", - ref_temperature="AmbientTemp", - time="0s", + assignment, + plot_type="Surface", + setup=None, + intrinsics=None, + variation_variable="Phi", + variations=None, + view="isometric", + show=True, + scale_min=None, + scale_max=None, + plot_cad_objs=True, + log_scale=True, + zoom=None, + export_gif=False, + export_path="", + force_opacity_value=0.1, + dark_mode=False, + show_grid=False, + show_bounding=False, + show_legend=True, + filter_objects=None, ): - """ - Add an entry in the field summary calculation requests. + """Create an animated field plot using Python PyVista and export to a gif file. + + .. note:: + The PyVista module rebuilds the mesh and the overlap fields on the mesh. Parameters ---------- - entity : str - Type of entity to perform the calculation on. Options are - ``"Boundary"``, ``"Monitor``", and ``"Object"``. - (``"Monitor"`` is available in AEDT 2024 R1 and later.) - geometry : str - Location to perform the calculation on. Options are - ``"Surface"`` and ``"Volume"``. - geometry_name : str or list of str - Objects to perform the calculation on. If a list is provided, - the calculation is performed on the combination of those - objects. quantity : str - Quantity to compute. - normal : list of floats - Coordinate values for direction relative to normal. The default is ``""``, - in which case the normal to the face is used. - side : str, optional - String containing which side of the face to use. The default is - ``"Default"``. Options are ``"Adjacent"``, ``"Combined"``, and - `"Default"``. - mesh : str, optional - Surface meshes to use. The default is ``"All"``. Options are ``"All"`` and - ``"Reduced"``. - ref_temperature : str, optional - Reference temperature to use in the calculation of the heat transfer - coefficient. The default is ``"AmbientTemp"``. - time : str, optional - Timestep to get the data from. Default is ``"0s"``. + Quantity to plot (for example, ``"Mag_E"``). + assignment : list, str + One or more objects or faces to apply the field plot to. + plot_type : str, optional + Plot type. The default is ``Surface``. Options are + ``"CutPlane"``, ``"Surface"``, and ``"Volume"``. + setup : str, optional + Setup and sweep name on which create the field plot. Default is None for nominal setup usage. + intrinsics : dict, str, optional + Intrinsic variables required to compute the field before the export. + These are typically: frequency, time and phase. + It can be provided either as a dictionary or as a string. + If it is a dictionary, keys depend on the solution type and can be expressed as: + - ``"Freq"`` or ``"Frequency"`` + - ``"Time"`` + - ``"Phase"`` + + If it is a string, it can either be ``"Freq"`` or ``"Time"`` depending on the solution type. + The default is ``None`` in which case the intrinsics value is automatically computed based on the setup. + variation_variable : str, optional + Variable to vary. The default is ``"Phi"``. + variations : list, optional + List of variation values with units. The default is ``["0deg"]``. + view : str, optional + View to export. Options are ``"isometric"``, ``"xy"``, ``"xz"``, ``"yz"``. + show : bool, optional + Export Image without plotting on UI. + scale_min : float, optional + Fix the Scale Minimum value. + scale_max : float, optional + Fix the Scale Maximum value. + plot_cad_objs : bool, optional + Whether to include objects in the plot. The default is ``True``. + log_scale : bool, optional + Whether to plot fields in log scale. The default is ``True``. + zoom : float, optional + Zoom factor. + export_gif : bool, optional + Whether to export an animated gif or not. The default is ``False``. + export_path : str, optional + Image export path. Default is ``None`` to not ``working_directory`` will be used to save the image. + 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. + dark_mode : bool, optional + Whether to display the model in dark mode or not. The default is ``False``. + show_grid : bool, optional + Whether to display the axes grid or not. The default is ``False``. + show_bounding : bool, optional + Whether to display the axes bounding box or not. The default is ``False``. + show_legend : bool, optional + Whether to display the legend or not. The default is ``True``. + filter_objects : list, optional + Objects list for filtering the ``CutPlane`` plots. + The default is ``None`` in which case an empty list is passed. Returns ------- - bool - ``True`` when successful, ``False`` when failed. + :class:`ansys.aedt.core.generic.plot.ModelPlotter` + Model Object. """ - if quantity not in AVAILABLE_QUANTITIES: - raise AttributeError( - "Quantity {} is not supported. Available quantities are:\n{}".format( - quantity, ", ".join(AVAILABLE_QUANTITIES) + intrinsics = self._app._check_intrinsics(intrinsics, setup=setup) + if variations is None: + variations = ["0deg"] + if os.getenv("PYAEDT_DOC_GENERATION", "False").lower() in ("true", "1", "t"): # pragma: no cover + show = False + if not export_path: + export_path = self._app.working_directory + if not filter_objects: + filter_objects = [] + + v = 0 + fields_to_add = [] + is_intrinsics = True + if variation_variable in self._app.variable_manager.independent_variables: + is_intrinsics = False + for el in variations: + if is_intrinsics: + intrinsics[variation_variable] = el + else: + self._app[variation_variable] = el + if plot_type == "Surface": + plotf = self.create_fieldplot_surface(assignment, quantity, setup, intrinsics) + elif plot_type == "Volume": + plotf = self.create_fieldplot_volume(assignment, quantity, setup, intrinsics) + else: + plotf = self.create_fieldplot_cutplane( + assignment, quantity, setup, intrinsics, filter_objects=filter_objects ) - ) - if isinstance(normal, list): - if not isinstance(normal[0], str): - normal = [str(i) for i in normal] - normal = ",".join(normal) - if isinstance(geometry_name, str): - geometry_name = [geometry_name] - calc_args = [ - entity, - geometry, - ",".join(geometry_name), - quantity, - normal, - side, - mesh, - ref_temperature, - False, - ] # TODO : last argument not documented - if self._app.solution_type == "Transient": - calc_args = [time] + calc_args - self.calculations.append(calc_args) - return True + if plotf: + file_to_add = self.export_field_plot(plotf.name, export_path, plotf.name + str(v)) + if file_to_add: + fields_to_add.append(file_to_add) + plotf.delete() + v += 1 + model = self.get_model_plotter_geometries( + generate_mesh=False, get_objects_from_aedt=plot_cad_objs, force_opacity_value=force_opacity_value + ) + model.off_screen = not show + if dark_mode: + model.background_color = [40, 40, 40] + model.bounding_box = show_bounding + model.show_grid = show_grid + model.show_legend = show_legend + if fields_to_add: + model.add_frames_from_file(fields_to_add, log_scale=log_scale) + if export_gif: + model.gif_file = os.path.join(self._app.working_directory, self._app.project_name + ".gif") + if view != "isometric" and view in ["xy", "xz", "yz"]: + model.camera_position = view + elif view != "isometric": + self.logger.warning("Wrong view setup. It has to be one of xy, xz, yz, isometric.") + + if scale_min and scale_max: + model.range_min = scale_min + model.range_max = scale_max + if zoom: + model.zoom = zoom + if show or export_gif: + model.animate() + return model + + @pyaedt_function_handler(plotname="plot_name", variation_list="variations") + def animate_fields_from_aedtplt( + self, + plot_name, + plot_folder=None, + variation_variable="Phase", + variations=["0deg"], + project_path="", + export_gif=False, + show=True, + dark_mode=False, + show_bounding=False, + show_grid=False, + ): + """Generate a field plot to an image file (JPG or PNG) using PyVista. - @pyaedt_function_handler(IntrinsincDict="intrinsics", setup_name="setup", design_variation="variation") - def get_field_summary_data(self, setup=None, variation=None, intrinsics="", pandas_output=False): - """ - Get field summary output computation. + .. note:: + The PyVista module rebuilds the mesh and the overlap fields on the mesh. Parameters ---------- - setup : str, optional - Setup name to use for the computation. The - default is ``None``, in which case the nominal variation is used. - variation : dict, optional - Dictionary containing the design variation to use for the computation. - The default is ``{}``, in which case nominal variation is used. - intrinsics : str, optional - Intrinsic values to use for the computation. The default is ``""``, - which is suitable when no frequency needs to be selected. - pandas_output : bool, optional - Whether to use pandas output. The default is ``False``, in - which case the dictionary output is used. + plot_name : str + Name of the plot or the name of the object. + plot_folder : str, optional + Name of the folder in which the plot resides. The default + is ``None``. + variation_variable : str, optional + Variable to vary. The default is ``"Phase"``. + variations : list, optional + List of variation values with units. The default is + ``["0deg"]``. + project_path : str, optional + Path for the export. The default is ``""``, in which case the file is exported + to the working directory. + export_gif : bool, optional + Whether to export the GIF file. The default is ``False``. + show : bool, optional + Generate the animation without showing an interactive plot. The default is ``True``. + dark_mode : bool, optional + Whether to display the model in dark mode or not. The default is ``False``. + show_grid : bool, optional + Whether to display the axes grid or not. The default is ``False``. + show_bounding : bool, optional + Whether to display the axes bounding box or not. The default is ``False``. Returns ------- - dict or pandas.DataFrame - Output type depending on the Boolean ``pandas_output`` parameter. - The output consists of information exported from the field summary. + :class:`ansys.aedt.core.generic.plot.ModelPlotter` + Model Object. """ - if variation is None: - variation = {} - with tempfile.NamedTemporaryFile(mode="w+", delete=False) as temp_file: - temp_file.close() - self.export_csv(temp_file.name, setup, variation, intrinsics) - with open_file(temp_file.name, "r") as f: - for _ in range(4): - _ = next(f) - reader = csv.DictReader(f) - out_dict = defaultdict(list) - for row in reader: - for key in row.keys(): - out_dict[key].append(row[key]) - os.remove(temp_file.name) - if pandas_output: - if pd is None: - raise ImportError("pandas package is needed.") - df = pd.DataFrame.from_dict(out_dict) - for col in ["Min", "Max", "Mean", "Stdev", "Total"]: - if col in df.columns: - df[col] = df[col].astype(float) - return df - return out_dict - - @pyaedt_function_handler(filename="output_file", design_variation="variations", setup_name="setup") - def export_csv(self, output_file, setup=None, variations=None, intrinsics=""): - """ - Get the field summary output computation. + if not plot_folder: + self.ofieldsreporter.UpdateAllFieldsPlots() + else: + self.ofieldsreporter.UpdateQuantityFieldsPlots(plot_folder) - Parameters - ---------- - output_file : str - Path and filename to write the output file to. - setup : str, optional - Setup name to use for the computation. The - default is ``None``, in which case the nominal variation is used. - variations : dict, optional - Dictionary containing the design variation to use for the computation. - The default is ``{}``, in which case the nominal variation is used. - intrinsics : str, optional - Intrinsic values to use for the computation. The default is ``""``, - which is suitable when no frequency needs to be selected. + fields_to_add = [] + if not project_path: + project_path = self._app.working_directory + for el in variations: + if plot_name in self.field_plots and variation_variable in self.field_plots[plot_name].intrinsics: + self.field_plots[plot_name].intrinsics[variation_variable] = el + self.field_plots[plot_name].update() + else: + self._app._odesign.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:FieldsPostProcessorTab", + ["NAME:PropServers", "FieldsReporter:" + plot_name], + ["NAME:ChangedProps", ["NAME:" + variation_variable, "Value:=", el]], + ], + ] + ) + fields_to_add.append( + self.export_field_plot( + plot_name, project_path, plot_name + variation_variable + str(el), file_format="case" + ) + ) + + model = self.get_model_plotter_geometries(generate_mesh=False) + model.off_screen = not show + if dark_mode: + model.background_color = [40, 40, 40] + model.bounding_box = show_bounding + model.show_grid = show_grid + if fields_to_add: + model.add_frames_from_file(fields_to_add) + if export_gif: + model.gif_file = os.path.join(self._app.working_directory, self._app.project_name + ".gif") + + if show or export_gif: + model.animate() + return model + + @pyaedt_function_handler() + def create_3d_plot( + self, + solution_data, + nominal_sweep=None, + nominal_value=None, + primary_sweep="Theta", + secondary_sweep="Phi", + snapshot_path=None, + show=True, + ): + """Create a 3D plot using Matplotlib. + + Parameters + ---------- + solution_data : :class:`ansys.aedt.core.modules.solutions.SolutionData` + Input data for the solution. + nominal_sweep : str, optional + Name of the nominal sweep. The default is ``None``. + nominal_value : str, optional + Value for the nominal sweep. The default is ``None``. + primary_sweep : str, optional + Primary sweep. The default is ``"Theta"``. + secondary_sweep : str, optional + Secondary sweep. The default is ``"Phi"``. + snapshot_path : str, optional + Full path to image file if a snapshot is needed. + The default is ``None``. + show : bool, optional + Whether if show the plot or not. Default is set to `True`. Returns ------- - bool - ``True`` when successful, ``False`` when failed. + bool + ``True`` when successful, ``False`` when failed. """ - if variations is None: - variations = {} - if not setup: - setup = self._app.nominal_sweep - dv_string = "" - for el in variations: - dv_string += el + "='" + variations[el] + "' " - self._create_field_summary(setup, dv_string) - self._app.osolution.ExportFieldsSummary( - [ - "SolutionName:=", - setup, - "DesignVariationKey:=", - dv_string, - "ExportFileName:=", - output_file, - "IntrinsicValue:=", - intrinsics, - ] + if nominal_value: + solution_data.intrinsics[nominal_sweep] = nominal_value + if nominal_value: + solution_data.primary_sweep = primary_sweep + return solution_data.plot_3d( + x_axis=primary_sweep, y_axis=secondary_sweep, snapshot_path=snapshot_path, show=show ) - return True - @pyaedt_function_handler() - def _create_field_summary(self, setup, variation): - arg = ["SolutionName:=", setup, "Variation:=", variation] - for i in self.calculations: - arg.append("Calculation:=") - arg.append(i) - self._app.osolution.EditFieldsSummarySetting(arg) + @pyaedt_function_handler(frames_list="frames", output_gif_path="gif_path") + def plot_scene( + self, + frames, + gif_path, + norm_index=0, + dy_rng=0, + fps=30, + show=True, + view="yz", + zoom=2.0, + convert_fields_in_db=False, + log_multiplier=10.0, + ): + """Plot the current model 3D scene with overlapping animation coming from a file list and save the gif. + + + Parameters + ---------- + frames : list or str + File list containing animation frames to plot in CSV format or + path to a text index file containing the full path to CSV files. + gif_path : str + Full path for outputting the GIF file. + norm_index : int, optional + Frame to use to normalize your images. + Data is already saved as dB : 100 for usual traffic scenes. + dy_rng : int, optional + Specify how many dB below you would like to specify the range_min. + Tweak this a couple of times with small number of frames. + fps : int, optional + Frames per Second. + show : bool, optional + Either if show or only export gif. + view : str, optional + View to export. Options are ``"isometric"``, ``"xy"``, ``"xz"``, and ``"yz"``. + The default is ``"isometric"``. + zoom : float, optional + Default zoom. Default Value is `2`. + convert_fields_in_db : bool, optional + Either if convert the fields before plotting in dB. Default Value is `False`. + log_multiplier : float, optional + Field multiplier if field in dB. Default Value is `10.0`. + + Returns + ------- + + """ + if isinstance(frames, str) and os.path.exists(frames): + with open_file(frames, "r") as f: + lines = f.read() + temp_list = lines.splitlines() + frames_paths_list = [i for i in temp_list] + elif isinstance(frames, str): + self.logger.error("Path doesn't exists") + return False + else: + frames_paths_list = frames + scene = self.get_model_plotter_geometries(generate_mesh=False) + + norm_data = np.loadtxt(frames_paths_list[norm_index], skiprows=1, delimiter=",") + norm_val = norm_data[:, -1] + v_max = np.max(norm_val) + v_min = v_max - dy_rng + scene.add_frames_from_file(frames_paths_list, log_scale=False, color_map="jet", header_lines=1, opacity=0.8) + + # Specifying the attributes of the scene through the ModelPlotter object + scene.off_screen = not show + if view != "isometric" and view in ["xy", "xz", "yz"]: + scene.camera_position = view + scene.range_min = v_min + scene.range_max = v_max + scene.show_grid = False + scene.windows_size = [1920, 1080] + scene.show_legend = False + scene.show_boundingbox = False + scene.legend = False + scene.frame_per_seconds = fps + scene.zoom = zoom + scene.bounding_box = False + scene.color_bar = False + scene.gif_file = gif_path # This GIF file may be a bit slower so it can be speed it up a bit + scene.convert_fields_in_db = convert_fields_in_db + scene.log_multiplier = log_multiplier + scene.animate() diff --git a/src/ansys/aedt/core/visualization/post/post_icepak.py b/src/ansys/aedt/core/visualization/post/post_icepak.py new file mode 100644 index 00000000000..3d722dfb44a --- /dev/null +++ b/src/ansys/aedt/core/visualization/post/post_icepak.py @@ -0,0 +1,407 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains the `PostProcessor` class. + +It contains all advanced postprocessing functionalities that require Python 3.x packages like NumPy and Matplotlib. +""" + +from __future__ import absolute_import # noreorder + +import csv +import os +import re + +from ansys.aedt.core import generate_unique_name +from ansys.aedt.core.generic.general_methods import open_file +from ansys.aedt.core.generic.general_methods import pyaedt_function_handler +from ansys.aedt.core.generic.settings import settings +from ansys.aedt.core.visualization.post.field_summary import FieldSummary +from ansys.aedt.core.visualization.post.field_summary import TOTAL_QUANTITIES +from ansys.aedt.core.visualization.post.post_common_3d import PostProcessor3D + + +class PostProcessorIcepak(PostProcessor3D): + """Manages the specific Icepak postprocessing functions. + + .. note:: + Some functionalities are available only when AEDT is running in the graphical mode. + + Parameters + ---------- + app : :class:`ansys.aedt.core.application.analysis_3d.FieldAnalysis3D` + Inherited parent object. The parent object must provide the members + `_modeler`, `_desktop`, `_odesign`, and `logger`. + + """ + + def __init__(self, app): + PostProcessor3D.__init__(self, app) + + @pyaedt_function_handler() + def create_field_summary(self): + """ + Create field summary object. + + Returns + ------- + :class:`ansys.aedt.core.visualization.post.field_summary.FieldSummary` + + """ + return FieldSummary(self._app) + + @pyaedt_function_handler(timestep="time_step", design_variation="variation") + def get_fans_operating_point(self, export_file=None, setup_name=None, time_step=None, variation=None): + """ + Get the operating point of the fans in the design. + + Parameters + ---------- + export_file : str, optional + Name of the file to save the operating point of the fans to. The default is + ``None``, in which case the filename is automatically generated. + setup_name : str, optional + Setup name to determine the operating point of the fans. The default is + ``None``, in which case the first available setup is used. + time_step : str, optional + Time, with units, at which to determine the operating point of the fans. The default + is ``None``, in which case the first available timestep is used. This parameter is + only relevant in transient simulations. + variation : str, optional + Design variation to determine the operating point of the fans from. The default is + ``None``, in which case the nominal variation is used. + + Returns + ------- + list + First element of the list is the CSV filename. The second and third elements + are the quantities with units describing the operating point of the fans. + The fourth element is a dictionary with the names of the fan instances + as keys and lists with volumetric flow rates and pressure rise floats associated + with the operating point as values. + + References + ---------- + + >>> oModule.ExportFanOperatingPoint + + Examples + -------- + >>> from ansys.aedt.core import Icepak + >>> ipk = Icepak() + >>> ipk.create_fan() + >>> filename, vol_flow_name, p_rise_name, op_dict= ipk.get_fans_operating_point() + """ + + if export_file is None: + path = self._app.temp_directory + base_name = "{}_{}_FanOpPoint".format(self._app.project_name, self._app.design_name) + export_file = os.path.join(path, base_name + ".csv") + while os.path.exists(export_file): + file_name = generate_unique_name(base_name) + export_file = os.path.join(path, file_name + ".csv") + if setup_name is None: + setup_name = "{} : {}".format(self._app.get_setups()[0], self._app.solution_type) + if time_step is None: + time_step = "" + if self._app.solution_type == "Transient": + self._app.logger.warning("No timestep is specified. First timestep is exported.") + else: + if not self._app.solution_type == "Transient": + self._app.logger.warning("Simulation is steady-state. Timestep argument is ignored.") + time_step = "" + if variation is None: + variation = "" + self._app.osolution.ExportFanOperatingPoint( + [ + "SolutionName:=", + setup_name, + "DesignVariationKey:=", + variation, + "ExportFilePath:=", + export_file, + "Overwrite:=", + True, + "TimeStep:=", + time_step, + ] + ) + with open_file(export_file, "r") as f: + reader = csv.reader(f) + for line in reader: + if "Fan Instances" in line: + vol_flow = line[1] + p_rise = line[2] + break + var = {line[0]: [float(line[1]), float(line[2])] for line in reader} + return [export_file, vol_flow, p_rise, var] + + @pyaedt_function_handler + def _parse_field_summary_content(self, fs, setup_name, design_variation, quantity_name): + content = fs.get_field_summary_data(setup=setup_name, variation=design_variation) + pattern = r"\[([^]]*)\]" + match = re.search(pattern, content["Quantity"][0]) + if match: + content["Unit"] = [match.group(1)] + else: # pragma: no cover + content["Unit"] = [None] + + if quantity_name in TOTAL_QUANTITIES: + return {i: content[i][0] for i in ["Total", "Unit"]} + return {i: content[i][0] for i in ["Min", "Max", "Mean", "Stdev", "Unit"]} + + @pyaedt_function_handler(faces_list="faces", quantity_name="quantity", design_variation="variation") + def evaluate_faces_quantity( + self, faces, quantity, side="Default", setup_name=None, variations=None, ref_temperature="", time="0s" + ): + """Export the field surface output. + + Parameters + ---------- + faces : list + List of faces to apply. + quantity : str + Name of the quantity to export. + side : str, optional + Which side of the mesh face to use. The default is ``Default``. + Options are ``"Adjacent"``, ``"Combined"``, and ``"Default"``. + setup_name : str, optional + Name of the setup and name of the sweep. For example, ``"IcepakSetup1 : SteatyState"``. + The default is ``None``, in which case the active setup and active sweep are used. + variations : dict, optional + Dictionary of parameters defined for the specific setup with values. The default is ``{}``. + ref_temperature : str, optional + Reference temperature to use for heat transfer coefficient computation. The default is ``""``. + time : str, optional + Timestep to get the data from. Default is ``"0s"``. + + Returns + ------- + dict + Output dictionary, which depending on the quantity chosen, contains one + of these sets of keys: + + - ``"Min"``, ``"Max"``, ``"Mean"``, ``"Stdev"``, and ``"Unit"`` + - ``"Total"`` and ``"Unit"`` + + References + ---------- + + >>> oModule.ExportFieldsSummary + """ + if variations is None: + variations = {} + facelist_name = generate_unique_name(quantity) + self._app.modeler.create_face_list(faces, facelist_name) + fs = self.create_field_summary() + fs.add_calculation( + "Object", "Surface", facelist_name, quantity, side=side, ref_temperature=ref_temperature, time=time + ) + out = self._parse_field_summary_content(fs, setup_name, variations, quantity) + self._app.oeditor.Delete(["NAME:Selections", "Selections:=", facelist_name]) + return out + + @pyaedt_function_handler(boundary_name="boundary", quantity_name="quantity", design_variation="variations") + def evaluate_boundary_quantity( + self, + boundary, + quantity, + side="Default", + volume=False, + setup_name=None, + variations=None, + ref_temperature="", + time="0s", + ): + """Export the field output on a boundary. + + Parameters + ---------- + boundary : str + Name of boundary to perform the computation on. + quantity : str + Name of the quantity to export. + side : str, optional + Side of the mesh face to use. The default is ``"Default"``. + Options are ``"Adjacent"``, ``"Combined"``, and ``"Default"``. + volume : bool, optional + Whether to compute the quantity on the volume or on the surface. + The default is ``False``, in which case the quantity will be evaluated + only on the surface . + setup_name : str, optional + Name of the setup and name of the sweep. For example, ``"IcepakSetup1 : SteatyState"``. + The default is ``None``, in which case the active setup and active sweep are used. + variations : dict, optional + Dictionary of parameters defined for the specific setup with values. The default is ``{}``. + ref_temperature : str, optional + Reference temperature to use for heat transfer coefficient computation. The default is ``""``. + time : str, optional + Timestep to get the data from. Default is ``"0s"``. + + Returns + ------- + dict + Output dictionary, which depending on the quantity chosen, contains one + of these sets of keys: + - ``"Min"``, ``"Max"``, ``"Mean"``, ``"Stdev"``, and ``"Unit"`` + - ``"Total"`` and ``"Unit"`` + + References + ---------- + + >>> oModule.ExportFieldsSummary + """ + if variations is None: + variations = {} + fs = self.create_field_summary() + fs.add_calculation( + "Boundary", + ["Surface", "Volume"][int(volume)], + boundary, + quantity, + side=side, + ref_temperature=ref_temperature, + time=time, + ) + return self._parse_field_summary_content(fs, setup_name, variations, quantity) + + @pyaedt_function_handler(monitor_name="monitor", quantity_name="quantity", design_variation="variations") + def evaluate_monitor_quantity( + self, monitor, quantity, side="Default", setup_name=None, variations=None, ref_temperature="", time="0s" + ): + """Export monitor field output. + + Parameters + ---------- + monitor : str + Name of monitor to perform the computation on. + quantity : str + Name of the quantity to export. + side : str, optional + Side of the mesh face to use. The default is ``"Default"``. + Options are ``"Adjacent"``, ``"Combined"``, and ``"Default"``. + setup_name : str, optional + Name of the setup and name of the sweep. For example, ``"IcepakSetup1 : SteatyState"``. + The default is ``None``, in which case the active setup and active sweep are used. + variations : dict, optional + Dictionary of parameters defined for the specific setup with values. The default is ``{}``. + ref_temperature : str, optional + Reference temperature to use for heat transfer coefficient computation. The default is ``""``. + time : str, optional + Timestep to get the data from. Default is ``"0s"``. + + Returns + ------- + dict + Output dictionary, which depending on the quantity chosen, contains one + of these sets of keys: + + - ``"Min"``, ``"Max"``, ``"Mean"``, ``"Stdev"``, and ``"Unit"`` + - ``"Total"`` and ``"Unit"`` + + References + ---------- + + >>> oModule.ExportFieldsSummary + """ + if variations is None: + variations = {} + if settings.aedt_version < "2024.1": + raise NotImplementedError("Monitors are not supported in field summary in versions earlier than 2024 R1.") + else: # pragma: no cover + if self._app.monitor.face_monitors.get(monitor, None): + field_type = "Surface" + elif self._app.monitor.point_monitors.get(monitor, None): + field_type = "Volume" + else: + raise AttributeError("Monitor {} is not found in the design.".format(monitor)) + fs = self.create_field_summary() + fs.add_calculation( + "Monitor", field_type, monitor, quantity, side=side, ref_temperature=ref_temperature, time=time + ) + return self._parse_field_summary_content(fs, setup_name, variations, quantity) + + @pyaedt_function_handler(design_variation="variations") + def evaluate_object_quantity( + self, + object_name, + quantity_name, + side="Default", + volume=False, + setup_name=None, + variations=None, + ref_temperature="", + time="0s", + ): + """Export the field output on or in an object. + + Parameters + ---------- + object_name : str + Name of object to perform the computation on. + quantity_name : str + Name of the quantity to export. + side : str, optional + Side of the mesh face to use. The default is ``"Default"``. + Options are ``"Adjacent"``, ``"Combined"``, and ``"Default"``. + volume : bool, optional + Whether to compute the quantity on the volume or on the surface. The default is ``False``. + setup_name : str, optional + Name of the setup and name of the sweep. For example, ``"IcepakSetup1 : SteatyState"``. + The default is ``None``, in which case the active setup and active sweep are used. + variations : dict, optional + Dictionary of parameters defined for the specific setup with values. The default is ``{}``. + ref_temperature : str, optional + Reference temperature to use for heat transfer coefficient computation. The default is ``""``. + time : str, optional + Timestep to get the data from. Default is ``"0s"``. + + Returns + ------- + dict + Output dictionary, which depending on the quantity chosen, contains one + of these sets of keys: + + - ``"Min"``, ``"Max"``, ``"Mean"``, ``"Stdev"``, and ``"Unit"`` + - ``"Total"`` and ``"Unit"`` + + References + ---------- + + >>> oModule.ExportFieldsSummary + """ + if variations is None: + variations = {} + fs = self.create_field_summary() + fs.add_calculation( + "Object", + ["Surface", "Volume"][int(volume)], + object_name, + quantity_name, + side=side, + ref_temperature=ref_temperature, + time=time, + ) + return self._parse_field_summary_content(fs, setup_name, variations, quantity_name) diff --git a/src/ansys/aedt/core/visualization/post/solution_data.py b/src/ansys/aedt/core/visualization/post/solution_data.py new file mode 100644 index 00000000000..3443fe3885a --- /dev/null +++ b/src/ansys/aedt/core/visualization/post/solution_data.py @@ -0,0 +1,1106 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import itertools +import math +import os +import warnings + +from ansys.aedt.core.generic.constants import AEDT_UNITS +from ansys.aedt.core.generic.constants import db10 +from ansys.aedt.core.generic.constants import db20 +from ansys.aedt.core.generic.general_methods import open_file +from ansys.aedt.core.generic.general_methods import pyaedt_function_handler +from ansys.aedt.core.generic.general_methods import write_csv +from ansys.aedt.core.generic.settings import settings +from ansys.aedt.core.visualization.plot.matplotlib import plot_2d_chart +from ansys.aedt.core.visualization.plot.matplotlib import plot_3d_chart +from ansys.aedt.core.visualization.plot.matplotlib import plot_polar_chart + +np = None +pd = None + +try: + import numpy as np +except ImportError: + np = None + warnings.warn( + "The NumPy module is required to run some functionalities of PostProcess.\n" + "Install with \n\npip install numpy" + ) +try: + import pandas as pd +except ImportError: + pd = None + warnings.warn( + "The Pandas module is required to run some functionalities of PostProcess.\n" + "Install with \n\npip install pandas" + ) + + +class SolutionData(object): + """Contains information from the :func:`GetSolutionDataPerVariation` method.""" + + def __init__(self, aedtdata): + self._original_data = aedtdata + self.number_of_variations = len(aedtdata) + self._enable_pandas_output = True if settings.enable_pandas_output and pd else False + self._expressions = None + self._intrinsics = None + self._nominal_variation = None + self._nominal_variation = self._original_data[0] + self.active_expression = self.expressions[0] + self._sweeps_names = [] + self.update_sweeps() + self.variations = self._get_variations() + self.active_intrinsic = {} + for k, v in self.intrinsics.items(): + self.active_intrinsic[k] = v[0] + if len(self.intrinsics) > 0: + self._primary_sweep = list(self.intrinsics.keys())[0] + else: + self._primary_sweep = self._sweeps_names[0] + self.active_variation = self.variations[0] + self.units_sweeps = {} + for intrinsic in self.intrinsics: + try: + self.units_sweeps[intrinsic] = self.nominal_variation.GetSweepUnits(intrinsic) + except Exception: + self.units_sweeps[intrinsic] = None + self.init_solutions_data() + self._ifft = None + + @property + def enable_pandas_output(self): + """ + Set/Get a flag to use Pandas to export dict and lists. This applies to Solution data output. + If ``True`` the property or method will return a pandas object in CPython environment. + Default is ``False``. + + Returns + ------- + bool + """ + return True if self._enable_pandas_output and pd else False + + @enable_pandas_output.setter + def enable_pandas_output(self, val): + if val != self._enable_pandas_output and pd: + self._enable_pandas_output = val + self.init_solutions_data() + + @pyaedt_function_handler() + def set_active_variation(self, var_id=0): + """Set the active variations to one of available variations in self.variations. + + Parameters + ---------- + var_id : int + Index of Variations to assign. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if var_id < len(self.variations): + self.active_variation = self.variations[var_id] + self.nominal_variation = var_id + self._expressions = None + self._intrinsics = None + return True + return False + + @pyaedt_function_handler() + def _get_variations(self): + variations_lists = [] + for data in self._original_data: + variations = {} + for v in data.GetDesignVariableNames(): + variations[v] = data.GetDesignVariableValue(v) + variations_lists.append(variations) + return variations_lists + + @pyaedt_function_handler(variation_name="variation") + def variation_values(self, variation): + """Get the list of the specific variation available values. + + Parameters + ---------- + variation : str + Name of variation to return. + + Returns + ------- + list + List of variation values. + """ + if variation in self.intrinsics: + return self.intrinsics[variation] + else: + vars_vals = [] + for el in self.variations: + if variation in el and el[variation] not in vars_vals: + vars_vals.append(el[variation]) + return vars_vals + + @property + def intrinsics(self): + """Get intrinsics dictionary on active variation.""" + if not self._intrinsics: + self._intrinsics = {} + intrinsics = [i for i in self._sweeps_names if i not in self.nominal_variation.GetDesignVariableNames()] + for el in intrinsics: + values = list(self.nominal_variation.GetSweepValues(el, False)) + self._intrinsics[el] = [i for i in values] + self._intrinsics[el] = list(dict.fromkeys(self._intrinsics[el])) + return self._intrinsics + + @property + def nominal_variation(self): + """Nominal variation.""" + return self._nominal_variation + + @nominal_variation.setter + def nominal_variation(self, val): + if 0 <= val <= self.number_of_variations: + self._nominal_variation = self._original_data[val] + else: + print(str(val) + " not in Variations") + + @property + def primary_sweep(self): + """Primary sweep. + + Parameters + ---------- + ps : float + Perimeter of the source. + """ + return self._primary_sweep + + @primary_sweep.setter + def primary_sweep(self, ps): + if ps in self._sweeps_names: + self._primary_sweep = ps + + @property + def expressions(self): + """Expressions.""" + if not self._expressions: + mydata = [i for i in self._nominal_variation.GetDataExpressions()] + self._expressions = list(dict.fromkeys(mydata)) + return self._expressions + + @pyaedt_function_handler() + def update_sweeps(self): + """Update sweeps. + + Returns + ------- + dict + Updated sweeps. + """ + + names = list(self.nominal_variation.GetSweepNames()) + for data in self._original_data: + for v in data.GetDesignVariableNames(): + if v not in self._sweeps_names: + self._sweeps_names.append(v) + self._sweeps_names.extend((reversed(names))) + + @staticmethod + @pyaedt_function_handler() + def _quantity(unit): + """Get the corresponding AEDT units. + + Parameters + ---------- + unit : str + The unit to be looked among the available AEDT units. + + Returns + ------- + str + The AEDT units. + + """ + for el in AEDT_UNITS: + keys_units = [i.lower() for i in list(AEDT_UNITS[el].keys())] + if unit.lower() in keys_units: + return el + return None + + @pyaedt_function_handler() + def init_solutions_data(self): + """Initialize the database and store info in variables.""" + self._solutions_real = self._init_solution_data_real() + self._solutions_imag = self._init_solution_data_imag() + self._solutions_mag = self._init_solution_data_mag() + self._solutions_phase = self._init_solution_data_phase() + + @pyaedt_function_handler() + def _init_solution_data_mag(self): + _solutions_mag = {} + self.units_data = {} + + for expr in self.expressions: + _solutions_mag[expr] = {} + self.units_data[expr] = self.nominal_variation.GetDataUnits(expr) + if self.enable_pandas_output: + _solutions_mag[expr] = np.sqrt(self._solutions_real[expr]) + else: + for i in self._solutions_real[expr]: + _solutions_mag[expr][i] = abs(complex(self._solutions_real[expr][i], self._solutions_imag[expr][i])) + if self.enable_pandas_output: + return pd.DataFrame.from_dict(_solutions_mag) + else: + return _solutions_mag + + @pyaedt_function_handler() + def _init_solution_data_real(self): + """ """ + sols_data = {} + + for expression in self.expressions: + solution_data = {} + + for data, comb in zip(self._original_data, self.variations): + solution = list(data.GetRealDataValues(expression, False)) + values = [] + for el in list(self.intrinsics.keys()): + values.append(list(dict.fromkeys(data.GetSweepValues(el, False)))) + + i = 0 + c = [comb[v] for v in list(comb.keys())] + for t in itertools.product(*values): + solution_data[tuple(c + list(t))] = solution[i] + i += 1 + sols_data[expression] = solution_data + if self.enable_pandas_output: + return pd.DataFrame.from_dict(sols_data) + else: + return sols_data + + @pyaedt_function_handler() + def _init_solution_data_imag(self): + """ """ + sols_data = {} + + for expression in self.expressions: + solution_data = {} + for data, comb in zip(self._original_data, self.variations): + if data.IsDataComplex(expression): + solution = list(data.GetImagDataValues(expression, False)) + else: + l = len(list(data.GetRealDataValues(expression, False))) + solution = [0] * l + values = [] + for el in list(self.intrinsics.keys()): + values.append(list(dict.fromkeys(data.GetSweepValues(el, False)))) + i = 0 + c = [comb[v] for v in list(comb.keys())] + for t in itertools.product(*values): + solution_data[tuple(c + list(t))] = solution[i] + i += 1 + sols_data[expression] = solution_data + if self.enable_pandas_output: + return pd.DataFrame.from_dict(sols_data) + else: + return sols_data + + @pyaedt_function_handler() + def _init_solution_data_phase(self): + data_phase = {} + for expr in self.expressions: + data_phase[expr] = {} + if self.enable_pandas_output: + data_phase[expr] = np.arctan2(self._solutions_imag[expr], self._solutions_real[expr]) + else: + for i in self._solutions_real[expr]: + data_phase[expr][i] = math.atan2(self._solutions_imag[expr][i], self._solutions_real[expr][i]) + if self.enable_pandas_output: + return pd.DataFrame.from_dict(data_phase) + else: + return data_phase + + @property + def full_matrix_real_imag(self): + """Get the full available solution data in Real and Imaginary parts. + + Returns + ------- + tuple of dicts + (Real Dict, Imag Dict) + """ + return self._solutions_real, self._solutions_imag + + @property + def full_matrix_mag_phase(self): + """Get the full available solution data magnitude and phase in radians. + + Returns + ------- + tuple of dicts + (Mag Dict, Phase Dict). + """ + return self._solutions_mag, self._solutions_phase + + @staticmethod + @pyaedt_function_handler() + def to_degrees(input_list): + """Convert an input list from radians to degrees. + + Parameters + ---------- + input_list : list + List of inputs in radians. + + Returns + ------- + list + List of inputs in degrees. + + """ + if isinstance(input_list, (tuple, list)): + return [i * 360 / (2 * math.pi) for i in input_list] + else: + return input_list * 360 / (2 * math.pi) + + @staticmethod + @pyaedt_function_handler() + def to_radians(input_list): + """Convert an input list from degrees to radians. + + Parameters + ---------- + input_list : list + List of inputs in degrees. + + Returns + ------- + type + List of inputs in radians. + """ + if isinstance(input_list, (tuple, list)): + return [i * 2 * math.pi / 360 for i in input_list] + else: + return input_list * 2 * math.pi / 360 + + @pyaedt_function_handler() + def _variation_tuple(self): + temp = [] + for it in self._sweeps_names: + try: + temp.append(self.active_variation[it]) + except KeyError: + temp.append(self.active_intrinsic[it]) + return temp + + @pyaedt_function_handler() + def data_magnitude(self, expression=None, convert_to_SI=False): + """Retrieve the data magnitude of an expression. + + Parameters + ---------- + expression : str, optional + Name of the expression. The default is ``None``, in which case the + active expression is used. + convert_to_SI : bool, optional + Whether to convert the data to the SI unit system. + The default is ``False``. + + Returns + ------- + list + List of data. + """ + if not expression: + expression = self.active_expression + elif expression not in self.expressions: + return False + temp = self._variation_tuple() + solution_data = self._solutions_mag[expression] + sol = [] + position = list(self._sweeps_names).index(self.primary_sweep) + sw = self.variation_values(self.primary_sweep) + for el in sw: + temp[position] = el + try: + sol.append(solution_data[tuple(temp)]) + except KeyError: + sol.append(None) + if convert_to_SI and self._quantity(self.units_data[expression]): + sol = self._convert_list_to_SI( + sol, self._quantity(self.units_data[expression]), self.units_data[expression] + ) + if self.enable_pandas_output: + return pd.Series(sol) + return sol + + @staticmethod + @pyaedt_function_handler(datalist="data", dataunits="data_units") + def _convert_list_to_SI(data, data_units, units): + """Convert a data list to the SI unit system. + + Parameters + ---------- + data : list + List of data to convert. + data_units : str + Data units. + units : str + SI units to convert data into. + + + Returns + ------- + list + List of the data converted to the SI unit system. + + """ + sol = data + if data_units in AEDT_UNITS and units in AEDT_UNITS[data_units]: + sol = [i * AEDT_UNITS[data_units][units] for i in data] + return sol + + @pyaedt_function_handler() + def data_db10(self, expression=None, convert_to_SI=False): + """Retrieve the data in the database for an expression and convert in db10. + + Parameters + ---------- + expression : str, optional + Name of the expression. The default is ``None``, + in which case the active expression is used. + convert_to_SI : bool, optional + Whether to convert the data to the SI unit system. + The default is ``False``. + + Returns + ------- + list + List of the data in the database for the expression. + """ + if not expression: + expression = self.active_expression + if self.enable_pandas_output: + return 10 * np.log10(self.data_magnitude(expression, convert_to_SI)) + return [db10(i) for i in self.data_magnitude(expression, convert_to_SI)] + + @pyaedt_function_handler() + def data_db20(self, expression=None, convert_to_SI=False): + """Retrieve the data in the database for an expression and convert in db20. + + Parameters + ---------- + expression : str, optional + Name of the expression. The default is ``None``, + in which case the active expression is used. + convert_to_SI : bool, optional + Whether to convert the data to the SI unit system. + The default is ``False``. + + Returns + ------- + list + List of the data in the database for the expression. + """ + if not expression: + expression = self.active_expression + if self.enable_pandas_output: + return 20 * np.log10(self.data_magnitude(expression, convert_to_SI)) + return [db20(i) for i in self.data_magnitude(expression, convert_to_SI)] + + @pyaedt_function_handler() + def data_phase(self, expression=None, radians=True): + """Retrieve the phase part of the data for an expression. + + Parameters + ---------- + expression : str, None + Name of the expression. The default is ``None``, + in which case the active expression is used. + radians : bool, optional + Whether to convert the data into radians or degree. + The default is ``True`` for radians. + + Returns + ------- + list + Phase data for the expression. + """ + if not expression: + expression = self.active_expression + coefficient = 1 + if not radians: + coefficient = 180 / math.pi + if self.enable_pandas_output: + return coefficient * np.arctan2(self.data_imag(expression), self.data_real(expression)) + return [coefficient * math.atan2(k, i) for i, k in zip(self.data_real(expression), self.data_imag(expression))] + + @property + def primary_sweep_values(self): + """Retrieve the primary sweep for a given data and primary variable. + + Returns + ------- + list + List of the primary sweep valid points for the expression. + """ + if self.enable_pandas_output: + return pd.Series(self.variation_values(self.primary_sweep)) + return self.variation_values(self.primary_sweep) + + @property + def primary_sweep_variations(self): + """Retrieve the variations lists for a given primary variable. + + Returns + ------- + list + List of the primary sweep valid points for the expression. + + """ + expression = self.active_expression + temp = self._variation_tuple() + + solution_data = list(self._solutions_real[expression].keys()) + sol = [] + position = list(self._sweeps_names).index(self.primary_sweep) + + for el in self.primary_sweep_values: + temp[position] = el + if tuple(temp) in solution_data: + sol_dict = {} + i = 0 + for sn in self._sweeps_names: + sol_dict[sn] = temp[i] + i += 1 + sol.append(sol_dict) + else: + sol.append(None) + if self.enable_pandas_output: + return pd.Series(sol) + return sol + + @pyaedt_function_handler() + def data_real(self, expression=None, convert_to_SI=False): + """Retrieve the real part of the data for an expression. + + Parameters + ---------- + expression : str, None + Name of the expression. The default is ``None``, + in which case the active expression is used. + convert_to_SI : bool, optional + Whether to convert the data to the SI unit system. + The default is ``False``. + + Returns + ------- + list + List of the real data for the expression. + """ + if not expression: + expression = self.active_expression + temp = self._variation_tuple() + + solution_data = self._solutions_real[expression] + sol = [] + position = list(self._sweeps_names).index(self.primary_sweep) + + for el in self.primary_sweep_values: + temp[position] = el + try: + sol.append(solution_data[tuple(temp)]) + except KeyError: + sol.append(None) + + if convert_to_SI and self._quantity(self.units_data[expression]): + sol = self._convert_list_to_SI( + sol, self._quantity(self.units_data[expression]), self.units_data[expression] + ) + if self.enable_pandas_output: + return pd.Series(sol) + return sol + + @pyaedt_function_handler() + def data_imag(self, expression=None, convert_to_SI=False): + """Retrieve the imaginary part of the data for an expression. + + Parameters + ---------- + expression : str, optional + Name of the expression. The default is ``None``, + in which case the active expression is used. + convert_to_SI : bool, optional + Whether to convert the data to the SI unit system. + The default is ``False``. + + Returns + ------- + list + List of the imaginary data for the expression. + """ + if not expression: + expression = self.active_expression + temp = self._variation_tuple() + + solution_data = self._solutions_imag[expression] + sol = [] + position = list(self._sweeps_names).index(self.primary_sweep) + for el in self.primary_sweep_values: + temp[position] = el + try: + sol.append(solution_data[tuple(temp)]) + except KeyError: + sol.append(None) + if convert_to_SI and self._quantity(self.units_data[expression]): + sol = self._convert_list_to_SI( + sol, self._quantity(self.units_data[expression]), self.units_data[expression] + ) + if self.enable_pandas_output: + return pd.Series(sol) + return sol + + @pyaedt_function_handler() + def is_real_only(self, expression=None): + """Check if the expression has only real values or not. + + Parameters + ---------- + expression : str, optional + Name of the expression. The default is ``None``, + in which case the active expression is used. + + Returns + ------- + bool + ``True`` if the Solution Data for specific expression contains only real values. + """ + if not expression: + expression = self.active_expression + if self.enable_pandas_output: + return True if self._solutions_imag[expression].abs().sum() > 0.0 else False + for v in list(self._solutions_imag[expression].values()): + if float(v) != 0.0: + return False + return True + + @pyaedt_function_handler() + def export_data_to_csv(self, output, delimiter=";"): + """Save to output csv file the Solution Data. + + Parameters + ---------- + output : str, + Full path to csv file. + delimiter : str, + CSV Delimiter. Default is ``";"``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + header = [] + des_var = self._original_data[0].GetDesignVariableNames() + sweep_var = self._original_data[0].GetSweepNames() + for el in self._sweeps_names: + unit = "" + if el in des_var: + unit = self._original_data[0].GetDesignVariableUnits(el) + elif el in sweep_var: + unit = self._original_data[0].GetSweepUnits(el) + if unit == "": + header.append("{}".format(el)) + else: + header.append("{} [{}]".format(el, unit)) + # header = [el for el in self._sweeps_names] + for el in self.expressions: + data_unit = self._original_data[0].GetDataUnits(el) + if data_unit: + data_unit = " [{}]".format(data_unit) + if not self.is_real_only(el): + + header.append(el + " (Real){}".format(data_unit)) + header.append(el + " (Imag){}".format(data_unit)) + else: + header.append(el + "{}".format(data_unit)) + + list_full = [header] + for e, v in self._solutions_real[self.active_expression].items(): + list_full.append(list(e)) + for el in self.expressions: + i = 1 + for e, v in self._solutions_real[el].items(): + list_full[i].extend([v]) + i += 1 + i = 1 + if not self.is_real_only(el): + for e, v in self._solutions_imag[el].items(): + list_full[i].extend([v]) + i += 1 + + return write_csv(output, list_full, delimiter=delimiter) + + @pyaedt_function_handler(math_formula="formula", xlabel="x_label", ylabel="y_label") + def plot( + self, + curves=None, + formula=None, + size=(2000, 1000), + show_legend=True, + x_label="", + y_label="", + title="", + snapshot_path=None, + is_polar=False, + show=True, + ): + """Create a matplotlib figure based on a list of data. + + Parameters + ---------- + curves : list + Curves to be plotted. The default is ``None``, in which case + the first curve is plotted. + formula : str , optional + Mathematical formula to apply to the plot curve. The default is ``None``, + in which case only real value of the data stored in the solution data is plotted. + Options are ``"abs"``, ``"db10"``, ``"db20"``, ``"im"``, ``"mag"``, ``"phasedeg"``, + ``"phaserad"``, and ``"re"``. + size : tuple, optional + Image size in pixels (width, height). + show_legend : bool + Whether to show the legend. The default is ``True``. + This parameter is ignored if the number of curves to plot is + greater than 15. + x_label : str + Plot X label. + y_label : str + Plot Y label. + title : str + Plot title label. + snapshot_path : str + Full path to image file if a snapshot is needed. + is_polar : bool, optional + Set to `True` if this is a polar plot. + show : bool, optional + Whether if show the plot or not. Default is set to `True`. + + Returns + ------- + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. + """ + if not curves: + curves = [self.active_expression] + if isinstance(curves, str): + curves = [curves] + data_plot = [] + sweep_name = self.primary_sweep + if is_polar: + sw = self.to_radians(self.primary_sweep_values) + else: + sw = self.primary_sweep_values + for curve in curves: + if not formula: + data_plot.append([sw, self.data_real(curve), curve]) + elif formula == "re": + data_plot.append([sw, self.data_real(curve), "{}({})".format(formula, curve)]) + elif formula == "im": + data_plot.append([sw, self.data_imag(curve), "{}({})".format(formula, curve)]) + elif formula == "db20": + data_plot.append([sw, self.data_db20(curve), "{}({})".format(formula, curve)]) + elif formula == "db10": + data_plot.append([sw, self.data_db10(curve), "{}({})".format(formula, curve)]) + elif formula == "mag": + data_plot.append([sw, self.data_magnitude(curve), "{}({})".format(formula, curve)]) + elif formula == "phasedeg": + data_plot.append([sw, self.data_phase(curve, False), "{}({})".format(formula, curve)]) + elif formula == "phaserad": + data_plot.append([sw, self.data_phase(curve, True), "{}({})".format(formula, curve)]) + if not x_label: + x_label = sweep_name + if not y_label: + y_label = formula + if not title: + title = "Simulation Results Plot" + if len(data_plot) > 15: + show_legend = False + if is_polar: + return plot_polar_chart(data_plot, size, show_legend, x_label, y_label, title, snapshot_path, show=show) + else: + return plot_2d_chart(data_plot, size, show_legend, x_label, y_label, title, snapshot_path, show=show) + + @pyaedt_function_handler(xlabel="x_label", ylabel="y_label", math_formula="formula") + def plot_3d( + self, + curve=None, + x_axis="Theta", + y_axis="Phi", + x_label="", + y_label="", + title="", + formula=None, + size=(2000, 1000), + snapshot_path=None, + show=True, + ): + """Create a matplotlib 3D figure based on a list of data. + + Parameters + ---------- + curve : str + Curve to be plotted. If None, the first curve will be plotted. + x_axis : str, optional + X-axis sweep. The default is ``"Theta"``. + y_axis : str, optional + Y-axis sweep. The default is ``"Phi"``. + x_label : str + Plot X label. + y_label : str + Plot Y label. + title : str + Plot title label. + formula : str , optional + Mathematical formula to apply to the plot curve. The default is ``None``. + Options are `"abs"``, ``"db10"``, ``"db20"``, ``"im"``, ``"mag"``, ``"phasedeg"``, + ``"phaserad"``, and ``"re"``. + size : tuple, optional + Image size in pixels (width, height). The default is ``(2000, 1000)``. + snapshot_path : str, optional + Full path to image file if a snapshot is needed. + The default is ``None``. + show : bool, optional + Whether if show the plot or not. Default is set to `True`. + + Returns + ------- + :class:`matplotlib.figure.Figure` + Matplotlib figure object. + """ + if not curve: + curve = self.active_expression + + if not formula: + formula = "mag" + theta = self.variation_values(x_axis) + y_axis_val = self.variation_values(y_axis) + + phi = [] + r = [] + for el in y_axis_val: + self.active_variation[y_axis] = el + phi.append(el * math.pi / 180) + + if formula == "re": + r.append(self.data_real(curve)) + elif formula == "im": + r.append(self.data_imag(curve)) + elif formula == "db20": + r.append(self.data_db20(curve)) + elif formula == "db10": + r.append(self.data_db10(curve)) + elif formula == "mag": + r.append(self.data_magnitude(curve)) + elif formula == "phasedeg": + r.append(self.data_phase(curve, False)) + elif formula == "phaserad": + r.append(self.data_phase(curve, True)) + active_sweep = self.active_intrinsic[self.primary_sweep] + position = self.variation_values(self.primary_sweep).index(active_sweep) + if len(self.variation_values(self.primary_sweep)) > 1: + new_r = [] + for el in r: + new_r.append([el[position]]) + r = new_r + data_plot = [theta, phi, r] + if not x_label: + x_label = x_axis + if not y_label: + y_label = y_axis + if not title: + title = "Simulation Results Plot" + return plot_3d_chart(data_plot, size, x_label, y_label, title, snapshot_path, show=show) + + @pyaedt_function_handler() + def ifft(self, curve_header="NearE", u_axis="_u", v_axis="_v", window=False): + """Create IFFT of given complex data. + + Parameters + ---------- + curve_header : curve header. Solution data must contain 3 curves with X, Y and Z components of curve header. + u_axis : str, optional + U Axis name. Default is Hfss name "_u" + v_axis : str, optional + V Axis name. Default is Hfss name "_v" + window : bool, optional + Either if Hanning windowing has to be applied. + + Returns + ------- + List + IFFT Matrix. + """ + u = self.variation_values(u_axis) + v = self.variation_values(v_axis) + + freq = self.variation_values("Freq") + if self.enable_pandas_output: + e_real_x = np.reshape(self._solutions_real[curve_header + "X"].copy().values, (len(freq), len(v), len(u))) + e_imag_x = np.reshape(self._solutions_imag[curve_header + "X"].copy().values, (len(freq), len(v), len(u))) + e_real_y = np.reshape(self._solutions_real[curve_header + "Y"].copy().values, (len(freq), len(v), len(u))) + e_imag_y = np.reshape(self._solutions_imag[curve_header + "Y"].copy().values, (len(freq), len(v), len(u))) + e_real_z = np.reshape(self._solutions_real[curve_header + "Z"].copy().values, (len(freq), len(v), len(u))) + e_imag_z = np.reshape(self._solutions_imag[curve_header + "Z"].copy().values, (len(freq), len(v), len(u))) + else: + vals_e_real_x = [j for j in self._solutions_real[curve_header + "X"].values()] + vals_e_imag_x = [j for j in self._solutions_imag[curve_header + "X"].values()] + vals_e_real_y = [j for j in self._solutions_real[curve_header + "Y"].values()] + vals_e_imag_y = [j for j in self._solutions_imag[curve_header + "Y"].values()] + vals_e_real_z = [j for j in self._solutions_real[curve_header + "Z"].values()] + vals_e_imag_z = [j for j in self._solutions_imag[curve_header + "Z"].values()] + + e_real_x = np.reshape(vals_e_real_x, (len(freq), len(v), len(u))) + e_imag_x = np.reshape(vals_e_imag_x, (len(freq), len(v), len(u))) + e_real_y = np.reshape(vals_e_real_y, (len(freq), len(v), len(u))) + e_imag_y = np.reshape(vals_e_imag_y, (len(freq), len(v), len(u))) + e_real_z = np.reshape(vals_e_real_z, (len(freq), len(v), len(u))) + e_imag_z = np.reshape(vals_e_imag_z, (len(freq), len(v), len(u))) + + temp_e_comp_x = e_real_x + 1j * e_imag_x # Here is the complex FD data matrix, ready for transforming + temp_e_comp_y = e_real_y + 1j * e_imag_y + temp_e_comp_z = e_real_z + 1j * e_imag_z + + e_comp_x = np.zeros((len(freq), len(v), len(u)), dtype="complex_") + e_comp_y = np.zeros((len(freq), len(v), len(u)), dtype="complex_") + e_comp_z = np.zeros((len(freq), len(v), len(u)), dtype="complex_") + if window: + timewin = np.hanning(len(freq)) + + for row in range(0, len(v)): + for col in range(0, len(u)): + e_comp_x[:, row, col] = np.multiply(temp_e_comp_x[:, row, col], timewin) + e_comp_y[:, row, col] = np.multiply(temp_e_comp_y[:, row, col], timewin) + e_comp_z[:, row, col] = np.multiply(temp_e_comp_z[:, row, col], timewin) + else: + e_comp_x = temp_e_comp_x + e_comp_y = temp_e_comp_y + e_comp_z = temp_e_comp_z + + e_time_x = np.fft.ifft(np.fft.fftshift(e_comp_x, 0), len(freq), 0, None) + e_time_y = np.fft.ifft(np.fft.fftshift(e_comp_y, 0), len(freq), 0, None) + e_time_z = np.fft.ifft(np.fft.fftshift(e_comp_z, 0), len(freq), 0, None) + e_time = np.zeros((np.size(freq), np.size(v), np.size(u))) + for i in range(0, len(freq)): + e_time[i, :, :] = np.abs( + np.sqrt(np.square(e_time_x[i, :, :]) + np.square(e_time_y[i, :, :]) + np.square(e_time_z[i, :, :])) + ) + self._ifft = e_time + + return self._ifft + + @pyaedt_function_handler(csv_dir="csv_path", name_str="csv_file_header") + def ifft_to_file( + self, + u_axis="_u", + v_axis="_v", + coord_system_center=None, + db_val=False, + num_frames=None, + csv_path=None, + csv_file_header="res_", + ): + """Save IFFT matrix to a list of CSV files (one per time step). + + Parameters + ---------- + u_axis : str, optional + U Axis name. Default is Hfss name "_u" + v_axis : str, optional + V Axis name. Default is Hfss name "_v" + coord_system_center : list, optional + List of UV GlobalCS Center. + db_val : bool, optional + Whether data must be exported into a database. The default is ``False``. + num_frames : int, optional + Number of frames to export. The default is ``None``. + csv_path : str, optional + Output path. The default is ``None``. + csv_file_header : str, optional + CSV file header. The default is ``"res_"``. + + Returns + ------- + str + Path to file containing the list of csv files. + """ + if not coord_system_center: + coord_system_center = [0, 0, 0] + t_matrix = self._ifft + x_c_list = self.variation_values(u_axis) + y_c_list = self.variation_values(v_axis) + + adj_x = coord_system_center[0] + adj_y = coord_system_center[1] + adj_z = coord_system_center[2] + if num_frames: + frames = num_frames + else: + frames = t_matrix.shape[0] + csv_list = [] + if os.path.exists(csv_path): + files = [os.path.join(csv_path, f) for f in os.listdir(csv_path) if csv_file_header in f and ".csv" in f] + for file in files: + os.remove(file) + else: + os.mkdir(csv_path) + + for frame in range(frames): + output = os.path.join(csv_path, csv_file_header + str(frame) + ".csv") + list_full = [["x", "y", "z", "val"]] + for i, y in enumerate(y_c_list): + for j, x in enumerate(x_c_list): + y_coord = y + adj_y + x_coord = x + adj_x + z_coord = adj_z + if db_val: + val = 10.0 * np.log10(np.abs(t_matrix[frame, i, j])) + else: + val = t_matrix[frame, i, j] + row_lst = [x_coord, y_coord, z_coord, val] + list_full.append(row_lst) + write_csv(output, list_full, delimiter=",") + csv_list.append(output) + + txt_file_name = csv_path + "fft_list.txt" + textfile = open_file(txt_file_name, "w") + + for element in csv_list: + textfile.write(element + "\n") + textfile.close() + return txt_file_name diff --git a/src/ansys/aedt/core/generic/spisim.py b/src/ansys/aedt/core/visualization/post/spisim.py similarity index 99% rename from src/ansys/aedt/core/generic/spisim.py rename to src/ansys/aedt/core/visualization/post/spisim.py index 214c88f033a..bff32e5204f 100644 --- a/src/ansys/aedt/core/generic/spisim.py +++ b/src/ansys/aedt/core/visualization/post/spisim.py @@ -37,7 +37,7 @@ from ansys.aedt.core.generic.settings import is_linux from ansys.aedt.core.generic.settings import settings from ansys.aedt.core.misc import current_version -from ansys.aedt.core.misc.spisim_com_configuration_files.com_parameters import COMParametersVer3p4 +from ansys.aedt.core.visualization.post.spisim_com_configuration_files.com_parameters import COMParametersVer3p4 from numpy import float64 from numpy import zeros diff --git a/src/ansys/aedt/core/misc/spisim_com_configuration_files/__init__.py b/src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/__init__.py similarity index 100% rename from src/ansys/aedt/core/misc/spisim_com_configuration_files/__init__.py rename to src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/__init__.py diff --git a/src/ansys/aedt/core/misc/spisim_com_configuration_files/com_120d_8.json b/src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/com_120d_8.json similarity index 100% rename from src/ansys/aedt/core/misc/spisim_com_configuration_files/com_120d_8.json rename to src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/com_120d_8.json diff --git a/src/ansys/aedt/core/misc/spisim_com_configuration_files/com_93_8.json b/src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/com_93_8.json similarity index 100% rename from src/ansys/aedt/core/misc/spisim_com_configuration_files/com_93_8.json rename to src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/com_93_8.json diff --git a/src/ansys/aedt/core/misc/spisim_com_configuration_files/com_94_17.json b/src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/com_94_17.json similarity index 100% rename from src/ansys/aedt/core/misc/spisim_com_configuration_files/com_94_17.json rename to src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/com_94_17.json diff --git a/src/ansys/aedt/core/misc/spisim_com_configuration_files/com_parameters.py b/src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/com_parameters.py similarity index 98% rename from src/ansys/aedt/core/misc/spisim_com_configuration_files/com_parameters.py rename to src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/com_parameters.py index f0af97873b3..a93e2570dcd 100644 --- a/src/ansys/aedt/core/misc/spisim_com_configuration_files/com_parameters.py +++ b/src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/com_parameters.py @@ -28,7 +28,9 @@ from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.generic.settings import settings -from ansys.aedt.core.misc.spisim_com_configuration_files.com_settings_mapping import spimsim_matlab_keywords_mapping +from ansys.aedt.core.visualization.post.spisim_com_configuration_files.com_settings_mapping import ( + spimsim_matlab_keywords_mapping, +) logger = settings.logger @@ -46,7 +48,7 @@ class COMStandards(Enum): class COMParameters: """Base class to manage COM parameters.""" - _CFG_DIR = Path(__file__).parent.parent / "spisim_com_configuration_files" + _CFG_DIR = Path(__file__).parent _STD_TABLE_MAPPING = { "COM_50GAUI_1_C2C": "com_120d_8.json", "COM_100GAUI_2_C2C": "com_120d_8.json", @@ -230,7 +232,7 @@ def load_spisim_cfg(self, file_path): Parameters ---------- - file_path: str + file_path : str Path of the configuration file. Returns diff --git a/src/ansys/aedt/core/misc/spisim_com_configuration_files/com_settings_mapping.py b/src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/com_settings_mapping.py similarity index 100% rename from src/ansys/aedt/core/misc/spisim_com_configuration_files/com_settings_mapping.py rename to src/ansys/aedt/core/visualization/post/spisim_com_configuration_files/com_settings_mapping.py diff --git a/src/ansys/aedt/core/visualization/post/vrt_data.py b/src/ansys/aedt/core/visualization/post/vrt_data.py new file mode 100644 index 00000000000..d556d4a20ed --- /dev/null +++ b/src/ansys/aedt/core/visualization/post/vrt_data.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os + +from ansys.aedt.core.generic.general_methods import pyaedt_function_handler + + +class VRTFieldPlot: + """Creates and edits VRT field plots for SBR+ and Creeping Waves. + + Parameters + ---------- + postprocessor : :class:`ansys.aedt.core.modules.post_general.PostProcessor` + is_creeping_wave : bool + Whether it is a creeping wave model or not. + quantity : str, optional + Name of the plot or the name of the object. + max_frequency : str, optional + Maximum Frequency. The default is ``"1GHz"``. + ray_density : int, optional + Ray Density. The default is ``2``. + bounces : int, optional + Maximum number of bounces. The default is ``5``. + intrinsics : dict, optional + Name of the intrinsic dictionary. The default is ``{}``. + + """ + + @pyaedt_function_handler(quantity_name="quantity") + def __init__( + self, + postprocessor, + is_creeping_wave=False, + quantity="QuantityName_SBR", + max_frequency="1GHz", + ray_density=2, + bounces=5, + intrinsics=None, + ): + self.is_creeping_wave = is_creeping_wave + self._postprocessor = postprocessor + self._ofield = postprocessor.ofieldsreporter + self.quantity = quantity + self.intrinsics = {} if intrinsics is None else intrinsics + self.name = "Field_Plot" + self.plot_folder = "Field_Plot" + self.max_frequency = max_frequency + self.ray_density = ray_density + self.number_of_bounces = bounces + self.multi_bounce_ray_density_control = False + self.mbrd_max_subdivision = 2 + self.shoot_utd_rays = False + self.shoot_type = "All Rays" + self.start_index = 0 + self.stop_index = 1 + self.step_index = 1 + self.is_plane_wave = True + self.incident_theta = "0deg" + self.incident_phi = "0deg" + self.vertical_polarization = False + self.custom_location = [0, 0, 0] + self.ray_box = None + self.ray_elevation = "0deg" + self.ray_azimuth = "0deg" + self.custom_coordinatesystem = 1 + self.ray_cutoff = 40 + self.sample_density = 10 + self.irregular_surface_tolerance = 50 + + @property + def intrinsicVar(self): + """Intrinsic variable. + + Returns + ------- + str + Variables for the field plot. + """ + var = "" + for a in self.intrinsics: + var += a + "='" + str(self.intrinsics[a]) + "' " + return var + + @pyaedt_function_handler() + def _create_args(self): + args = [ + "NAME:" + self.name, + "UserSpecifyName:=", + 0, + "UserSpecifyFolder:=", + 0, + "QuantityName:=", + self.quantity, + "PlotFolder:=", + "Visual Ray Trace SBR", + "IntrinsicVar:=", + self.intrinsicVar, + "MaxFrequency:=", + self.max_frequency, + "RayDensity:=", + self.ray_density, + "NumberBounces:=", + self.number_of_bounces, + "Multi-Bounce Ray Density Control:=", + self.multi_bounce_ray_density_control, + "MBRD Max sub divisions:=", + self.mbrd_max_subdivision, + "Shoot UTD Rays:=", + self.shoot_utd_rays, + "ShootFilterType:=", + self.shoot_type, + ] + if self.shoot_type == "Rays by index": + args.extend( + [ + "start index:=", + self.start_index, + "stop index:=", + self.stop_index, + "index step:=", + self.step_index, + ] + ) + elif self.shoot_type == "Rays in box": + box_id = None + if isinstance(self.ray_box, int): + box_id = self.ray_box + elif isinstance(self.ray_box, str): + box_id = self._postprocessor._primitives.objects[self.ray_box].id + else: + box_id = self.ray_box.id + args.extend("FilterBoxID:=", box_id) + elif self.shoot_type == "Single ray": + args.extend("Ray elevation:=", self.ray_elevation, "Ray azimuth:=", self.ray_azimuth) + args.append("LaunchFrom:=") + if self.is_plane_wave: + args.append("Launch from Plane-Wave") + args.append("Incident direction theta:=") + args.append(self.incident_theta) + args.append("Incident direction phi:=") + args.append(self.incident_phi) + args.append("Vertical Incident Polarization:=") + args.append(self.vertical_polarization) + else: + args.append("Launch from Custom") + args.append("LaunchFromPointID:=") + args.append(-1) + args.append("CustomLocationCoordSystem:=") + args.append(self.custom_coordinatesystem) + args.append("CustomLocation:=") + args.append(self.custom_location) + return args + + @pyaedt_function_handler() + def _create_args_creeping(self): + args = [ + "NAME:" + self.name, + "UserSpecifyName:=", + 0, + "UserSpecifyFolder:=", + 0, + "QuantityName:=", + self.quantity, + "PlotFolder:=", + "Visual Ray Trace CW", + "IntrinsicVar:=", + "", + "MaxFrequency:=", + self.max_frequency, + "SampleDensity:=", + self.sample_density, + "RayCutOff:=", + self.ray_cutoff, + "Irregular Surface Tolerance:=", + self.irregular_surface_tolerance, + "LaunchFrom:=", + ] + if self.is_plane_wave: + args.append("Launch from Plane-Wave") + args.append("Incident direction theta:=") + args.append(self.incident_theta) + args.append("Incident direction phi:=") + args.append(self.incident_phi) + args.append("Vertical Incident Polarization:=") + args.append(self.vertical_polarization) + else: + args.append("Launch from Custom") + args.append("LaunchFromPointID:=") + args.append(-1) + args.append("CustomLocationCoordSystem:=") + args.append(self.custom_coordinatesystem) + args.append("CustomLocation:=") + args.append(self.custom_location) + args.append("SBRRayDensity:=") + args.append(self.ray_density) + return args + + @pyaedt_function_handler() + def create(self): + """Create a field plot. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + try: + if self.is_creeping_wave: + self._ofield.CreateFieldPlot(self._create_args_creeping(), "CreepingWave_VRT") + else: + self._ofield.CreateFieldPlot(self._create_args(), "VRT") + return True + except Exception: + return False + + @pyaedt_function_handler() + def update(self): + """Update the field plot. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + try: + if self.is_creeping_wave: + self._ofield.ModifyFieldPlot(self.name, self._create_args_creeping()) + + else: + self._ofield.ModifyFieldPlot(self.name, self._create_args()) + return True + except Exception: + return False + + @pyaedt_function_handler() + def delete(self): + """Delete the field plot.""" + self._ofield.DeleteFieldPlot([self.name]) + return True + + @pyaedt_function_handler(path_to_hdm_file="path") + def export(self, path=None): + """Export the Visual Ray Tracing to ``hdm`` file. + + Parameters + ---------- + path : str, optional + Full path to the output file. The default is ``None``, in which case the file is + exported to the working directory. + + Returns + ------- + str + Path to the file. + """ + if not path: + path = os.path.join(self._postprocessor._app.working_directory, self.name + ".hdm") + self._ofield.ExportFieldPlot(self.name, False, path) + return path diff --git a/src/ansys/aedt/core/visualization/report/__init__.py b/src/ansys/aedt/core/visualization/report/__init__.py new file mode 100644 index 00000000000..9c4476773da --- /dev/null +++ b/src/ansys/aedt/core/visualization/report/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/src/ansys/aedt/core/visualization/report/common.py b/src/ansys/aedt/core/visualization/report/common.py new file mode 100644 index 00000000000..884e583eba6 --- /dev/null +++ b/src/ansys/aedt/core/visualization/report/common.py @@ -0,0 +1,2662 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import copy +import os + +from ansys.aedt.core.generic.constants import LineStyle +from ansys.aedt.core.generic.constants import SymbolStyle +from ansys.aedt.core.generic.constants import TraceType +from ansys.aedt.core.generic.general_methods import generate_unique_name +from ansys.aedt.core.generic.general_methods import pyaedt_function_handler +from ansys.aedt.core.generic.general_methods import write_configuration_file +from ansys.aedt.core.modeler.geometry_operators import GeometryOperators + + +class LimitLine(object): + """Line Limit Management Class.""" + + def __init__(self, report_setup, trace_name, oo=None): + self._oo = oo + self._oreport_setup = report_setup + self.line_name = trace_name + self.LINESTYLE = LineStyle() + + @property + def properties(self): + """Line properties. + + Returns + ------- + :class:`ansys.aedt.core.modeler.cad.elements_3d.BinaryTree` when successful, + ``False`` when failed. + + """ + from ansys.aedt.core.modeler.cad.elements_3d import BinaryTreeNode + + try: + parent = BinaryTreeNode(self.line_name, self._oo, False) + return parent.props + except Exception: + return [] + + @pyaedt_function_handler() + def _change_property(self, props_value): + self._oreport_setup.ChangeProperty( + ["NAME:AllTabs", ["NAME:Limit Line", ["NAME:PropServers", self.line_name], props_value]] + ) + return True + + @pyaedt_function_handler() + def set_line_properties( + self, style=None, width=None, hatch_above=None, violation_emphasis=None, hatch_pixels=None, color=None + ): + """Set trace properties. + + Parameters + ---------- + style : str, optional + Style for the limit line. The default is ``None``. You can also use + the ``LIFESTYLE`` property. + width : int, optional + Width of the limit line. The default is ``None``. + hatch_above : bool + Whether the hatch is above the limit line. The default is ``None``. + violation_emphasis : bool + Whether to add violation emphasis. The default is ``None``. + hatch_pixels : int + Number of pixels for the hatch. The default is ``None``. + color : tuple, list + Trace color as a tuple (R,G,B) or a list of integers [0,255]. + The default is ``None``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + props = ["NAME:ChangedProps"] + if style: + props.append(["NAME:Line Style", "Value:=", style]) + if width and isinstance(width, (int, float, str)): + props.append(["NAME:Line Width", "Value:=", str(width)]) + if hatch_above is not None and isinstance(hatch_pixels, (int, str)): + props.append(["NAME:Hatch Above", "Value:=", hatch_above]) + if hatch_pixels and isinstance(hatch_pixels, (int, str)): + props.append(["NAME:Hatch Pixels", "Value:=", str(hatch_pixels)]) + if violation_emphasis: + props.append(["NAME:Violation Emphasis", "Value:=", violation_emphasis]) + if color and isinstance(color, (list, tuple)) and len(color) == 3: + props.append(["NAME:Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) + return self._change_property(props) + + +class Note(object): + """Note Management Class.""" + + def __init__(self, report_setup, plot_note_name, oo=None): + self._oo = oo + self._oreport_setup = report_setup + self.plot_note_name = plot_note_name + + @property + def properties(self): + """Note properties. + + Returns + ------- + :class:`ansys.aedt.core.modeler.cad.elements_3d.BinaryTree` when successful, + ``False`` when failed. + + """ + from ansys.aedt.core.modeler.cad.elements_3d import BinaryTreeNode + + try: + parent = BinaryTreeNode(self.plot_note_name, self._oo, False) + return parent.props + except Exception: + return [] + + @pyaedt_function_handler() + def _change_property(self, props_value): + prop_server_name = self.plot_note_name + self._oreport_setup.ChangeProperty( + ["NAME:AllTabs", ["NAME:Note", ["NAME:PropServers", prop_server_name], props_value]] + ) + return True + + @pyaedt_function_handler() + def set_note_properties( + self, + text=None, + back_color=None, + background_visibility=None, + border_color=None, + border_visibility=None, + border_width=None, + font="Arial", + font_size=12, + italic=False, + bold=False, + color=(0, 0, 0), + ): + """Set note properties. + + Parameters + ---------- + text : str, optional + Style for the limit line. The default is ``None``. You can also use + the ``LIFESTYLE`` property. + back_color : int + Background color specified as a tuple (R,G,B) or a list of integers [0,255]. + The default is ``None``. + background_visibility : bool + Whether to view background. The default is ``None``. + border_color : int + Trace color specified as a tuple (R,G,B) or a list of integers [0,255]. + The default is ``None``. + border_visibility : bool + Whether to view text border. The default is ``None``. + The default is ``None``. + border_width : int + Text boarder width. + The default is ``None``. + font : str, optional + The default is ``None``. + font_size : int, optional + The default is ``None``. + italic : bool + Whether the text is italic. + The default is ``None``. + bold : bool + Whether the text is bold. + The default is ``None``. + color : int =(0, 0, 0) + Trace color specified as a tuple (R,G,B) or a list of integers [0,255]. + The default is ``None``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + props = ["NAME:ChangedProps"] + if text: + props.append(["NAME:Note Text", "Value:=", text]) + if back_color and isinstance(back_color, (list, tuple)) and len(back_color) == 3: + props.append(["NAME:Back Color", "R:=", back_color[0], "G:=", back_color[1], "B:=", back_color[2]]) + if background_visibility is not None: + props.append(["NAME:Background Visibility", "Value:=", background_visibility]) + if border_color and isinstance(border_color, (list, tuple)) and len(border_color) == 3: + props.append(["NAME:Border Color", "R:=", border_color[0], "G:=", border_color[1], "B:=", border_color[2]]) + if border_visibility is not None: + props.append(["NAME:Border Visibility", "Value:=", border_visibility]) + if border_width and isinstance(border_width, (int, float)): + props.append(["NAME:Border Width", "Value:=", str(border_width)]) + + font_props = [ + "NAME:Note Font", + "Height:=", + -1 * font_size - 2, + "Width:=", + 0, + "Escapement:=", + 0, + "Orientation:=", + 0, + "Weight:=", + 700 if bold else 400, + "Italic:=", + 255 if italic else 0, + "Underline:=", + 0, + "StrikeOut:=", + 0, + "CharSet:=", + 0, + "OutPrecision:=", + 3, + "ClipPrecision:=", + 2, + "Quality:=", + 1, + "PitchAndFamily:=", + 34, + "FaceName:=", + font, + "R:=", + color[0], + "G:=", + color[1], + "B:=", + color[2], + ] + props.append(font_props) + return self._change_property(props) + + +class Trace(object): + """Provides trace management.""" + + def __init__( + self, + report_setup, + aedt_name, + trace_name, + oo=None, + ): + self._oo = oo + self._oreport_setup = report_setup + self.aedt_name = aedt_name + self._name = trace_name + self.LINESTYLE = LineStyle() + self.TRACETYPE = TraceType() + self.SYMBOLSTYLE = SymbolStyle() + self._trace_style = None + self._trace_width = None + self._trace_color = None + self._symbol_style = None + self._show_arrows = None + self._fill_symbol = None + self._symbol_color = None + self._show_symbol = False + self._available_props = [] + + @property + def __all_props(self): + from ansys.aedt.core.modeler.cad.elements_3d import BinaryTreeNode + + try: + parent = BinaryTreeNode(self.aedt_name, self._oo, False) + return parent + except Exception: + return [] + + @property + def properties(self): + """All available properties. + + Returns + ------- + :class:`ansys.aedt.core.modeler.cad.elements_3d.BinaryTree` when successful, + ``False`` when failed. + + """ + try: + return self.__all_props.props + except Exception: + return {} + + @property + def curve_properties(self): + """All curve graphical properties. It includes colors, trace and symbol settings. + + Returns + ------- + :class:`ansys.aedt.core.modeler.cad.elements_3d.BinaryTree` when successful, + ``False`` when failed. + + """ + if self.aedt_name.split(":")[-1] in self.__all_props.children: + return self.__all_props.children[self.aedt_name.split(":")[-1]].props + return {} + + @property + def name(self): + """Trace name. + + Returns + ------- + str + Trace name. + """ + return self._name + + @name.setter + def name(self, value): + report_name = self.aedt_name.split(":")[0] + prop_name = report_name + ":" + self.name + + self._oreport_setup.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:Trace", + ["NAME:PropServers", prop_name], + ["NAME:ChangedProps", ["NAME:Specify Name", "Value:=", True]], + ], + ] + ) + self._oreport_setup.ChangeProperty( + [ + "NAME:AllTabs", + ["NAME:Trace", ["NAME:PropServers", prop_name], ["NAME:ChangedProps", ["NAME:Name", "Value:=", value]]], + ] + ) + self.aedt_name = self.aedt_name.replace(self.name, value) + self.trace_name = value + + @pyaedt_function_handler() + def _change_property(self, props_value): + self._oreport_setup.ChangeProperty( + ["NAME:AllTabs", ["NAME:Attributes", ["NAME:PropServers", self.aedt_name], props_value]] + ) + return True + + @pyaedt_function_handler(trace_style="style") + def set_trace_properties(self, style=None, width=None, trace_type=None, color=None): + """Set trace properties. + + Parameters + ---------- + style : str, optional + Style for the trace line. The default is ``None``. You can also use + the ``LINESTYLE`` property. + width : int, optional + Width of the trace line. The default is ``None``. + trace_type : str + Type of the trace line. The default is ``None``. You can also use the ``TRACETYPE`` + property. + color : tuple, list + Trace line color specified as a tuple (R,G,B) or a list of integers [0,255]. + The default is ``None``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + props = ["NAME:ChangedProps"] + if style: + props.append(["NAME:Line Style", "Value:=", style]) + if width and isinstance(width, (int, float, str)): + props.append(["NAME:Line Width", "Value:=", str(width)]) + if trace_type: + props.append(["NAME:Trace Type", "Value:=", trace_type]) + if color and isinstance(color, (list, tuple)) and len(color) == 3: + props.append(["NAME:Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) + return self._change_property(props) + + @pyaedt_function_handler() + def set_symbol_properties(self, show=True, style=None, show_arrows=None, fill=None, color=None): + """Set symbol properties. + + Parameters + ---------- + show : bool, optional + Whether to show the symbol. The default is ``True``. + style : str, optional + Style of the style. The default is ``None``. You can also use the ``SYMBOLSTYLE`` + property. + show_arrows : bool, optional + Whether to show arrows. The default is ``None``. + fill : bool, optional + Whether to fill the symbol with a color. The default is ``None``. + color : tuple, list + Symbol fill color specified as a tuple (R,G,B) or a list of integers [0,255]. + The default is ``None``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + props = ["NAME:ChangedProps", ["NAME:Show Symbol", "Value:=", show]] + if style: + props.append(["NAME:Symbol Style", "Value:=", style]) + if show_arrows: + props.append(["NAME:Show Arrows", "Value:=", show_arrows]) + if fill: + props.append(["NAME:Fill Symbol", "Value:=", fill]) + if color and isinstance(color, (list, tuple)) and len(color) == 3: + props.append(["NAME:Symbol Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) + return self._change_property(props) + + +class CommonReport(object): + """Provides common reports.""" + + def __init__(self, app, report_category, setup_name, expressions=None): + self._post = app + self._props = {} + self._props["report_category"] = report_category + self.setup = setup_name + self._props["report_type"] = "Rectangular Plot" + self._props["context"] = {} + self._props["context"]["domain"] = "Sweep" + self._props["context"]["primary_sweep"] = "Freq" + self._props["context"]["primary_sweep_range"] = ["All"] + self._props["context"]["secondary_sweep_range"] = ["All"] + self._props["context"]["variations"] = {"Freq": ["All"]} + if hasattr(self._post._app, "available_variations") and self._post._app.available_variations: + for el, k in self._post._app.available_variations.nominal_w_values_dict.items(): + self._props["context"]["variations"][el] = k + self._props["expressions"] = None + self._props["plot_name"] = None + if expressions: + self.expressions = expressions + self._is_created = False + self.siwave_dc_category = 0 + self._traces = [] + self._child_object = None + + @property + def _all_props(self): + if self._child_object: + return self._child_object + from ansys.aedt.core.modeler.cad.elements_3d import BinaryTreeNode + + try: + oo = self._post.oreportsetup.GetChildObject(self._props["plot_name"]) + self._child_object = BinaryTreeNode(self.plot_name, oo, False) + for var in [i.split(" ,")[-1] for i in list(self._child_object.props.values())[4:]]: + if var in self._child_object.children: + del self._child_object.children[var] + els = [i for i in self._child_object.children.keys() if i.startswith("LimitLine") or i.startswith("Note")] + for var in els: + del self._child_object.children[var] + return self._child_object + except Exception: + return {} + + @property + def properties(self): + """Report properties. + + Returns + ------- + :class:`ansys.aedt.core.modeler.cad.elements_3d.BinaryTree` when successful, + ``False`` when failed. + + """ + try: + return self._all_props + except Exception: + return {} + + @pyaedt_function_handler() + def delete(self): + """Delete current report.""" + self._post.oreportsetup.DeleteReports([self.plot_name]) + for i in self._post.plots: + if i.plot_name == self.plot_name: + del i + break + return True + + @property + def differential_pairs(self): + """Differential pairs flag. + + Returns + ------- + bool + ``True`` when differential pairs is enabled, ``False`` otherwise. + """ + return self._props["context"].get("differential_pairs", False) + + @differential_pairs.setter + def differential_pairs(self, value): + self._props["context"]["differential_pairs"] = value + + @property + def matrix(self): + """Maxwell 2D/3D or Q2D/Q3D matrix name. + + Returns + ------- + str + Matrix name. + """ + if self._is_created and ( + self._post._app.design_type in ["Q3D Extractor", "2D Extractor"] + or ( + self._post._app.design_type in ["Maxwell 2D", "Maxwell 3D"] + and self._post._app.solution_type == "EddyCurrent" + ) + ): + try: + if "Parameter" in self.traces[0].properties: + self._props["context"]["matrix"] = self.traces[0].properties["Parameter"] + elif "Matrix" in self.traces[0].properties: + self._props["context"]["matrix"] = self.traces[0].properties["Matrix"] + except Exception: + self._post._app.logger.warning("Property `matrix` not found.") + return self._props["context"].get("matrix", None) + + @matrix.setter + def matrix(self, value): + self._props["context"]["matrix"] = value + + @property + def reduced_matrix(self): + """Maxwell 2D/3D reduced matrix name for eddy current solvers. + + Returns + ------- + str + Reduced matrix name. + """ + return self._props["context"].get("reduced_matrix", None) + + @reduced_matrix.setter + def reduced_matrix(self, value): + self._props["context"]["reduced_matrix"] = value + + @property + def polyline(self): + """Polyline name for the field report. + + Returns + ------- + str + Polyline name. + """ + if self._is_created and self.report_category != "Far Fields" and self.report_category.endswith("Fields"): + try: + self._props["context"]["polyline"] = self.traces[0].properties["Geometry"] + except Exception: + pass + return self._props["context"].get("polyline", None) + + @polyline.setter + def polyline(self, value): + self._props["context"]["polyline"] = value + + @property + def expressions(self): + """Expressions. + + Returns + ------- + list + Expressions. + """ + if self._is_created: + return [i.split(" ,")[-1] for i in list(self.properties.props.values())[4:]] + if self._props.get("expressions", None) is None: + return [] + return [k.get("name", None) for k in self._props["expressions"] if k.get("name", None) is not None] + + @expressions.setter + def expressions(self, value): + if isinstance(value, dict): + self._props["expressions"].append(value) + elif isinstance(value, list): + self._props["expressions"] = [] + for el in value: + if isinstance(el, dict): + self._props["expressions"].append(el) + else: + self._props["expressions"].append({"name": el}) + elif isinstance(value, str): + if isinstance(self._props["expressions"], list): + self._props["expressions"].append({"name": value}) + else: + self._props["expressions"] = [{"name": value}] + + @property + def report_category(self): + """Report category. + + Returns + ------- + str + Report category. + """ + if self._is_created: + try: + return self.properties.props["Report Type"] + except Exception: + return self._props["report_type"] + return self._props["report_category"] + + @report_category.setter + def report_category(self, value): + if not self._is_created: + self._props["report_category"] = value + + @property + def report_type(self): + """Report type. Options are ``"3D Polar Plot"``, ``"3D Spherical Plot"``, + ``"Radiation Pattern"``, ``"Rectangular Plot"``, ``"Data Table"``, + ``"Smith Chart"``, and ``"Rectangular Contour Plot"``. + + Returns + ------- + str + Report type. + """ + if self._is_created: + try: + return self.properties.props["Display Type"] + except Exception: + return self._props["report_type"] + return self._props["report_type"] + + @report_type.setter + def report_type(self, report): + if not self._is_created: + self._props["report_type"] = report + if not self.primary_sweep: + if self._props["report_type"] in ["3D Polar Plot", "3D Spherical Plot"]: + self.primary_sweep = "Phi" + self.secondary_sweep = "Theta" + elif self._props["report_type"] == "Radiation Pattern": + self.primary_sweep = "Phi" + elif self.domain == "Sweep": + self.primary_sweep = "Freq" + elif self.domain == "Time": + self.primary_sweep = "Time" + + @property + def traces(self): + """List of available traces in the report. + + .. note:: + This property works in version 2022 R1 and later. However, it works only in + non-graphical mode in version 2022 R2 and later. + + Returns + ------- + List of :class:`ansys.aedt.core.modules.report_templates.Trace` + """ + _ = self.expressions[::] + _traces = [] + try: + oo = self._post.oreportsetup.GetChildObject(self.plot_name) + oo_names = self._post.oreportsetup.GetChildObject(self.plot_name).GetChildNames() + except Exception: + return _traces + for el in oo_names: + if "Families" not in oo.GetChildObject(el).GetPropNames(): + continue + try: + oo1 = oo.GetChildObject(el) + oo1_name = oo1.GetChildNames() + if not oo1_name: + aedt_name = "{}:{}".format(self.plot_name, el) + _traces.append(Trace(self._post.oreportsetup, aedt_name, el, oo1)) + else: + for i in oo1_name: + aedt_name = "{}:{}:{}".format(self.plot_name, el, i) + _traces.append(Trace(self._post.oreportsetup, aedt_name, el, oo1)) + except Exception: + self._post._app.logger.debug("Something went wrong while processing element {}.".format(el)) + return _traces + + @pyaedt_function_handler() + def _update_traces(self): + for trace in self.traces[::]: + trace_name = trace.name + for trace_val in self._props["expressions"]: + if trace_val["name"] == trace_name: + trace_style = self.__props_with_default(trace_val, "trace_style") + trace_width = self.__props_with_default(trace_val, "width") + trace_type = self.__props_with_default(trace_val, "trace_type") + trace_color = self.__props_with_default(trace_val, "color") + + if trace_style or trace_width or trace_type or trace_color: + trace.set_trace_properties( + style=trace_style, width=trace_width, trace_type=trace_type, color=trace_color + ) + for trace in self.traces[::]: + trace_name = trace.name + for trace_val in self._props["expressions"]: + if trace_val["name"] == trace_name: + if self.report_category in ["Eye Diagram", "Spectrum"]: + continue + symbol_show = self.__props_with_default(trace_val, "show_symbols", False) + symbol_style = self.__props_with_default(trace_val, "symbol_style", None) + symbol_arrows = self.__props_with_default(trace_val, "show_arrows", None) + symbol_fill = self.__props_with_default(trace_val, "symbol_fill", False) + symbol_color = self.__props_with_default(trace_val, "symbol_color", None) + if symbol_style or symbol_color or symbol_fill or symbol_arrows: + trace.set_symbol_properties( + show=symbol_show, + style=symbol_style, + show_arrows=symbol_arrows, + fill=symbol_fill, + color=symbol_color, + ) + for trace in self.traces[::]: + trace_name = trace.name + for trace_val in self._props["expressions"]: + if trace_val["name"] == trace_name: + y_axis = self.__props_with_default(trace_val, "y_axis", "Y1") + if y_axis != "Y1": + self._change_property( + "Trace", + trace_name, + [ + "NAME:ChangedProps", + [ + "NAME:Y Axis", + "Value:=", + y_axis, + ], + ], + ) + + if ( + "eye_mask" in self._props + and self.report_category in ["Eye Diagram", "Statistical Eye"] + or ("quantity_type" in self._props and self.report_type == "Rectangular Contour Plot") + ): + eye_xunits = self.__props_with_default(self._props["eye_mask"], "xunits", "ns") + eye_yunits = self.__props_with_default(self._props["eye_mask"], "yunits", "mV") + eye_points = self.__props_with_default(self._props["eye_mask"], "points") + eye_enable = self.__props_with_default(self._props["eye_mask"], "enable_limits", False) + eye_upper = self.__props_with_default(self._props["eye_mask"], "upper_limit", 500) + eye_lower = self.__props_with_default(self._props["eye_mask"], "lower_limit", 0.3) + eye_transparency = self.__props_with_default(self._props["eye_mask"], "transparency", 0.3) + eye_color = self.__props_with_default(self._props["eye_mask"], "color", (0, 128, 0)) + eye_xoffset = self.__props_with_default(self._props["eye_mask"], "X Offset", "0ns") + eye_yoffset = self.__props_with_default(self._props["eye_mask"], "Y Offset", "0V") + if "quantity_type" in self._props and self.report_type == "Rectangular Contour Plot": + if "contours_number" in self._props.get("general", {}): + self._change_property( + "Contour", + " Plot {}".format(self.traces[0].name), + [ + "NAME:ChangedProps", + ["NAME:Num. Contours", "Value:=", str(self._props["general"]["contours_number"])], + ], + ) + if "contours_scale" in self._props.get("general", {}): + self._change_property( + "Contour", + " Plot {}".format(self.traces[0].name), + [ + "NAME:ChangedProps", + ["NAME:Axis Scale", "Value:=", str(self._props["general"]["contours_scale"])], + ], + ) + if "enable_contours_auto_limit" in self._props.get("general", {}): + self._change_property( + "Contour", + " Plot {}".format(self.traces[0].name), + ["NAME:ChangedProps", ["NAME:Scale Type", "Value:=", "Auto Limits"]], + ) + elif "contours_min_limit" in self._props.get("general", {}): + self._change_property( + "Contour", + " Plot {}".format(self.traces[0].name), + [ + "NAME:ChangedProps", + ["NAME:Min", "Value:=", str(self._props["general"]["contours_min_limit"])], + ], + ) + elif "contours_max_limit" in self._props.get("general", {}): + self._change_property( + "Contour", + " Plot {}".format(self.traces[0].name), + [ + "NAME:ChangedProps", + ["NAME:Max", "Value:=", str(self._props["general"]["contours_max_limit"])], + ], + ) + self.eye_mask( + points=eye_points, + x_units=eye_xunits, + y_units=eye_yunits, + enable_limits=eye_enable, + upper_limit=eye_upper, + lower_limit=eye_lower, + color=eye_color, + transparency=eye_transparency, + x_offset=eye_xoffset, + y_offset=eye_yoffset, + ) + if "limitLines" in self._props and self.report_category not in ["Eye Diagram", "Statistical Eye"]: + for line in self._props["limitLines"].values(): + if "equation" in line: + line_start = self.__props_with_default(line, "start") + line_stop = self.__props_with_default(line, "stop") + line_step = self.__props_with_default(line, "step") + line_equation = self.__props_with_default(line, "equation") + line_axis = self.__props_with_default(line, "y_axis", 1) + if not line_start or not line_step or not line_stop or not line_equation: + self._post._app.logger.error( + "Equation Limit Lines needs Start, Stop, Step and Equation fields." + ) + continue + self.add_limit_line_from_equation( + start_x=line_start, stop_x=line_stop, step=line_step, equation=line_equation, y_axis=line_axis + ) + else: + line_x = self.__props_with_default(line, "xpoints") + line_y = self.__props_with_default(line, "ypoints") + line_xunits = self.__props_with_default(line, "xunits") + line_yunits = self.__props_with_default(line, "yunits", "") + line_axis = self.__props_with_default(line, "y_axis", "Y1") + self.add_limit_line_from_points(line_x, line_y, line_xunits, line_yunits, line_axis) + line_style = self.__props_with_default(line, "trace_style") + line_width = self.__props_with_default(line, "width") + line_hatchabove = self.__props_with_default(line, "hatch_above") + line_viol = self.__props_with_default(line, "violation_emphasis") + line_hatchpix = self.__props_with_default(line, "hatch_pixels") + line_color = self.__props_with_default(line, "color") + self.limit_lines[-1].set_line_properties( + style=line_style, + width=line_width, + hatch_above=line_hatchabove, + violation_emphasis=line_viol, + hatch_pixels=line_hatchpix, + color=line_color, + ) + if "notes" in self._props: + for note in self._props["notes"].values(): + note_text = self.__props_with_default(note, "text") + note_position = self.__props_with_default(note, "position", [0, 0]) + self.add_note(note_text, note_position[0], note_position[1]) + note_back_color = self.__props_with_default(note, "background_color") + note_background_visibility = self.__props_with_default(note, "background_visibility") + note_border_color = self.__props_with_default(note, "border_color") + note_border_visibility = self.__props_with_default(note, "border_visibility") + note_border_width = self.__props_with_default(note, "border_width") + note_font = self.__props_with_default(note, "font", "Arial") + note_font_size = self.__props_with_default(note, "font_size", 12) + note_italic = self.__props_with_default(note, "italic") + note_bold = self.__props_with_default(note, "bold") + note_color = self.__props_with_default(note, "color", (0, 0, 0)) + + self.notes[-1].set_note_properties( + back_color=note_back_color, + background_visibility=note_background_visibility, + border_color=note_border_color, + border_visibility=note_border_visibility, + border_width=note_border_width, + font=note_font, + font_size=note_font_size, + italic=note_italic, + bold=note_bold, + color=note_color, + ) + if "general" in self._props: + if "show_rectangular_plot" in self._props["general"] and self.report_category in ["Eye Diagram"]: + eye_rectangular = self.__props_with_default(self._props["general"], "show_rectangular_plot", True) + self.rectangular_plot(eye_rectangular) + if "legend" in self._props["general"] and self.report_type != "Rectangular Contour Plot": + legend = self._props["general"]["legend"] + legend_sol_name = self.__props_with_default(legend, "show_solution_name", True) + legend_var_keys = self.__props_with_default(legend, "show_variation_key", True) + leend_trace_names = self.__props_with_default(legend, "show_trace_name", True) + legend_color = self.__props_with_default(legend, "back_color", (255, 255, 255)) + legend_font_color = self.__props_with_default(legend, "font_color", (0, 0, 0)) + self.edit_legend( + show_solution_name=legend_sol_name, + show_variation_key=legend_var_keys, + show_trace_name=leend_trace_names, + back_color=legend_color, + font_color=legend_font_color, + ) + if "grid" in self._props["general"]: + grid = self._props["general"]["grid"] + grid_major_color = self.__props_with_default(grid, "major_color", (200, 200, 200)) + grid_minor_color = self.__props_with_default(grid, "minor_color", (230, 230, 230)) + grid_enable_major_x = self.__props_with_default(grid, "major_x", True) + grid_enable_major_y = self.__props_with_default(grid, "major_y", True) + grid_enable_minor_x = self.__props_with_default(grid, "minor_x", True) + grid_enable_minor_y = self.__props_with_default(grid, "minor_y", True) + grid_style_minor = self.__props_with_default(grid, "style_minor", "Solid") + grid_style_major = self.__props_with_default(grid, "style_major", "Solid") + self.edit_grid( + minor_x=grid_enable_minor_x, + minor_y=grid_enable_minor_y, + major_x=grid_enable_major_x, + major_y=grid_enable_major_y, + minor_color=grid_minor_color, + major_color=grid_major_color, + style_minor=grid_style_minor, + style_major=grid_style_major, + ) + if "appearance" in self._props["general"]: + general = self._props["general"]["appearance"] + general_back_color = self.__props_with_default(general, "background_color", (255, 255, 255)) + general_plot_color = self.__props_with_default(general, "plot_color", (255, 255, 255)) + enable_y_stripes = self.__props_with_default(general, "enable_y_stripes", True) + if self._props["report_type"] == "Radiation Pattern": + enable_y_stripes = None + general_field_width = self.__props_with_default(general, "field_width", 4) + general_precision = self.__props_with_default(general, "precision", 4) + general_use_scientific_notation = self.__props_with_default(general, "use_scientific_notation", True) + self.edit_general_settings( + background_color=general_back_color, + plot_color=general_plot_color, + enable_y_stripes=enable_y_stripes, + field_width=general_field_width, + precision=general_precision, + use_scientific_notation=general_use_scientific_notation, + ) + if "header" in self._props["general"]: + header = self._props["general"]["header"] + company_name = self.__props_with_default(header, "company_name", "") + show_design_name = self.__props_with_default(header, "show_design_name", True) + header_font = self.__props_with_default(header, "font", "Arial") + header_title_size = self.__props_with_default(header, "title_size", 12) + header_subtitle_size = self.__props_with_default(header, "subtitle_size", 12) + header_italic = self.__props_with_default(header, "italic", False) + header_bold = self.__props_with_default(header, "bold", False) + header_color = self.__props_with_default(header, "color", (0, 0, 0)) + self.edit_header( + company_name=company_name, + show_design_name=show_design_name, + font=header_font, + title_size=header_title_size, + subtitle_size=header_subtitle_size, + italic=header_italic, + bold=header_bold, + color=header_color, + ) + + for i in list(self._props["general"].keys()): + if "axis" in i: + axis = self._props["general"][i] + axis_font = self.__props_with_default(axis, "font", "Arial") + axis_size = self.__props_with_default(axis, "font_size", 12) + axis_italic = self.__props_with_default(axis, "italic", False) + axis_bold = self.__props_with_default(axis, "bold", False) + axis_color = self.__props_with_default(axis, "color", (0, 0, 0)) + axis_label = self.__props_with_default(axis, "label") + axis_linear_scaling = self.__props_with_default(axis, "linear_scaling", True) + axis_min_scale = self.__props_with_default(axis, "min_scale") + axis_max_scale = self.__props_with_default(axis, "max_scale") + axis_min_trick_div = self.__props_with_default(axis, "minor_tick_divs", 5) + specify_spacing = self.__props_with_default(axis, "specify_spacing", True) + if not specify_spacing: + axis_min_spacing = None + else: + axis_min_spacing = self.__props_with_default(axis, "min_spacing") + axis_units = self.__props_with_default(axis, "units") + if i == "axisx": + self.edit_x_axis( + font=axis_font, + font_size=axis_size, + italic=axis_italic, + bold=axis_bold, + color=axis_color, + label=axis_label, + ) + if self.report_category in ["Eye Diagram", "Statistical Eye"]: + continue + self.edit_x_axis_scaling( + linear_scaling=axis_linear_scaling, + min_scale=axis_min_scale, + max_scale=axis_max_scale, + minor_tick_divs=axis_min_trick_div, + min_spacing=axis_min_spacing, + units=axis_units, + ) + else: + self.edit_y_axis( + font=axis_font, + font_size=axis_size, + italic=axis_italic, + bold=axis_bold, + color=axis_color, + label=axis_label, + ) + if self.report_category in ["Eye Diagram", "Statistical Eye"]: + continue + self.edit_y_axis_scaling( + name=i.replace("axis", "").upper(), + linear_scaling=axis_linear_scaling, + min_scale=axis_min_scale, + max_scale=axis_max_scale, + minor_tick_divs=axis_min_trick_div, + min_spacing=axis_min_spacing, + units=axis_units, + ) + + @property + def limit_lines(self): + """List of available limit lines in the report. + + .. note:: + This property works in version 2022 R1 and later. However, it works only in + non-graphical mode in version 2022 R2 and later. + + Returns + ------- + List of :class:`ansys.aedt.core.modules.report_templates.LimitLine` + """ + _traces = [] + oo_names = self._post._app.get_oo_name(self._post.oreportsetup, self.plot_name) + for el in oo_names: + if "LimitLine" in el: + _traces.append( + LimitLine( + self._post.oreportsetup, + "{}:{}".format(self.plot_name, el), + self._post.oreportsetup.GetChildObject(self.plot_name).GetChildObject(el), + ) + ) + + return _traces + + @property + def notes(self): + """List of available notes in the report. + + .. note:: + This property works in version 2022 R1 and later. However, it works only in + non-graphical mode in version 2022 R2 and later. + + Returns + ------- + List of :class:`ansys.aedt.core.modules.report_templates.Note` + """ + _notes = [] + try: + oo_names = self._post.oreportsetup.GetChildObject(self.plot_name).GetChildNames() + except Exception: + return _notes + for el in oo_names: + if "Note" in el: + _notes.append( + Note( + self._post.oreportsetup, + "{}:{}".format(self.plot_name, el), + self._post.oreportsetup.GetChildObject(self.plot_name).GetChildObject(el), + ) + ) + + return _notes + + @property + def plot_name(self): + """Plot name. + + Returns + ------- + str + Plot name. + """ + return self._props["plot_name"] + + @plot_name.setter + def plot_name(self, name): + if self._is_created: + if name not in self._post.oreportsetup.GetAllReportNames(): + self._post.oreportsetup.RenameReport(self._props["plot_name"], name) + self._props["plot_name"] = name + + @property + def variations(self): + """Variations. + + Returns + ------- + str + Variations. + """ + if self._is_created: + try: + variations = {} + for tr in self.traces: + for fam in tr.properties["Families"]: + k = 0 + while k < len(fam): + key = fam[k][:-2] + v = fam[k + 1] + if key in variations: + variations[key].extend(v) + variations[key] = list(set(variations[key])) + else: + variations[key] = v + k += 2 + variations[tr.properties["Primary Sweep"]] = ["All"] + if tr.properties.get("Secondary Sweep", None): + variations[tr.properties["Secondary Sweep"]] = ["All"] + self._props["context"]["variations"] = variations + except Exception: + pass + return self._props["context"]["variations"] + + @variations.setter + def variations(self, value): + + self._props["context"]["variations"] = value + + @property + def primary_sweep(self): + """Primary sweep report. + + Returns + ------- + str + Primary sweep. + """ + if self._is_created: + return list(self.properties.props.values())[4].split(" ,")[0] + return self._props["context"]["primary_sweep"] + + @primary_sweep.setter + def primary_sweep(self, value): + if value == self._props["context"].get("secondary_sweep", None): + self._props["context"]["secondary_sweep"] = self._props["context"]["primary_sweep"] + self._props["context"]["primary_sweep"] = value + if value == "Time": + self.variations.pop("Freq", None) + self.variations["Time"] = ["All"] + elif value == "Freq": + self.variations.pop("Time", None) + self.variations["Freq"] = ["All"] + + @property + def secondary_sweep(self): + """Secondary sweep report. + + Returns + ------- + str + Secondary sweep. + """ + if self._is_created: + els = list(self.properties.props.values())[4].split(" ,") + + return els[1] if len(els) == 3 else None + return self._props["context"].get("secondary_sweep", None) + + @secondary_sweep.setter + def secondary_sweep(self, value): + if value == self._props["context"]["primary_sweep"]: + self._props["context"]["primary_sweep"] = self._props["context"]["secondary_sweep"] + self._props["context"]["secondary_sweep"] = value + if value == "Time": + self.variations.pop("Freq", None) + self.variations["Time"] = ["All"] + elif value == "Freq": + self.variations.pop("Time", None) + self.variations["Freq"] = ["All"] + + @property + def primary_sweep_range(self): + """Primary sweep range report. + + Returns + ------- + str + Primary sweep range. + """ + return self._props["context"]["primary_sweep_range"] + + @primary_sweep_range.setter + def primary_sweep_range(self, value): + self._props["context"]["primary_sweep_range"] = value + + @property + def secondary_sweep_range(self): + """Secondary sweep range report. + + Returns + ------- + str + Secondary sweep range. + """ + return self._props["context"]["secondary_sweep_range"] + + @secondary_sweep_range.setter + def secondary_sweep_range(self, value): + self._props["context"]["secondary_sweep_range"] = value + + @property + def _context(self): + return [] + + @pyaedt_function_handler() + def update_expressions_with_defaults(self, quantities_category=None): + """Update the list of expressions by taking all quantities from a given category. + + Parameters + ---------- + quantities_category : str, optional + Quantities category to use. The default is ``None``, in which case the default + category for the specified report is used. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + self.expressions = self._post.available_report_quantities( + self.report_category, self.report_type, self.setup, quantities_category + ) + + @property + def _trace_info(self): + if not self.expressions: + self.update_expressions_with_defaults() + if isinstance(self.expressions, list): + expr = self.expressions + else: + expr = [self.expressions] + arg = ["X Component:=", self.primary_sweep, "Y Component:=", expr] + if self.report_type in ["3D Polar Plot", "3D Spherical Plot"]: + arg = [ + "Phi Component:=", + self.primary_sweep, + "Theta Component:=", + self.secondary_sweep, + "Mag Component:=", + expr, + ] + elif self.report_type == "Radiation Pattern": + arg = ["Ang Component:=", self.primary_sweep, "Mag Component:=", expr] + elif self.report_type in ["Smith Chart", "Polar Plot"]: + arg = ["Polar Component:=", expr] + elif self.report_type == "Rectangular Contour Plot": + arg = [ + "X Component:=", + self.primary_sweep, + "Y Component:=", + self.secondary_sweep, + "Z Component:=", + expr, + ] + return arg + + @property + def domain(self): + """Plot domain. + + Returns + ------- + str + Plot domain. + """ + if self._is_created: + try: + return self.traces[0].properties["Domain"] + except Exception: + pass + return self._props["context"]["domain"] + + @domain.setter + def domain(self, domain): + self._props["context"]["domain"] = domain + if self._post._app.design_type in ["Maxwell 3D", "Maxwell 2D"]: + return + if self.primary_sweep == "Freq" and domain == "Time": + self.primary_sweep = "Time" + self.variations.pop("Freq", None) + self.variations["Time"] = ["All"] + elif self.primary_sweep == "Time" and domain == "Sweep": + self.primary_sweep = "Freq" + self.variations.pop("Time", None) + self.variations["Freq"] = ["All"] + + @property + def use_pulse_in_tdr(self): + """Defines if the TDR should use a pulse or step. + + Returns + ------- + bool + ``True`` when option is enabled, ``False`` otherwise. + """ + return self._props["context"].get("use_pulse_in_tdr", False) + + @use_pulse_in_tdr.setter + def use_pulse_in_tdr(self, val): + self._props["context"]["use_pulse_in_tdr"] = val + + @pyaedt_function_handler() + def _convert_dict_to_report_sel(self, sweeps): + if not sweeps: + return [] + sweep_list = [] + if self.primary_sweep: + sweep_list.append(self.primary_sweep + ":=") + if self.primary_sweep_range == ["All"] and self.primary_sweep in self.variations: + sweep_list.append(self.variations[self.primary_sweep]) + else: + sweep_list.append(self.primary_sweep_range) + if self.secondary_sweep: + sweep_list.append(self.secondary_sweep + ":=") + if self.secondary_sweep_range == ["All"] and self.secondary_sweep in self.variations: + sweep_list.append(self.variations[self.secondary_sweep]) + else: + sweep_list.append(self.secondary_sweep_range) + for el in sweeps: + if el in [self.primary_sweep, self.secondary_sweep]: + continue + sweep_list.append(el + ":=") + if isinstance(sweeps[el], list): + sweep_list.append(sweeps[el]) + else: + sweep_list.append([sweeps[el]]) + for el in list(self._post._app.available_variations.nominal_w_values_dict.keys()): + if el not in sweeps: + sweep_list.append(el + ":=") + sweep_list.append(["Nominal"]) + return sweep_list + + @pyaedt_function_handler(plot_name="name") + def create(self, name=None): + """Create a report. + + Parameters + ---------- + name : str, optional + Name for the plot. The default is ``None``, in which case the + default name is used. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if not name: + self.plot_name = generate_unique_name("Plot") + else: + self.plot_name = name + if self.setup not in self._post._app.existing_analysis_sweeps and "AdaptivePass" not in self.setup: + self._post._app.logger.error("Setup doesn't exist in this design.") + return False + self._post.oreportsetup.CreateReport( + self.plot_name, + self.report_category, + self.report_type, + self.setup, + self._context, + self._convert_dict_to_report_sel(self.variations), + self._trace_info, + ) + self._post.plots.append(self) + self._is_created = True + return True + + @pyaedt_function_handler() + def _export_context(self, output_dict): + from ansys.aedt.core.visualization.report.eye import AMIEyeDiagram + from ansys.aedt.core.visualization.report.eye import EyeDiagram + from ansys.aedt.core.visualization.report.field import AntennaParameters + from ansys.aedt.core.visualization.report.field import FarField + from ansys.aedt.core.visualization.report.field import Fields + + output_dict["context"] = { + "domain": self.domain, + "primary_sweep": self.primary_sweep, + "primary_sweep_range": self.primary_sweep_range if self.primary_sweep_range else ["All"], + } + if isinstance(self, (FarField, AntennaParameters)): + output_dict["context"]["far_field_sphere"] = self.far_field_sphere + elif isinstance(self, Fields) and self.polyline: + output_dict["context"]["polyline"] = self.polyline + output_dict["context"]["point_number"] = self.point_number + elif self._post._app.design_type in ["Q3D Extractor", "2D Extractor"] or ( + self._post._app.design_type in ["Maxwell 2D", "Maxwell 3D"] + and self._post._app.solution_type == "EddyCurrent" + ): + output_dict["context"]["matrix"] = self.matrix + elif self.traces and any( + [i for i in self._post.available_report_quantities(differential_pairs=True) if i in self.traces[0].name] + ): + output_dict["context"]["differential_pairs"] = True + if self.secondary_sweep: + output_dict["context"]["secondary_sweep"] = self.secondary_sweep + output_dict["context"]["secondary_sweep_range"] = ( + self.secondary_sweep_range if self.secondary_sweep_range else ["All"] + ) + output_dict["context"]["variations"] = self.variations + + if isinstance(self, (AMIEyeDiagram, EyeDiagram)): + output_dict["context"]["unit_interval"] = self.unit_interval + output_dict["context"]["offset"] = self.offset + output_dict["context"]["auto_delay"] = self.auto_delay + output_dict["context"]["manual_delay"] = self.manual_delay + output_dict["context"]["auto_cross_amplitude"] = self.auto_cross_amplitude + output_dict["context"]["cross_amplitude"] = self.cross_amplitude + output_dict["context"]["auto_compute_eye_meas"] = self.auto_compute_eye_meas + output_dict["context"]["eye_measurement_point"] = self.eye_measurement_point + try: + trace_name = self.traces[0].name + if "Initial" in trace_name: + output_dict["quantity_type"] = 0 + elif "AfterSource" in trace_name: + output_dict["quantity_type"] = 1 + elif "AfterChannel" in trace_name: + output_dict["quantity_type"] = 2 + elif "AfterProbe" in trace_name: + output_dict["quantity_type"] = 3 + else: + output_dict["quantity_type"] = 0 + except Exception: + output_dict["quantity_type"] = 0 + if isinstance(self, EyeDiagram): + output_dict["context"]["time_start"] = self.time_start + output_dict["context"]["time_stop"] = self.time_stop + output_dict["context"]["thinning"] = self.thinning + output_dict["context"]["dy_dx_tolerance"] = self.dy_dx_tolerance + output_dict["context"]["thinning_points"] = self.thinning_points + + @pyaedt_function_handler() + def _export_expressions(self, output_dict): + output_dict["expressions"] = {} + for expr in self.traces: + name = self.properties.props[expr.name].split(" ,")[-1] + pr = expr.curve_properties + output_dict["expressions"][name] = {} + if "Trace Type" in pr: + output_dict["expressions"][name] = { + "color": [pr["Color/Red"], pr["Color/Green"], pr["Color/Blue"]], + "trace_style": pr["Line Style"], + "width": pr["Line Width"], + } + if "Y Axis" in expr.properties: + output_dict["expressions"][name]["y_axis"] = expr.properties["Y Axis"] + if "Show Symbol" in pr: + symbol_dict = { + "symbol_color": [pr["Symbol Color/Red"], pr["Symbol Color/Green"], pr["Symbol Color/Blue"]], + "show_symbols": pr["Show Symbol"], + "symbol_style": pr["Symbol Style"], + "symbol_fill": pr["Fill Symbol"], + "show_arrows": pr["Show Arrows"], + "symbol_frequency": pr["Symbol Frequency"], + } + output_dict["expressions"][name].update(symbol_dict) + + @pyaedt_function_handler() + def _export_graphical_objects(self, output_dict): + from ansys.aedt.core.visualization.report.eye import AMIEyeDiagram + from ansys.aedt.core.visualization.report.eye import EyeDiagram + + if isinstance(self, (AMIEyeDiagram, EyeDiagram)) and "EyeDisplayTypeProperty" in self.properties.children: + pr = self.properties.children["EyeDisplayTypeProperty"].props + if pr.get("Mask/MaskPoints", None) and len(pr["Mask/MaskPoints"]) > 1: + pts_x = pr["Mask/MaskPoints"][1::2] + pts_y = pr["Mask/MaskPoints"][2::2] + + output_dict["eye_mask"] = { + "xunits": pr["Mask/XUnits"], + "yunits": pr["Mask/YUnits"], + "points": [[i, j] for i, j in zip(pts_x, pts_y)], + "enable_limits": pr["Mask/ShowLimits"], + "upper_limit": pr["Mask/UpperLimit"], + "lower_limit": pr["Mask/LowerLimit"], + "color": [pr["Mask Fill Color/Red"], pr["Mask Fill Color/Green"], pr["Mask Fill Color/Blue"]], + "transparency": pr["Mask Trans/Transparency"], + } + + output_dict["limitLines"] = {} + for expr in self.limit_lines: + pr = expr.properties + name = expr.line_name + output_dict["limitLines"][name] = { + "color": [pr["Color/Red"], pr["Color/Green"], pr["Color/Blue"]], + "trace_style": pr["Line Style"], + "width": pr["Line Width"], + "hatch_above": pr["Hatch Above"], + "violation_emphasis": pr["Violation Emphasis"], + "hatch_pixels": pr["Hatch Pixels"], + "y_axis": pr["Y Axis"], + } + if "Point Data" in pr: # supported only point data + pdata = pr["Point Data"] + output_dict["limitLines"][name]["xunits"] = "" + output_dict["limitLines"][name]["yunits"] = "" + output_dict["limitLines"][name]["xpoints"] = pdata[-1][1::2] + output_dict["limitLines"][name]["ypoints"] = pdata[-1][2::2] + else: + output_dict["limitLines"][name]["start"] = pr["Start"] + output_dict["limitLines"][name]["stop"] = pr["Stop"] + output_dict["limitLines"][name]["step"] = pr["Step"] + output_dict["limitLines"][name]["equation"] = pr["Equation"] + output_dict["limitLines"][name]["y_axis"] = pr["Y Axis"] + + output_dict["notes"] = {} + position = [1000, 1000] # no way to retrieve position of notes + for expr in self.notes: + pr = expr.properties + name = expr.plot_note_name + output_dict["notes"][name] = { + "text": pr["Note Text"][1], + "color": [pr["Note Font/R"], pr["Note Font/G"], pr["Note Font/B"]], + "font": pr["Note Font/FaceName"], + "font_size": pr["Note Font/Height"], + "italic": True if pr["Note Font/Italic"] else False, + "background_color": [pr["Back Color/Red"], pr["Back Color/Green"], pr["Back Color/Blue"]], + "background_visibility": pr["Background Visibility"], + "border_color": [pr["Border Color/Red"], pr["Border Color/Green"], pr["Border Color/Blue"]], + "border_visibiliy": pr["Border Visibility"], + "border_width": pr["Border Width"], + "position": position, + } + + @pyaedt_function_handler() + def _export_general_appearance(self, output_dict): + from ansys.aedt.core.visualization.report.eye import AMIEyeDiagram + from ansys.aedt.core.visualization.report.eye import EyeDiagram + + output_dict["general"] = {} + if "AxisX" in self.properties.children: + axis = self.properties.children["AxisX"].props + output_dict["general"]["axisx"] = { + "label": axis["Name"], + "font": axis["Text Font/FaceName"], + "font_size": axis["Text Font/Height"], + "italic": True if axis["Text Font/Italic"] else False, + "color": [axis["Text Font/R"], axis["Text Font/G"], axis["Text Font/B"]], + "specify_spacing": axis.get("Specify Spacing", False), + "spacing": axis.get("Spacing", "1GHz"), + "minor_tick_divs": axis.get("Minor Tick Divs", None), + "auto_units": axis["Auto Units"], + "units": axis["Units"], + } + if not isinstance(self, (AMIEyeDiagram, EyeDiagram)): + output_dict["general"]["axisx"]["linear_scaling"] = True if axis["Axis Scaling"] == "Linear" else False + y_axis_available = [i for i in self.properties.children.keys() if i.startswith("AxisY")] + for yaxis in y_axis_available: + axis = self.properties.children[yaxis].props + output_dict["general"][yaxis.lower()] = { + "label": axis["Name"], + "font": axis["Text Font/FaceName"], + "font_size": axis["Text Font/Height"], + "italic": True if axis["Text Font/Italic"] else False, + "color": [axis["Text Font/R"], axis["Text Font/G"], axis["Text Font/B"]], + "specify_spacing": axis.get("Specify Spacing", False), + "minor_tick_divs": axis.get("Minor Tick Divs", None), + "auto_units": axis["Auto Units"], + "units": axis["Units"], + } + if not isinstance(self, (AMIEyeDiagram, EyeDiagram)): + output_dict["general"][yaxis.lower()]["linear_scaling"] = ( + True if axis["Axis Scaling"] == "Linear" else False + ) + if "General" in self.properties.children: + props = self.properties.children["General"].props + output_dict["general"]["appearance"] = { + "background_color": [ + props["Back Color/Red"], + props["Back Color/Green"], + props["Back Color/Blue"], + ], + "plot_color": [ + props["Plot Area Color/Red"], + props["Plot Area Color/Green"], + props["Plot Area Color/Blue"], + ], + "enable_y_stripes": props.get("Enable Y Axis Stripes", None), + "Auto Scale Fonts": props["Auto Scale Fonts"], + "field_width": props["Field Width"], + "precision": props["Precision"], + "use_scientific_notation": props["Use Scientific Notation"], + } + + if "Grid" in self.properties.children: + props = self.properties.children["Grid"].props + output_dict["general"]["grid"] = { + "major_color": [ + props["Major grid line color/Red"], + props["Major grid line color/Green"], + props["Major grid line color/Blue"], + ], + "minor_color": [ + props["Minor grid line color/Red"], + props["Minor grid line color/Green"], + props["Minor grid line color/Blue"], + ], + "major_x": props["Show major X grid"], + "major_y": props["Show major Y grid"], + "minor_x": props["Show minor X grid"], + "minor_y": props["Show minor Y grid"], + "style_major": props["Major grid line style"], + "style_minor": props["Minor grid line style"], + } + if "Legend" in self.properties.children: + props = self.properties.children["Legend"].props + output_dict["general"]["legend"] = { + "back_color": [ + props["Back Color/Red"], + props["Back Color/Green"], + props["Back Color/Blue"], + ], + "font_color": [ + props["Font/R"], + props["Font/G"], + props["Font/B"], + ], + "show_solution_name": props["Show Solution Name"], + "show_variation_key": props["Show Variation Key"], + "show_trace_name": props["Show Trace Name"], + } + if "Header" in self.properties.children: + props = self.properties.children["Header"].props + output_dict["general"]["header"] = { + "font": props["Title Font/FaceName"], + "title_size": props["Title Font/Height"], + "color": [ + props["Title Font/R"], + props["Title Font/G"], + props["Title Font/B"], + ], + "italic": True if props["Title Font/Italic"] else False, + "subtitle_size": props["Sub Title Font/Height"], + "company_name": props["Company Name"], + "show_design_name": props["Show Design Name"], + } + + @pyaedt_function_handler() + def export_config(self, output_file): + """Generate a configuration file from active report. + + Parameters + ---------- + output_file : str + Full path to json file. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + output_dict = {} + output_dict["Help"] = "Report Generated automatically by PyAEDT" + output_dict["report_category"] = self.report_category + output_dict["report_type"] = self.report_type + output_dict["plot_name"] = self.plot_name + + self._export_context(output_dict) + self._export_expressions(output_dict) + self._export_graphical_objects(output_dict) + self._export_general_appearance(output_dict) + + return write_configuration_file(output_dict, output_file) + + @pyaedt_function_handler() + def get_solution_data(self): + """Get the report solution data. + + Returns + ------- + :class:`ansys.aedt.core.modules.solutions.SolutionData` + Solution data object. + """ + if self._is_created: + expr = [i.name for i in self.traces] + elif not self.expressions: + self.update_expressions_with_defaults() + expr = [i for i in self.expressions] + else: + expr = [i for i in self.expressions] + if not expr: + self._post._app.logger.warning("No Expressions Available. Check inputs") + return False + solution_data = self._post.get_solution_data_per_variation( + self.report_category, self.setup, self._context, self.variations, expr + ) + if not solution_data: + self._post._app.logger.warning("No Data Available. Check inputs") + return False + if self.primary_sweep: + solution_data.primary_sweep = self.primary_sweep + return solution_data + + @pyaedt_function_handler() + def add_limit_line_from_points(self, x_list, y_list, x_units="", y_units="", y_axis="Y1"): # pragma: no cover + """Add a Cartesian limit line from point lists. This method works only in graphical mode. + + Parameters + ---------- + x_list : list + List of float inputs. + y_list : list + List of float y values. + x_units : str, optional + Units for the ``x_list`` parameter. The default is ``""``. + y_units : str, optional + Units for the ``y_list`` parameter. The default is ``""``. + y_axis : int, optional + Y axis. The default is `"Y1"`. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + x_list = [GeometryOperators.parse_dim_arg(str(i) + x_units) for i in x_list] + y_list = [GeometryOperators.parse_dim_arg(str(i) + y_units) for i in y_list] + if self.plot_name and self._is_created: + xvals = ["NAME:XValues"] + xvals.extend(x_list) + yvals = ["NAME:YValues"] + yvals.extend(y_list) + self._post.oreportsetup.AddCartesianLimitLine( + self.plot_name, + [ + "NAME:CartesianLimitLine", + xvals, + "XUnits:=", + x_units, + yvals, + "YUnits:=", + y_units, + "YAxis:=", + y_axis, + ], + ) + return True + return False + + @pyaedt_function_handler() + def add_limit_line_from_equation( + self, start_x, stop_x, step, equation="x", units="GHz", y_axis=1 + ): # pragma: no cover + """Add a Cartesian limit line from point lists. This method works only in graphical mode. + + Parameters + ---------- + start_x : float + Start X value. + stop_x : float + Stop X value. + step : float + X step value. + equation : str, optional + Y equation to apply. The default is Y=X. + units : str + Units for the X axis. The default is ``"GHz"``. + y_axis : str, int, optional + Y axis. The default is ``1``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if self.plot_name and self._is_created: + self._post.oreportsetup.AddCartesianLimitLineFromEquation( + self.plot_name, + [ + "NAME:CartesianLimitLineFromEquation", + "YAxis:=", + int(str(y_axis).replace("Y", "")), + "Start:=", + self._post._app.value_with_units(start_x, units), + "Stop:=", + self._post._app.value_with_units(stop_x, units), + "Step:=", + self._post._app.value_with_units(step, units), + "Equation:=", + equation, + ], + ) + return True + return False + + @pyaedt_function_handler() + def add_note(self, text, x_position=0, y_position=0): # pragma: no cover + """Add a note at a position. + + Parameters + ---------- + text : string + Text of the note. + x_position : float, optional + x position of the note. The default is ``0.0``. + y_position : float, optional + y position of the note. The default is ``0.0``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + note_name = generate_unique_name("Note", n=3) + if self.plot_name and self._is_created: + self._post.oreportsetup.AddNote( + self.plot_name, + [ + "NAME:NoteDataSource", + [ + "NAME:NoteDataSource", + "SourceName:=", + note_name, + "HaveDefaultPos:=", + True, + "DefaultXPos:=", + x_position, + "DefaultYPos:=", + y_position, + "String:=", + text, + ], + ], + ) + return True + return False + + @pyaedt_function_handler(val="value") + def add_cartesian_x_marker(self, value, name=None): # pragma: no cover + """Add a cartesian X marker. + + .. note:: + This method only works in graphical mode. + + Parameters + ---------- + value : str + Value to apply with units. + name : str, optional + Marker name. The default is ``None``. + + Returns + ------- + str + Marker name if created. + """ + if not name: + name = generate_unique_name("MX") + self._post.oreportsetup.AddCartesianXMarker(self.plot_name, name, GeometryOperators.parse_dim_arg(value)) + return name + return "" + + @pyaedt_function_handler(val="value") + def add_cartesian_y_marker(self, value, name=None, y_axis=1): # pragma: no cover + """Add a cartesian Y marker. + + .. note:: + This method only works in graphical mode. + + Parameters + ---------- + value : str, float + Value to apply with units. + name : str, optional + Marker name. The default is ``None``. + y_axis : str, optional + Y axis. The default is ``"Y1"``. + + Returns + ------- + str + Marker name if created. + """ + if not name: + name = generate_unique_name("MY") + self._post.oreportsetup.AddCartesianYMarker( + self.plot_name, name, "Y{}".format(y_axis), GeometryOperators.parse_dim_arg(value), "" + ) + return name + return "" + + @pyaedt_function_handler(tabname="tab_name") + def _change_property(self, tab_name, property_name, property_val): + if not self._is_created: + self._post._app.logger.error("Plot has not been created. Create it and then change the properties.") + return False + arg = [ + "NAME:AllTabs", + ["NAME:" + tab_name, ["NAME:PropServers", "{}:{}".format(self.plot_name, property_name)], property_val], + ] + self._post.oreportsetup.ChangeProperty(arg) + return True + + @pyaedt_function_handler() + def edit_grid( + self, + minor_x=True, + minor_y=True, + major_x=True, + major_y=True, + style_minor="Solid", + style_major="Solid", + minor_color=(0, 0, 0), + major_color=(0, 0, 0), + ): + """Edit the grid settings for the plot. + + Parameters + ---------- + minor_x : bool, optional + Whether to enable the minor X grid. The default is ``True``. + minor_y : bool, optional + Whether to enable the minor Y grid. The default is ``True``. + major_x : bool, optional + Whether to enable the major X grid. The default is ``True``. + major_y : bool, optional + Whether to enable the major Y grid. The default is ``True``. + style_minor : str, optional + Minor grid style. The default is ``"Solid"``. + style_major : str, optional + Major grid style. The default is ``"Solid"``. + minor_color : tuple, optional + Minor grid (R, G, B) color. The default is ``(0, 0, 0)``. + Each color value must be an integer in a range from 0 to 255. + major_color : tuple, optional + Major grid (R, G, B) color. The default is ``(0, 0, 0)``. + Each color value must be an integer in a range from 0 to 255. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + props = [ + "NAME:ChangedProps", + ["NAME:Show minor X grid", "Value:=", minor_x], + ["NAME:Show minor Y grid", "Value:=", minor_y], + ["NAME:Show major X grid", "Value:=", major_x], + ["NAME:Show major Y grid", "Value:=", major_y], + ["NAME:Minor grid line style", "Value:=", style_minor], + ["NAME:Major grid line style", "Value:=", style_major], + ["NAME:Minor grid line color", "R:=", minor_color[0], "G:=", minor_color[1], "B:=", minor_color[2]], + ["NAME:Major grid line color", "R:=", major_color[0], "G:=", major_color[1], "B:=", major_color[2]], + ] + return self._change_property("Grid", "Grid", props) + + @pyaedt_function_handler() + def edit_x_axis(self, font="Arial", font_size=12, italic=False, bold=False, color=(0, 0, 0), label=None): + """Edit the X-axis settings. + + Parameters + ---------- + font : str, optional + Font name. The default is ``"Arial"``. + font_size : int, optional + Font size. The default is ``12``. + italic : bool, optional + Whether to use italic type. The default is ``False``. + bold : bool, optional + Whether to use bold type. The default is ``False``. + color : tuple, optional + Font (R, G, B) color. The default is ``(0, 0, 0)``. Each color value + must be an integer in a range from 0 to 255. + label : str, optional + Label for the Y axis. The default is ``None``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + props = [ + "NAME:ChangedProps", + [ + "NAME:Text Font", + "Height:=", + -1 * font_size - 2, + "Width:=", + 0, + "Escapement:=", + 0, + "Orientation:=", + 0, + "Weight:=", + 700 if bold else 400, + "Italic:=", + 255 if italic else 0, + "Underline:=", + 0, + "StrikeOut:=", + 0, + "CharSet:=", + 0, + "OutPrecision:=", + 3, + "ClipPrecision:=", + 2, + "Quality:=", + 1, + "PitchAndFamily:=", + 34, + "FaceName:=", + font, + "R:=", + color[0], + "G:=", + color[1], + "B:=", + color[2], + ], + ] + if label: + props.append(["NAME:Name", "Value:=", label]) + props.append(["NAME:Axis Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) + return self._change_property("Axis", "AxisX", props) + + @pyaedt_function_handler() + def edit_x_axis_scaling( + self, linear_scaling=True, min_scale=None, max_scale=None, minor_tick_divs=5, min_spacing=None, units=None + ): + """Edit the X-axis scaling settings. + + Parameters + ---------- + linear_scaling : bool, optional + Whether to use the linear scale. The default is ``True``. + When ``False``, the log scale is used. + min_scale : str, optional + Minimum scale value with units. The default is ``None``. + max_scale : str, optional + Maximum scale value with units. The default is ``None``. + minor_tick_divs : int, optional + Minor tick division. The default is ``5``. + min_spacing : str, optional + Minimum spacing with units. The default is ``None``. + units : str, optional + Units in the plot. The default is ``None``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if linear_scaling: + props = ["NAME:ChangedProps", ["NAME:Axis Scaling", "Value:=", "Linear"]] + else: + props = ["NAME:ChangedProps", ["NAME:Axis Scaling", "Value:=", "Log"]] + if min_scale: + props.append(["NAME:Min", "Value:=", min_scale]) + if max_scale: + props.append(["NAME:Max", "Value:=", max_scale]) + if minor_tick_divs and linear_scaling: + props.append(["NAME:Minor Tick Divs", "Value:=", str(minor_tick_divs)]) + if min_spacing: + props.append(["NAME:Spacing", "Value:=", min_spacing]) + if units: + props.append((["NAME:Auto Units", "Value:=", False])) + props.append(["NAME:Units", "Value:=", units]) + return self._change_property("Scaling", "AxisX", props) + + @pyaedt_function_handler() + def edit_legend( + self, + show_solution_name=True, + show_variation_key=True, + show_trace_name=True, + back_color=(255, 255, 255), + font_color=(0, 0, 0), + ): + """Edit the plot legend. + + Parameters + ---------- + show_solution_name : bool, optional + Whether to show the solution name. The default is ``True``. + show_variation_key : bool, optional + Whether to show the variation key. The default is ``True``. + show_trace_name : bool, optional + Whether to show the trace name. The default is ``True``. + back_color : tuple, optional + Background (R, G, B) color. The default is ``(255, 255, 255)``. Each color value + must be an integer in a range from 0 to 255. + font_color : tuple, optional + Legend font (R, G, B) color. The default is ``(0, 0, 0)``. Each color value + must be an integer in a range from 0 to 255. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + props = [ + "NAME:ChangedProps", + ["NAME:Show Solution Name", "Value:=", show_solution_name], + ["NAME:Show Variation Key", "Value:=", show_variation_key], + ["NAME:Show Trace Name", "Value:=", show_trace_name], + ["NAME:Back Color", "R:=", back_color[0], "G:=", back_color[1], "B:=", back_color[2]], + ["NAME:Font", "R:=", font_color[0], "G:=", font_color[1], "B:=", font_color[2]], + ] + return self._change_property("legend", "legend", props) + + @pyaedt_function_handler(font_height="font_size") + def hide_legend(self, solution_name=True, trace_name=True, variation_key=True, font_size=1): + """Hide the Legend. + + Parameters + ---------- + solution_name : bool, optional + Whether to show or hide the solution name. Default is ``True``. + trace_name : bool, optional + Whether to show or hide the trace name. Default is ``True``. + variation_key : bool, optional + Whether to show or hide the variations. Default is ``True``. + font_size : int + Font size. The default is ``1``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + try: + legend = self._post.oreportsetup.GetChildObject(self.plot_name).GetChildObject("Legend") + legend.Show_Solution_Name = not solution_name + legend.Show_Trace_Name = not trace_name + legend.Show_Variation_Key = not variation_key + legend.SetPropValue("Font/Height", font_size) + legend.SetPropValue("Header Row Font/Height", font_size) + return True + except Exception: + self._post._app.logger.error("Failed to hide legend.") + return False + + @pyaedt_function_handler(axis_name="name") + def edit_y_axis(self, name="Y1", font="Arial", font_size=12, italic=False, bold=False, color=(0, 0, 0), label=None): + """Edit the Y-axis settings. + + Parameters + ---------- + name : str, optional + Name for the main Y axis. The default is ``"Y1"``. + font : str, optional + Font name. The default is ``"Arial"``. + font_size : int, optional + Font size. The default is ``12``. + italic : bool, optional + Whether to use italic type. The default is ``False``. + bold : bool, optional + Whether to use bold type. The default is ``False``. + color : tuple, optional + Font (R, G, B) color. The default is ``(0, 0, 0)``. Each color value + must be an integer in a range from 0 to 255. + label : str, optional + Label for the Y axis. The default is ``None``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + props = [ + "NAME:ChangedProps", + [ + "NAME:Text Font", + "Height:=", + -1 * font_size - 2, + "Width:=", + 0, + "Escapement:=", + 0, + "Orientation:=", + 0, + "Weight:=", + 700 if bold else 400, + "Italic:=", + 255 if italic else 0, + "Underline:=", + 0, + "StrikeOut:=", + 0, + "CharSet:=", + 0, + "OutPrecision:=", + 3, + "ClipPrecision:=", + 2, + "Quality:=", + 1, + "PitchAndFamily:=", + 34, + "FaceName:=", + font, + "R:=", + color[0], + "G:=", + color[1], + "B:=", + color[2], + ], + ] + if label: + props.append(["NAME:Name", "Value:=", label]) + props.append(["NAME:Axis Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) + return self._change_property("Axis", "Axis" + name, props) + + @pyaedt_function_handler(axis_name="name") + def edit_y_axis_scaling( + self, + name="Y1", + linear_scaling=True, + min_scale=None, + max_scale=None, + minor_tick_divs=5, + min_spacing=None, + units=None, + ): + """Edit the Y-axis scaling settings. + + Parameters + ---------- + axis name : str, optional + Axis name. The default is ``Y``. + linear_scaling : bool, optional + Whether to use the linear scale. The default is ``True``. + When ``False``, the log scale is used. + min_scale : str, optional + Minimum scale value with units. The default is ``None``. + max_scale : str, optional + Maximum scale value with units. The default is ``None``. + minor_tick_divs : int, optional + Minor tick division. The default is ``5``. + min_spacing : str, optional + Minimum spacing with units. The default is ``None``. + units : str, optional + Units in the plot. The default is ``None``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if linear_scaling: + props = ["NAME:ChangedProps", ["NAME:Axis Scaling", "Value:=", "Linear"]] + else: + props = ["NAME:ChangedProps", ["NAME:Axis Scaling", "Value:=", "Log"]] + if min_scale: + props.append(["NAME:Min", "Value:=", min_scale]) + if max_scale: + props.append(["NAME:Max", "Value:=", max_scale]) + if minor_tick_divs and linear_scaling: + props.append(["NAME:Minor Tick Divs", "Value:=", str(minor_tick_divs)]) + if min_spacing: + props.append(["NAME:Spacing", "Value:=", min_spacing]) + if units: + props.append((["NAME:Auto Units", "Value:=", False])) + props.append(["NAME:Units", "Value:=", units]) + return self._change_property("Scaling", "Axis" + name, props) + + @pyaedt_function_handler() + def edit_general_settings( + self, + background_color=(255, 255, 255), + plot_color=(255, 255, 255), + enable_y_stripes=True, + field_width=4, + precision=4, + use_scientific_notation=True, + ): + """Edit general settings for the plot. + + Parameters + ---------- + background_color : tuple, optional + Backgoround (R, G, B) color. The default is ``(255, 255, 255)``. Each color value + must be an integer in a range from 0 to 255. + plot_color : tuple, optional + Plot (R, G, B) color. The default is ``(255, 255, 255)``. Each color value + must be an integer in a range from 0 to 255. + enable_y_stripes : bool, optional + Whether to enable Y stripes. The default is ``True``. + field_width : int, optional + Field width. The default is ``4``. + precision : int, optional + Field precision. The default is ``4``. + use_scientific_notation : bool, optional + Whether to enable scientific notation. The default is ``True``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if enable_y_stripes is None: + props = [ + "NAME:ChangedProps", + ["NAME:Back Color", "R:=", background_color[0], "G:=", background_color[1], "B:=", background_color[2]], + ["NAME:Plot Area Color", "R:=", plot_color[0], "G:=", plot_color[1], "B:=", plot_color[2]], + ["NAME:Field Width", "Value:=", str(field_width)], + ["NAME:Precision", "Value:=", str(precision)], + ["NAME:Use Scientific Notation", "Value:=", use_scientific_notation], + ] + else: + props = [ + "NAME:ChangedProps", + ["NAME:Back Color", "R:=", background_color[0], "G:=", background_color[1], "B:=", background_color[2]], + ["NAME:Plot Area Color", "R:=", plot_color[0], "G:=", plot_color[1], "B:=", plot_color[2]], + ["NAME:Enable Y Axis Stripes", "Value:=", enable_y_stripes], + ["NAME:Field Width", "Value:=", str(field_width)], + ["NAME:Precision", "Value:=", str(precision)], + ["NAME:Use Scientific Notation", "Value:=", use_scientific_notation], + ] + return self._change_property("general", "general", props) + + @pyaedt_function_handler() + def edit_header( + self, + company_name="PyAEDT", + show_design_name=True, + font="Arial", + title_size=12, + subtitle_size=12, + italic=False, + bold=False, + color=(0, 0, 0), + ): + """Edit the plot header. + + Parameters + ---------- + company_name : str, optional + Company name. The default is ``PyAEDT``. + show_design_name : bool, optional + Whether to show the design name in the plot. The default is ``True``. + font : str, optional + Font name. The default is ``"Arial"``. + title_size : int, optional + Title font size. The default is ``12``. + subtitle_size : int, optional + Subtitle font size. The default is ``12``. + italic : bool, optional + Whether to use italic type. The default is ``False``. + bold : bool, optional + Whether to use bold type. The default is ``False``. + color : tuple, optional + Title (R, G, B) color. The default is ``(0, 0, 0)``. + Each color value must be an integer in a range from 0 to 255. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + props = [ + "NAME:ChangedProps", + [ + "NAME:Title Font", + "Height:=", + -1 * title_size - 2, + "Width:=", + 0, + "Escapement:=", + 0, + "Orientation:=", + 0, + "Weight:=", + 700 if bold else 400, + "Italic:=", + 255 if italic else 0, + "Underline:=", + 0, + "StrikeOut:=", + 0, + "CharSet:=", + 0, + "OutPrecision:=", + 3, + "ClipPrecision:=", + 2, + "Quality:=", + 1, + "PitchAndFamily:=", + 34, + "FaceName:=", + font, + "R:=", + color[0], + "G:=", + color[1], + "B:=", + color[2], + ], + [ + "NAME:Sub Title Font", + "Height:=", + -1 * subtitle_size - 2, + "Width:=", + 0, + "Escapement:=", + 0, + "Orientation:=", + 0, + "Weight:=", + 700 if bold else 400, + "Italic:=", + 255 if italic else 0, + "Underline:=", + 0, + "StrikeOut:=", + 0, + "CharSet:=", + 0, + "OutPrecision:=", + 3, + "ClipPrecision:=", + 2, + "Quality:=", + 1, + "PitchAndFamily:=", + 34, + "FaceName:=", + font, + "R:=", + color[0], + "G:=", + color[1], + "B:=", + color[2], + ], + ["NAME:Company Name", "Value:=", company_name], + ["NAME:Show Design Name", "Value:=", show_design_name], + ] + return self._change_property("Header", "Header", props) + + @pyaedt_function_handler(file_path="input_file") + def import_traces(self, input_file, plot_name): + """Import report data from a file into a specified report. + + Parameters + ---------- + input_file : str + Path for the file to import. The extensions supported are ``".csv"``, + ``".tab"``, ``".dat"``, and ``".rdat"``. + plot_name : str + Name of the plot to import the file data into. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if not os.path.exists(input_file): + msg = "File does not exist." + raise FileExistsError(msg) + + if not plot_name: + msg = "Plot name can't be None." + raise ValueError(msg) + else: + if plot_name not in self._post.all_report_names: + msg = "Plot name provided doesn't exists in current report." + raise ValueError(msg) + self.plot_name = plot_name + + split_path = os.path.splitext(input_file) + extension = split_path[1] + + supported_ext = [".csv", ".tab", ".dat", ".rdat"] + if extension not in supported_ext: + msg = f"Extension {extension} is not supported. Use one of {', '.join(supported_ext)}" + raise ValueError(msg) + + try: + if extension == ".rdat": + self._post.oreportsetup.ImportReportDataIntoReport(self.plot_name, input_file) + else: + self._post.oreportsetup.ImportIntoReport(self.plot_name, input_file) + return True + except Exception: + return False + + @pyaedt_function_handler() + def delete_traces(self, plot_name, traces_list): + """Delete an existing trace or traces. + + Parameters + ---------- + plot_name : str + Plot name. + traces_list : list + List of one or more traces to delete. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if plot_name not in self._post.all_report_names: + raise ValueError("Plot does not exist in current project.") + + for trace in traces_list: + if trace not in self._trace_info[3]: + raise ValueError("Trace does not exist in the selected plot.") + + props = ["{}:=".format(plot_name), traces_list] + try: + self._post.oreportsetup.DeleteTraces(props) + return True + except Exception: + return False + + @pyaedt_function_handler() + def add_trace_to_report(self, traces, setup_name=None, variations=None, context=None): + """Add a trace to a specific report. + + Parameters + ---------- + traces : list + List of traces to add. + setup_name : str, optional + Name of the setup. The default is ``None`` which automatically take ``nominal_adaptive`` setup. + Please make sure to build a setup string in the form of ``"SetupName : SetupSweep"`` + where ``SetupSweep`` is the Sweep name to use in the export or ``LastAdaptive``. + variations : dict, optional + Dictionary of variations. The default is ``None``. + context : list, optional + List of solution context. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + expr = copy.deepcopy(self.expressions) + self.expressions = traces + + try: + self._post.oreportsetup.AddTraces( + self.plot_name, + setup_name if setup_name else self.setup, + context if context else self._context, + self._convert_dict_to_report_sel(variations if variations else self.variations), + self._trace_info, + ) + return True + except Exception: + return False + finally: + self.expressions = expr + + @pyaedt_function_handler() + def update_trace_in_report(self, traces, setup_name=None, variations=None, context=None): + """Update a trace in a specific report. + + Parameters + ---------- + traces : list + List of traces to add. + setup_name : str, optional + Name of the setup. The default is ``None``. + variations : dict, optional + Dictionary of variations. The default is ``None``. + context : list, optional + List of solution context. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + expr = copy.deepcopy(self.expressions) + self.expressions = traces + + try: + self._post.oreportsetup.UpdateTraces( + self.plot_name, + traces, + setup_name if setup_name else self.setup, + context if context else self._context, + self._convert_dict_to_report_sel(variations if variations else self.variations), + self._trace_info, + ) + return True + except Exception: + return False + finally: + self.expressions = expr + + @pyaedt_function_handler() + def apply_report_template(self, input_file, property_type="Graphical"): # pragma: no cover + """Apply report template. + + .. note:: + This method works in only in graphical mode. + + Parameters + ---------- + input_file : str + Path for the file to import. The extension supported is ``".rpt"``. + property_type : str, optional + Property types to apply. Options are ``"Graphical"``, ``"Data"``, and ``"All"``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + References + ---------- + + >>> oModule.ApplyReportTemplate + """ + if not os.path.exists(input_file): # pragma: no cover + msg = "File does not exist." + self._post.logger.error(msg) + return False + + split_path = os.path.splitext(input_file) + extension = split_path[1] + + supported_ext = [".rpt"] + if extension not in supported_ext: # pragma: no cover + msg = "Extension {} is not supported.".format(extension) + self._post.logger.error(msg) + return False + + if property_type not in ["Graphical", "Data", "All"]: # pragma: no cover + msg = "Invalid value for `property_type`. The value must be 'Graphical', 'Data', or 'All'." + self._post.logger.error(msg) + return False + self._post.oreportsetup.ApplyReportTemplate(self.plot_name, input_file, property_type) + return True + + @staticmethod + @pyaedt_function_handler() + def __props_with_default(dict_in, key, default_value=None): + """Update dictionary value.""" + return dict_in[key] if dict_in.get(key, None) is not None else default_value diff --git a/src/ansys/aedt/core/visualization/report/constants.py b/src/ansys/aedt/core/visualization/report/constants.py new file mode 100644 index 00000000000..4af74811b1d --- /dev/null +++ b/src/ansys/aedt/core/visualization/report/constants.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +TEMPLATES_BY_DESIGN = { + "HFSS": [ + "Modal Solution Data", + "Terminal Solution Data", + "Eigenmode Parameters", + "Fields", + "Far Fields", + "Emissions", + "Near Fields", + "Antenna Parameters", + ], + "Maxwell 3D": [ + "Transient", + "EddyCurrent", + "Magnetostatic", + "Electrostatic", + "DCConduction", + "ElectroDCConduction", + "ElectricTransient", + "Fields", + "Spectrum", + ], + "Maxwell 2D": [ + "Transient", + "EddyCurrent", + "Magnetostatic", + "Electrostatic", + "ElectricTransient", + "ElectroDCConduction", + "Fields", + "Spectrum", + ], + "Icepak": ["Monitor", "Fields"], + "Circuit Design": ["Standard", "Eye Diagram", "Statistical Eye", "Spectrum", "EMIReceiver"], + "HFSS 3D Layout": ["Standard", "Fields", "Spectrum"], + "HFSS 3D Layout Design": ["Standard", "Fields", "Spectrum"], + "Mechanical": ["Standard", "Fields"], + "Q3D Extractor": ["Matrix", "CG Fields", "DC R/L Fields", "AC R/L Fields"], + "2D Extractor": ["Matrix", "CG Fields", "RL Fields"], + "Twin Builder": ["Standard", "Spectrum"], +} + +ORIENTATION_TO_VIEW = { + "isometric": "iso", + "top": "XY", + "bottom": "XY", + "right": "XZ", + "left": "XZ", + "front": "YZ", + "back": "YZ", +} diff --git a/src/ansys/aedt/core/visualization/report/emi.py b/src/ansys/aedt/core/visualization/report/emi.py new file mode 100644 index 00000000000..4cb56665397 --- /dev/null +++ b/src/ansys/aedt/core/visualization/report/emi.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains this class: `EMIReceiver`. + +This module provides all functionalities for creating and editing reports. + +""" + +from ansys.aedt.core import generate_unique_name +from ansys.aedt.core import pyaedt_function_handler +from ansys.aedt.core.visualization.report.common import CommonReport + + +class EMIReceiver(CommonReport): + """Provides for managing EMI receiver reports.""" + + def __init__(self, app, setup_name, expressions=None): + CommonReport.__init__(self, app, "EMIReceiver", setup_name, expressions) + self.logger = app.logger + self.domain = "EMI Receiver" + self.available_nets = [] + self._net = "0" + for comp in app.modeler.components.components.values(): + if comp.name == "CompInst@EMI_RCVR": + self.available_nets.append(comp.pins[0].net) + if self.available_nets: + self._net = self.available_nets[0] + self.time_start = "0ns" + self.time_stop = "200ns" + self._emission = "CE" + self.overlap_rate = 95 + self.band = "0" + self.primary_sweep = "Freq" + + @property + def net(self): + """Net attached to the EMI receiver. + + Returns + ------- + str + Net name. + """ + return self._net + + @net.setter + def net(self, value): + if value not in self.available_nets: + self.logger.error("Net not available.") + else: + self._net = value + + @property + def band(self): + """Band attached to the EMI receiver. + + Returns + ------- + str + Band name. + """ + return self._props["context"].get("band", None) + + @band.setter + def band(self, value): + self._props["context"]["band"] = value + + @property + def emission(self): + """Emission test. + + Options are ``"CE"`` and ``"RE"``. + + Returns + ------- + str + Emission. + """ + return self._emission + + @emission.setter + def emission(self, value): + if value == "CE": + self._emission = value + self._props["context"]["emission"] = "0" + elif value == "RE": + self._emission = value + self._props["context"]["emission"] = "1" + else: + self.logger.error("Emission must be 'CE' or 'RE', value '{}' is not valid.".format(value)) + + @property + def time_start(self): + """Time start value. + + Returns + ------- + str + Time start. + """ + return self._props["context"].get("time_start", None) + + @time_start.setter + def time_start(self, value): + self._props["context"]["time_start"] = value + + @property + def time_stop(self): + """Time stop value. + + Returns + ------- + str + Time stop. + """ + return self._props["context"].get("time_stop", None) + + @time_stop.setter + def time_stop(self, value): + self._props["context"]["time_stop"] = value + + @property + def _context(self): + + if self.emission == "CE": + em = "0" + else: + em = "1" + + arg = [ + "NAME:Context", + "SimValueContext:=", + [ + 55830, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + self.net, + 0, + 0, + "BAND", + False, + self.band, + "CG", + False, + "1", + "EM", + False, + em, + "KP", + False, + "0", + "NUMLEVELS", + False, + "0", + "OR", + False, + str(self.overlap_rate), + "RBW", + False, + "9000Hz", + "SIG", + False, + "0", + "TCT", + False, + "1ms", + "TDT", + False, + "160ms", + "TE", + False, + self.time_stop, + "TS", + False, + self.time_start, + "WT", + False, + "6", + "WW", + False, + "100", + ], + ] + return arg + + @property + def _trace_info(self): + if isinstance(self.expressions, list): + return self.expressions + else: + return [self.expressions] + + @pyaedt_function_handler() + def create(self, name=None): + """Create an EMI receiver report. + + Parameters + ---------- + name : str, optional + Plot name. The default is ``None``, in which case + the default name is used. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if not name: + self.plot_name = generate_unique_name("Plot") + else: + self.plot_name = name + self._post.oreportsetup.CreateReport( + self.plot_name, + "Standard", + self.report_type, + self.setup, + self._context, + self._convert_dict_to_report_sel(self.variations), + [ + "X Component:=", + self.primary_sweep, + "Y Component:=", + self._trace_info, + ], + ) + self._post.plots.append(self) + self._is_created = True + return self diff --git a/src/ansys/aedt/core/visualization/report/eye.py b/src/ansys/aedt/core/visualization/report/eye.py new file mode 100644 index 00000000000..716ca521b2d --- /dev/null +++ b/src/ansys/aedt/core/visualization/report/eye.py @@ -0,0 +1,1255 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains these classes: `AMIConturEyeDiagram`, `AMIEyeDiagram`, and `EyeDiagram`. + +This module provides all functionalities for creating and editing reports. + +""" +import os + +from ansys.aedt.core import generate_unique_name +from ansys.aedt.core import pyaedt_function_handler +from ansys.aedt.core.visualization.report.common import CommonReport + + +class AMIConturEyeDiagram(CommonReport): + """Provides for managing eye contour diagram reports in AMI analysis.""" + + def __init__(self, app, report_category, setup_name, expressions=None): + CommonReport.__init__(self, app, report_category, setup_name, expressions) + self.domain = "Time" + self._props["report_type"] = "Rectangular Contour Plot" + self.variations.pop("Time", None) + self._props["context"]["variations"]["__UnitInterval"] = "All" + self._props["context"]["variations"]["__Amplitude"] = "All" + self._props["context"]["variations"]["__EyeOpening"] = "0" + self.quantity_type = 0 + self.min_latch_overlay = "0" + self.noise_floor = "1e-16" + self.enable_jitter_distribution = False + self.rx_rj = "0" + self.rx_dj = "0" + self.rx_sj = "0" + self.rx_dcd = "0" + self.rx_gaussian_noise = "0" + self.rx_uniform_noise = "0" + + @property + def expressions(self): + """Expressions. + + Returns + ------- + list + Expressions. + """ + if self._is_created: + return [i.split(" ,")[-1] for i in list(self.properties.props.values())[4:]] + if self._props.get("expressions", None) is None: + return [] + expr_head = "Eye" + new_exprs = [] + for expr_dict in self._props["expressions"]: + expr = expr_dict["name"] + if not ".int_ami" in expr: + qtype = int(self.quantity_type) + if qtype == 0: + new_exprs.append("Initial{}(".format(expr_head) + expr + ".int_ami_tx)") + elif qtype == 1: + new_exprs.append("{}AfterSource(".format(expr_head) + expr + ".int_ami_tx)") + elif qtype == 2: + new_exprs.append("{}AfterChannel(".format(expr_head) + expr + ".int_ami_rx)") + elif qtype == 3: + new_exprs.append("{}AfterProbe(".format(expr_head) + expr + ".int_ami_rx)") + else: + new_exprs.append(expr) + return new_exprs + + @expressions.setter + def expressions(self, value): + if isinstance(value, dict): + self._props["expressions"].append = value + elif isinstance(value, list): + self._props["expressions"] = [] + for el in value: + if isinstance(el, dict): + self._props["expressions"].append(el) + else: + self._props["expressions"].append({"name": el}) + elif isinstance(value, str): + if isinstance(self._props["expressions"], list): + self._props["expressions"].append({"name": value}) + else: + self._props["expressions"] = [{"name": value}] + + @property + def quantity_type(self): + """Quantity type used in the AMI analysis plot. + + Returns + ------- + int + Quantity type. + """ + return self._props.get("quantity_type", 0) + + @quantity_type.setter + def quantity_type(self, value): + self._props["quantity_type"] = value + + @property + def report_category(self): + """Report category. + + Returns + ------- + str + Report category. + """ + return self._props["report_category"] + + @property + def _context(self): + if self.primary_sweep == "__InitialTime": + cid = 55824 + else: + cid = 55819 + sim_context = [ + cid, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "MLO", + False, + str(self.min_latch_overlay), + "NUMLEVELS", + False, + "1", + "ORJ", + False, + "1", + "PCID", + False, + "-1", + "PID", + False, + "0", + "PRIDIST", + False, + "0", + "QTID", + False, + str(self.quantity_type), + "USE_PRI_DIST", + False, + "0" if not self.enable_jitter_distribution else "1", + "SID", + False, + "0", + ] + if self.enable_jitter_distribution and str(self.quantity_type) == "3": + sim_context = [ + 55819, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "DCD", + False, + str(self.rx_dcd), + "DJ", + False, + str(self.rx_dj), + "GNOI", + False, + "3.5", + "MLO", + False, + str(self.min_latch_overlay), + "NF", + False, + str(self.noise_floor), + "NUMLEVELS", + False, + "1", + "ORJ", + False, + "1", + "PCID", + False, + "-1", + "PID", + False, + "0", + "SID", + False, + "0", + "PRIDIST", + False, + "0", + "QTID", + False, + str(self.quantity_type), + "RJ", + False, + str(self.rx_rj), + "SJ", + False, + str(self.rx_sj), + "UNOI", + False, + str(self.rx_uniform_noise), + "USE_PRI_DIST", + False, + "0" if not self.enable_jitter_distribution else "1", + ] + + arg = [ + "NAME:Context", + "SimValueContext:=", + sim_context, + ] + if len(self.expressions) == 1: + sid = 0 + pid = 0 + expr = self.expressions[0] + category = "Eye" + found = False + while not found: + available_quantities = self._post.available_report_quantities( + self.report_category, self.report_type, self.setup, category, arg + ) + if len(available_quantities) == 1 and available_quantities[0].lower() == expr.lower(): + found = True + else: + sid += 1 + pid += 1 + arg[2][arg[2].index("SID") + 2] = str(sid) + arg[2][arg[2].index("PID") + 2] = str(pid) + # Limited maximum iterations to 1000 in While loop (Too many probes to analyze even in a single design) + if sid > 1000: + self._post.logger.error( + "Failed to find right context for expression : {}".format(",".join(self.expressions)) + ) + # arg[2][arg[2].index("SID") + 2] = "0" + # arg[2][arg[2].index("PID") + 2] = "0" + break + return arg + + @property + def _trace_info(self): + new_exprs = self.expressions if isinstance(self.expressions, list) else [self.expressions] + if self.secondary_sweep: + return ["X Component:=", self.primary_sweep, "Y Component:=", "__Amplitude", "Z Component:=", new_exprs] + else: + return ["X Component:=", self.primary_sweep, "Y Component:=", new_exprs] + + @pyaedt_function_handler() + def create(self, name=None): + """Create an eye diagram report. + + Parameters + ---------- + name : str, optional + Plot name. The default is ``None``, in which case + the default name is used. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if not name: + self.plot_name = generate_unique_name("Plot") + else: + self.plot_name = name + self._post.oreportsetup.CreateReport( + self.plot_name, + self.report_category, + self.report_type, + self.setup, + self._context, + self._convert_dict_to_report_sel(self.variations), + self._trace_info, + ) + self._post.plots.append(self) + self._is_created = True + + return True + + @pyaedt_function_handler(xunits="x_units", yunits="y_units", xoffset="x_offset", yoffset="y_offset") + def eye_mask( + self, + points, + x_units="ns", + y_units="mV", + enable_limits=False, + upper_limit=500, + lower_limit=-500, + color=(0, 255, 0), + x_offset="0ns", + y_offset="0V", + transparency=0.3, + ): + """Create an eye diagram in the plot. + + Parameters + ---------- + points : list + Points of the eye mask in the format ``[[x1,y1,],[x2,y2],...]``. + x_units : str, optional + X points units. The default is ``"ns"``. + y_units : str, optional + Y points units. The default is ``"mV"``. + enable_limits : bool, optional + Whether to enable the upper and lower limits. The default is ``False``. + upper_limit : float, optional + Upper limit if limits are enabled. The default is ``500``. + lower_limit : str, optional + Lower limit if limits are enabled. The default is ``-500``. + color : tuple, optional + Mask in (R, G, B) color. The default is ``(0, 255, 0)``. + Each color value must be an integer in a range from 0 to 255. + x_offset : str, optional + Mask time offset with units. The default is ``"0ns"``. + y_offset : str, optional + Mask value offset with units. The default is ``"0V"``. + transparency : float, optional + Mask transparency. The default is ``0.3``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if "quantity_type" in dir(self) and self.report_type == "Rectangular Contour Plot": + props = [ + "NAME:AllTabs", + ["NAME:Mask", ["NAME:PropServers", "{}: Plot {}".format(self.plot_name, self.traces[0].name)]], + ] + else: + props = [ + "NAME:AllTabs", + ["NAME:Mask", ["NAME:PropServers", "{}:EyeDisplayTypeProperty".format(self.plot_name)]], + ] + arg = [ + "NAME:Mask", + "Version:=", + 1, + "ShowLimits:=", + enable_limits, + "UpperLimit:=", + upper_limit if upper_limit else 1, + "LowerLimit:=", + lower_limit if lower_limit else 0, + "XUnits:=", + x_units, + "YUnits:=", + y_units, + ] + mask_points = ["NAME:MaskPoints"] + for point in points: + mask_points.append(point[0]) + mask_points.append(point[1]) + arg.append(mask_points) + args = ["NAME:ChangedProps", arg] + if not ("quantity_type" in dir(self) and self.report_type == "Rectangular Contour Plot"): + args.append(["NAME:Mask Fill Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) + args.append(["NAME:X Offset", "Value:=", x_offset]) + args.append(["NAME:Y Offset", "Value:=", y_offset]) + args.append(["NAME:Mask Trans", "Transparency:=", transparency]) + props[1].append(args) + self._post.oreportsetup.ChangeProperty(props) + + return True + + @pyaedt_function_handler(value="enable") + def rectangular_plot(self, enable=True): + """Enable or disable the rectangular plot on the chart. + + Parameters + ---------- + enable : bool + Whether to enable the rectangular plot. The default is ``True``. If + ``False``, the rectangular plot is disabled. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + props = [ + "NAME:AllTabs", + ["NAME:Eye", ["NAME:PropServers", "{}:EyeDisplayTypeProperty".format(self.plot_name)]], + ] + args = ["NAME:ChangedProps", ["NAME:Rectangular Plot", "Value:=", enable]] + props[1].append(args) + self._post.oreportsetup.ChangeProperty(props) + + return True + + @pyaedt_function_handler() + def add_all_eye_measurements(self): + """Add all eye measurements to the plot. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + self._post.oreportsetup.AddAllEyeMeasurements(self.plot_name) + return True + + @pyaedt_function_handler() + def clear_all_eye_measurements(self): + """Clear all eye measurements from the plot. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + self._post.oreportsetup.ClearAllTraceCharacteristics(self.plot_name) + return True + + @pyaedt_function_handler(trace_name="name") + def add_trace_characteristics(self, name, arguments=None, solution_range=None): + """Add a trace characteristic to the plot. + + Parameters + ---------- + name : str + Name of the trace characteristic. + arguments : list, optional + Arguments if any. The default is ``None``. + solution_range : list, optional + Output range. The default is ``None``, in which case + the full range is used. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if not arguments: + arguments = [] + if not solution_range: + solution_range = ["Full"] + self._post.oreportsetup.AddTraceCharacteristics(self.plot_name, name, arguments, solution_range) + return True + + @pyaedt_function_handler(out_file="output_file") + def export_mask_violation(self, output_file=None): + """Export the eye diagram mask violations to a TAB file. + + Parameters + ---------- + output_file : str, optional + Full path to the TAB file. The default is ``None``, in which case + the violations are exported to a TAB file in the working directory. + + Returns + ------- + str + Output file path if a TAB file is created. + """ + if not output_file: + output_file = os.path.join(self._post._app.working_directory, "{}_violations.tab".format(self.plot_name)) + self._post.oreportsetup.ExportEyeMaskViolation(self.plot_name, output_file) + return output_file + + +class AMIEyeDiagram(CommonReport): + """Provides for managing eye diagram reports.""" + + def __init__(self, app, report_category, setup_name, expressions=None): + CommonReport.__init__(self, app, report_category, setup_name, expressions) + self.domain = "Time" + if report_category == "Statistical Eye": + self._props["report_type"] = "Statistical Eye Plot" + self.variations.pop("Time", None) + self.variations["__UnitInterval"] = "All" + self.variations["__Amplitude"] = "All" + self.unit_interval = "0s" + self.offset = "0ms" + self.auto_delay = True + self.manual_delay = "0ps" + self.auto_cross_amplitude = True + self.cross_amplitude = "0mV" + self.auto_compute_eye_meas = True + self.eye_measurement_point = "5e-10s" + self.quantity_type = 0 + + @property + def expressions(self): + """Expressions. + + Returns + ------- + list + Expressions. + """ + if self._is_created: + return [i.split(" ,")[-1] for i in list(self.properties.props.values())[4:]] + if self._props.get("expressions", None) is None: + return [] + expr_head = "Wave" + if self.report_category == "Statistical Eye": + expr_head = "Eye" + new_exprs = [] + for expr_dict in self._props["expressions"]: + expr = expr_dict["name"] + if not ".int_ami" in expr: + qtype = int(self.quantity_type) + if qtype == 0: + new_exprs.append("Initial{}<".format(expr_head) + expr + ".int_ami_tx>") + elif qtype == 1: + new_exprs.append("{}AfterSource<".format(expr_head) + expr + ".int_ami_tx>") + elif qtype == 2: + new_exprs.append("{}AfterChannel<".format(expr_head) + expr + ".int_ami_rx>") + elif qtype == 3: + new_exprs.append("{}AfterProbe<".format(expr_head) + expr + ".int_ami_rx>") + return new_exprs + + @property + def quantity_type(self): + """Quantity type used in the AMI analysis plot. + + Returns + ------- + int + Quantity type. + """ + return self._props.get("quantity_type", 0) + + @quantity_type.setter + def quantity_type(self, value): + self._props["quantity_type"] = value + + @property + def report_category(self): + """Report category. + + Returns + ------- + str + Report category. + """ + return self._props["report_category"] + + @report_category.setter + def report_category(self, value): + self._props["report_category"] = value + if self._props["report_category"] == "Statistical Eye" and self.report_type == "Rectangular Plot": + self._props["report_type"] = "Statistical Eye Plot" + self.variations.pop("Time", None) + self.variations["__UnitInterval"] = "All" + self.variations["__Amplitude"] = "All" + elif self._props["report_category"] == "Eye Diagram" and self.report_type == "Statistical Eye Plot": + self._props["report_type"] = "Rectangular Plot" + self.variations.pop("__UnitInterval", None) + self.variations.pop("__Amplitude", None) + self.variations["Time"] = "All" + + @property + def unit_interval(self): + """Unit interval value. + + Returns + ------- + str + Unit interval. + """ + return self._props["context"].get("unit_interval", None) + + @unit_interval.setter + def unit_interval(self, value): + self._props["context"]["unit_interval"] = value + + @property + def offset(self): + """Offset value. + + Returns + ------- + str + Offset value. + """ + return self._props["context"].get("offset", None) + + @offset.setter + def offset(self, value): + self._props["context"]["offset"] = value + + @property + def auto_delay(self): + """Auto-delay flag. + + Returns + ------- + bool + ``True`` if auto-delay is enabled, ``False`` otherwise. + """ + return self._props["context"].get("auto_delay", None) + + @auto_delay.setter + def auto_delay(self, value): + self._props["context"]["auto_delay"] = value + + @property + def manual_delay(self): + """Manual delay value when ``auto_delay=False``. + + Returns + ------- + str + ``True`` if manual-delay is enabled, ``False`` otherwise. + """ + return self._props["context"].get("manual_delay", None) + + @manual_delay.setter + def manual_delay(self, value): + self._props["context"]["manual_delay"] = value + + @property + def auto_cross_amplitude(self): + """Auto-cross amplitude flag. + + Returns + ------- + bool + ``True`` if auto-cross amplitude is enabled, ``False`` otherwise. + """ + return self._props["context"].get("auto_cross_amplitude", None) + + @auto_cross_amplitude.setter + def auto_cross_amplitude(self, value): + self._props["context"]["auto_cross_amplitude"] = value + + @property + def cross_amplitude(self): + """Cross-amplitude value when ``auto_cross_amplitude=False``. + + Returns + ------- + str + Cross-amplitude. + """ + return self._props["context"].get("cross_amplitude", None) + + @cross_amplitude.setter + def cross_amplitude(self, value): + self._props["context"]["cross_amplitude"] = value + + @property + def auto_compute_eye_meas(self): + """Flag for automatically computing eye measurements. + + Returns + ------- + bool + ``True`` to compute eye measurements, ``False`` otherwise. + """ + return self._props["context"].get("auto_compute_eye_meas", None) + + @auto_compute_eye_meas.setter + def auto_compute_eye_meas(self, value): + self._props["context"]["auto_compute_eye_meas"] = value + + @property + def eye_measurement_point(self): + """Eye measurement point. + + Returns + ------- + str + Eye measurement point. + """ + return self._props["context"].get("eye_measurement_point", None) + + @eye_measurement_point.setter + def eye_measurement_point(self, value): + self._props["context"]["eye_measurement_point"] = value + + @property + def _context(self): + arg = [ + "NAME:Context", + "SimValueContext:=", + [ + 1, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "-1", + False, + "0", + "NUMLEVELS", + False, + "1", + "PCID", + False, + "-1", + "PID", + False, + "0", + "QTID", + False, + str(self.quantity_type), + "SCID", + False, + "-1", + "SID", + False, + "0", + ], + ] + if self.report_category == "Statistical Eye": + arg = [ + "NAME:Context", + "SimValueContext:=", + [ + 55819, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "NUMLEVELS", + False, + "1", + "PCID", + False, + "-1", + "PID", + False, + "0", + "QTID", + False, + str(self.quantity_type), + "SCID", + False, + "-1", + "SID", + False, + "0", + ], + ] + if len(self.expressions) == 1: + sid = 0 + pid = 0 + expr = self.expressions[0] + category = "Wave" + if self.report_category == "Statistical Eye": + category = "Eye" + if self.report_category == "Eye Diagram" and self.report_type == "Rectangular Plot": + category = "Voltage" + found = False + while not found: + available_quantities = self._post.available_report_quantities( + self.report_category, self.report_type, self.setup, category, arg + ) + if len(available_quantities) == 1 and available_quantities[0].lower() == expr.lower(): + found = True + else: + sid += 1 + pid += 1 + arg[2][arg[2].index("SID") + 2] = str(sid) + arg[2][arg[2].index("PID") + 2] = str(pid) + # Limited maximum iterations to 1000 in While loop (Too many probes to analyze even in a single design) + if sid > 1000: + self._post.logger.error( + "Failed to find right context for expression : {}".format(",".join(self.expressions)) + ) + # arg[2][arg[2].index("SID") + 2] = "0" + # arg[2][arg[2].index("PID") + 2] = "0" + break + return arg + + @property + def _trace_info(self): + new_exprs = self.expressions if isinstance(self.expressions, list) else [self.expressions] + if self.report_category == "Statistical Eye": + return [ + "X Component:=", + "__UnitInterval", + "Y Component:=", + "__Amplitude", + "Eye Diagram Component:=", + new_exprs, + ] + return ["Component:=", new_exprs] + + @pyaedt_function_handler() + def create(self, name=None): + """Create an eye diagram report. + + Parameters + ---------- + name : str, optional + Plot name. The default is ``None``, in which case + the default name is used. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if not name: + self.plot_name = generate_unique_name("Plot") + else: + self.plot_name = name + options = [ + "Unit Interval:=", + self.unit_interval, + "Offset:=", + self.offset, + "Auto Delay:=", + self.auto_delay, + "Manual Delay:=", + self.manual_delay, + "AutoCompCrossAmplitude:=", + self.auto_cross_amplitude, + "CrossingAmplitude:=", + self.cross_amplitude, + "AutoCompEyeMeasurementPoint:=", + self.auto_compute_eye_meas, + "EyeMeasurementPoint:=", + self.eye_measurement_point, + ] + if self.report_category == "Statistical Eye": + self._post.oreportsetup.CreateReport( + self.plot_name, + self.report_category, + self.report_type, + self.setup, + self._context, + self._convert_dict_to_report_sel(self.variations), + self._trace_info, + ) + else: + self._post.oreportsetup.CreateReport( + self.plot_name, + self.report_category, + self.report_type, + self.setup, + self._context, + self._convert_dict_to_report_sel(self.variations), + self._trace_info, + options, + ) + self._post.plots.append(self) + self._is_created = True + + return True + + @pyaedt_function_handler(xunits="x_units", yunits="y_units", xoffset="x_offset", yoffset="y_offset") + def eye_mask( + self, + points, + x_units="ns", + y_units="mV", + enable_limits=False, + upper_limit=500, + lower_limit=-500, + color=(0, 255, 0), + x_offset="0ns", + y_offset="0V", + transparency=0.3, + ): + """Create an eye diagram in the plot. + + Parameters + ---------- + points : list + Points of the eye mask in the format ``[[x1,y1,],[x2,y2],...]``. + x_units : str, optional + X points units. The default is ``"ns"``. + y_units : str, optional + Y points units. The default is ``"mV"``. + enable_limits : bool, optional + Whether to enable the upper and lower limits. The default is ``False``. + upper_limit : float, optional + Upper limit if limits are enabled. The default is ``500``. + lower_limit : str, optional + Lower limit if limits are enabled. The default is ``-500``. + color : tuple, optional + Mask in (R, G, B) color. The default is ``(0, 255, 0)``. + Each color value must be an integer in a range from 0 to 255. + x_offset : str, optional + Mask time offset with units. The default is ``"0ns"``. + y_offset : str, optional + Mask value offset with units. The default is ``"0V"``. + transparency : float, optional + Mask transparency. The default is ``0.3``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + props = [ + "NAME:AllTabs", + ["NAME:Mask", ["NAME:PropServers", "{}:EyeDisplayTypeProperty".format(self.plot_name)]], + ] + arg = [ + "NAME:Mask", + "Version:=", + 1, + "ShowLimits:=", + enable_limits, + "UpperLimit:=", + upper_limit if upper_limit else 1, + "LowerLimit:=", + lower_limit if lower_limit else 0, + "XUnits:=", + x_units, + "YUnits:=", + y_units, + ] + mask_points = ["NAME:MaskPoints"] + for point in points: + mask_points.append(point[0]) + mask_points.append(point[1]) + arg.append(mask_points) + args = ["NAME:ChangedProps", arg] + args.append(["NAME:Mask Fill Color", "R:=", color[0], "G:=", color[1], "B:=", color[2]]) + args.append(["NAME:X Offset", "Value:=", x_offset]) + args.append(["NAME:Y Offset", "Value:=", y_offset]) + args.append(["NAME:Mask Trans", "Transparency:=", transparency]) + props[1].append(args) + self._post.oreportsetup.ChangeProperty(props) + + return True + + @pyaedt_function_handler(value="enable") + def rectangular_plot(self, enable=True): + """Enable or disable the rectangular plot on the chart. + + Parameters + ---------- + enable : bool + Whether to enable the rectangular plot. The default is ``True``. When + ``False``, the rectangular plot is disabled. + + Returns + ------- + bool + """ + props = [ + "NAME:AllTabs", + ["NAME:Eye", ["NAME:PropServers", "{}:EyeDisplayTypeProperty".format(self.plot_name)]], + ] + args = ["NAME:ChangedProps", ["NAME:Rectangular Plot", "Value:=", enable]] + props[1].append(args) + self._post.oreportsetup.ChangeProperty(props) + + return True + + @pyaedt_function_handler() + def add_all_eye_measurements(self): + """Add all eye measurements to the plot. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + self._post.oreportsetup.AddAllEyeMeasurements(self.plot_name) + return True + + @pyaedt_function_handler() + def clear_all_eye_measurements(self): + """Clear all eye measurements from the plot. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + self._post.oreportsetup.ClearAllTraceCharacteristics(self.plot_name) + return True + + @pyaedt_function_handler(trace_name="name") + def add_trace_characteristics(self, name, arguments=None, solution_range=None): + """Add a trace characteristic to the plot. + + Parameters + ---------- + name : str + Name of the trace characteristic. + arguments : list, optional + Arguments if any. The default is ``None``. + solution_range : list, optional + Output range. The default is ``None``, in which case + the full range is used. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if not arguments: + arguments = [] + if not solution_range: + solution_range = ["Full"] + self._post.oreportsetup.AddTraceCharacteristics(self.plot_name, name, arguments, solution_range) + return True + + @pyaedt_function_handler(out_file="output_file") + def export_mask_violation(self, output_file=None): + """Export the eye diagram mask violations to a TAB file. + + Parameters + ---------- + output_file : str, optional + Full path to the TAB file. The default is ``None``, in which case + the violations are exported to a TAB file in the working directory. + + Returns + ------- + str + Output file path if a TAB file is created. + """ + if not output_file: + output_file = os.path.join(self._post._app.working_directory, "{}_violations.tab".format(self.plot_name)) + self._post.oreportsetup.ExportEyeMaskViolation(self.plot_name, output_file) + return output_file + + +class EyeDiagram(AMIEyeDiagram): + """Provides for managing eye diagram reports.""" + + def __init__(self, app, report_category, setup_name, expressions=None): + AMIEyeDiagram.__init__(self, app, report_category, setup_name, expressions) + self.time_start = "0ns" + self.time_stop = "200ns" + self.thinning = False + self.dy_dx_tolerance = 0.001 + self.thinning_points = 500000000 + + @property + def expressions(self): + """Expressions. + + Returns + ------- + list + Expressions. + """ + if self._is_created: + return [i.split(" ,")[-1] for i in list(self.properties.props.values())[4:]] + if self._props.get("expressions", None) is None: + return [] + return [k.get("name", None) for k in self._props["expressions"] if k.get("name", None) is not None] + + @expressions.setter + def expressions(self, value): + if isinstance(value, dict): + self._props["expressions"].append = value + elif isinstance(value, list): + self._props["expressions"] = [] + for el in value: + if isinstance(el, dict): + self._props["expressions"].append(el) + else: + self._props["expressions"].append({"name": el}) + elif isinstance(value, str): + if isinstance(self._props["expressions"], list): + self._props["expressions"].append({"name": value}) + else: + self._props["expressions"] = [{"name": value}] + + @property + def time_start(self): + """Time start value. + + Returns + ------- + str + Time start. + """ + return self._props["context"].get("time_start", None) + + @time_start.setter + def time_start(self, value): + self._props["context"]["time_start"] = value + + @property + def time_stop(self): + """Time stop value. + + Returns + ------- + str + Time stop. + """ + return self._props["context"].get("time_stop", None) + + @time_stop.setter + def time_stop(self, value): + self._props["context"]["time_stop"] = value + + @property + def thinning(self): + """Thinning flag. + + Returns + ------- + bool + ``True`` if thinning is enabled, ``False`` otherwise. + """ + return self._props["context"].get("thinning", None) + + @thinning.setter + def thinning(self, value): + self._props["context"]["thinning"] = value + + @property + def dy_dx_tolerance(self): + """DY DX tolerance. + + Returns + ------- + float + DY DX tolerance. + """ + return self._props["context"].get("dy_dx_tolerance", None) + + @dy_dx_tolerance.setter + def dy_dx_tolerance(self, value): + self._props["context"]["dy_dx_tolerance"] = value + + @property + def thinning_points(self): + """Number of thinning points. + + Returns + ------- + int + Number of thinning points. + """ + return self._props["context"].get("thinning_points", None) + + @thinning_points.setter + def thinning_points(self, value): + self._props["context"]["thinning_points"] = value + + @property + def _context(self): + if self.thinning: + val = "1" + else: + val = "0" + arg = [ + "NAME:Context", + "SimValueContext:=", + [ + 1, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "DE", + False, + val, + "DP", + False, + str(self.thinning_points), + "DT", + False, + str(self.dy_dx_tolerance), + "NUMLEVELS", + False, + "0", + "WE", + False, + self.time_stop, + "WM", + False, + "200ns", + "WN", + False, + "0ps", + "WS", + False, + self.time_start, + ], + ] + return arg + + @property + def _trace_info(self): + if isinstance(self.expressions, list): + return ["Component:=", self.expressions] + else: + return ["Component:=", [self.expressions]] diff --git a/src/ansys/aedt/core/visualization/report/field.py b/src/ansys/aedt/core/visualization/report/field.py new file mode 100644 index 00000000000..5d0813a61bf --- /dev/null +++ b/src/ansys/aedt/core/visualization/report/field.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains these classes: `AntennaParameters`, `Fields`, `NearField`, `FarField`, and `Emission`. + +This module provides all functionalities for creating and editing reports. + +""" +from ansys.aedt.core.visualization.report.common import CommonReport +from ansys.aedt.core.visualization.report.standard import Standard + + +class AntennaParameters(Standard): + """Provides a reporting class that fits antenna parameter reports in an HFSS plot.""" + + def __init__(self, app, report_category, setup_name, far_field_sphere=None, expressions=None): + Standard.__init__(self, app, report_category, setup_name, expressions) + self.far_field_sphere = far_field_sphere + + @property + def far_field_sphere(self): + """Far field sphere name. + + Returns + ------- + str + Name of the far field sphere. + """ + if self._is_created: + try: + self._props["context"]["far_field_sphere"] = self.traces[0].properties["Geometry"] + except Exception: + self._post._app.logger.warning("Property `far_field_sphere` not found.") + + return self._props["context"].get("far_field_sphere", None) + + @far_field_sphere.setter + def far_field_sphere(self, value): + self._props["context"]["far_field_sphere"] = value + + @property + def _context(self): + ctxt = ["Context:=", self.far_field_sphere] + return ctxt + + +class Fields(CommonReport): + """Provides for managing fields.""" + + def __init__(self, app, report_category, setup_name, expressions=None): + CommonReport.__init__(self, app, report_category, setup_name, expressions) + self.domain = "Sweep" + self.primary_sweep = "Distance" + + @property + def point_number(self): + """Polygon point number. + + Returns + ------- + str + Point number. + """ + return self._props["context"].get("point_number", 1001) + + @point_number.setter + def point_number(self, value): + self._props["context"]["point_number"] = value + + @property + def _context(self): + ctxt = [] + if self.polyline: + ctxt = ["Context:=", self.polyline, "PointCount:=", self.point_number] + return ctxt + + +class NearField(CommonReport): + """Provides for managing near field reports.""" + + def __init__(self, app, report_category, setup_name, expressions=None): + CommonReport.__init__(self, app, report_category, setup_name, expressions) + self.domain = "Sweep" + + @property + def _context(self): + return ["Context:=", self.near_field] + + @property + def near_field(self): + """Near field name. + + Returns + ------- + str + Field name. + """ + return self._props["context"].get("near_field", None) + + @near_field.setter + def near_field(self, value): + self._props["context"]["near_field"] = value + + +class FarField(CommonReport): + """Provides for managing far field reports.""" + + def __init__(self, app, report_category, setup_name, expressions=None): + CommonReport.__init__(self, app, report_category, setup_name, expressions) + self.domain = "Sweep" + self.primary_sweep = "Phi" + self.secondary_sweep = "Theta" + self.source_context = None + self.source_group = None + if "Phi" not in self.variations: + self.variations["Phi"] = ["All"] + if "Theta" not in self.variations: + self.variations["Theta"] = ["All"] + if "Freq" not in self.variations: + self.variations["Freq"] = ["Nominal"] + + @property + def far_field_sphere(self): + """Far field sphere name. + + Returns + ------- + str + Field name. + """ + if self._is_created: + try: + self._props["context"]["far_field_sphere"] = self.traces[0].properties["Geometry"] + except Exception: + self._post._app.logger.warning("Property `far_field_sphere` not found.") + return self._props["context"].get("far_field_sphere", None) + + @far_field_sphere.setter + def far_field_sphere(self, value): + self._props["context"]["far_field_sphere"] = value + + @property + def _context(self): + if self.source_context: + return ["Context:=", self.far_field_sphere, "SourceContext:=", self.source_context] + if self.source_group: + return ["Context:=", self.far_field_sphere, "Source Group:=", self.source_group] + return ["Context:=", self.far_field_sphere] + + +class Emission(CommonReport): + """Provides for managing emission reports.""" + + def __init__(self, app, report_category, setup_name, expressions=None): + CommonReport.__init__(self, app, report_category, setup_name, expressions) + self.domain = "Sweep" diff --git a/src/ansys/aedt/core/visualization/report/standard.py b/src/ansys/aedt/core/visualization/report/standard.py new file mode 100644 index 00000000000..7c267dc89c4 --- /dev/null +++ b/src/ansys/aedt/core/visualization/report/standard.py @@ -0,0 +1,652 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains these classes: `Standard`, and `Spectral`. + +This module provides all functionalities for creating and editing reports. + +""" +import re + +from ansys.aedt.core import generate_unique_name +from ansys.aedt.core import pyaedt_function_handler +from ansys.aedt.core.visualization.report.common import CommonReport + + +class Standard(CommonReport): + """Provides a reporting class that fits most of the app's standard reports.""" + + def __init__(self, app, report_category, setup_name, expressions=None): + CommonReport.__init__(self, app, report_category, setup_name, expressions) + + @property + def sub_design_id(self): + """Sub design ID for a Circuit or HFSS 3D Layout sub design. + + Returns + ------- + int + Number of the sub design ID. + """ + return self._props["context"].get("Sub Design ID", None) + + @sub_design_id.setter + def sub_design_id(self, value): + self._props["context"]["Sub Design ID"] = value + + @property + def time_start(self): + """Time start value. + + Returns + ------- + str + Time start value. + """ + return self._props["context"].get("time_start", None) + + @time_start.setter + def time_start(self, value): + self._props["context"]["time_start"] = value + + @property + def time_stop(self): + """Time stop value. + + Returns + ------- + str + Time stop value. + """ + return self._props["context"].get("time_stop", None) + + @time_stop.setter + def time_stop(self, value): + self._props["context"]["time_stop"] = value + + @property + def _did(self): + if self.domain == "Sweep": + return 3 + elif self.domain == "Clock Times": + return 55827 + elif self.domain in [ + "UI", + ]: + return 55819 + elif self.domain in ["Initial Response"]: + return 55824 + else: + return 1 + + @property + def pulse_rise_time(self): + """Value of Pulse rise time for TDR plot. + + Returns + ------- + float + Pulse rise time. + """ + return self._props["context"].get("pulse_rise_time", 0) if self.domain == "Time" else 0 + + @pulse_rise_time.setter + def pulse_rise_time(self, val): + self._props["context"]["pulse_rise_time"] = val + + @property + def time_windowing(self): + """Returns the TDR time windowing. Options are: + * ``0`` : Rectangular + * ``1`` : Bartlett + * ``2`` : Blackman + * ``3`` : Hamming + * ``4`` : Hanning + * ``5`` : Kaiser + * ``6`` : Welch + * ``7`` : Weber + * ``8`` : Lanzcos. + + Returns + ------- + int + Time windowing. + """ + _time_windowing = self._props["context"].get("time_windowing", 0) + return _time_windowing if self.domain == "Time" and self.pulse_rise_time != 0 else 0 + + @time_windowing.setter + def time_windowing(self, val): + available_values = { + "rectangular": 0, + "bartlett": 1, + "blackman": 2, + "hamming": 3, + "hanning": 4, + "kaiser": 5, + "welch": 6, + "weber": 7, + "lanzcos": 8, + } + if isinstance(val, int): + self._props["context"]["time_windowing"] = val + elif isinstance(val, str) and val.lower in available_values: + self._props["context"]["time_windowing"] = available_values[val.lower()] + + @property + def _context(self): + ctxt = [] + if self._post.post_solution_type in ["TR", "AC", "DC"]: + ctxt = [ + "NAME:Context", + "SimValueContext:=", + [self._did, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0], + ] + elif self._post._app.design_type in ["Q3D Extractor", "2D Extractor"]: + if not self.matrix: + ctxt = ["Context:=", "Original"] + else: + ctxt = ["Context:=", self.matrix] + elif ( + self._post._app.design_type in ["Maxwell 2D", "Maxwell 3D"] + and self._post._app.solution_type == "EddyCurrent" + ): + if not self.matrix: + ctxt = ["Context:=", "Original"] + elif not self.reduced_matrix: + ctxt = ["Context:=", self.matrix] + elif self.reduced_matrix: + ctxt = ["Context:=", self.matrix, "Matrix:=", self.reduced_matrix] + elif self._post.post_solution_type in ["HFSS3DLayout"]: + if self.domain == "DCIR": + ctxt = [ + "NAME:Context", + "SimValueContext:=", + [ + 37010, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "DCIRID", + False, + str(self.siwave_dc_category), + "IDIID", + False, + "1", + ], + ] + elif self.differential_pairs: + ctxt = [ + "NAME:Context", + "SimValueContext:=", + [ + self._did, + 0, + 2, + self.pulse_rise_time, + self.use_pulse_in_tdr if self.pulse_rise_time else False, + False, + -1, + 1, + self.time_windowing, + 1, + 1, + "", + self.pulse_rise_time / 5, + self.pulse_rise_time * 100, + "EnsDiffPairKey", + False, + "1", + "IDIID", + False, + "1" if not self.pulse_rise_time else "3", + ], + ] + else: + ctxt = [ + "NAME:Context", + "SimValueContext:=", + [ + self._did, + 0, + 2, + self.pulse_rise_time, + self.use_pulse_in_tdr if self.pulse_rise_time else False, + False, + -1, + 1, + self.time_windowing, + 1, + 1, + "", + self.pulse_rise_time / 5, + self.pulse_rise_time * 100, + "IDIID", + False, + "1" if not self.pulse_rise_time else "3", + ], + ] + elif self._post.post_solution_type in ["NexximLNA", "NexximTransient"]: + ctxt = [ + "NAME:Context", + "SimValueContext:=", + [ + self._did, + 0, + 2, + self.pulse_rise_time, + self.use_pulse_in_tdr if self.pulse_rise_time else False, + False, + -1, + 1, + self.time_windowing, + 1, + 1, + "", + self.pulse_rise_time / 5, + self.pulse_rise_time * 100, + ], + ] + if self.sub_design_id: + ctxt_temp = ["NUMLEVELS", False, "1", "SUBDESIGNID", False, str(self.sub_design_id)] + for el in ctxt_temp: + ctxt[2].append(el) + if self.differential_pairs: + ctxt_temp = ["USE_DIFF_PAIRS", False, "1"] + for el in ctxt_temp: + ctxt[2].append(el) + + elif self.domain == "UI": + if self.report_type == "Rectangular Contour Plot": + ctxt[2].extend( + [ + "MLO", + False, + "0", + "NF", + False, + "1e-16", + "NUMLEVELS", + False, + "1", + "PCID", + False, + "-1", + "PID", + False, + "0", + "PRIDIST", + False, + "-1", + "USE_PRI_DIST", + False, + "0", + ] + ) + else: + ctxt[2].extend( + [ + "AMPUI", + False, + "1", + "AMPUIVAL", + False, + "0", + "MLO", + False, + "0", + "NF", + False, + "1e-16", + "NUMLEVELS", + False, + "1", + "PCID", + False, + "-1", + "PID", + False, + "0", + "PRIDIST", + False, + "-1", + "USE_PRI_DIST", + False, + ] + ) + elif self.domain == "Initial Response": + ctxt[2].extend(["IRID", False, "0", "NUMLEVELS", False, "1", "SCID", False, "-1", "SID", False, "0"]) + if self.domain == "Time": + if self.time_start: + ctxt[2].extend(["WS", False, self.time_start]) + if self.time_stop: + ctxt[2].extend(["WE", False, self.time_stop]) + elif self._post.post_solution_type in ["NexximAMI"]: + ctxt = [ + "NAME:Context", + "SimValueContext:=", + [self._did, 0, 2, 0, False, False, -1, 1, 0, 1, 1, "", 0, 0, "NUMLEVELS", False, "1"], + ] + qty = re.sub("<[^>]+>", "", self.expressions[0]) + if qty == "InitialWave": + ctxt_temp = ["QTID", False, "0", "SCID", False, "-1", "SID", False, "0"] + elif qty == "WaveAfterSource": + ctxt_temp = ["QTID", False, "1", "SCID", False, "-1", "SID", False, "0"] + elif qty == "WaveAfterChannel": + ctxt_temp = [ + "PCID", + False, + "-1", + "PID", + False, + "0", + "QTID", + False, + "2", + "SCID", + False, + "-1", + "SID", + False, + "0", + ] + elif qty == "WaveAfterProbe": + ctxt_temp = [ + "PCID", + False, + "-1", + "PID", + False, + "0", + "QTID", + False, + "3", + "SCID", + False, + "-1", + "SID", + False, + "0", + ] + elif qty == "ClockTics": + ctxt_temp = ["PCID", False, "-1", "PID", False, "0"] + else: + return None + for el in ctxt_temp: + ctxt[2].append(el) + + elif self.differential_pairs: + ctxt = ["Diff:=", "differential_pairs", "Domain:=", self.domain] + else: + ctxt = ["Domain:=", self.domain] + return ctxt + + +class Spectral(CommonReport): + """Provides for managing spectral reports from transient data.""" + + def __init__(self, app, report_category, setup_name, expressions=None): + CommonReport.__init__(self, app, report_category, setup_name, expressions) + self.domain = "Spectrum" + self.algorithm = "FFT" + self.time_start = "0ns" + self.time_stop = "200ns" + self.window = "Rectangular" + self.kaiser_coeff = 0 + self.adjust_coherent_gain = True + self.max_frequency = "10MHz" + self.plot_continous_spectrum = False + self.primary_sweep = "Spectrum" + + @property + def time_start(self): + """Time start value. + + Returns + ------- + str + Time start. + """ + return self._props["context"].get("time_start", "0s") + + @time_start.setter + def time_start(self, value): + self._props["context"]["time_start"] = value + + @property + def time_stop(self): + """Time stop value. + + Returns + ------- + str + Time stop. + """ + return self._props["context"].get("time_stop", "100ns") + + @time_stop.setter + def time_stop(self, value): + self._props["context"]["time_stop"] = value + + @property + def window(self): + """Window value. + + Returns + ------- + str + Window. + """ + return self._props["context"].get("window", "Rectangular") + + @window.setter + def window(self, value): + self._props["context"]["window"] = value + + @property + def kaiser_coeff(self): + """Kaiser value. + + Returns + ------- + str + Kaiser coefficient. + """ + return self._props["context"].get("kaiser_coeff", 0) + + @kaiser_coeff.setter + def kaiser_coeff(self, value): + self._props["context"]["kaiser_coeff"] = value + + @property + def adjust_coherent_gain(self): + """Coherent gain flag. + + Returns + ------- + bool + ``True`` if coherent gain is enabled, ``False`` otherwise. + """ + return self._props["context"].get("adjust_coherent_gain", False) + + @adjust_coherent_gain.setter + def adjust_coherent_gain(self, value): + self._props["context"]["adjust_coherent_gain"] = value + + @property + def plot_continous_spectrum(self): + """Continuous spectrum flag. + + Returns + ------- + bool + ``True`` if continuous spectrum is enabled, ``False`` otherwise. + """ + return self._props["context"].get("plot_continous_spectrum", False) + + @plot_continous_spectrum.setter + def plot_continous_spectrum(self, value): + self._props["context"]["plot_continous_spectrum"] = value + + @property + def max_frequency(self): + """Maximum spectrum frequency. + + Returns + ------- + str + Maximum spectrum frequency. + """ + return self._props["context"].get("max_frequency", "10GHz") + + @max_frequency.setter + def max_frequency(self, value): + self._props["context"]["max_frequency"] = value + + @property + def _context(self): + if self.algorithm == "FFT": + it = "1" + elif self.algorithm == "Fourier Integration": + it = "0" + else: + it = "2" + WT = { + "Rectangular": "0", + "Bartlett": "1", + "Blackman": "2", + "Hamming": "3", + "Hanning": "4", + "Kaiser": "5", + "Welch": "6", + "Weber": "7", + "Lanzcos": "8", + } + wt = WT[self.window] + arg = [ + "NAME:Context", + "SimValueContext:=", + [ + 2, + 0, + 2, + 0, + False, + False, + -1, + 1, + 0, + 1, + 1, + "", + 0, + 0, + "CP", + False, + "1" if self.plot_continous_spectrum else "0", + "IT", + False, + it, + "MF", + False, + self.max_frequency, + "NUMLEVELS", + False, + "0", + "TE", + False, + self.time_stop, + "TS", + False, + self.time_start, + "WT", + False, + wt, + "WW", + False, + "100", + "KP", + False, + str(self.kaiser_coeff), + "CG", + False, + "1" if self.adjust_coherent_gain else "0", + ], + ] + return arg + + @property + def _trace_info(self): + if isinstance(self.expressions, list): + return self.expressions + else: + return [self.expressions] + + @pyaedt_function_handler() + def create(self, name=None): + """Create an eye diagram report. + + Parameters + ---------- + name : str, optional + Plot name. The default is ``None``, in which case + the default name is used. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if not name: + self.plot_name = generate_unique_name("Plot") + else: + self.plot_name = name + self._post.oreportsetup.CreateReport( + self.plot_name, + "Standard", + self.report_type, + self.setup, + self._context, + self._convert_dict_to_report_sel(self.variations), + [ + "X Component:=", + self.primary_sweep, + "Y Component:=", + self._trace_info, + ], + ) + self._post.plots.append(self) + self._is_created = True + return True diff --git a/src/ansys/aedt/core/workflows/project/create_report.py b/src/ansys/aedt/core/workflows/project/create_report.py index 3ecc8419d66..f7395deea38 100644 --- a/src/ansys/aedt/core/workflows/project/create_report.py +++ b/src/ansys/aedt/core/workflows/project/create_report.py @@ -27,7 +27,7 @@ import ansys.aedt.core from ansys.aedt.core import get_pyaedt_app from ansys.aedt.core.generic.general_methods import is_windows -from ansys.aedt.core.generic.pdf import AnsysReport +from ansys.aedt.core.visualization.plot.pdf import AnsysReport from ansys.aedt.core.workflows.misc import get_aedt_version from ansys.aedt.core.workflows.misc import get_arguments from ansys.aedt.core.workflows.misc import get_port diff --git a/src/ansys/aedt/core/workflows/project/import_nastran.py b/src/ansys/aedt/core/workflows/project/import_nastran.py index 357c93436ca..906963fe23f 100644 --- a/src/ansys/aedt/core/workflows/project/import_nastran.py +++ b/src/ansys/aedt/core/workflows/project/import_nastran.py @@ -25,7 +25,7 @@ import ansys.aedt.core from ansys.aedt.core import get_pyaedt_app -from ansys.aedt.core.modules.solutions import nastran_to_stl +from ansys.aedt.core.visualization.advanced.misc import nastran_to_stl import ansys.aedt.core.workflows from ansys.aedt.core.workflows.misc import get_aedt_version from ansys.aedt.core.workflows.misc import get_arguments @@ -128,7 +128,7 @@ def preview(): if master.file_path_ui.endswith(".nas"): nastran_to_stl(input_file=master.file_path_ui, decimation=master.decimate_ui, preview=True) else: - from ansys.aedt.core.modules.solutions import simplify_stl + from ansys.aedt.core.visualization.advanced.misc import simplify_stl simplify_stl(master.file_path_ui, decimation=master.decimate_ui, preview=True) @@ -182,7 +182,7 @@ def main(extension_args): file_path, import_as_light_weight=lightweight, decimation=decimate, enable_planar_merge=str(planar) ) else: - from ansys.aedt.core.modules.solutions import simplify_stl + from ansys.aedt.core.visualization.advanced.misc import simplify_stl outfile = simplify_stl(file_path, decimation=decimate) aedtapp.modeler.import_3d_cad( diff --git a/src/pyaedt/generic/com_parameters.py b/src/pyaedt/generic/com_parameters.py index eb981f31c6b..3f386ac9f2f 100644 --- a/src/pyaedt/generic/com_parameters.py +++ b/src/pyaedt/generic/com_parameters.py @@ -1 +1 @@ -from ansys.aedt.core.generic.com_parameters import * +from ansys.aedt.core.visualization.post.spisim_com_configuration_files.com_parameters import * \ No newline at end of file diff --git a/src/pyaedt/generic/compliance.py b/src/pyaedt/generic/compliance.py index 76bf5e86407..e91a0f1141e 100644 --- a/src/pyaedt/generic/compliance.py +++ b/src/pyaedt/generic/compliance.py @@ -1 +1 @@ -from ansys.aedt.core.generic.compliance import * +from ansys.aedt.core.visualization.post.compliance import * diff --git a/src/pyaedt/generic/farfield_visualization.py b/src/pyaedt/generic/farfield_visualization.py index b59f7919123..8dd7a6de05c 100644 --- a/src/pyaedt/generic/farfield_visualization.py +++ b/src/pyaedt/generic/farfield_visualization.py @@ -1 +1,2 @@ -from ansys.aedt.core.generic.farfield_visualization import * +from ansys.aedt.core.visualization.advanced.farfield_visualization import * +from ansys.aedt.core.visualization.post.farfield_exporter import * diff --git a/src/pyaedt/generic/near_field_import.py b/src/pyaedt/generic/near_field_import.py index 5cd8be91b35..77553045f70 100644 --- a/src/pyaedt/generic/near_field_import.py +++ b/src/pyaedt/generic/near_field_import.py @@ -1 +1 @@ -from ansys.aedt.core.generic.near_field_import import * +from ansys.aedt.core.visualization.advanced.misc import convert_nearfield_data diff --git a/src/pyaedt/generic/pdf.py b/src/pyaedt/generic/pdf.py index 74b99f0b480..1edd9f87a69 100644 --- a/src/pyaedt/generic/pdf.py +++ b/src/pyaedt/generic/pdf.py @@ -1 +1 @@ -from ansys.aedt.core.generic.pdf import * +from ansys.aedt.core.visualization.plot.pdf import * diff --git a/src/pyaedt/generic/plot.py b/src/pyaedt/generic/plot.py deleted file mode 100644 index e3bc4ac8d22..00000000000 --- a/src/pyaedt/generic/plot.py +++ /dev/null @@ -1 +0,0 @@ -from ansys.aedt.core.generic.plot import * diff --git a/src/pyaedt/generic/report_file_parser.py b/src/pyaedt/generic/report_file_parser.py index 86bd9907b1e..4b934d438d1 100644 --- a/src/pyaedt/generic/report_file_parser.py +++ b/src/pyaedt/generic/report_file_parser.py @@ -1 +1 @@ -from ansys.aedt.core.generic.report_file_parser import * +from ansys.aedt.core.visualization.advanced.misc import parse_rdat_file diff --git a/src/pyaedt/generic/spisim.py b/src/pyaedt/generic/spisim.py index f487b32ef9e..d872ef58822 100644 --- a/src/pyaedt/generic/spisim.py +++ b/src/pyaedt/generic/spisim.py @@ -1 +1 @@ -from ansys.aedt.core.generic.spisim import * +from ansys.aedt.core.visualization.post.spisim import * diff --git a/src/pyaedt/generic/touchstone_parser.py b/src/pyaedt/generic/touchstone_parser.py index 927cd4b1acf..42f0ad4645c 100644 --- a/src/pyaedt/generic/touchstone_parser.py +++ b/src/pyaedt/generic/touchstone_parser.py @@ -1 +1 @@ -from ansys.aedt.core.generic.touchstone_parser import * +from ansys.aedt.core.visualization.advanced.touchstone_parser import * diff --git a/src/pyaedt/misc/spisim_com_configuration_files/__init__.py b/src/pyaedt/misc/spisim_com_configuration_files/__init__.py deleted file mode 100644 index 1af173ba9d4..00000000000 --- a/src/pyaedt/misc/spisim_com_configuration_files/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from ansys.aedt.core.misc.spisim_com_configuration_files import * diff --git a/src/pyaedt/misc/spisim_com_configuration_files/com_parameters.py b/src/pyaedt/misc/spisim_com_configuration_files/com_parameters.py deleted file mode 100644 index d8d9fce472b..00000000000 --- a/src/pyaedt/misc/spisim_com_configuration_files/com_parameters.py +++ /dev/null @@ -1 +0,0 @@ -from ansys.aedt.core.misc.spisim_com_configuration_files.com_parameters import * diff --git a/src/pyaedt/misc/spisim_com_configuration_files/com_settings_mapping.py b/src/pyaedt/misc/spisim_com_configuration_files/com_settings_mapping.py deleted file mode 100644 index 6bf5666f6d9..00000000000 --- a/src/pyaedt/misc/spisim_com_configuration_files/com_settings_mapping.py +++ /dev/null @@ -1 +0,0 @@ -from ansys.aedt.core.misc.spisim_com_configuration_files.com_settings_mapping import * diff --git a/src/pyaedt/modules/AdvancedPostProcessing.py b/src/pyaedt/modules/AdvancedPostProcessing.py deleted file mode 100644 index c9f2bfe96ba..00000000000 --- a/src/pyaedt/modules/AdvancedPostProcessing.py +++ /dev/null @@ -1 +0,0 @@ -from ansys.aedt.core.modules.advanced_post_processing import * diff --git a/src/pyaedt/modules/PostProcessor.py b/src/pyaedt/modules/PostProcessor.py deleted file mode 100644 index 2dcb14fe7b5..00000000000 --- a/src/pyaedt/modules/PostProcessor.py +++ /dev/null @@ -1 +0,0 @@ -from ansys.aedt.core.modules.post_processor import * diff --git a/src/pyaedt/modules/fields_calculator.py b/src/pyaedt/modules/fields_calculator.py index b6bf56f79e5..ed1ca1e9a8e 100644 --- a/src/pyaedt/modules/fields_calculator.py +++ b/src/pyaedt/modules/fields_calculator.py @@ -1 +1 @@ -from ansys.aedt.core.modules.fields_calculator import * +from ansys.aedt.core.visualization.post.fields_calculator import * diff --git a/src/pyaedt/modules/monitor_icepak.py b/src/pyaedt/modules/monitor_icepak.py deleted file mode 100644 index f798d53bc89..00000000000 --- a/src/pyaedt/modules/monitor_icepak.py +++ /dev/null @@ -1 +0,0 @@ -from ansys.aedt.core.modules.monitor_icepak import * diff --git a/src/pyaedt/modules/report_templates.py b/src/pyaedt/modules/report_templates.py deleted file mode 100644 index 5bcc446212d..00000000000 --- a/src/pyaedt/modules/report_templates.py +++ /dev/null @@ -1 +0,0 @@ -from ansys.aedt.core.modules.report_templates import * diff --git a/src/pyaedt/modules/solutions.py b/src/pyaedt/modules/solutions.py deleted file mode 100644 index 36f151281f2..00000000000 --- a/src/pyaedt/modules/solutions.py +++ /dev/null @@ -1 +0,0 @@ -from ansys.aedt.core.modules.solutions import * diff --git a/src/pyaedt/sbrplus/__init__.py b/src/pyaedt/sbrplus/__init__.py index 1c544c1e609..8b137891791 100644 --- a/src/pyaedt/sbrplus/__init__.py +++ b/src/pyaedt/sbrplus/__init__.py @@ -1 +1 @@ -from ansys.aedt.core.sbrplus import * + diff --git a/src/pyaedt/sbrplus/hdm_parser.py b/src/pyaedt/sbrplus/hdm_parser.py index 0ae1e4dc62a..302448f1d17 100644 --- a/src/pyaedt/sbrplus/hdm_parser.py +++ b/src/pyaedt/sbrplus/hdm_parser.py @@ -1 +1 @@ -from ansys.aedt.core.sbrplus.hdm_parser import * +from ansys.aedt.core.visualization.advanced.sbrplus.hdm_parser import * diff --git a/src/pyaedt/sbrplus/hdm_utils.py b/src/pyaedt/sbrplus/hdm_utils.py index 773d826c043..941420e671f 100644 --- a/src/pyaedt/sbrplus/hdm_utils.py +++ b/src/pyaedt/sbrplus/hdm_utils.py @@ -1 +1 @@ -from ansys.aedt.core.sbrplus.hdm_utils import * +from ansys.aedt.core.visualization.advanced.sbrplus.hdm_utils import * diff --git a/src/pyaedt/sbrplus/plot.py b/src/pyaedt/sbrplus/plot.py index 1800dc82483..c476355166b 100644 --- a/src/pyaedt/sbrplus/plot.py +++ b/src/pyaedt/sbrplus/plot.py @@ -1 +1 @@ -from ansys.aedt.core.sbrplus.plot import * +from ansys.aedt.core.visualization.advanced.hdm_plot import * diff --git a/src/pyaedt/visualization/__init__.py b/src/pyaedt/visualization/__init__.py new file mode 100644 index 00000000000..97ec33ca698 --- /dev/null +++ b/src/pyaedt/visualization/__init__.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization import * diff --git a/src/pyaedt/visualization/advanced/__init__.py b/src/pyaedt/visualization/advanced/__init__.py new file mode 100644 index 00000000000..a0c613d4a65 --- /dev/null +++ b/src/pyaedt/visualization/advanced/__init__.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.advanced import * diff --git a/src/pyaedt/visualization/advanced/farfield_visualization.py b/src/pyaedt/visualization/advanced/farfield_visualization.py new file mode 100644 index 00000000000..0d7574a278f --- /dev/null +++ b/src/pyaedt/visualization/advanced/farfield_visualization.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.advanced.farfield_visualization import * diff --git a/src/pyaedt/visualization/advanced/hdm_plot.py b/src/pyaedt/visualization/advanced/hdm_plot.py new file mode 100644 index 00000000000..c476355166b --- /dev/null +++ b/src/pyaedt/visualization/advanced/hdm_plot.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.advanced.hdm_plot import * diff --git a/src/pyaedt/visualization/advanced/misc.py b/src/pyaedt/visualization/advanced/misc.py new file mode 100644 index 00000000000..b9570bf33c1 --- /dev/null +++ b/src/pyaedt/visualization/advanced/misc.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.advanced.misc import * diff --git a/src/pyaedt/visualization/advanced/sprplus/__init__.py b/src/pyaedt/visualization/advanced/sprplus/__init__.py new file mode 100644 index 00000000000..1f572f1d501 --- /dev/null +++ b/src/pyaedt/visualization/advanced/sprplus/__init__.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.advanced.sbrplus import * diff --git a/src/pyaedt/visualization/advanced/sprplus/hdm_parser.py b/src/pyaedt/visualization/advanced/sprplus/hdm_parser.py new file mode 100644 index 00000000000..302448f1d17 --- /dev/null +++ b/src/pyaedt/visualization/advanced/sprplus/hdm_parser.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.advanced.sbrplus.hdm_parser import * diff --git a/src/pyaedt/visualization/advanced/sprplus/hdm_utils.py b/src/pyaedt/visualization/advanced/sprplus/hdm_utils.py new file mode 100644 index 00000000000..941420e671f --- /dev/null +++ b/src/pyaedt/visualization/advanced/sprplus/hdm_utils.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.advanced.sbrplus.hdm_utils import * diff --git a/src/pyaedt/visualization/advanced/touchstone_parser.py b/src/pyaedt/visualization/advanced/touchstone_parser.py new file mode 100644 index 00000000000..42f0ad4645c --- /dev/null +++ b/src/pyaedt/visualization/advanced/touchstone_parser.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.advanced.touchstone_parser import * diff --git a/src/pyaedt/visualization/plot/__init__.py b/src/pyaedt/visualization/plot/__init__.py new file mode 100644 index 00000000000..f7f32ed10b1 --- /dev/null +++ b/src/pyaedt/visualization/plot/__init__.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.plot import * diff --git a/src/pyaedt/visualization/plot/matplotlib.py b/src/pyaedt/visualization/plot/matplotlib.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pyaedt/visualization/plot/pdf.py b/src/pyaedt/visualization/plot/pdf.py new file mode 100644 index 00000000000..1edd9f87a69 --- /dev/null +++ b/src/pyaedt/visualization/plot/pdf.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.plot.pdf import * diff --git a/src/pyaedt/visualization/plot/pyvista.py b/src/pyaedt/visualization/plot/pyvista.py new file mode 100644 index 00000000000..f20c24d1878 --- /dev/null +++ b/src/pyaedt/visualization/plot/pyvista.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.plot.pyvista import * diff --git a/src/pyaedt/visualization/post/__init__.py b/src/pyaedt/visualization/post/__init__.py new file mode 100644 index 00000000000..d59a4a9f8f9 --- /dev/null +++ b/src/pyaedt/visualization/post/__init__.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post import * diff --git a/src/pyaedt/visualization/post/common.py b/src/pyaedt/visualization/post/common.py new file mode 100644 index 00000000000..0b432576f48 --- /dev/null +++ b/src/pyaedt/visualization/post/common.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.common import * diff --git a/src/pyaedt/visualization/post/compliance.py b/src/pyaedt/visualization/post/compliance.py new file mode 100644 index 00000000000..e91a0f1141e --- /dev/null +++ b/src/pyaedt/visualization/post/compliance.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.compliance import * diff --git a/src/pyaedt/visualization/post/farfield_exporter.py b/src/pyaedt/visualization/post/farfield_exporter.py new file mode 100644 index 00000000000..8de5b643250 --- /dev/null +++ b/src/pyaedt/visualization/post/farfield_exporter.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.farfield_exporter import * diff --git a/src/pyaedt/visualization/post/field_data.py b/src/pyaedt/visualization/post/field_data.py new file mode 100644 index 00000000000..bcb8b9e61df --- /dev/null +++ b/src/pyaedt/visualization/post/field_data.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.field_data import * diff --git a/src/pyaedt/visualization/post/field_summary.py b/src/pyaedt/visualization/post/field_summary.py new file mode 100644 index 00000000000..315cfeea7a5 --- /dev/null +++ b/src/pyaedt/visualization/post/field_summary.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.field_summary import * diff --git a/src/pyaedt/visualization/post/fields_calculator.py b/src/pyaedt/visualization/post/fields_calculator.py new file mode 100644 index 00000000000..ed1ca1e9a8e --- /dev/null +++ b/src/pyaedt/visualization/post/fields_calculator.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.fields_calculator import * diff --git a/src/pyaedt/visualization/post/monitor_icepak.py b/src/pyaedt/visualization/post/monitor_icepak.py new file mode 100644 index 00000000000..f0101176584 --- /dev/null +++ b/src/pyaedt/visualization/post/monitor_icepak.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.monitor_icepak import * diff --git a/src/pyaedt/visualization/post/post_circuit.py b/src/pyaedt/visualization/post/post_circuit.py new file mode 100644 index 00000000000..538f802ebbf --- /dev/null +++ b/src/pyaedt/visualization/post/post_circuit.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.post_circuit import * diff --git a/src/pyaedt/visualization/post/post_common_3d.py b/src/pyaedt/visualization/post/post_common_3d.py new file mode 100644 index 00000000000..604b6c0f779 --- /dev/null +++ b/src/pyaedt/visualization/post/post_common_3d.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.post_common_3d import * diff --git a/src/pyaedt/visualization/post/post_icepak.py b/src/pyaedt/visualization/post/post_icepak.py new file mode 100644 index 00000000000..6ca6145e440 --- /dev/null +++ b/src/pyaedt/visualization/post/post_icepak.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.post_icepak import * diff --git a/src/pyaedt/visualization/post/solution_data.py b/src/pyaedt/visualization/post/solution_data.py new file mode 100644 index 00000000000..0aa91f0f8fa --- /dev/null +++ b/src/pyaedt/visualization/post/solution_data.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.solution_data import * diff --git a/src/pyaedt/visualization/post/spisim.py b/src/pyaedt/visualization/post/spisim.py new file mode 100644 index 00000000000..d872ef58822 --- /dev/null +++ b/src/pyaedt/visualization/post/spisim.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.spisim import * diff --git a/src/pyaedt/visualization/post/spisim_com_configuration_files/__init__.py b/src/pyaedt/visualization/post/spisim_com_configuration_files/__init__.py new file mode 100644 index 00000000000..54f8d16b285 --- /dev/null +++ b/src/pyaedt/visualization/post/spisim_com_configuration_files/__init__.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.plot.matplotlib import * diff --git a/src/pyaedt/misc/spisim_com_configuration_files/com_120d_8.json b/src/pyaedt/visualization/post/spisim_com_configuration_files/com_120d_8.json similarity index 100% rename from src/pyaedt/misc/spisim_com_configuration_files/com_120d_8.json rename to src/pyaedt/visualization/post/spisim_com_configuration_files/com_120d_8.json diff --git a/src/pyaedt/misc/spisim_com_configuration_files/com_93_8.json b/src/pyaedt/visualization/post/spisim_com_configuration_files/com_93_8.json similarity index 100% rename from src/pyaedt/misc/spisim_com_configuration_files/com_93_8.json rename to src/pyaedt/visualization/post/spisim_com_configuration_files/com_93_8.json diff --git a/src/pyaedt/misc/spisim_com_configuration_files/com_94_17.json b/src/pyaedt/visualization/post/spisim_com_configuration_files/com_94_17.json similarity index 100% rename from src/pyaedt/misc/spisim_com_configuration_files/com_94_17.json rename to src/pyaedt/visualization/post/spisim_com_configuration_files/com_94_17.json diff --git a/src/pyaedt/visualization/post/spisim_com_configuration_files/com_parameters.py b/src/pyaedt/visualization/post/spisim_com_configuration_files/com_parameters.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/src/pyaedt/visualization/post/spisim_com_configuration_files/com_parameters.py @@ -0,0 +1 @@ + diff --git a/src/pyaedt/visualization/post/spisim_com_configuration_files/com_settings_mapping.py b/src/pyaedt/visualization/post/spisim_com_configuration_files/com_settings_mapping.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/src/pyaedt/visualization/post/spisim_com_configuration_files/com_settings_mapping.py @@ -0,0 +1 @@ + diff --git a/src/pyaedt/visualization/post/vrt_data.py b/src/pyaedt/visualization/post/vrt_data.py new file mode 100644 index 00000000000..693a94867db --- /dev/null +++ b/src/pyaedt/visualization/post/vrt_data.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.post.vrt_data import * diff --git a/src/pyaedt/visualization/report/__init__.py b/src/pyaedt/visualization/report/__init__.py new file mode 100644 index 00000000000..773d04952a8 --- /dev/null +++ b/src/pyaedt/visualization/report/__init__.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.report import * diff --git a/src/pyaedt/visualization/report/common.py b/src/pyaedt/visualization/report/common.py new file mode 100644 index 00000000000..b3cf934ee61 --- /dev/null +++ b/src/pyaedt/visualization/report/common.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.report.common import * diff --git a/src/pyaedt/visualization/report/constants.py b/src/pyaedt/visualization/report/constants.py new file mode 100644 index 00000000000..f7f276a30d2 --- /dev/null +++ b/src/pyaedt/visualization/report/constants.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.report.constants import * diff --git a/src/pyaedt/visualization/report/emi.py b/src/pyaedt/visualization/report/emi.py new file mode 100644 index 00000000000..6c30dc31739 --- /dev/null +++ b/src/pyaedt/visualization/report/emi.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.report.emi import * diff --git a/src/pyaedt/visualization/report/eye.py b/src/pyaedt/visualization/report/eye.py new file mode 100644 index 00000000000..3ecd2cc9481 --- /dev/null +++ b/src/pyaedt/visualization/report/eye.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.report.eye import * diff --git a/src/pyaedt/visualization/report/field.py b/src/pyaedt/visualization/report/field.py new file mode 100644 index 00000000000..7bb0dd5ddf9 --- /dev/null +++ b/src/pyaedt/visualization/report/field.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.report.field import * diff --git a/src/pyaedt/visualization/report/standard.py b/src/pyaedt/visualization/report/standard.py new file mode 100644 index 00000000000..8a2bc8f2c34 --- /dev/null +++ b/src/pyaedt/visualization/report/standard.py @@ -0,0 +1 @@ +from ansys.aedt.core.visualization.report.standard import * From 016e679f565711c1c25ec6bc413b325041449027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Morais?= <146729917+SMoraisAnsys@users.noreply.github.com> Date: Fri, 27 Sep 2024 08:50:02 +0200 Subject: [PATCH 02/20] FIX: Wrong call to logging atexit (#5214) --- src/ansys/aedt/core/desktop.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ansys/aedt/core/desktop.py b/src/ansys/aedt/core/desktop.py index 5f4061f8a22..0eed9c71e56 100644 --- a/src/ansys/aedt/core/desktop.py +++ b/src/ansys/aedt/core/desktop.py @@ -1686,7 +1686,6 @@ def close_desktop(self): """ if self.__closed is True: # pragma: no cover - self.log.debug("Connection is already closed. Ignoring request.") return return self.release_desktop(close_projects=True, close_on_exit=True) From c8da6ce5475958f5fc1d04f8f1ee75deb6fdfb1d Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:38:00 +0200 Subject: [PATCH 03/20] FIX: make more robust material read when binary info are stored. (#5211) Co-authored-by: maxcapodi78 --- src/ansys/aedt/core/modules/material_lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ansys/aedt/core/modules/material_lib.py b/src/ansys/aedt/core/modules/material_lib.py index 7ff41d680d5..345eaa67866 100644 --- a/src/ansys/aedt/core/modules/material_lib.py +++ b/src/ansys/aedt/core/modules/material_lib.py @@ -170,10 +170,14 @@ def _read_materials(self): def get_mat_list(file_name): mats = [] _begin_search = re.compile(r"^\$begin '(.+)'") - with open_file(file_name, "r") as aedt_fh: + with open_file(file_name, "rb") as aedt_fh: raw_lines = aedt_fh.read().splitlines() for line in raw_lines: - b = _begin_search.search(line) + try: + ascii_line = line.decode("utf-8") + except UnicodeDecodeError: + continue + b = _begin_search.search(ascii_line) if b: # walk down a level mats.append(b.group(1)) return mats From 814ef79379089ae5cf0f789bf77f53d6c4c83d13 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:39:54 +0200 Subject: [PATCH 04/20] FEAT: Added method to plot nets in Hfss 3d Layout which uses Edb Plot (#5210) Co-authored-by: maxcapodi78 --- _unittest/test_01_3dlayout_edb.py | 3 ++ .../aedt/core/modeler/pcb/object_3d_layout.py | 52 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/_unittest/test_01_3dlayout_edb.py b/_unittest/test_01_3dlayout_edb.py index 45a0b2add6d..10aa4547428 100644 --- a/_unittest/test_01_3dlayout_edb.py +++ b/_unittest/test_01_3dlayout_edb.py @@ -244,6 +244,9 @@ def test_07_nets(self): assert nets["GND"].name == "GND" assert len(nets) > 0 assert len(nets["GND"].components) > 0 + local_png1 = os.path.join(self.local_scratch.path, "test1.png") + nets["AVCC_1V3"].plot(save_plot=local_png1, show=False) + assert os.path.exists(local_png1) def test_07a_nets_count(self): nets = self.aedtapp.modeler.nets diff --git a/src/ansys/aedt/core/modeler/pcb/object_3d_layout.py b/src/ansys/aedt/core/modeler/pcb/object_3d_layout.py index 6ba33a8c067..713487ff0e8 100644 --- a/src/ansys/aedt/core/modeler/pcb/object_3d_layout.py +++ b/src/ansys/aedt/core/modeler/pcb/object_3d_layout.py @@ -844,6 +844,58 @@ def geometry_names(self): geo = [i for i in self._oeditor.FindObjects("Net", self.name) if i not in comps] return geo + @pyaedt_function_handler() + def plot( + self, + layers=None, + show_legend=True, + save_plot=None, + outline=None, + size=(2000, 1000), + plot_components_on_top=False, + plot_components_on_bottom=False, + show=True, + ): + """Plot a Net to Matplotlib 2D Chart. + + Parameters + ---------- + layers : str, list, optional + Name of the layers to include in the plot. If ``None`` all the signal layers will be considered. + show_legend : bool, optional + If ``True`` the legend is shown in the plot. (default) + If ``False`` the legend is not shown. + save_plot : str, optional + If a path is specified the plot will be saved in this location. + If ``save_plot`` is provided, the ``show`` parameter is ignored. + outline : list, optional + List of points of the outline to plot. + size : tuple, int, optional + Image size in pixel (width, height). Default value is ``(2000, 1000)`` + plot_components_on_top : bool, optional + If ``True`` the components placed on top layer are plotted. + If ``False`` the components are not plotted. (default) + If nets and/or layers is specified, only the components belonging to the specified nets/layers are plotted. + plot_components_on_bottom : bool, optional + If ``True`` the components placed on bottom layer are plotted. + If ``False`` the components are not plotted. (default) + If nets and/or layers is specified, only the components belonging to the specified nets/layers are plotted. + show : bool, optional + Whether to show the plot or not. Default is `True`. + """ + return self._primitives.edb.nets.plot( + self.name, + layers=layers, + color_by_net=False, + show_legend=show_legend, + save_plot=save_plot, + outline=outline, + size=size, + plot_components_on_top=plot_components_on_top, + plot_components_on_bottom=plot_components_on_bottom, + show=show, + ) + class Pins3DLayout(Object3DLayout, object): """Contains the pins in HFSS 3D Layout.""" From e658d505a71a637cfe4b5b3dee090ca664d9c24a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Morais?= <146729917+SMoraisAnsys@users.noreply.github.com> Date: Fri, 27 Sep 2024 13:47:45 +0200 Subject: [PATCH 05/20] CI: Avoid using self-hosted runners in drafts (#5203) --- .github/workflows/ci_cd.yml | 4 + .github/workflows/manual_draft.yml | 286 +++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 .github/workflows/manual_draft.yml diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 49048e51208..d48960205a9 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -227,6 +227,7 @@ jobs: test-solvers-windows: name: Testing solvers and coverage (Windows) + if: github.event.pull_request.draft == false needs: [smoke-tests] runs-on: [ self-hosted, Windows, pyaedt ] steps: @@ -287,6 +288,7 @@ jobs: # TODO: Si if we can use ansys/actions test-solvers-linux: name: Testing solvers and coverage (Linux) + if: github.event.pull_request.draft == false needs: [smoke-tests] runs-on: [ self-hosted, Linux, pyaedt ] env: @@ -343,6 +345,7 @@ jobs: test-windows: name: Testing and coverage (Windows) + if: github.event.pull_request.draft == false needs: [smoke-tests] runs-on: [ self-hosted, Windows, pyaedt ] steps: @@ -408,6 +411,7 @@ jobs: # TODO: Si if we can use ansys/actions test-linux: name: Testing and coverage (Linux) + if: github.event.pull_request.draft == false needs: [smoke-tests] runs-on: [ self-hosted, Linux, pyaedt ] env: diff --git a/.github/workflows/manual_draft.yml b/.github/workflows/manual_draft.yml new file mode 100644 index 00000000000..133d612d8b9 --- /dev/null +++ b/.github/workflows/manual_draft.yml @@ -0,0 +1,286 @@ +name: Draft workflow + +on: + workflow_dispatch: + inputs: + test-solvers-windows: + description: "Testing solvers and coverage (Windows)" + default: 'no' + type: choice + options: + - 'yes' + - 'no' + test-solvers-linux: + description: "Testing solvers and coverage (Linux)" + default: 'no' + type: choice + options: + - 'yes' + - 'no' + test-windows: + description: "Testing solvers and coverage (Windows)" + default: 'no' + type: choice + options: + - 'yes' + - 'no' + test-linux: + description: "Testing solvers and coverage (Linux)" + type: choice + default: 'no' + options: + - 'yes' + - 'no' + +jobs: + +# # ================================================================================================= +# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +# # ================================================================================================= + + test-solvers-windows: + name: Testing solvers and coverage (Windows) + if: github.event.inputs.test-solvers-windows == 'yes' + needs: [smoke-tests] + runs-on: [ self-hosted, Windows, pyaedt ] + steps: + - name: Install Git and checkout project + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Create virtual environment + run: | + python -m venv .venv + .venv\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 and tests dependencies + run: | + .venv\Scripts\Activate.ps1 + pip install .[tests] + pip install pytest-azurepipelines + + - name: Install CI dependencies (e.g. vtk-osmesa) + run: | + .venv\Scripts\Activate.ps1 + # Uninstall conflicting dependencies + pip uninstall --yes vtk + pip install --extra-index-url https://wheels.vtk.org vtk-osmesa + + - name: Run tests on _unittest_solvers + env: + PYTHONMALLOC: malloc + run: | + .venv\Scripts\Activate.ps1 + pytest --durations=50 -v --cov=ansys.aedt.core --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest_solvers + + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: codecov-system-solver-tests + file: ./coverage.xml + flags: system,solver + + - name: Upload pytest test results + uses: actions/upload-artifact@v3 + with: + name: pytest-solver-results + path: junit/test-results.xml + if: ${{ always() }} + +# # ================================================================================================= +# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +# # ================================================================================================= + + test-solvers-linux: + name: Testing solvers and coverage (Linux) + if: github.event.inputs.test-solvers-linux == 'yes' + needs: [smoke-tests] + runs-on: [ self-hosted, Linux, pyaedt ] + env: + ANSYSEM_ROOT242: '/opt/AnsysEM/v242/Linux64' + ANS_NODEPCHECK: '1' + steps: + - name: Install Git and checkout project + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Create virtual environment + run: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT242 }}/common/mono/Linux64/lib64:${{ env.ANSYSEM_ROOT242 }}/Delcross:$LD_LIBRARY_PATH + python -m venv .venv + source .venv/bin/activate + python -m pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org pip -U + python -m pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org wheel setuptools -U + python -c "import sys; print(sys.executable)" + + - name: Install pyaedt and tests dependencies + run: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT242 }}/common/mono/Linux64/lib64:${{ env.ANSYSEM_ROOT242 }}/Delcross:$LD_LIBRARY_PATH + source .venv/bin/activate + pip install .[tests] + pip install pytest-azurepipelines + + - name: Run tests on _unittest_solvers + run: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT242 }}/common/mono/Linux64/lib64:${{ env.ANSYSEM_ROOT242 }}/Delcross:$LD_LIBRARY_PATH + source .venv/bin/activate + pytest --durations=50 -v --cov=ansys.aedt.core --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest_solvers + + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: codecov-system-solver-tests + file: ./coverage.xml + flags: system,solver + + - name: Upload pytest test results + uses: actions/upload-artifact@v3 + with: + name: pytest-solver-results + path: junit/test-results.xml + if: ${{ always() }} + +# # ================================================================================================= +# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +# # ================================================================================================= + + test-windows: + name: Testing and coverage (Windows) + if: github.event.inputs.test-windows == 'yes' + needs: [smoke-tests] + runs-on: [ self-hosted, Windows, pyaedt ] + steps: + - name: Install Git and checkout project + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Create virtual environment + run: | + python -m venv .venv + .venv\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 and tests dependencies + run: | + .venv\Scripts\Activate.ps1 + pip install .[tests] + pip install pytest-azurepipelines + + - name: Install CI dependencies (e.g. vtk-osmesa) + run: | + .venv\Scripts\Activate.ps1 + # Uninstall conflicting dependencies + pip uninstall --yes vtk + pip install --extra-index-url https://wheels.vtk.org vtk-osmesa + + - name: Run tests on _unittest + uses: nick-fields/retry@v3 + env: + PYTHONMALLOC: malloc + with: + max_attempts: 2 + retry_on: error + timeout_minutes: 50 + command: | + .venv\Scripts\Activate.ps1 + pytest -n 4 --dist loadfile --durations=50 -v --cov=ansys.aedt.core --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest + + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: codecov-system-tests + file: ./coverage.xml + flags: system + + - name: Upload pytest test results + uses: actions/upload-artifact@v3 + with: + name: pytest-results + path: junit/test-results.xml + if: ${{ always() }} + +# # ================================================================================================= +# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +# # ================================================================================================= + + test-linux: + name: Testing and coverage (Linux) + if: github.event.inputs.test-linux == 'yes' + needs: [smoke-tests] + runs-on: [ self-hosted, Linux, pyaedt ] + env: + ANSYSEM_ROOT242: '/opt/AnsysEM/v242/Linux64' + ANS_NODEPCHECK: '1' + steps: + - name: Install Git and checkout project + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.MAIN_PYTHON_VERSION }} + + - name: Create virtual environment + run: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT242 }}/common/mono/Linux64/lib64:${{ env.ANSYSEM_ROOT242 }}/Delcross:$LD_LIBRARY_PATH + python -m venv .venv + source .venv/bin/activate + python -m pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org pip -U + python -m pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org wheel setuptools -U + python -c "import sys; print(sys.executable)" + + - name: Install pyaedt and tests dependencies + run: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT242 }}/common/mono/Linux64/lib64:${{ env.ANSYSEM_ROOT242 }}/Delcross:$LD_LIBRARY_PATH + source .venv/bin/activate + pip install .[tests] + pip install pytest-azurepipelines + + - name: Install CI dependencies (e.g. vtk-osmesa) + run: | + source .venv/bin/activate + # Uninstall conflicting dependencies + pip uninstall --yes vtk + pip install --extra-index-url https://wheels.vtk.org vtk-osmesa + + - name: Run tests on _unittest + uses: nick-fields/retry@v3 + with: + max_attempts: 2 + retry_on: error + timeout_minutes: 50 + command: | + export LD_LIBRARY_PATH=${{ env.ANSYSEM_ROOT242 }}/common/mono/Linux64/lib64:${{ env.ANSYSEM_ROOT242 }}/Delcross:$LD_LIBRARY_PATH + source .venv/bin/activate + pytest -n 2 --dist loadfile --durations=50 -v --cov=ansys.aedt.core --cov-report=xml --cov-report=html --junitxml=junit/test-results.xml _unittest + + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: codecov-system-solver-tests + file: ./coverage.xml + flags: system,solver + + - name: Upload pytest test results + uses: actions/upload-artifact@v3 + with: + name: pytest-solver-results + path: junit/test-results.xml + if: ${{ always() }} From e6280c771be427c864822a261cfc8a5394b42893 Mon Sep 17 00:00:00 2001 From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> Date: Sat, 28 Sep 2024 14:00:33 +0200 Subject: [PATCH 06/20] FIX: Examples repo compatibility (#5219) Co-authored-by: maxcapodi78 Co-authored-by: Lorenzo Vecchietti --- examples/07-Circuit/Virtual_Compliance.py | 2 +- .../aedt/core/application/analysis_3d.py | 20 ++++++++++++------- src/ansys/aedt/core/circuit.py | 2 +- src/ansys/aedt/core/generic/configurations.py | 2 +- .../aedt/core/modeler/advanced_cad/parts.py | 2 +- .../aedt/core/modeler/cad/components_3d.py | 5 +++-- .../aedt/core/modeler/cad/primitives_3d.py | 2 +- .../aedt/core/visualization/plot/pyvista.py | 5 +++-- .../aedt/core/visualization/post/common.py | 13 ++++++++++-- .../aedt/core/visualization/report/eye.py | 8 +++++--- 10 files changed, 40 insertions(+), 21 deletions(-) diff --git a/examples/07-Circuit/Virtual_Compliance.py b/examples/07-Circuit/Virtual_Compliance.py index 9ba6d67d4c0..dc2aad6d3ff 100644 --- a/examples/07-Circuit/Virtual_Compliance.py +++ b/examples/07-Circuit/Virtual_Compliance.py @@ -12,7 +12,7 @@ import os.path import ansys.aedt.core -from ansys.aedt.core.post.compliance import VirtualCompliance +from ansys.aedt.core.visualization.post.compliance import VirtualCompliance ########################################################## # Set AEDT version diff --git a/src/ansys/aedt/core/application/analysis_3d.py b/src/ansys/aedt/core/application/analysis_3d.py index 322acfa218a..b0d82e54948 100644 --- a/src/ansys/aedt/core/application/analysis_3d.py +++ b/src/ansys/aedt/core/application/analysis_3d.py @@ -491,9 +491,11 @@ def copy_solid_bodies_from(self, design, assignment=None, no_vacuum=True, no_pec or original_design_type == dest_design_type ): new_udc_list.append(udc) - for part_id in design.modeler.user_defined_components[udc].parts: - if design.modeler.user_defined_components[udc].parts[part_id].name in body_list: - body_list.remove(design.modeler.user_defined_components[udc].parts[part_id].name) + parts = design.modeler.user_defined_components[udc].parts + for part_id in parts: + part_name = parts[part_id].name + if part_name in body_list: + body_list.remove(part_name) selection_list = [] udc_selection = [] @@ -1087,10 +1089,12 @@ def flatten_3d_components(self, components=None, purge_history=True, password=No """ if password is None: password = os.getenv("PYAEDT_ENCRYPTED_PASSWORD", "") - native_comp_names = [nc for nc in self.native_components.keys()] + native_comp_names = [nc.definition_name for nc in self.native_components.values()] if not components: components = [ - key for key, val in self.modeler.user_defined_components.items() if val.name not in native_comp_names + key + for key, val in self.modeler.user_defined_components.items() + if val.definition_name not in native_comp_names ] else: if isinstance(components, str): @@ -1101,6 +1105,7 @@ def flatten_3d_components(self, components=None, purge_history=True, password=No for cmp in components: comp = self.modeler.user_defined_components[cmp] + # TODO: Call edit_definition only once target_cs = self.modeler._create_reference_cs_from_3dcomp(comp, password=password) app = comp.edit_definition(password=password) for var, val in comp.parameters.items(): @@ -1110,7 +1115,8 @@ def flatten_3d_components(self, components=None, purge_history=True, password=No monitor_cache = {} if self.design_type == "Icepak": objs_monitors = [part.name for _, part in comp.parts.items()] - for mon_name, mon_obj in self.monitor.all_monitors.items(): + all_monitors = self.monitor.all_monitors.items() + for _, mon_obj in all_monitors: obj_name = mon_obj.properties["Geometry Assignment"] if obj_name in objs_monitors: monitor_cache.update({mon_obj.name: mon_obj.properties}) @@ -1147,7 +1153,7 @@ def flatten_3d_components(self, components=None, purge_history=True, password=No m_type, m_obj, dict_in["monitor"][monitor_obj]["Quantity"], monitor_obj ): # pragma: no cover return False - app.oproject.Close() + app.close_project() if not self.design_type == "Icepak": self.mesh._refresh_mesh_operations() diff --git a/src/ansys/aedt/core/circuit.py b/src/ansys/aedt/core/circuit.py index 946a892ca56..47aa0d6b6f5 100644 --- a/src/ansys/aedt/core/circuit.py +++ b/src/ansys/aedt/core/circuit.py @@ -1610,7 +1610,7 @@ def connect_circuit_models_from_multi_zone_cutout( except Exception: print("failed to get pin2") if pin1 and pin2: - pin1.connect_to_component(component_pin=pin2, use_wire=False) + pin1.connect_to_component(assignment=pin2, use_wire=False) for model_name, ports in ports.items(): if any(cmp for cmp in list(self.modeler.schematic.components.values()) if model_name in cmp.name): model = next( diff --git a/src/ansys/aedt/core/generic/configurations.py b/src/ansys/aedt/core/generic/configurations.py index a26f1d41dbc..776578e28d0 100644 --- a/src/ansys/aedt/core/generic/configurations.py +++ b/src/ansys/aedt/core/generic/configurations.py @@ -2059,7 +2059,7 @@ def apply_operations_to_native_components(obj, operation_dict, native_dict): # vector_list = [decompose_variable_value(operation_dict["Props"]["Vector"][i])[0] for i in range(3)] new_objs = obj.duplicate_along_line( vector_list, - nclones=operation_dict["Props"]["Total Number"], + clones=operation_dict["Props"]["Total Number"], attach_object=operation_dict["Props"]["Attach To Original Object"], ) elif operation_dict["Props"]["Command"] == "DuplicateAroundAxis": diff --git a/src/ansys/aedt/core/modeler/advanced_cad/parts.py b/src/ansys/aedt/core/modeler/advanced_cad/parts.py index 8078b49fc2a..dd6e4b1dbc2 100644 --- a/src/ansys/aedt/core/modeler/advanced_cad/parts.py +++ b/src/ansys/aedt/core/modeler/advanced_cad/parts.py @@ -449,7 +449,7 @@ def insert(self, app): if self["duplicate_vector"]: d_vect = [float(i) for i in self["duplicate_vector"]] duplicate_result = app.modeler.duplicate_along_line( - aedt_objects[0], d_vect, nclones=int(self["duplicate_number"]), is_3d_comp=True + aedt_objects[0], d_vect, clones=int(self["duplicate_number"]), is_3d_comp=True ) if duplicate_result[0]: for d in duplicate_result[1]: diff --git a/src/ansys/aedt/core/modeler/cad/components_3d.py b/src/ansys/aedt/core/modeler/cad/components_3d.py index 32fb372aa9b..a3eb7b1139a 100644 --- a/src/ansys/aedt/core/modeler/cad/components_3d.py +++ b/src/ansys/aedt/core/modeler/cad/components_3d.py @@ -890,11 +890,10 @@ def edit_definition(self, password=None): """ # TODO: Edit documentation to include all supported returned classes. - from ansys.aedt.core.generic.design_types import get_pyaedt_app - # from ansys.aedt.core.generic.general_methods import is_linux if password is None: password = os.getenv("PYAEDT_ENCRYPTED_PASSWORD", "") + project_list = [i for i in self._primitives._app.project_list] self._primitives.oeditor.Edit3DComponentDefinition( @@ -907,6 +906,8 @@ def edit_definition(self, password=None): new_project = [i for i in self._primitives._app.project_list if i not in project_list] if new_project: + from ansys.aedt.core.generic.design_types import get_pyaedt_app + project = self._primitives._app.desktop_class.active_project(new_project[0]) # project = self._primitives._app.odesktop.GetActiveProject() project_name = project.GetName() diff --git a/src/ansys/aedt/core/modeler/cad/primitives_3d.py b/src/ansys/aedt/core/modeler/cad/primitives_3d.py index 9f022fa6598..c1b79ed959c 100644 --- a/src/ansys/aedt/core/modeler/cad/primitives_3d.py +++ b/src/ansys/aedt/core/modeler/cad/primitives_3d.py @@ -1461,7 +1461,7 @@ def _create_reference_cs_from_3dcomp(self, assignment, password): ) return cs_name else: - app.oproject.Close() + app.close_project() return assignment.target_coordinate_system @staticmethod diff --git a/src/ansys/aedt/core/visualization/plot/pyvista.py b/src/ansys/aedt/core/visualization/plot/pyvista.py index d942f724a14..ecc2de28a3a 100644 --- a/src/ansys/aedt/core/visualization/plot/pyvista.py +++ b/src/ansys/aedt/core/visualization/plot/pyvista.py @@ -1540,8 +1540,9 @@ def p_callback(): break i = 0 first_loop = False - scalars = self.frames[i]._cached_polydata.point_data[self.frames[i].scalar_name] - self.pv.update_scalars(scalars, render=False) + mesh_i = self.frames[i]._cached_polydata + scalars = mesh_i.point_data[self.frames[i].scalar_name] + mesh_i.point_data[self.frames[i].scalar_name] = scalars if not hasattr(self.pv, "ren_win"): break time.sleep(max(0, (1 / self.frame_per_seconds) - (time.time() - start))) diff --git a/src/ansys/aedt/core/visualization/post/common.py b/src/ansys/aedt/core/visualization/post/common.py index 374fef3f469..22dda32715b 100644 --- a/src/ansys/aedt/core/visualization/post/common.py +++ b/src/ansys/aedt/core/visualization/post/common.py @@ -1663,8 +1663,17 @@ def create_report_from_configuration(self, input_file=None, report_settings=None else: report_temp = TEMPLATES_BY_NAME[props["report_category"]] report = report_temp(self, props["report_category"], solution_name) - for k, v in props.items(): - report._props[k] = v + + def _update_props(prop_in, props_out): + for k, v in prop_in.items(): + if isinstance(v, dict): + if k not in props_out: + props_out[k] = {} + _update_props(v, props_out[k]) + else: + props_out[k] = v + + _update_props(props, report._props) for el, k in self._app.available_variations.nominal_w_values_dict.items(): if ( report._props.get("context", None) diff --git a/src/ansys/aedt/core/visualization/report/eye.py b/src/ansys/aedt/core/visualization/report/eye.py index 716ca521b2d..5346913f059 100644 --- a/src/ansys/aedt/core/visualization/report/eye.py +++ b/src/ansys/aedt/core/visualization/report/eye.py @@ -43,9 +43,11 @@ def __init__(self, app, report_category, setup_name, expressions=None): self.domain = "Time" self._props["report_type"] = "Rectangular Contour Plot" self.variations.pop("Time", None) - self._props["context"]["variations"]["__UnitInterval"] = "All" - self._props["context"]["variations"]["__Amplitude"] = "All" - self._props["context"]["variations"]["__EyeOpening"] = "0" + self._props["context"]["variations"]["__UnitInterval"] = ["All"] + self._props["context"]["variations"]["__Amplitude"] = ["All"] + self._props["context"]["variations"]["__EyeOpening"] = ["0"] + self._props["context"]["primary_sweep"] = "__UnitInterval" + self._props["context"]["secondary_sweep"] = "__Amplitude" self.quantity_type = 0 self.min_latch_overlay = "0" self.noise_floor = "1e-16" From 78edb4533f715d78fb67327199e58f8f7c0205e6 Mon Sep 17 00:00:00 2001 From: Isaac Waldron Date: Tue, 1 Oct 2024 03:32:07 -0400 Subject: [PATCH 07/20] FIX: Issue error and skip material on parse error (#5217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sébastien Morais <146729917+SMoraisAnsys@users.noreply.github.com> --- _unittest/test_03_Materials.py | 32 +++++++++++++++++++++ src/ansys/aedt/core/modules/material_lib.py | 14 +++++---- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/_unittest/test_03_Materials.py b/_unittest/test_03_Materials.py index 7339d1eda5b..dcecf5a5526 100644 --- a/_unittest/test_03_Materials.py +++ b/_unittest/test_03_Materials.py @@ -22,7 +22,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import builtins +import logging import os +from unittest.mock import mock_open from _unittest.conftest import config from _unittest.conftest import local_path @@ -30,10 +33,27 @@ from ansys.aedt.core import Maxwell3d from ansys.aedt.core.modules.material import MatProperties from ansys.aedt.core.modules.material import SurfMatProperties +from mock import patch import pytest test_subfolder = "T03" +MISSING_RGB_MATERIALS = b""" +{ + "materials": { + "copper_5540": { + "AttachedData": { + "MatAppearanceData": { + "property_data": "appearance_data" + } + }, + "permeability": "0.999991", + "conductivity": "58000000" + } + } +} +""" + @pytest.fixture(scope="class") def aedtapp(add_app): @@ -527,3 +547,15 @@ def test_16_import_materials_from_workbench(self): self.testapp2.materials.material_keys["84zn_12ag_4au_imp"].thermal_expansion_coefficient.thermalmodifier == "pwl($TM_84Zn_12Ag_4Au_imp_thermal_expansion_coefficient, Temp)" ) + + @patch.object(builtins, "open", new_callable=mock_open, read_data=MISSING_RGB_MATERIALS) + def test_17_json_missing_rgb(self, _mock_file_open, caplog: pytest.LogCaptureFixture): + input_path = os.path.join(local_path, "example_models", test_subfolder, "mats.json") + with pytest.raises(KeyError): + self.aedtapp.materials.import_materials_from_file(input_path) + assert [ + record + for record in caplog.records + if record.levelno == logging.ERROR + and record.message == f"Failed to import material 'copper_5540' from {input_path!r}: key error on 'Red'" + ] diff --git a/src/ansys/aedt/core/modules/material_lib.py b/src/ansys/aedt/core/modules/material_lib.py index 345eaa67866..07164a4a563 100644 --- a/src/ansys/aedt/core/modules/material_lib.py +++ b/src/ansys/aedt/core/modules/material_lib.py @@ -854,11 +854,15 @@ def import_materials_from_file(self, input_file=None, **kwargs): self.logger.warning("Material %s already exists. Renaming to %s", el, newname) else: newname = el - newmat = Material(self, newname, val, material_update=False) - newmat.update() - newmat._material_update = True - self.material_keys[newname] = newmat - materials_added.append(newmat) + try: + newmat = Material(self, newname, val, material_update=False) + newmat.update() + newmat._material_update = True + self.material_keys[newname] = newmat + materials_added.append(newmat) + except KeyError as e: + self.logger.error(f"Failed to import material {el!r} from {input_file!r}: key error on {e}") + raise e else: for mat_name in data: invalid_names = ["$base_index$", "$index$"] From b7d29a46a28663c5719d95f2b5c84e101735cb11 Mon Sep 17 00:00:00 2001 From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:57:47 +0200 Subject: [PATCH 08/20] FIX: Skip test until it is fixed (#5235) --- _unittest_solvers/test_45_workflows.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/_unittest_solvers/test_45_workflows.py b/_unittest_solvers/test_45_workflows.py index 1c83eabddd5..e200e352601 100644 --- a/_unittest_solvers/test_45_workflows.py +++ b/_unittest_solvers/test_45_workflows.py @@ -153,6 +153,10 @@ def test_07_twinbuilder_convert_circuit(self, add_app): assert main({"is_test": True}) + @pytest.mark.skipif( + TEST_REVIEW_FLAG, + reason="Test under review in 2024.2", + ) def test_08_configure_a3d(self, local_scratch): from ansys.aedt.core.workflows.project.configure_edb import main From 06cd5949d573c01dbaf5fcfd34c34664d85393b3 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:05:56 +0200 Subject: [PATCH 09/20] FIX: AMI reports are not working in all cases. (#5228) Co-authored-by: maxcapodi78 Co-authored-by: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> --- src/ansys/aedt/core/visualization/report/eye.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ansys/aedt/core/visualization/report/eye.py b/src/ansys/aedt/core/visualization/report/eye.py index 5346913f059..c24a6d21c83 100644 --- a/src/ansys/aedt/core/visualization/report/eye.py +++ b/src/ansys/aedt/core/visualization/report/eye.py @@ -47,7 +47,6 @@ def __init__(self, app, report_category, setup_name, expressions=None): self._props["context"]["variations"]["__Amplitude"] = ["All"] self._props["context"]["variations"]["__EyeOpening"] = ["0"] self._props["context"]["primary_sweep"] = "__UnitInterval" - self._props["context"]["secondary_sweep"] = "__Amplitude" self.quantity_type = 0 self.min_latch_overlay = "0" self.noise_floor = "1e-16" @@ -553,6 +552,8 @@ def expressions(self): new_exprs.append("{}AfterChannel<".format(expr_head) + expr + ".int_ami_rx>") elif qtype == 3: new_exprs.append("{}AfterProbe<".format(expr_head) + expr + ".int_ami_rx>") + else: + new_exprs.append(expr) return new_exprs @property From 0318017ca5c3906b813c2d9503cbe16a4ad4330d Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Thu, 3 Oct 2024 08:28:00 +0200 Subject: [PATCH 10/20] FEAT: LOG messages (#5218) Co-authored-by: maxcapodi78 --- examples/04-Icepak/Icepak_ECAD_Import.py | 6 ++--- src/ansys/aedt/core/aedt_logger.py | 7 +++--- src/ansys/aedt/core/application/design.py | 25 +++++++++++++------ .../core/generic/grpc_plugin_dll_class.py | 2 +- src/ansys/aedt/core/modeler/cad/primitives.py | 6 ++++- src/ansys/aedt/core/modeler/modeler_pcb.py | 6 ++++- 6 files changed, 35 insertions(+), 17 deletions(-) diff --git a/examples/04-Icepak/Icepak_ECAD_Import.py b/examples/04-Icepak/Icepak_ECAD_Import.py index 1bdbc73dc74..b51476a40cc 100644 --- a/examples/04-Icepak/Icepak_ECAD_Import.py +++ b/examples/04-Icepak/Icepak_ECAD_Import.py @@ -16,11 +16,9 @@ # PyAEDT Packages import ansys.aedt.core -from ansys.aedt.core import Icepak -from ansys.aedt.core import Desktop from ansys.aedt.core import Hfss3dLayout -from ansys.aedt.core.modules.boundary import BoundaryObject - +from ansys.aedt.core import settings +settings.enable_debug_grpc_api_logger = True ########################################################## # Set AEDT version # ~~~~~~~~~~~~~~~~ diff --git a/src/ansys/aedt/core/aedt_logger.py b/src/ansys/aedt/core/aedt_logger.py index fe7c5cf88ba..88108cb9250 100644 --- a/src/ansys/aedt/core/aedt_logger.py +++ b/src/ansys/aedt/core/aedt_logger.py @@ -629,7 +629,7 @@ def _log_on_handler(self, message_type, message_text, *args, **kwargs): self._messages.append([message_type, msg1, self.project_name, self.design_name]) if not (self._log_on_file or self._log_on_screen) or not self._global: return - if len(message_text) > 250: + if len(message_text) > 250 and message_type < 3: message_text = message_text[:250] + "..." if message_type == 0: self._global.info(message_text, *args, **kwargs) @@ -922,7 +922,7 @@ def error(self, msg, *args, **kwargs): def debug(self, msg, *args, **kwargs): """Write a debug message to the global logger.""" - if not settings.enable_debug_logger or not settings.enable_logger: + if not (settings.enable_debug_logger or settings.enable_debug_grpc_api_logger) or not settings.enable_logger: return if args: try: @@ -931,7 +931,8 @@ def debug(self, msg, *args, **kwargs): msg1 = msg else: msg1 = msg - self._log_on_dekstop(0, msg1, "Global") + if not settings.enable_debug_grpc_api_logger: + self._log_on_dekstop(0, msg1, "Global") return self._log_on_handler(3, msg, *args, **kwargs) @property diff --git a/src/ansys/aedt/core/application/design.py b/src/ansys/aedt/core/application/design.py index f4f9b846ba0..afacd86d07a 100644 --- a/src/ansys/aedt/core/application/design.py +++ b/src/ansys/aedt/core/application/design.py @@ -237,6 +237,7 @@ def __init__( ): self._design_name = None self._project_name = None + self._project_path = None self.__t = None if ( not is_ironpython @@ -653,15 +654,16 @@ def design_name(self): >>> hfss = Hfss() >>> hfss.design_name = 'new_design' """ + if self._design_name: + return self._design_name from ansys.aedt.core.generic.general_methods import _retry_ntimes if not self.odesign: return None self._design_name = _retry_ntimes(5, self.odesign.GetName) if ";" in self._design_name: - return self._design_name.split(";")[1] - else: - return self._design_name + self._design_name = self._design_name.split(";")[1] + return self._design_name @design_name.setter def design_name(self, new_name): @@ -684,7 +686,7 @@ def design_name(self, new_name): timeout -= timestep if timeout < 0: raise RuntimeError("Timeout reached while checking design renaming.") - self._design_name = new_name + self._design_name = new_name @property def design_list(self): @@ -739,9 +741,12 @@ def project_name(self): >>> oProject.GetName """ + if self._project_name: + return self._project_name if self.oproject: try: self._project_name = self.oproject.GetName() + self._project_path = None return self._project_name except Exception: return None @@ -778,9 +783,9 @@ def project_path(self): >>> oProject.GetPath """ - if self.oproject: - return self.oproject.GetPath() - return None + if not self._project_path and self.oproject: + self._project_path = self.oproject.GetPath() + return self._project_path @property def project_time_stamp(self): @@ -1154,6 +1159,7 @@ def odesign(self, des_name): ): self.set_oo_property_value(self.odesign, "Design Settings", "Design Mode/IC", self._ic_mode) self.desktop_class.active_design(self.oproject, des_name) + self._design_name = None @property def oproject(self): @@ -1278,6 +1284,8 @@ def oproject(self, proj_name=None): self._oproject = self.desktop_class.active_project(new_project_list[0]) self._add_handler() self.logger.info("Project %s has been created.", self._oproject.GetName()) + self._project_name = None + self._project_path = None def _add_handler(self): if ( @@ -3560,6 +3568,7 @@ def rename_design(self, name, save=True): >>> oDesign.RenameDesignInstance """ self._odesign.RenameDesignInstance(self.design_name, name) + self._design_name = None if save: self.oproject.Save() self._project_dictionary = None @@ -3802,6 +3811,8 @@ def save_project(self, file_name=None, overwrite=True, refresh_ids=False): self.modeler.refresh_all_ids() self.modeler._refresh_all_ids_from_aedt_file() self.mesh._refresh_mesh_operations() + self._project_name = None + self._project_path = None msg_text = "Project {0} Saved correctly".format(self.project_name) self.logger.info(msg_text) return True diff --git a/src/ansys/aedt/core/generic/grpc_plugin_dll_class.py b/src/ansys/aedt/core/generic/grpc_plugin_dll_class.py index 443a8cc3674..83dfad63e73 100644 --- a/src/ansys/aedt/core/generic/grpc_plugin_dll_class.py +++ b/src/ansys/aedt/core/generic/grpc_plugin_dll_class.py @@ -111,7 +111,7 @@ def __str__(self): def __Invoke__(self, funcName, argv): if settings.enable_debug_grpc_api_logger: - settings.logger.debug("{} {}".format(funcName, argv)) + settings.logger.debug(" {}{}".format(funcName, argv)) try: if (settings.use_multi_desktop and funcName not in exclude_list) or funcName in inclusion_list: self.dllapi.recreate_application(True) diff --git a/src/ansys/aedt/core/modeler/cad/primitives.py b/src/ansys/aedt/core/modeler/cad/primitives.py index c48bbc99cdc..d643dc1fcb6 100644 --- a/src/ansys/aedt/core/modeler/cad/primitives.py +++ b/src/ansys/aedt/core/modeler/cad/primitives.py @@ -261,6 +261,7 @@ def __init__(self, app, is3d=True): self._points = [] self._unclassified = [] self._all_object_names = [] + self._model_units = None self._object_names_to_ids = {} self.objects = Objects(self, "o") self.user_defined_components = Objects(self, "u") @@ -429,12 +430,15 @@ def model_units(self): >>> oEditor.GetModelUnits >>> oEditor.SetModelUnits """ - return self.oeditor.GetModelUnits() + if not self._model_units: + self._model_units = self.oeditor.GetModelUnits() + return self._model_units @model_units.setter def model_units(self, units): assert units in AEDT_UNITS["Length"], "Invalid units string {0}.".format(units) self.oeditor.SetModelUnits(["NAME:Units Parameter", "Units:=", units, "Rescale:=", False]) + self._model_units = units @property def selections(self): diff --git a/src/ansys/aedt/core/modeler/modeler_pcb.py b/src/ansys/aedt/core/modeler/modeler_pcb.py index 3a4ecb095f9..46a0026fc46 100644 --- a/src/ansys/aedt/core/modeler/modeler_pcb.py +++ b/src/ansys/aedt/core/modeler/modeler_pcb.py @@ -61,6 +61,7 @@ def __init__(self, app): self._app = app self._edb = None self.logger.info("Loading Modeler.") + self._model_units = None Modeler.__init__(self, app) self.logger.info("Modeler loaded.") self.logger.info("EDB loaded.") @@ -176,12 +177,15 @@ def model_units(self): >>> oEditor.GetActiveUnits >>> oEditor.SetActiveUnits """ - return self.oeditor.GetActiveUnits() + if not self._model_units: + self._model_units = self.oeditor.GetActiveUnits() + return self._model_units @model_units.setter def model_units(self, units): assert units in AEDT_UNITS["Length"], "Invalid units string {0}.".format(units) self.oeditor.SetActiveUnits(units) + self._model_units = units @property def primitives(self): From 885657ce16e568c09e7c52ff8c00c966eb2ab8fb Mon Sep 17 00:00:00 2001 From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> Date: Thu, 3 Oct 2024 08:34:47 +0200 Subject: [PATCH 11/20] FIX: Remove unused lines (#5229) --- src/ansys/aedt/core/modeler/cad/primitives_3d.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/ansys/aedt/core/modeler/cad/primitives_3d.py b/src/ansys/aedt/core/modeler/cad/primitives_3d.py index c1b79ed959c..4be44774a17 100644 --- a/src/ansys/aedt/core/modeler/cad/primitives_3d.py +++ b/src/ansys/aedt/core/modeler/cad/primitives_3d.py @@ -2685,14 +2685,7 @@ def _make_winding(self, name, material, in_rad, out_rad, height, teta, turns, ch else: union_polyline2 = [] union_polyline = union_polyline1 + union_polyline2 - list_positions2 = [] - for i, p in enumerate(union_polyline): - if i == 0: - list_positions2.extend(self.get_vertices_of_line(p)) - else: - list_positions2.extend(self.get_vertices_of_line(p)[1:]) self.delete(union_polyline) - # del list_positions[0] if sep_layer: for i in range(4): From 3a14ab22e1abd6e7187854caa532e1334af59f7c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 22:21:09 +0200 Subject: [PATCH 12/20] CHORE: Bump pre-commit version (#5227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Co-authored-by: Sébastien Morais <146729917+SMoraisAnsys@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8409e50c7a5..76e554f3329 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: # validate GitHub workflow files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.2 + rev: 0.29.3 hooks: - id: check-github-workflows From aabca8bb89a5edeb87bd478eca15875ba43c60aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Morais?= <146729917+SMoraisAnsys@users.noreply.github.com> Date: Thu, 3 Oct 2024 23:14:00 +0200 Subject: [PATCH 13/20] CI: Trigger workflow on ready for review event (#5224) Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> --- .github/workflows/ci_cd.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index d48960205a9..32a1a601f9a 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -1,6 +1,9 @@ name: GitHub CI CD on: pull_request: + # GitHub default types + ready_for_review to trigger the workflow on PRs no longer in draft mode. + # See https://github.com/ansys/pyaedt/issues/5223 for more information + types: [opened, synchronize, reopened, ready_for_review] workflow_dispatch: push: tags: From d5e74a981dec101caf4ffd2a20f8a41d4d929c46 Mon Sep 17 00:00:00 2001 From: gmalinve <103059376+gmalinve@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:43:52 +0200 Subject: [PATCH 14/20] FIX: Get variables with "sweep" enabled (#5230) Co-authored-by: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> --- _unittest/test_09_VariableManager.py | 11 +++++--- src/ansys/aedt/core/application/analysis.py | 10 +++---- src/ansys/aedt/core/application/variables.py | 28 +++++++++++++++++++ .../core/visualization/post/post_common_3d.py | 15 ++++++---- 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/_unittest/test_09_VariableManager.py b/_unittest/test_09_VariableManager.py index 006458c9023..846f3670c01 100644 --- a/_unittest/test_09_VariableManager.py +++ b/_unittest/test_09_VariableManager.py @@ -158,12 +158,15 @@ def test_03_test_evaluated_value(self): eval_p3_nom = v._app.get_evaluated_value("p3") assert isclose(eval_p3_nom, 0.0002) v_app = self.aedtapp.variable_manager - assert v_app["p1"].read_only == False + assert v_app["p1"].sweep + v_app["p1"].sweep = False + assert not v_app["p1"].sweep + assert not v_app["p1"].read_only v_app["p1"].read_only = True - assert v_app["p1"].read_only == True - assert v_app["p1"].hidden == False + assert v_app["p1"].read_only + assert not v_app["p1"].hidden v_app["p1"].hidden = True - assert v_app["p1"].hidden == True + assert v_app["p1"].hidden assert v_app["p2"].description == "" v_app["p2"].description = "myvar" assert v_app["p2"].description == "myvar" diff --git a/src/ansys/aedt/core/application/analysis.py b/src/ansys/aedt/core/application/analysis.py index fc5b616be48..06b3cfd1050 100644 --- a/src/ansys/aedt/core/application/analysis.py +++ b/src/ansys/aedt/core/application/analysis.py @@ -1183,12 +1183,12 @@ def nominal(self): @property def nominal_w_values(self): - """Nominal with values. + """Nominal independent with values in a list. Returns ------- - dict - Dictionary of nominal variations with expressions. + list + List of nominal independent variations with expressions. References ---------- @@ -1229,12 +1229,12 @@ def nominal_w_values_dict(self): @property def nominal_w_values_dict_w_dependent(self): - """Nominal with values in a dictionary. + """Nominal independent and dependent with values in a dictionary. Returns ------- dict - Dictionary of nominal variations with values. + Dictionary of nominal independent and dependent variations with values. References ---------- diff --git a/src/ansys/aedt/core/application/variables.py b/src/ansys/aedt/core/application/variables.py index 7091243ab02..cebec59aeea 100644 --- a/src/ansys/aedt/core/application/variables.py +++ b/src/ansys/aedt/core/application/variables.py @@ -964,6 +964,7 @@ def set_variable( read_only=False, hidden=False, description=None, + sweep=True, overwrite=True, is_post_processing=False, circuit_parameter=True, @@ -988,6 +989,11 @@ def set_variable( description : str, optional Text to display for the design property or project variable in the ``Properties`` window. The default is ``None``. + sweep : bool, optional + Allows you to designate variables to include in solution indexing as a way to + permit faster post-processing. + Variables with the Sweep check box cleared are not used in solution indexing. + The default is ``True``. overwrite : bool, optional Whether to overwrite an existing value for the design property or project variable. The default is ``False``, in @@ -1128,6 +1134,8 @@ def set_variable( read_only, "Hidden:=", hidden, + "Sweep:=", + sweep, ], ], ], @@ -1178,6 +1186,8 @@ def set_variable( read_only, "Hidden:=", hidden, + "Sweep:=", + sweep, ], ], ], @@ -1441,6 +1451,7 @@ def __init__( app=None, readonly=False, hidden=False, + sweep=True, description=None, postprocessing=False, circuit_parameter=True, @@ -1451,6 +1462,7 @@ def __init__( self._app = app self._readonly = readonly self._hidden = hidden + self._sweep = sweep self._postprocessing = postprocessing self._circuit_parameter = circuit_parameter self._description = description @@ -1503,6 +1515,7 @@ def _update_var(self): self._expression, read_only=self._readonly, hidden=self._hidden, + sweep=self._sweep, description=self._description, is_post_processing=self._postprocessing, circuit_parameter=self._circuit_parameter, @@ -1715,6 +1728,21 @@ def hidden(self, value): if self._app: self._app.logger.error('Failed to update property "hidden".') + @property + def sweep(self): + """Sweep flag value.""" + self._sweep = self._get_prop_val("Sweep") + return self._sweep + + @sweep.setter + def sweep(self, value): + fallback_val = self._sweep + self._sweep = value + if not self._update_var(): + self._sweep = fallback_val + if self._app: + self._app.logger.error('Failed to update property "sweep".') + @property def description(self): """Description value.""" diff --git a/src/ansys/aedt/core/visualization/post/post_common_3d.py b/src/ansys/aedt/core/visualization/post/post_common_3d.py index 48b60aadb53..c00ec7f0640 100644 --- a/src/ansys/aedt/core/visualization/post/post_common_3d.py +++ b/src/ansys/aedt/core/visualization/post/post_common_3d.py @@ -494,8 +494,9 @@ def get_scalar_field_value( variation = [] for el, value in variations.items(): - variation.append(el + ":=") - variation.append(value) + if self._app.variable_manager.variables[el].sweep: + variation.append(el + ":=") + variation.append(value) variation.extend(intrinsics) @@ -674,8 +675,9 @@ def export_field_file_on_grid( variation = [] for el, value in variations.items(): - variation.append(el + ":=") - variation.append(value) + if self._app.variable_manager.variables[el].sweep: + variation.append(el + ":=") + variation.append(value) variation.extend(intrinsics) export_options = [ @@ -842,8 +844,9 @@ def export_field_file( variation = [] for el, value in variations.items(): - variation.append(el + ":=") - variation.append(value) + if self._app.variable_manager.variables[el].sweep: + variation.append(el + ":=") + variation.append(value) variation.extend(intrinsics) if not sample_points_file and not sample_points: From c71c824147946f517d8e1cc9ce5aef0877adc072 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:52:06 +0200 Subject: [PATCH 15/20] FEAT: New class to create Virtual Compliance file from existing reports configuration files. (#5237) Co-authored-by: maxcapodi78 --- _unittest_solvers/test_01_pdf.py | 20 +- doc/source/API/visualization/post.rst | 1 + examples/07-Circuit/Virtual_Compliance.py | 1 + .../aedt/core/generic/general_methods.py | 2 + .../aedt/core/modeler/cad/elements_3d.py | 42 ++- src/ansys/aedt/core/visualization/plot/pdf.py | 2 +- .../aedt/core/visualization/post/common.py | 7 +- .../core/visualization/post/compliance.py | 284 ++++++++++++++++-- .../aedt/core/visualization/report/common.py | 2 +- .../aedt/core/visualization/report/eye.py | 19 +- .../core/visualization/report/standard.py | 3 + 11 files changed, 334 insertions(+), 49 deletions(-) diff --git a/_unittest_solvers/test_01_pdf.py b/_unittest_solvers/test_01_pdf.py index 69b8294f3fc..cf984423e2f 100644 --- a/_unittest_solvers/test_01_pdf.py +++ b/_unittest_solvers/test_01_pdf.py @@ -5,7 +5,7 @@ import pytest from ansys.aedt.core import Circuit -from ansys.aedt.core.visualization.post.compliance import VirtualCompliance +from ansys.aedt.core.visualization.post.compliance import VirtualCompliance, VirtualComplianceGenerator from ansys.aedt.core.visualization.plot.pdf import AnsysReport tol = 1e-12 @@ -81,6 +81,24 @@ def test_virtual_compliance(self, local_scratch, aedtapp): f.seek(0) json.dump(data, f, indent=4) f.truncate() + compliance_folder = os.path.join(local_scratch.path, "vc") + os.makedirs(compliance_folder, exist_ok=True) + vc = VirtualComplianceGenerator("Test_full", "Diff_Via") + for plot in aedtapp.post.plots[::]: + try: + plot.export_config(f"{compliance_folder}\\report_{plot.plot_name}.json") + except Exception: + print(f"Failed to generate {plot.plot_name}") + + vc.add_report_from_folder(os.path.join(local_path, "example_models", test_subfolder, "compliance"), + design_name="Circuit1", group_plots=True, + project=aedtapp.project_file) + vc.add_erl_parameters(design_name=aedtapp.design_name, config_file=f"{compliance_folder}\\config.cfg", + traces=["RX1", "RX3"], pins=[ + ["X1_A5_PCIe_Gen4_RX1_P", "X1_A6_PCIe_Gen4_RX1_N", "U1_AR25_PCIe_Gen4_RX1_P", + "U1_AP25_PCIe_Gen4_RX1_N"], [7, 8, 18, 17]], pass_fail=True, pass_fail_criteria=3, name="ERL") + vc.save_configuration(f"{compliance_folder}\\main.json") + assert os.path.exists(os.path.join(compliance_folder, "main.json")) v = VirtualCompliance(aedtapp.desktop_class, template) assert v.create_compliance_report() diff --git a/doc/source/API/visualization/post.rst b/doc/source/API/visualization/post.rst index fdc7dfbfb60..d8490fc6933 100644 --- a/doc/source/API/visualization/post.rst +++ b/doc/source/API/visualization/post.rst @@ -262,4 +262,5 @@ If you are looking for Virtual Compliance post processing, you should use this s :toctree: _autosummary :nosignatures: + VirtualComplianceGenerator VirtualCompliance \ No newline at end of file diff --git a/examples/07-Circuit/Virtual_Compliance.py b/examples/07-Circuit/Virtual_Compliance.py index dc2aad6d3ff..47d8b9c669b 100644 --- a/examples/07-Circuit/Virtual_Compliance.py +++ b/examples/07-Circuit/Virtual_Compliance.py @@ -185,6 +185,7 @@ v.reports["eye1"].traces = eye_curve_tx v.reports["eye3"].traces = eye_curve_tx v.reports["tdr from circuit"].traces = tdr_probe_name +v.parameters = {} v.parameters["erl"].trace_pins = [ ["X1.A5.PCIe_Gen4_RX1_P", "X1.A6.PCIe_Gen4_RX1_N", "U1.AR25.PCIe_Gen4_RX1_P", "U1.AP25.PCIe_Gen4_RX1_N"], [7, 8, 18, 17]] diff --git a/src/ansys/aedt/core/generic/general_methods.py b/src/ansys/aedt/core/generic/general_methods.py index 6a1242803e1..786f4741f44 100644 --- a/src/ansys/aedt/core/generic/general_methods.py +++ b/src/ansys/aedt/core/generic/general_methods.py @@ -1349,6 +1349,7 @@ def _dict_toml(d): new_dict = _dict_toml(input_dict) with open_file(full_toml_path, "w") as fp: tomllib.dump(new_dict, fp) + settings.logger.info(f"{full_toml_path} correctly created.") return True @@ -1370,6 +1371,7 @@ def _create_json_file(json_dict, full_json_path): with open_file(full_json_path, "w") as file: file.write(filedata) os.remove(temp_path) + settings.logger.info(f"{full_json_path} correctly created.") return True diff --git a/src/ansys/aedt/core/modeler/cad/elements_3d.py b/src/ansys/aedt/core/modeler/cad/elements_3d.py index b0f1d2446f9..cccb91e3b5c 100644 --- a/src/ansys/aedt/core/modeler/cad/elements_3d.py +++ b/src/ansys/aedt/core/modeler/cad/elements_3d.py @@ -1386,6 +1386,7 @@ class BinaryTreeNode: """Manages an object's history structure.""" def __init__(self, node, child_object, first_level=False, get_child_obj_arg=None, root_name=None): + self._props = None if not root_name: root_name = node saved_root_name = node if first_level else root_name @@ -1418,34 +1419,53 @@ def __init__(self, node, child_object, first_level=False, get_child_obj_arg=None ) if first_level: self.child_object = self.children[name].child_object - self.props = self.children[name].props + self._props = self.children[name]._props if name == "CreatePolyline:1": self.segments = self.children[name].children del self.children[name] - else: - self.props = {} + + @property + def props(self): + """Properties data. + + Returns + ------- + :class:``ansys.aedt.coree.modeler.cad.elements_3d.HistoryProps`` + """ + if self._props is None: + self._props = {} if settings.aedt_version >= "2024.2": try: props = self._get_data_model() for p in self.child_object.GetPropNames(): if p in props: - self.props[p] = props[p] + self._props[p] = props[p] else: - self.props[p] = None + self._props[p] = None except Exception: for p in self.child_object.GetPropNames(): try: - self.props[p] = self.child_object.GetPropValue(p) + self._props[p] = self.child_object.GetPropValue(p) except Exception: - self.props[p] = None + self._props[p] = None else: for p in self.child_object.GetPropNames(): try: - self.props[p] = self.child_object.GetPropValue(p) + self._props[p] = self.child_object.GetPropValue(p) except Exception: - self.props[p] = None - self.props = HistoryProps(self, self.props) - self.command = self.props.get("Command", "") + self._props[p] = None + self._props = HistoryProps(self, self._props) + return self._props + + @property + def command(self): + """Command of the modeler hystory if available. + + Returns + ------- + str + """ + return self.props.get("Command", "") def _get_data_model(self): import ast diff --git a/src/ansys/aedt/core/visualization/plot/pdf.py b/src/ansys/aedt/core/visualization/plot/pdf.py index a16a4c6b3f8..031c70bc01d 100644 --- a/src/ansys/aedt/core/visualization/plot/pdf.py +++ b/src/ansys/aedt/core/visualization/plot/pdf.py @@ -285,7 +285,7 @@ def add_project_info(self, design): msg = f"Furthermore, the layout has {stats.num_nets} nets, {stats.num_traces} traces," msg += f" {stats.num_vias} vias. The stackup total thickness is {stats.stackup_thickness}." image_path = os.path.join(design.working_directory, "model.jpg") - design.modeler.edb.nets.plot() + design.modeler.edb.nets.plot(show=False, save_plot=image_path) if os.path.exists(image_path): self.add_image(image_path, "Model Image") elif design.design_type in ["Circuit Design"]: diff --git a/src/ansys/aedt/core/visualization/post/common.py b/src/ansys/aedt/core/visualization/post/common.py index 22dda32715b..16f4f249438 100644 --- a/src/ansys/aedt/core/visualization/post/common.py +++ b/src/ansys/aedt/core/visualization/post/common.py @@ -1646,7 +1646,7 @@ def create_report_from_configuration(self, input_file=None, report_settings=None props["expressions"] = {props["expressions"]: {}} _dict_items_to_list_items(props, "expressions") if not solution_name: - if "Fields" in props.get("report_category", ""): + if "Fields" not in props.get("report_category", ""): solution_name = self._app.nominal_sweep else: solution_name = self._app.nominal_adaptive @@ -1673,6 +1673,11 @@ def _update_props(prop_in, props_out): else: props_out[k] = v + if ( + props.get("context", {"context": {}}).get("secondary_sweep", "") == "" + and props.get("report_type", "") != "Rectangular Contour Plot" + ): + report._props["context"]["secondary_sweep"] = "" _update_props(props, report._props) for el, k in self._app.available_variations.nominal_w_values_dict.items(): if ( diff --git a/src/ansys/aedt/core/visualization/post/compliance.py b/src/ansys/aedt/core/visualization/post/compliance.py index 53cfb0707cf..dd0fb7fb38c 100644 --- a/src/ansys/aedt/core/visualization/post/compliance.py +++ b/src/ansys/aedt/core/visualization/post/compliance.py @@ -21,7 +21,7 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. - +import copy import os.path from pathlib import Path import time @@ -32,10 +32,12 @@ from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.generic.general_methods import read_configuration_file from ansys.aedt.core.generic.general_methods import read_csv +from ansys.aedt.core.generic.general_methods import write_configuration_file from ansys.aedt.core.generic.general_methods import write_csv from ansys.aedt.core.modeler.geometry_operators import GeometryOperators from ansys.aedt.core.visualization.plot.pdf import AnsysReport from ansys.aedt.core.visualization.post.spisim import SpiSim +from pyedb.generic.constants import unit_converter default_keys = [ "file", @@ -64,6 +66,8 @@ def __init__(self, report): self.traces = report.get("traces", []) self.pass_fail = report.get("pass_fail", False) self.group_plots = report.get("group_plots", False) + self._project_name = None + self.project = report.get("project", None) @property def name(self): @@ -75,6 +79,33 @@ def name(self): """ return self._name + @property + def project_name(self): + """Project name. + + Returns + ------- + str + """ + if self._project_name: + return self._project_name + if self.project and self.project.endswith(".aedt"): + return os.path.split(os.path.splitext(self.project)[0])[-1] + return + + @project_name.setter + def project_name(self, val): + self._project_name = val + + @property + def project(self): + """Project path.""" + return self._project + + @project.setter + def project(self, val): + self._project = val + @property def report_type(self): """Report type. @@ -204,6 +235,170 @@ def pass_fail_criteria(self, val): self._pass_fail_criteria = val +class VirtualComplianceGenerator: + """Class to generate a Virtual Compliance configuration.""" + + def __init__(self, compliance_name, project_name, specification_folder=None): + self.config = { + "general": { + "name": compliance_name, + "version": "1.0", + "add_project_info": True, + "add_specs_info": True if specification_folder else False, + "project_info_keys": [ + "file", + "power", + "buffer", + "trise", + "tfall", + "UIorBPSValue", + "BitPattern", + "R", + "L", + "C", + "DC", + "V1", + "V2", + "TD", + ], + "specs_folder": specification_folder if specification_folder else "", + "delete_after_export": True, + "project": project_name, + "use_portrait": True, + }, + "parameters": [], + "reports": [], + } + + @pyaedt_function_handler() + def add_erl_parameters( + self, design_name, config_file, traces, pins, pass_fail, pass_fail_criteria=0, name="ERL", project=None + ): + """Add Com parameters computed by SpiSim into the configuration. + + Parameters + ---------- + design_name : str + Design name. + config_file : str + Full path to ``cfg`` file. + traces : list + List of traces to compute com parameters. + pins : list + List of list containing input pints and output pins on which compute com parameters. + Pins can be names or numbers. + pass_fail : bool + Whether if to compute pass fail on this parameter or not. + If True, then the parameter ``pass_fail_criteria`` has to be set accordingly. + pass_fail_criteria : float + If the criteria is greater + name : str, optional + Name of the report. + project : str, optional + Full path to the project to use for the computation of this report. + If ``None`` the default project will be used. + """ + pars = { + "name": name, + "design_name": design_name, + "config": config_file, + "traces": traces, + "trace_pins": pins, + "pass_fail": pass_fail, + "pass_fail_criteria": pass_fail_criteria, + } + if project: + pars["project"] = project + self.config["parameters"].append(pars) + + @pyaedt_function_handler() + def add_report(self, design_name, config_file, traces, report_type, pass_fail, group_plots, name, project=None): + """Add Com parameters computed by SpiSim into the configuration. + + Parameters + ---------- + design_name : str + Design name. + config_file : str + Full path to ``cfg`` file. + traces : list + List of traces to compute com parameters. + report_type : str + Report Type. + pass_fail : bool + Whether if to compute pass fail on this parameter or not. + group_plots : bool + Whether if to group all the traces in the same plot or not. + name : str, optional + Name of the report. + project : str, optional + Full path to the project to use for the computation of this report. + If ``None`` the default project will be used. + """ + pars = { + "name": name, + "design_name": design_name, + "type": report_type, + "config": config_file, + "traces": traces, + "pass_fail": pass_fail, + "group_plots": group_plots, + } + if project: + pars["project"] = project + self.config["reports"].append(pars) + + @pyaedt_function_handler() + def add_report_from_folder(self, input_folder, design_name, group_plots=False, project=None): + """Add multiple reports from a folder. + + Parameters + ---------- + input_folder : str + Full path to the folder containing configuration files. + design_name : str + Name of design to apply the configuration. + group_plots : bool + Whether to group plot traces or not. + """ + input_reports = search_files(input_folder, "*.json") + for input_report in input_reports: + conf = read_configuration_file(input_report) + if "limitLines" in conf and conf["limitLines"] or "eye_mask" in conf: + pass_fail = True + else: + pass_fail = False + expr = list(conf["expressions"].keys()) if conf.get("expressions", []) else [] + rep_type = conf.get("report_category", "frequency").lower() + name = conf.get("plot_name", generate_unique_name("Report")) + self.add_report( + design_name, + input_report, + traces=expr, + report_type=rep_type, + pass_fail=pass_fail, + group_plots=group_plots, + name=name, + project=project, + ) + + @pyaedt_function_handler() + def save_configuration(self, output_file): + """Save the configuration to a json file. + + Parameters + ---------- + output_file : str + Full path of the output file. + + Returns + ------- + bool + ``True`` if export is successful, ``False`` if not. + """ + return write_configuration_file(self.config, output_file) + + class VirtualCompliance: """Provides automatic report generation with pass/fail criteria on virtual compliance. @@ -213,8 +408,7 @@ class VirtualCompliance: Desktop object. template : str Full path to the template. Supported formats are JSON and TOML. - project_path : str, optional - Full path to the project. If a path is provided, the project field inside the template is ignored. + """ def __init__(self, desktop, template): @@ -233,6 +427,7 @@ def __init__(self, desktop, template): self._parse_template() self._desktop_class = desktop + @pyaedt_function_handler() @pyaedt_function_handler() def load_project(self): """Open the aedt project in Electronics Desktop. @@ -463,9 +658,14 @@ def _create_aedt_reports(self, pdf_report): design_name = template_report.design_name report_type = template_report.report_type group = template_report.group_plots + if template_report.project_name: + if template_report.project_name not in self._desktop_class.project_list(): + self._desktop_class.load_project(template_report.project) + else: + template_report.project_name = self._project_name if _design and _design.design_name != design_name or _design is None: try: - _design = get_pyaedt_app(self._project_name, design_name) + _design = get_pyaedt_app(template_report.project_name, design_name) except Exception: # pragma: no cover self._desktop_class.logger.error(f"Failed to retrieve design {design_name}") continue @@ -478,8 +678,17 @@ def _create_aedt_reports(self, pdf_report): pdf_report.add_section() pdf_report.add_chapter("Compliance Results") start = False - if group and report_type in ["frequency", "time"]: - local_config["expressions"] = {trace: {} for trace in traces} + if group and report_type in ["standard", "frequency", "time"]: + new_dict = {} + for trace in traces: + if local_config.get("expressions", {}): + if isinstance(local_config["expressions"], dict): + if trace in local_config["expressions"]: + new_dict[trace] = local_config["expressions"][trace] + else: + new_dict[trace] = {} + + local_config["expressions"] = new_dict image_name = name sw_name = self._get_sweep_name(_design, local_config.get("solution_name", None)) _design.logger.info(f"Creating report {name}") @@ -520,7 +729,9 @@ def _create_aedt_reports(self, pdf_report): time.sleep(1) sleep_time -= 1 if ( - pass_fail and report_type in ["frequency", "time"] and local_config.get("limitLines", None) + pass_fail + and report_type in ["standard", "frequency", "time"] + and local_config.get("limitLines", None) ): # pragma: no cover _design.logger.info("Checking lines violations") table = self._add_lna_violations(aedt_report, pdf_report, image_name, local_config) @@ -529,15 +740,25 @@ def _create_aedt_reports(self, pdf_report): aedt_report.delete() _design.logger.info(f"Successfully parsed report {name}") else: + legacy_local_config = copy.deepcopy(local_config) for trace in traces: - local_config["expressions"] = {trace: {}} + if local_config.get("expressions", {}): + if isinstance(local_config["expressions"], dict): + if trace in legacy_local_config["expressions"]: + local_config["expressions"] = {trace: legacy_local_config["expressions"][trace]} + elif len(legacy_local_config["expressions"]) == 1: + local_config["expressions"] = { + trace: list(legacy_local_config["expressions"].values())[-1] + } + else: + local_config["expressions"] = {trace: {}} image_name = name + f"_{trace}" sw_name = self._get_sweep_name(_design, local_config.get("solution_name", None)) _design.logger.info(f"Creating report {name} for trace {trace}") aedt_report = _design.post.create_report_from_configuration( report_settings=local_config, solution_name=sw_name ) - if report_type != "contour eye diagram": + if report_type != "contour eye diagram" and "3D" not in local_config["report_type"]: aedt_report.hide_legend() time.sleep(1) out = _design.post.export_report_to_jpg(self._output_folder, aedt_report.plot_name) @@ -571,6 +792,7 @@ def _create_aedt_reports(self, pdf_report): time.sleep(1) sleep_time -= 1 if pass_fail: + table = None if report_type in ["frequency", "time"] and local_config.get("limitLines", None): _design.logger.info("Checking lines violations") table = self._add_lna_violations(aedt_report, pdf_report, image_name, local_config) @@ -587,12 +809,19 @@ def _create_aedt_reports(self, pdf_report): table = self._add_contour_eye_diagram_violations( aedt_report, pdf_report, image_name, local_config ) - write_csv(os.path.join(self._output_folder, f"{name}{trace}_pass_fail.csv"), table) - + if table: # pragma: no cover + write_csv(os.path.join(self._output_folder, f"{name}{trace}_pass_fail.csv"), table) + else: + _design.logger.warning(f"Failed to compute violation for chart {name}{trace}") if report_type in ["eye diagram", "statistical eye"]: _design.logger.info("Adding eye measurements.") table = self._add_eye_measurement(aedt_report, pdf_report, image_name) - write_csv(os.path.join(self._output_folder, f"{name}{trace}_eye_meas.csv"), table) + write_csv( + os.path.join( + self._output_folder, f"{name}{trace}_eye_meas.csv".replace("<", "").replace(">", "") + ), + table, + ) if self.local_config.get("delete_after_export", True): aedt_report.delete() _design.logger.info(f"Successfully parsed report {name} for trace {trace}") @@ -666,17 +895,27 @@ def _add_lna_violations(self, report, pdf_report, image_name, local_config): self._desktop_class.logger.error(msg) return pass_fail_table for trace_name in trace_data.expressions: - trace_values = [(k[0], v) for k, v in trace_data.full_matrix_real_imag[0][trace_name].items()] + trace_values = [(k[-1], v) for k, v in trace_data.full_matrix_real_imag[0][trace_name].items()] for limit_v in local_config["limitLines"].values(): yy = 0 zones = 0 - while yy < len(limit_v["xpoints"]) - 1: - if limit_v["xpoints"][yy] != limit_v["xpoints"][yy + 1]: + if trace_data.primary_sweep == "Freq": + default = "Hz" + else: + default = "s" + limit_x = unit_converter( + values=limit_v["xpoints"], + unit_system=trace_data.primary_sweep, + input_units=default if limit_v.get("xunits", "") == "" else limit_v["xunits"], + output_units=trace_data.units_sweeps[trace_data.primary_sweep], + ) + while yy < len(limit_x) - 1: + if limit_x[yy] != limit_x[yy + 1]: zones += 1 - result_range = self._get_frequency_range( - trace_values, limit_v["xpoints"][yy], limit_v["xpoints"][yy + 1] - ) + result_range = self._get_frequency_range(trace_values, limit_x[yy], limit_x[yy + 1]) freq = [i[0] for i in result_range] + if not freq: + return False slope = (limit_v["ypoints"][yy + 1] - limit_v["ypoints"][yy]) / (freq[-1] - freq[0]) ypoints = [] for i in range(len(freq)): @@ -689,8 +928,7 @@ def _add_lna_violations(self, report, pdf_report, image_name, local_config): hatch_above = True test_value = limit_v["ypoints"][yy] range_value, x_value, result_value = self._check_test_value(result_range, ypoints, hatch_above) - units = limit_v.get("y_units", "") - xunits = limit_v.get("x_units", "") + units = limit_v.get("yunits", "") mystr = f"Zone {zones}" font_table.append([None, [255, 0, 0]] if result_value == "FAIL" else ["", None]) pass_fail_table.append( @@ -700,7 +938,7 @@ def _add_lna_violations(self, report, pdf_report, image_name, local_config): "Upper Limit" if hatch_above else "Lower Limit", f"{test_value}{units}", f"{range_value}{units}", - f"{x_value}{xunits}", + f"{x_value}{trace_data.units_sweeps[trace_data.primary_sweep]}", result_value, ] ) @@ -721,7 +959,7 @@ def _add_statistical_violations(self, report, pdf_report, image_name, local_conf msg = "Failed to get Solution Data. Check if the design is solved or the report data are correct." self._desktop_class.logger.error(msg) return - mag_data = {i: k for i, k in sols.full_matrix_real_imag[0][sols.expressions[0]].items() if k > 0} + mag_data = [i for i, k in sols.full_matrix_real_imag[0][sols.expressions[0]].items() if k > 0] # mag_data is a dictionary. The key isa tuple (__AMPLITUDE, __UI), and the value is the eye value. mystr = "Eye Mask Violation:" result_value = "PASS" @@ -866,7 +1104,7 @@ def _create_project_info(self, report): continue designs.append(design_name) if _design and _design.design_name != design_name or _design is None: - _design = get_pyaedt_app(self._project_name, design_name) + _design = get_pyaedt_app(template_report.project_name, design_name) report.add_project_info(_design) report.add_empty_line(3) diff --git a/src/ansys/aedt/core/visualization/report/common.py b/src/ansys/aedt/core/visualization/report/common.py index 884e583eba6..07dc242253e 100644 --- a/src/ansys/aedt/core/visualization/report/common.py +++ b/src/ansys/aedt/core/visualization/report/common.py @@ -630,7 +630,7 @@ def report_category(self): try: return self.properties.props["Report Type"] except Exception: - return self._props["report_type"] + return self._props["report_category"] return self._props["report_category"] @report_category.setter diff --git a/src/ansys/aedt/core/visualization/report/eye.py b/src/ansys/aedt/core/visualization/report/eye.py index c24a6d21c83..cb6eee61451 100644 --- a/src/ansys/aedt/core/visualization/report/eye.py +++ b/src/ansys/aedt/core/visualization/report/eye.py @@ -47,6 +47,7 @@ def __init__(self, app, report_category, setup_name, expressions=None): self._props["context"]["variations"]["__Amplitude"] = ["All"] self._props["context"]["variations"]["__EyeOpening"] = ["0"] self._props["context"]["primary_sweep"] = "__UnitInterval" + self._props["context"]["secondary_sweep"] = "__Amplitude" self.quantity_type = 0 self.min_latch_overlay = "0" self.noise_floor = "1e-16" @@ -121,17 +122,6 @@ def quantity_type(self): def quantity_type(self, value): self._props["quantity_type"] = value - @property - def report_category(self): - """Report category. - - Returns - ------- - str - Report category. - """ - return self._props["report_category"] - @property def _context(self): if self.primary_sweep == "__InitialTime": @@ -552,6 +542,8 @@ def expressions(self): new_exprs.append("{}AfterChannel<".format(expr_head) + expr + ".int_ami_rx>") elif qtype == 3: new_exprs.append("{}AfterProbe<".format(expr_head) + expr + ".int_ami_rx>") + else: + new_exprs.append(expr) else: new_exprs.append(expr) return new_exprs @@ -580,6 +572,11 @@ def report_category(self): str Report category. """ + if self._is_created: + try: + return self.properties.props["Report Type"] + except Exception: + return self._props["report_category"] return self._props["report_category"] @report_category.setter diff --git a/src/ansys/aedt/core/visualization/report/standard.py b/src/ansys/aedt/core/visualization/report/standard.py index 7c267dc89c4..6655c8dace0 100644 --- a/src/ansys/aedt/core/visualization/report/standard.py +++ b/src/ansys/aedt/core/visualization/report/standard.py @@ -253,6 +253,9 @@ def _context(self): "", self.pulse_rise_time / 5, self.pulse_rise_time * 100, + "EnsDiffPairKey", + False, + "0", "IDIID", False, "1" if not self.pulse_rise_time else "3", From 08db6d08f19a63fefaefe47c608a9847eb02540a Mon Sep 17 00:00:00 2001 From: Ramin Aghajafari <153928265+ramin4667@users.noreply.github.com> Date: Fri, 4 Oct 2024 12:06:02 -0400 Subject: [PATCH 16/20] FIX: Defining Modelithics attributes change (#5233) --- .../test_lumped_export/test_export_to_aedt.py | 85 ++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/_unittest/test_45_FilterSolutions/test_lumped_export/test_export_to_aedt.py b/_unittest/test_45_FilterSolutions/test_lumped_export/test_export_to_aedt.py index 9ca433a806f..38921ff8467 100644 --- a/_unittest/test_45_FilterSolutions/test_lumped_export/test_export_to_aedt.py +++ b/_unittest/test_45_FilterSolutions/test_lumped_export/test_export_to_aedt.py @@ -50,11 +50,19 @@ class TestClass: def test_modelithics_inductor_list_count(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_capacitor_list_count + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS assert lumpdesign.export_to_aedt.modelithics_inductor_list_count == 116 def test_modelithics_inductor_list(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_inductor_list(0) + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_inductor_list(-1) assert info.value.args[0] == "The Modelithics inductor at the given index is not available" @@ -64,6 +72,10 @@ def test_modelithics_inductor_list(self): def test_modelithics_inductor_selection(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_inductor_selection + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_inductor_selection assert info.value.args[0] == "No Modelithics inductor is selected" @@ -73,6 +85,10 @@ def test_modelithics_inductor_selection(self): def test_modelithics_inductor_family_list_count(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_inductor_family_list_count + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS assert lumpdesign.export_to_aedt.modelithics_inductor_family_list_count == 0 lumpdesign.export_to_aedt.modelithics_inductor_add_family(second_modelithics_inductor) assert lumpdesign.export_to_aedt.modelithics_inductor_family_list_count == 1 @@ -82,6 +98,10 @@ def test_modelithics_inductor_family_list_count(self): def test_modelithics_inductor_family_list(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_inductor_family_list(0) + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_inductor_family_list(0) assert info.value.args[0] == "The Modelithics inductor family at the given index is not available" @@ -93,6 +113,10 @@ def test_modelithics_inductor_family_list(self): def test_modelithics_inductor_family_list_add_family(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_inductor_add_family(second_modelithics_inductor) + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_inductor_family_list(0) assert info.value.args[0] == "The Modelithics inductor family at the given index is not available" @@ -104,6 +128,9 @@ def test_modelithics_inductor_family_list_add_family(self): def test_modelithics_inductor_family_list_remove_family(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_inductor_remove_family(second_modelithics_inductor) + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_inductor_family_list(0) assert info.value.args[0] == "The Modelithics inductor family at the given index is not available" @@ -116,11 +143,19 @@ def test_modelithics_inductor_family_list_remove_family(self): def test_modelithics_capacitor_list_count(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() - assert lumpdesign.export_to_aedt.modelithics_capacitor_list_count == 140 + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_capacitor_list_count + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS + assert lumpdesign.export_to_aedt.modelithics_capacitor_list_count == 143 def test_modelithics_capacitor_list(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_capacitor_list(0) + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_capacitor_list(-1) assert info.value.args[0] == "The Modelithics capacitor at the given index is not available" @@ -130,6 +165,10 @@ def test_modelithics_capacitor_list(self): def test_modelithics_capacitor_selection(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_capacitor_selection + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_capacitor_selection assert info.value.args[0] == "No Modelithics capacitor is selected" @@ -139,6 +178,10 @@ def test_modelithics_capacitor_selection(self): def test_modelithics_capacitor_family_list_count(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_capacitor_family_list_count + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS assert lumpdesign.export_to_aedt.modelithics_capacitor_family_list_count == 0 lumpdesign.export_to_aedt.modelithics_capacitor_add_family(first_modelithics_capacitor) assert lumpdesign.export_to_aedt.modelithics_capacitor_family_list_count == 1 @@ -148,6 +191,10 @@ def test_modelithics_capacitor_family_list_count(self): def test_modelithics_capacitor_family_list(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_capacitor_family_list(0) + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_capacitor_family_list(0) assert info.value.args[0] == "The Modelithics capacitor family at the given index is not available" @@ -159,6 +206,10 @@ def test_modelithics_capacitor_family_list(self): def test_modelithics_capacitor_family_list_add_family(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_capacitor_add_family(first_modelithics_capacitor) + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_capacitor_family_list(0) assert info.value.args[0] == "The Modelithics capacitor family at the given index is not available" @@ -170,6 +221,10 @@ def test_modelithics_capacitor_family_list_add_family(self): def test_modelithics_capacitor_family_list_remove_family(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_capacitor_remove_family(second_modelithics_capacitor) + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_capacitor_family_list(0) assert info.value.args[0] == "The Modelithics capacitor family at the given index is not available" @@ -182,11 +237,19 @@ def test_modelithics_capacitor_family_list_remove_family(self): def test_modelithics_resistor_list_count(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_resistor_list_count + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS assert lumpdesign.export_to_aedt.modelithics_resistor_list_count == 39 def test_modelithics_resistor_list(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_resistor_list(0) + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_resistor_list(-1) assert info.value.args[0] == "The Modelithics resistor at the given index is not available" @@ -196,6 +259,10 @@ def test_modelithics_resistor_list(self): def test_modelithics_resistor_selection(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_resistor_selection + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_resistor_selection assert info.value.args[0] == "No Modelithics resistor is selected" @@ -205,6 +272,10 @@ def test_modelithics_resistor_selection(self): def test_modelithics_resistor_family_list_count(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_resistor_family_list_count + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS assert lumpdesign.export_to_aedt.modelithics_resistor_family_list_count == 0 lumpdesign.export_to_aedt.modelithics_resistor_add_family(first_modelithics_resistor) assert lumpdesign.export_to_aedt.modelithics_resistor_family_list_count == 1 @@ -214,6 +285,10 @@ def test_modelithics_resistor_family_list_count(self): def test_modelithics_resistor_family_list(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_resistor_family_list(0) + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_resistor_family_list(0) assert info.value.args[0] == "The Modelithics resistor family at the given index is not available" @@ -225,6 +300,10 @@ def test_modelithics_resistor_family_list(self): def test_modelithics_resistor_family_list_add_family(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_resistor_add_family(first_modelithics_resistor) + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_resistor_family_list(0) assert info.value.args[0] == "The Modelithics resistor family at the given index is not available" @@ -236,6 +315,10 @@ def test_modelithics_resistor_family_list_add_family(self): def test_modelithics_resistor_family_list_remove_family(self): lumpdesign = ansys.aedt.core.FilterSolutions(implementation_type=FilterImplementation.LUMPED) lumpdesign.export_to_aedt._open_aedt_export() + with pytest.raises(RuntimeError) as info: + lumpdesign.export_to_aedt.modelithics_resistor_remove_family(second_modelithics_resistor) + assert info.value.args[0] == "The part library is not set to Modelithics" + lumpdesign.export_to_aedt.part_libraries = PartLibraries.MODELITHICS with pytest.raises(RuntimeError) as info: lumpdesign.export_to_aedt.modelithics_resistor_family_list(0) assert info.value.args[0] == "The Modelithics resistor family at the given index is not available" From c9df21df4435f4e0b966727ec5d8cc24f8d984db Mon Sep 17 00:00:00 2001 From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> Date: Sat, 5 Oct 2024 19:05:49 +0200 Subject: [PATCH 17/20] DOCS: Remove examples (#5242) Co-authored-by: maxcapodi78 Co-authored-by: Sebastien Morais --- .github/workflows/ci_cd.yml | 111 +-- doc/Makefile | 27 +- doc/make.bat | 40 +- doc/source/User_guide/index.rst | 2 +- doc/source/conf.py | 54 -- doc/source/index.rst | 6 +- examples/00-EDB/Readme.txt | 20 - examples/01-HFSS3DLayout/Dcir_in_3DLayout.py | 131 --- examples/01-HFSS3DLayout/EDB_in_3DLayout.py | 124 --- examples/01-HFSS3DLayout/HFSS3DLayout_Via.py | 127 --- examples/01-HFSS3DLayout/Hfss3DComponent.py | 245 ------ examples/01-HFSS3DLayout/Readme.txt | 4 - examples/01-Modeling-Setup/Configurations.py | 135 ---- .../HFSS_CoordinateSystem.py | 281 ------- examples/01-Modeling-Setup/Optimetrics.py | 156 ---- .../01-Modeling-Setup/Polyline_Primitives.py | 295 ------- examples/01-Modeling-Setup/Readme.txt | 4 - examples/02-HFSS/Advanced_Far_Field.py.back | 218 ----- examples/02-HFSS/Array.py | 151 ---- .../02-HFSS/Create_3d_Component_and_use_it.py | 174 ---- examples/02-HFSS/Flex_CPWG.py | 208 ----- examples/02-HFSS/HFSS_Choke.py | 228 ------ examples/02-HFSS/HFSS_Dipole.py | 220 ----- examples/02-HFSS/HFSS_FSS_unitcell.py | 149 ---- examples/02-HFSS/HFSS_Spiral.py | 205 ----- examples/02-HFSS/HFSS_eigenmode.py | 162 ---- examples/02-HFSS/Probe_Fed_Patch.py | 119 --- examples/02-HFSS/Readme.txt | 4 - examples/02-HFSS/Waveguide_Filter.py | 242 ------ examples/02-SBR+/Readme.txt | 4 - examples/02-SBR+/SBR_City_Import.py | 80 -- examples/02-SBR+/SBR_Doppler_Example.py | 139 ---- examples/02-SBR+/SBR_Example.py | 122 --- examples/02-SBR+/SBR_Time_Plot.py | 77 -- examples/03-Maxwell/Maxwell2D_DCConduction.py | 270 ------- .../03-Maxwell/Maxwell2D_Electrostatic.py | 214 ----- .../Maxwell2D_PMSynchronousMotor.py | 759 ------------------ .../03-Maxwell/Maxwell2D_Transformer_LL.py | 274 ------- examples/03-Maxwell/Maxwell2D_Transient.py | 157 ---- examples/03-Maxwell/Maxwell3DTeam7.py | 429 ---------- examples/03-Maxwell/Maxwell3D_Choke.py | 222 ----- examples/03-Maxwell/Maxwell3D_Segmentation.py | 116 --- .../03-Maxwell/Maxwell3D_Team3_bath_plate.py | 240 ------ .../03-Maxwell/Maxwell_Control_Program.py | 95 --- examples/03-Maxwell/Maxwell_Magnet.py | 138 ---- .../Maxwell_Transformer_Coreloss.py | 98 --- examples/03-Maxwell/RMxprt.py | 156 ---- examples/03-Maxwell/Readme.txt | 6 - .../04-Icepak/Icepak_3DComponents_Example.py | 180 ----- examples/04-Icepak/Icepak_CSV_Import.py | 116 --- examples/04-Icepak/Icepak_ECAD_Import.py | 135 ---- examples/04-Icepak/Icepak_Example.py | 117 --- examples/04-Icepak/Readme.txt | 5 - examples/04-Icepak/Sherlock_Example.py | 207 ----- examples/05-Q3D/Q2D_Armoured_Cable.py | 255 ------ examples/05-Q3D/Q2D_Example_CPWG.py | 214 ----- examples/05-Q3D/Q2D_Example_Stripline.py | 225 ------ examples/05-Q3D/Q3D_DC_IR.py | 258 ------ examples/05-Q3D/Q3D_Example.py | 158 ---- examples/05-Q3D/Q3D_from_EDB.py | 154 ---- examples/05-Q3D/Readme.txt | 5 - .../Circuit-HFSS-Icepak-coupling.py | 319 -------- .../06-Multiphysics/Hfss_Icepak_Coupling.py | 335 -------- examples/06-Multiphysics/Hfss_Mechanical.py | 163 ---- examples/06-Multiphysics/MRI.py | 293 ------- .../Maxwell3D_Icepak_2Way_Coupling.py | 281 ------- examples/06-Multiphysics/Readme.txt | 5 - examples/07-Circuit/Circuit_AMI.py | 271 ------- examples/07-Circuit/Circuit_Example.py | 126 --- .../07-Circuit/Circuit_Siwave_Multizones.py | 108 --- .../07-Circuit/Circuit_Subcircuit_Example.py | 76 -- examples/07-Circuit/Circuit_Transient.py | 183 ----- examples/07-Circuit/Create_Netlist.py | 71 -- examples/07-Circuit/Readme.txt | 5 - examples/07-Circuit/Reports.py | 134 ---- examples/07-Circuit/Touchstone_Management.py | 70 -- examples/07-Circuit/Virtual_Compliance.py | 217 ----- examples/07-EMIT/ComputeInterferenceType.py | 201 ----- examples/07-EMIT/ComputeProtectionLevels.py | 266 ------ examples/07-EMIT/EMIT_Example.py | 95 --- examples/07-EMIT/EMIT_HFSS_Example.py | 145 ---- examples/07-EMIT/Readme.txt | 5 - examples/07-EMIT/interference_gui.py | 621 -------------- .../07-TwinBuilder/01-RC_Circuit_Example.py | 112 --- .../07-TwinBuilder/02-Wiring_A_Rectifier.py | 153 ---- ...-Dynamic_ROM_Creation_And_Visualization.py | 184 ----- ...4-Static_ROM_Creation_And_Visualization.py | 189 ----- examples/07-TwinBuilder/Readme.txt | 4 - .../Lumped_Element_Response.py | 65 -- examples/Readme.txt | 10 - pyproject.toml | 35 +- 91 files changed, 20 insertions(+), 14189 deletions(-) delete mode 100644 examples/00-EDB/Readme.txt delete mode 100644 examples/01-HFSS3DLayout/Dcir_in_3DLayout.py delete mode 100644 examples/01-HFSS3DLayout/EDB_in_3DLayout.py delete mode 100644 examples/01-HFSS3DLayout/HFSS3DLayout_Via.py delete mode 100644 examples/01-HFSS3DLayout/Hfss3DComponent.py delete mode 100644 examples/01-HFSS3DLayout/Readme.txt delete mode 100644 examples/01-Modeling-Setup/Configurations.py delete mode 100644 examples/01-Modeling-Setup/HFSS_CoordinateSystem.py delete mode 100644 examples/01-Modeling-Setup/Optimetrics.py delete mode 100644 examples/01-Modeling-Setup/Polyline_Primitives.py delete mode 100644 examples/01-Modeling-Setup/Readme.txt delete mode 100644 examples/02-HFSS/Advanced_Far_Field.py.back delete mode 100644 examples/02-HFSS/Array.py delete mode 100644 examples/02-HFSS/Create_3d_Component_and_use_it.py delete mode 100644 examples/02-HFSS/Flex_CPWG.py delete mode 100644 examples/02-HFSS/HFSS_Choke.py delete mode 100644 examples/02-HFSS/HFSS_Dipole.py delete mode 100644 examples/02-HFSS/HFSS_FSS_unitcell.py delete mode 100644 examples/02-HFSS/HFSS_Spiral.py delete mode 100644 examples/02-HFSS/HFSS_eigenmode.py delete mode 100644 examples/02-HFSS/Probe_Fed_Patch.py delete mode 100644 examples/02-HFSS/Readme.txt delete mode 100644 examples/02-HFSS/Waveguide_Filter.py delete mode 100644 examples/02-SBR+/Readme.txt delete mode 100644 examples/02-SBR+/SBR_City_Import.py delete mode 100644 examples/02-SBR+/SBR_Doppler_Example.py delete mode 100644 examples/02-SBR+/SBR_Example.py delete mode 100644 examples/02-SBR+/SBR_Time_Plot.py delete mode 100644 examples/03-Maxwell/Maxwell2D_DCConduction.py delete mode 100644 examples/03-Maxwell/Maxwell2D_Electrostatic.py delete mode 100644 examples/03-Maxwell/Maxwell2D_PMSynchronousMotor.py delete mode 100644 examples/03-Maxwell/Maxwell2D_Transformer_LL.py delete mode 100644 examples/03-Maxwell/Maxwell2D_Transient.py delete mode 100644 examples/03-Maxwell/Maxwell3DTeam7.py delete mode 100644 examples/03-Maxwell/Maxwell3D_Choke.py delete mode 100644 examples/03-Maxwell/Maxwell3D_Segmentation.py delete mode 100644 examples/03-Maxwell/Maxwell3D_Team3_bath_plate.py delete mode 100644 examples/03-Maxwell/Maxwell_Control_Program.py delete mode 100644 examples/03-Maxwell/Maxwell_Magnet.py delete mode 100644 examples/03-Maxwell/Maxwell_Transformer_Coreloss.py delete mode 100644 examples/03-Maxwell/RMxprt.py delete mode 100644 examples/03-Maxwell/Readme.txt delete mode 100644 examples/04-Icepak/Icepak_3DComponents_Example.py delete mode 100644 examples/04-Icepak/Icepak_CSV_Import.py delete mode 100644 examples/04-Icepak/Icepak_ECAD_Import.py delete mode 100644 examples/04-Icepak/Icepak_Example.py delete mode 100644 examples/04-Icepak/Readme.txt delete mode 100644 examples/04-Icepak/Sherlock_Example.py delete mode 100644 examples/05-Q3D/Q2D_Armoured_Cable.py delete mode 100644 examples/05-Q3D/Q2D_Example_CPWG.py delete mode 100644 examples/05-Q3D/Q2D_Example_Stripline.py delete mode 100644 examples/05-Q3D/Q3D_DC_IR.py delete mode 100644 examples/05-Q3D/Q3D_Example.py delete mode 100644 examples/05-Q3D/Q3D_from_EDB.py delete mode 100644 examples/05-Q3D/Readme.txt delete mode 100644 examples/06-Multiphysics/Circuit-HFSS-Icepak-coupling.py delete mode 100644 examples/06-Multiphysics/Hfss_Icepak_Coupling.py delete mode 100644 examples/06-Multiphysics/Hfss_Mechanical.py delete mode 100644 examples/06-Multiphysics/MRI.py delete mode 100644 examples/06-Multiphysics/Maxwell3D_Icepak_2Way_Coupling.py delete mode 100644 examples/06-Multiphysics/Readme.txt delete mode 100644 examples/07-Circuit/Circuit_AMI.py delete mode 100644 examples/07-Circuit/Circuit_Example.py delete mode 100644 examples/07-Circuit/Circuit_Siwave_Multizones.py delete mode 100644 examples/07-Circuit/Circuit_Subcircuit_Example.py delete mode 100644 examples/07-Circuit/Circuit_Transient.py delete mode 100644 examples/07-Circuit/Create_Netlist.py delete mode 100644 examples/07-Circuit/Readme.txt delete mode 100644 examples/07-Circuit/Reports.py delete mode 100644 examples/07-Circuit/Touchstone_Management.py delete mode 100644 examples/07-Circuit/Virtual_Compliance.py delete mode 100644 examples/07-EMIT/ComputeInterferenceType.py delete mode 100644 examples/07-EMIT/ComputeProtectionLevels.py delete mode 100644 examples/07-EMIT/EMIT_Example.py delete mode 100644 examples/07-EMIT/EMIT_HFSS_Example.py delete mode 100644 examples/07-EMIT/Readme.txt delete mode 100644 examples/07-EMIT/interference_gui.py delete mode 100644 examples/07-TwinBuilder/01-RC_Circuit_Example.py delete mode 100644 examples/07-TwinBuilder/02-Wiring_A_Rectifier.py delete mode 100644 examples/07-TwinBuilder/03-Dynamic_ROM_Creation_And_Visualization.py delete mode 100644 examples/07-TwinBuilder/04-Static_ROM_Creation_And_Visualization.py delete mode 100644 examples/07-TwinBuilder/Readme.txt delete mode 100644 examples/08-FilterSolutions/Lumped_Element_Response.py delete mode 100644 examples/Readme.txt diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 32a1a601f9a..7270a15244c 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -73,7 +73,7 @@ jobs: # TODO: Update to ansys/actions/doc-build@v6 once we remove examples doc-build: - name: Documentation build without examples + name: Documentation build runs-on: ubuntu-latest needs: [doc-style] steps: @@ -91,7 +91,7 @@ jobs: - name: Install pyaedt and documentation dependencies run: | - pip install .[doc-no-examples] + pip install .[doc] - name: Retrieve PyAEDT version id: version @@ -105,119 +105,28 @@ jobs: sudo apt install graphviz texlive-latex-extra latexmk texlive-xetex texlive-fonts-extra -y # TODO: Update this step once pyaedt-examples is ready - - name: Build HTML documentation without examples + - name: Build HTML documentation run: | make -C doc clean - make -C doc html-no-examples + make -C doc html # Verify that sphinx generates no warnings - name: Check for warnings run: | python doc/print_errors.py - - name: Upload HTML documentation without examples artifact + - name: Upload HTML documentation without artifact uses: actions/upload-artifact@v3 with: - name: documentation-no-examples-html + name: documentation-html path: doc/_build/html retention-days: 7 - - name: Build PDF documentation without examples + - name: Build PDF documentation run: | - make -C doc pdf-no-examples - - - name: Upload PDF documentation without examples artifact - uses: actions/upload-artifact@v3 - with: - name: documentation-no-examples-pdf - path: doc/_build/latex/PyAEDT-Documentation-*.pdf - retention-days: 7 - -# # ================================================================================================= -# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv RUNNING ON SELF-HOSTED RUNNER vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv -# # ================================================================================================= - - doc-build-with-examples: - name: Documentation build with examples - if: github.event_name == 'push' && contains(github.ref, 'refs/tags') - runs-on: [ self-hosted, Windows, pyaedt ] - needs: [doc-style] - timeout-minutes: 720 - steps: - - name: Install Git and checkout project - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.MAIN_PYTHON_VERSION }} - - - name: Create virtual environment - run: | - python -m venv .venv - .venv\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 and documentation dependencies - run: | - .venv\Scripts\Activate.ps1 - pip install .[doc] - - - name: Retrieve PyAEDT version - id: version - run: | - .venv\Scripts\Activate.ps1 - echo "PYAEDT_VERSION=$(python -c 'from ansys.aedt.core import __version__; print(__version__)')" >> $GITHUB_OUTPUT - echo "PyAEDT version is: $(python -c "from ansys.aedt.core import __version__; print(__version__)")" - - - name: Install CI dependencies (e.g. vtk-osmesa) - run: | - .venv\Scripts\Activate.ps1 - # Uninstall conflicting dependencies - pip uninstall --yes vtk - pip install --extra-index-url https://wheels.vtk.org vtk-osmesa - - # TODO: Update this step once pyaedt-examples is ready - # NOTE: Use environment variable to keep the doctree and avoid redundant build for PDF pages - - name: Build HTML documentation with examples - env: - SPHINXBUILD_KEEP_DOCTREEDIR: "1" - run: | - .venv\Scripts\Activate.ps1 - .\doc\make.bat clean - .\doc\make.bat html - - # TODO: Keeping this commented as reminder of https://github.com/ansys/pyaedt/issues/4296 - # # Verify that sphinx generates no warnings - # - name: Check for warnings - # run: | - # .venv\Scripts\Activate.ps1 - # python doc/print_errors.py - - # Use environment variable to remove the doctree after the build of PDF pages - - name: Build PDF documentation with examples - env: - SPHINXBUILD_KEEP_DOCTREEDIR: "0" - run: | - .venv\Scripts\Activate.ps1 - .\doc\make.bat pdf - - # - name: Add assets to HTML docs - # run: | - # zip -r documentation-html.zip ./doc/_build/html - # mv documentation-html.zip ./doc/_build/html/_static/assets/download/ - # cp doc/_build/latex/PyAEDT-Documentation-*.pdf ./doc/_build/html/_static/assets/download/pyaedt.pdf - - - name: Upload HTML documentation with examples artifact - uses: actions/upload-artifact@v3 - with: - name: documentation-html - path: doc/_build/html - retention-days: 7 + make -C doc pdf - - name: Upload PDF documentation without examples artifact + - name: Upload PDF documentation uses: actions/upload-artifact@v3 with: name: documentation-pdf @@ -492,7 +401,7 @@ jobs: release: name: Release project if: github.event_name == 'push' && contains(github.ref, 'refs/tags') - needs: [package, doc-build-with-examples] + needs: [package, doc-build] runs-on: ubuntu-latest steps: - name: Release to the public PyPI repository diff --git a/doc/Makefile b/doc/Makefile index 6b135dfaa78..8fefd56a79d 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -43,39 +43,16 @@ clean: .install-deps # @echo # @echo "Check finished. Report is in $(LINKCHECKDIR)." -html-no-examples: .install-deps - @echo "Building HTML pages without examples." - export PYAEDT_DOC_RUN_EXAMPLES="0" - export PYAEDT_DOC_USE_GIF="1" - @# FIXME: currently linkcheck freezes and further investigation must be performed - @# @$(SPHINXBUILD) -M linkcheck "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(LINKCHECKOPTS) $(O) - @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)." - html: .install-deps - @echo "Building HTML pages with examples." - export PYAEDT_DOC_RUN_EXAMPLES="1" - export PYAEDT_DOC_USE_GIF="1" + @echo "Building HTML pages." @# FIXME: currently linkcheck freezes and further investigation must be performed @# @$(SPHINXBUILD) -M linkcheck "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(LINKCHECKOPTS) $(O) @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)." -pdf-no-examples: .install-deps - @echo "Building PDF pages without examples." - export PYAEDT_DOC_RUN_EXAMPLES="0" - export PYAEDT_DOC_USE_GIF="0" - @$(SPHINXBUILD) -M latex "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - cd $(BUILDDIR)/latex && latexmk -r latexmkrc -pdf *.tex -interaction=nonstopmode || true - (test -f $(BUILDDIR)/latex/PyAEDT-Documentation-*.pdf && echo pdf exists) || exit 1 - @echo "Build finished. The PDF pages are in $(BUILDDIR)." - pdf: .install-deps - @echo "Building PDF pages with examples." - export PYAEDT_DOC_RUN_EXAMPLES="1" - export PYAEDT_DOC_USE_GIF="0" + @echo "Building PDF pages." @$(SPHINXBUILD) -M latex "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) cd $(BUILDDIR)/latex && latexmk -r latexmkrc -pdf *.tex -interaction=nonstopmode || true (test -f $(BUILDDIR)/latex/PyAEDT-Documentation-*.pdf && echo pdf exists) || exit 1 diff --git a/doc/make.bat b/doc/make.bat index bc9100f4b20..111ff76f89a 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -27,9 +27,7 @@ REM End of CICD dedicated setup if "%1" == "" goto help if "%1" == "clean" goto clean if "%1" == "html" goto html -if "%1" == "html-no-examples" goto html-no-examples if "%1" == "pdf" goto pdf -if "%1" == "pdf-no-examples" goto pdf-no-examples %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( @@ -59,23 +57,7 @@ for /d /r %SOURCEDIR% %%d in (_autosummary) do @if exist "%%d" rmdir /s /q "%%d" goto end :html -echo Building HTML pages with examples -set PYAEDT_DOC_RUN_EXAMPLES=1 -set PYAEDT_DOC_USE_GIF=1 -::FIXME: currently linkcheck freezes and further investigation must be performed -::%SPHINXBUILD% -M linkcheck %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %LINKCHECKOPTS% %O% -%SPHINXBUILD% -M html %SOURCEDIR% %BUILDDIR% -echo -echo "Build finished. The HTML pages are in %BUILDDIR%." -goto end - -:html-no-examples -echo Building HTML pages without examples -set PYAEDT_DOC_RUN_EXAMPLES=0 -set PYAEDT_DOC_USE_GIF=1 -if not exist "source\examples" mkdir "source\examples" -echo Examples> source\examples\index.rst -echo ========> source\examples\index.rst +echo Building HTML pages ::FIXME: currently linkcheck freezes and further investigation must be performed ::%SPHINXBUILD% -M linkcheck %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %LINKCHECKOPTS% %O% %SPHINXBUILD% -M html %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% @@ -84,25 +66,7 @@ echo "Build finished. The HTML pages are in %BUILDDIR%." goto end :pdf -echo Building PDF pages with examples -set PYAEDT_DOC_RUN_EXAMPLES=1 -set PYAEDT_DOC_USE_GIF=0 -%SPHINXBUILD% -M latex %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -cd "%BUILDDIR%\latex" -for %%f in (*.tex) do ( -xelatex "%%f" --interaction=nonstopmode) -echo "Build finished. The PDF pages are in %BUILDDIR%." -goto end - -:pdf-no-examples -echo Building PDF pages without examples -set PYAEDT_DOC_RUN_EXAMPLES=0 -set PYAEDT_DOC_USE_GIF=0 -if not exist "source\examples" mkdir "source\examples" -echo Examples> source\examples\index.rst -echo ========> source\examples\index.rst -::FIXME: currently linkcheck freezes and further investigation must be performed -::%SPHINXBUILD% -M linkcheck %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %LINKCHECKOPTS% %O% +echo Building PDF pages %SPHINXBUILD% -M latex %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% cd "%BUILDDIR%\latex" for %%f in (*.tex) do ( diff --git a/doc/source/User_guide/index.rst b/doc/source/User_guide/index.rst index 1c1ae73d09a..4fc34833d2b 100644 --- a/doc/source/User_guide/index.rst +++ b/doc/source/User_guide/index.rst @@ -6,7 +6,7 @@ User guide This section provides brief tutorials for helping you understand how to use PyAEDT effectively. -For end-to-end examples, see `Examples `_. +For end-to-end examples, see `Examples `_. .. grid:: 2 diff --git a/doc/source/conf.py b/doc/source/conf.py index 67397d9d91f..ba352a4086c 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -129,9 +129,6 @@ def setup(app): os.environ["PYAEDT_NON_GRAPHICAL"] = "1" os.environ["PYAEDT_DOC_GENERATION"] = "1" -# Do not run examples by default -run_examples = bool(int(os.getenv("PYAEDT_DOC_RUN_EXAMPLES", "0"))) -use_gif = bool(int(os.getenv("PYAEDT_DOC_USE_GIF", "1"))) # -- General configuration --------------------------------------------------- @@ -258,57 +255,6 @@ def setup(app): # gallery build requires AEDT install # if is_windows and bool(os.getenv("PYAEDT_CI_RUN_EXAMPLES", "0")): -if run_examples: - import pyvista - - # PyVista settings - - # Ensure that offscreen rendering is used for docs generation - pyvista.OFF_SCREEN = True - # Save figures in specified directory - pyvista.FIGURE_PATH = os.path.join(os.path.abspath("./images/"), "auto-generated/") - if not os.path.exists(pyvista.FIGURE_PATH): - os.makedirs(pyvista.FIGURE_PATH) - # Necessary for pyvista when building the sphinx gallery - pyvista.BUILDING_GALLERY = True - - # Manage errors - pyvista.set_error_output_file("errors.txt") - # Must be less than or equal to the XVFB window size - pyvista.global_theme["window_size"] = np.array([1024, 768]) - - # suppress annoying matplotlib bug - warnings.filterwarnings( - "ignore", - category=UserWarning, - message="Matplotlib is currently using agg, which is a non-GUI backend, so it cannot show the figure.", - ) - - extensions.append("sphinx_gallery.gen_gallery") - sphinx_gallery_conf = { - # convert rst to md for ipynb - "pypandoc": True, - # path to your examples scripts - "examples_dirs": ["../../examples/"], - # path where to save gallery generated examples - "gallery_dirs": ["examples"], - # Pattern to search for examples files - "filename_pattern": r"\.py", - # Remove the "Download all examples" button from the top level gallery - "download_all_examples": False, - # Sort gallery examples by file name instead of number of lines (default) - "within_subsection_order": FileNameSortKey, - # Directory where function granular galleries are stored - "backreferences_dir": None, - # Modules for which function level galleries are created. In - "doc_module": "ansys-pyaedt", - "image_scrapers": ("pyvista", "matplotlib"), - "ignore_pattern": r"flycheck.*", - "thumbnail_size": (350, 350), - } - if not use_gif: - gif_ignore_pattern = r"|.*Maxwell2D_Transient\.py|.*Maxwell2D_DCConduction\.py|.*Hfss_Icepak_Coupling\.py|.*SBR_Time_Plot\.py" - sphinx_gallery_conf["ignore_pattern"] = sphinx_gallery_conf["ignore_pattern"] + gif_ignore_pattern # -- Options for HTML output ------------------------------------------------- html_short_title = html_title = "PyAEDT" diff --git a/doc/source/index.rst b/doc/source/index.rst index 253bc4889b2..32528e3a1a9 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -46,8 +46,7 @@ enabling straightforward and efficient automation in your workflow. It describes how the methods work and the parameters that can be used. .. grid-item-card:: Examples :fa:`scroll` - :link: examples/index - :link-type: doc + :link: https://examples.aedt.docs.pyansys.com/ Explore examples that show how to use PyAEDT to perform different types of simulations. @@ -59,6 +58,7 @@ enabling straightforward and efficient automation in your workflow. Learn how to contribute to the PyAEDT codebase or documentation. + .. toctree:: :hidden: @@ -66,4 +66,4 @@ enabling straightforward and efficient automation in your workflow. Getting_started/index User_guide/index API/index - examples/index + Examples diff --git a/examples/00-EDB/Readme.txt b/examples/00-EDB/Readme.txt deleted file mode 100644 index 2294f151360..00000000000 --- a/examples/00-EDB/Readme.txt +++ /dev/null @@ -1,20 +0,0 @@ -EDB examples -~~~~~~~~~~~~ -EDB is a powerful API for efficiently controlling PCB data. -You can either use EDB standalone or embedded in HFSS 3D Layout in AEDT. -The ``EDB`` class in now part of the PyEDB package, which is currently installed with PyAEDT and backward-compatible with PyAEDT. -All EDB related examples have been moved -to the `Examples page `_ in the PyEDB -documentation. -These examples use EDB (Electronics Database) with PyAEDT. - -.. code:: python - - # Launch the latest installed version of AEDB. - import ansys.aedt.core - edb = ansys.aedt.core.Edb("mylayout.aedb") - - # You can also launch EDB directly from PyEDB. - - import pyedb - edb = pyedb.Edb("mylayout.aedb") diff --git a/examples/01-HFSS3DLayout/Dcir_in_3DLayout.py b/examples/01-HFSS3DLayout/Dcir_in_3DLayout.py deleted file mode 100644 index 8aac5bd56c3..00000000000 --- a/examples/01-HFSS3DLayout/Dcir_in_3DLayout.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -HFSS 3D Layout: SIwave DCIR analysis in HFSS 3D Layout ------------------------------------------------------- -This example shows how you can use configure HFSS 3D Layout for SIwave DCIR -analysis. -""" - -import os -import tempfile -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Configure EDB for DCIR analysis -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Copy example into temporary folder -temp_dir = tempfile.gettempdir() -dst_dir = os.path.join(temp_dir, ansys.aedt.core.generate_unique_name("pyaedt_dcir")) -os.mkdir(dst_dir) -local_path = ansys.aedt.core.downloads.download_aedb(dst_dir) - -##################################################################################### -# Load example board into EDB - -appedb = ansys.aedt.core.Edb(local_path, edbversion=aedt_version) - -##################################################################################### -# Create pin group on VRM positive pins - -gnd_name = "GND" -appedb.siwave.create_pin_group_on_net( - reference_designator="U3A1", - net_name="BST_V3P3_S5", - group_name="U3A1-BST_V3P3_S5") - -##################################################################################### -# Create pin group on VRM negative pins - -appedb.siwave.create_pin_group_on_net( - reference_designator="U3A1", - net_name="GND", - group_name="U3A1-GND") - -##################################################################################### -# Create voltage source between VRM positive and negative pin groups -appedb.siwave.create_voltage_source_on_pin_group( - pos_pin_group_name="U3A1-BST_V3P3_S5", - neg_pin_group_name="U3A1-GND", - magnitude=3.3, - name="U3A1-BST_V3P3_S5" -) - -##################################################################################### -# Create pin group on sink component positive pins - -appedb.siwave.create_pin_group_on_net( - reference_designator="U2A5", - net_name="V3P3_S5", - group_name="U2A5-V3P3_S5") - -##################################################################################### -# Create pin group on sink component negative pins - -appedb.siwave.create_pin_group_on_net( - reference_designator="U2A5", - net_name="GND", - group_name="U2A5-GND") - -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create place current source between sink component positive and negative pin groups -appedb.siwave.create_current_source_on_pin_group( - pos_pin_group_name="U2A5-V3P3_S5", - neg_pin_group_name="U2A5-GND", - magnitude=1, - name="U2A5-V3P3_S5" -) - -############################################################################### -# Add SIwave DCIR analysis - -appedb.siwave.add_siwave_dc_analysis(name="my_setup") - -############################################################################### -# Save and close EDB -# ~~~~~~~~~~~~~~~~~~ -# Save and close EDB. - -appedb.save_edb() -appedb.close_edb() - -############################################################################### -# Analysis DCIR in AEDT -# ~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT and import the configured EDB and analysis DCIR -desktop = ansys.aedt.core.Desktop(aedt_version, non_graphical=False, new_desktop=True) -hfss3dl = ansys.aedt.core.Hfss3dLayout(local_path) -hfss3dl.analyze() -hfss3dl.save_project() - -############################################################################### -# Get element data -# ~~~~~~~~~~~~~~~~~~~ -# Get current source - -current_source = hfss3dl.get_dcir_element_data_current_source(setup="my_setup") -print(current_source) - -# ~~~~~~~~~~~~~~~~~~~ -# Get via information - -via = hfss3dl.get_dcir_element_data_via(setup="my_setup") -print(via) - - -############################################################################### -# Get voltage -# ~~~~~~~~~~~ -# Get voltage from dcir solution data -voltage = hfss3dl.get_dcir_solution_data(setup="my_setup", show="Sources", category="Voltage") -print({expression: voltage.data_magnitude(expression) for expression in voltage.expressions}) - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -desktop.release_desktop() diff --git a/examples/01-HFSS3DLayout/EDB_in_3DLayout.py b/examples/01-HFSS3DLayout/EDB_in_3DLayout.py deleted file mode 100644 index 7690e708c6b..00000000000 --- a/examples/01-HFSS3DLayout/EDB_in_3DLayout.py +++ /dev/null @@ -1,124 +0,0 @@ -""" -HFSS 3D Layout: PCB and EDB in 3D layout ----------------------------------------- -This example shows how you can use HFSS 3D Layout combined with EDB to -interact with a 3D layout. -""" - - -import os -import tempfile -import ansys.aedt.core - -tmpfold = tempfile.gettempdir() -temp_folder = os.path.join(tmpfold, ansys.aedt.core.generate_unique_name("Example")) -if not os.path.exists(temp_folder): - os.makedirs(temp_folder) -print(temp_folder) - -############################################################################### -# Copy example into temporary folder -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Copy an example into the temporary folder. - -targetfile = ansys.aedt.core.downloads.download_aedb() -print(targetfile) -aedt_file = targetfile[:-12] + "aedt" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False -NewThread = True - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Initialize AEDT and launch HFSS 3D Layout -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize AEDT and launch HFSS 3D Layout. -# The ``h3d`` object contains the :class:`ansys.aedt.core.Edb` class query methods. - -d = ansys.aedt.core.launch_desktop(aedt_version, non_graphical, NewThread) -if os.path.exists(aedt_file): - os.remove(aedt_file) -h3d = ansys.aedt.core.Hfss3dLayout(targetfile) -h3d.save_project(os.path.join(temp_folder, "edb_demo.aedt")) - -############################################################################### -# Print boundaries -# ~~~~~~~~~~~~~~~~ -# Print boundaries from the ``setups`` object. - -h3d.boundaries - -############################################################################### -# Hide all nets -# ~~~~~~~~~~~~~ -# Hide all nets. - -h3d.modeler.change_net_visibility(visible=False) - -############################################################################### -# Show only two nets -# ~~~~~~~~~~~~~~~~~~ -# Show only two specified nets. - -h3d.modeler.change_net_visibility(["A0_GPIO", "A0_MUX"], visible=True) -edb = h3d.modeler.edb -edb.nets.plot(["A0_GPIO", "A0_MUX"]) - -############################################################################### -# Show all layers -# ~~~~~~~~~~~~~~~ -# Show all layers. - -for layer in h3d.modeler.layers.all_signal_layers: - layer.is_visible = True - -############################################################################### -# Change layer color -# ~~~~~~~~~~~~~~~~~~ -# Change the layer color. - -layer = h3d.modeler.layers.layers[h3d.modeler.layers.layer_id("TOP")] -layer.set_layer_color(0, 255, 0) -h3d.modeler.fit_all() - -############################################################################### -# Disable component visibility -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Disable component visibility for ``"TOP"`` and ``"BOTTOM"``. -# The :func:`ansys.aedt.core.modules.layer_stackup.Layer.update_stackup_layer` method -# applies modifications to the layout. - -top = h3d.modeler.layers.layers[h3d.modeler.layers.layer_id("TOP")] -top.is_visible_component = False - -bot = h3d.modeler.layers.layers[h3d.modeler.layers.layer_id("BOTTOM")] -bot.is_visible_component = False - -############################################################################### -# Fit all -# ~~~~~~~ -# Fit all so that you can visualize all. - -h3d.modeler.fit_all() - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.release_desktop` method. -# All methods provide for saving the project before closing. - -h3d.close_project() -d.release_desktop() diff --git a/examples/01-HFSS3DLayout/HFSS3DLayout_Via.py b/examples/01-HFSS3DLayout/HFSS3DLayout_Via.py deleted file mode 100644 index 3be42cefa3c..00000000000 --- a/examples/01-HFSS3DLayout/HFSS3DLayout_Via.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -HFSS 3D Layout: parametric via analysis ---------------------------------------- -This example shows how you can use HFSS 3D Layout to create and solve a -parametric via analysis. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. -import ansys.aedt.core -import os - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = True - -############################################################################### -# Launch AEDT -# ~~~~~~~~~~~ -# Launch AEDT 2023 R2 in graphical mode. - -h3d = ansys.aedt.core.Hfss3dLayout(version=aedt_version, new_desktop=True, non_graphical=non_graphical) - -############################################################################### -# Set up variables -# ~~~~~~~~~~~~~~~~ -# Set up all parametric variables to use in the layout. - -h3d["viatotrace"] = "5mm" -h3d["viatovia"] = "10mm" -h3d["w1"] = "1mm" -h3d["sp"] = "0.5mm" -h3d["len"] = "50mm" - -############################################################################### -# Add stackup layers -# ~~~~~~~~~~~~~~~~~~ -# Add stackup layers. - -h3d.modeler.layers.add_layer(layer="GND", layer_type="signal", thickness="0", isnegative=True) -h3d.modeler.layers.add_layer(layer="diel", layer_type="dielectric", thickness="0.2mm", material="FR4_epoxy") -h3d.modeler.layers.add_layer(layer="TOP", layer_type="signal", thickness="0.035mm", elevation="0.2mm") - - -############################################################################### -# Create signal net and ground planes -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a signal net and ground planes. - -h3d.modeler.create_line(layer="TOP", - center_line_coordinates=[[0, 0], ["len", 0]], - lw="w1", - name="microstrip", - net="microstrip") -h3d.modeler.create_rectangle(layer="TOP", origin=[0, "-w1/2-sp"], sizes=["len", "-w1/2-sp-20mm"]) -h3d.modeler.create_rectangle(layer="TOP", origin=[0, "w1/2+sp"], sizes=["len", "w1/2+sp+20mm"]) - -############################################################################### -# Create vias -# ~~~~~~~~~~~ -# Create vias with parametric positions. - -h3d.modeler.create_via(x="viatovia", y="-viatotrace", name="via1") -h3d.modeler.create_via(x="viatovia", y="viatotrace", name="via2") -h3d.modeler.create_via(x="2*viatovia", y="-viatotrace") -h3d.modeler.create_via(x="2*viatovia", y="viatotrace") -h3d.modeler.create_via(x="3*viatovia", y="-viatotrace") -h3d.modeler.create_via(x="3*viatovia", y="viatotrace") - -############################################################################### -# Create circuit ports -# ~~~~~~~~~~~~~~~~~~~~ -# Create circuit ports. - -h3d.create_edge_port("microstrip", 0) -h3d.create_edge_port("microstrip", 2) - -############################################################################### -# Create setup and sweep -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create a setup and a sweep. - -setup = h3d.create_setup() -h3d.create_linear_count_sweep(setup=setup.name, unit="GHz", start_frequency=3, stop_frequency=7, - num_of_freq_points=1001, save_fields=False, sweep_type="Interpolating", - interpolation_tol_percent=1, interpolation_max_solutions=255, use_q3d_for_dc=False) - -############################################################################### -# Solve and plot results -# ~~~~~~~~~~~~~~~~~~~~~~ -# Solve and plot the results. - -h3d.analyze() -traces = h3d.get_traces_for_plot(first_element_filter="Port1") -h3d.post.create_report(traces, variations=h3d.available_variations.nominal_w_values_dict) - -############################################################################### -# Create report outside AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a report using Matplotlib. - -traces = h3d.get_traces_for_plot(first_element_filter="Port1", category="S") - -solutions = h3d.post.get_solution_data(expressions=traces) -solutions.plot(formula="db20") - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.release_desktop` method. -# All methods provide for saving the project before closing. - -h3d.release_desktop() diff --git a/examples/01-HFSS3DLayout/Hfss3DComponent.py b/examples/01-HFSS3DLayout/Hfss3DComponent.py deleted file mode 100644 index 2d18d039838..00000000000 --- a/examples/01-HFSS3DLayout/Hfss3DComponent.py +++ /dev/null @@ -1,245 +0,0 @@ -""" -HFSS: 3D Components -------------------- -This example shows how you can use PyAEDT to place 3D Components in Hfss and in Hfss 3D Layout. -""" -import os -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Common Properties -# ~~~~~~~~~~~~~~~~~ -# Set common properties. - -trace_width = 0.6 -trace_length = 30 -diel_height = "121mil" -sig_height = "5mil" -max_steps = 3 -freq = "3GHz" -new_session = True - -############################################################################### -# 3D Component Definition -# ~~~~~~~~~~~~~~~~~~~~~~~ -# File to be used in the example - -component3d = ansys.aedt.core.downloads.download_file("component_3d", "SMA_RF_Jack.a3dcomp",) - -############################################################################### -# Hfss Example -# ------------ -# This example will create a stackup in Hfss place a 3d component, build a ground plane, a trace, -# create excitation and solve it in Hfss. - -############################################################################### -# Launch Hfss -# ~~~~~~~~~~~ -# Launch HFSS application - -hfss = ansys.aedt.core.Hfss(new_desktop=True, version=aedt_version, non_graphical=non_graphical) - -hfss.solution_type = "Terminal" - -############################################################################### -# Insert 3d Component -# ~~~~~~~~~~~~~~~~~~~ -# To insert a 3d component we need to read parameters and then import in Hfss. - -comp_param = hfss.get_components3d_vars(component3d) -hfss.modeler.insert_3d_component(component3d, comp_param) - -############################################################################### -# Add a new Stackup -# ~~~~~~~~~~~~~~~~~ -# Pyaedt has a Stackup class which allows to parametrize stacked structures. - -stackup = hfss.add_stackup_3d() -s1 = stackup.add_signal_layer("L1", thickness=sig_height) -d1 = stackup.add_dielectric_layer("D1", thickness=diel_height) -g1 = stackup.add_ground_layer("G1", thickness=sig_height) - -############################################################################### -# Define stackup extensions -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define stackup elevation and size. Defines also the stackup origin. - -stackup.start_position = "-131mil" -stackup.dielectric_width = "20mm" -stackup.dielectric_length = "40mm" -stackup.dielectric_y_position = "-dielectric_width/2" -stackup.dielectric_x_position = "-dielectric_length/4" - -############################################################################### -# Padstack Definition -# ~~~~~~~~~~~~~~~~~~~ -# Padstacks are needed to create a clearance around 3d component since -# intersections are not allowed. There will be 1 padstack for Gnd and 1 for pin. - -p1 = stackup.add_padstack("gnd_via", material="cloned_copper") -p1.set_start_layer("L1") -p1.set_stop_layer("G1") -p1.set_all_antipad_value(1.3) -p1.set_all_pad_value(0) -p1.num_sides = 8 -p1.add_via(-3.2, -3.2) -p1.add_via(-3.2, 3.2) -p1.add_via(3.2, -3.2) -p1.add_via(3.2, 3.2) -p2 = stackup.add_padstack("signal_via", material="cloned_copper") - -p2.set_start_layer("L1") -p2.set_stop_layer("G1") -p2.set_all_antipad_value(0.7) -p2.set_all_pad_value(0) -p2.padstacks_by_layer["L1"].pad_radius = 0.3048 -p2.add_via(0, 0) - -############################################################################### -# Trace Definition -# ~~~~~~~~~~~~~~~~ -# The trace will connect the pin to the port on layer L1. - -t1 = s1.add_trace(trace_width, trace_length) -rect1 = hfss.modeler.create_rectangle(orientation=hfss.PLANE.YZ, - origin=["0.75*dielectric_length", "-5*" + t1.width.name, "0mm"], - sizes=["15*" + t1.width.name, "-3*" + stackup.thickness.name]) -p1 = hfss.wave_port(assignment=rect1, reference="G1", name="P1") - -############################################################################### -# Set Simulation Boundaries -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define regione and simulation boundaries. - -hfss.change_material_override(True) -region = hfss.modeler.create_region([0, 0, 0, 0, 0, 100]) -sheets = [i for i in region.faces] -hfss.assign_radiation_boundary_to_faces(sheets) - -############################################################################### -# Create Setup -# ~~~~~~~~~~~~ -# Iterations will be reduced to reduce simulation time. - -setup1 = hfss.create_setup() -sweep1 = hfss.create_linear_count_sweep(setup1.name, "GHz", 0.01, 8, 1601, sweep_type="Interpolating") -setup1.props["Frequency"] = freq -setup1.props["MaximumPasses"] = max_steps - -############################################################################### -# Solve Setup -# ~~~~~~~~~~~ -# Save the project first and then solve the setup. - -hfss.save_project() -hfss.analyze() - -############################################################################### -# Plot results -# ~~~~~~~~~~~~ -# Plot the results when analysis is completed. - -traces = hfss.get_traces_for_plot(category="S") -solutions = hfss.post.get_solution_data(traces) -solutions.plot(traces, formula="db20") - -############################################################################### -# Hfss 3D Layout Example -# ---------------------- -# Previous example will be repeated this time in Hfss 3d Layout. -# Small differences are expected in layout but results should be similar. - -############################################################################### -# Launch Hfss3dLayout -# ~~~~~~~~~~~~~~~~~~~ -# Launch HFSS3dLayout application - -h3d = ansys.aedt.core.Hfss3dLayout() - -############################################################################### -# Add stackup layers -# ~~~~~~~~~~~~~~~~~~ -# Add stackup layers. - -l1 = h3d.modeler.layers.add_layer("L1", "signal", thickness=sig_height) -h3d.modeler.layers.add_layer("diel", "dielectric", thickness=diel_height, material="FR4_epoxy") -h3d.modeler.layers.add_layer("G1", "signal", thickness=sig_height, isnegative=True) - - -############################################################################### -# Place 3d Component -# ~~~~~~~~~~~~~~~~~~ -# Place a 3d component by specifying the .a3dcomp file path. - -comp = h3d.modeler.place_3d_component( - component_path=component3d, number_of_terminals=1, placement_layer="G1", component_name="my_connector", - pos_x=0.000, pos_y=0.000 -) - -############################################################################### -# Create signal net and ground planes -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a signal net and ground planes. - -h3d["len"] = str(trace_length) + "mm" -h3d["w1"] = str(trace_width) + "mm" - -line = h3d.modeler.create_line("L1", [[0, 0], ["len", 0]], lw="w1", name="microstrip", net="microstrip") -h3d.create_edge_port(line, h3d.modeler[line.name].top_edge_x, is_wave_port=True, wave_horizontal_extension=15) - -############################################################################### -# Create void on Ground plane for pin -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a void. - -h3d.modeler.create_circle("G1", 0, 0, 0.5) - -############################################################################### -# Create Setup -# ~~~~~~~~~~~~ -# Iterations will be reduced to reduce simulation time. - -h3d.set_meshing_settings(mesh_method="PhiPlus", enable_intersections_check=False) -h3d.edit_hfss_extents(diel_extent_horizontal_padding="0.2", air_vertical_positive_padding="0", - air_vertical_negative_padding="2", airbox_values_as_dim=False) -setup1 = h3d.create_setup() -sweep1 = h3d.create_linear_count_sweep(setup1.name, - "GHz", - 0.01, - 8, - 1601, - sweep_type="Interpolating") -setup1.props["AdaptiveSettings"]["SingleFrequencyDataList"]["AdaptiveFrequencyData"]["AdaptiveFrequency"] = freq -setup1.props["AdaptiveSettings"]["SingleFrequencyDataList"]["AdaptiveFrequencyData"]["MaxPasses"] = max_steps - -############################################################################### -# Solve Setup -# ~~~~~~~~~~~ - -h3d.analyze() - -############################################################################### -# Plot results -# ~~~~~~~~~~~~ - -traces = h3d.get_traces_for_plot(category="S") -solutions = h3d.post.get_solution_data(traces) -solutions.plot(traces, formula="db20") - -h3d.save_project() -h3d.release_desktop() diff --git a/examples/01-HFSS3DLayout/Readme.txt b/examples/01-HFSS3DLayout/Readme.txt deleted file mode 100644 index daf9b1637e8..00000000000 --- a/examples/01-HFSS3DLayout/Readme.txt +++ /dev/null @@ -1,4 +0,0 @@ -HFSS 3D Layout examples -~~~~~~~~~~~~~~~~~~~~~~~ -These examples use PyAEDT to show some end-to-end workflows for HFSS 3D Layout. -It includes model generation, setup, meshing, and post-processing. diff --git a/examples/01-Modeling-Setup/Configurations.py b/examples/01-Modeling-Setup/Configurations.py deleted file mode 100644 index 5f720144699..00000000000 --- a/examples/01-Modeling-Setup/Configurations.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -General: configuration files ----------------------------- -This example shows how you can use PyAEDT to export configuration files and re-use -them to import in a new project. A configuration file is supported by these applications: - -* HFSS -* 2D Extractor and Q3D Extractor -* Maxwell -* Icepak (in AEDT) -* Mechanical (in AEDT) - -The following sections are covered: - -* Variables -* Mesh operations (except Icepak) -* Setup and optimetrics -* Material properties -* Object properties -* Boundaries and excitations - -When a boundary is attached to a face, the tool tries to match it with a -``FaceByPosition`` on the same object name on the target design. If, for -any reason, this face position has changed or the object name in the target -design has changed, the boundary fails to apply. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports from ansys.aedt.core. - -import os -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Open project -# ~~~~~~~~~~~~ -# Download the project, open it, and save it to the temporary folder. - -project_full_name = ansys.aedt.core.downloads.download_icepak(ansys.aedt.core.generate_unique_folder_name(folder_name="Graphic_Card")) - -ipk = ansys.aedt.core.Icepak(project=project_full_name, version=aedt_version, - new_desktop=True, non_graphical=non_graphical) -ipk.autosave_disable() - -############################################################################### -# Create source blocks -# ~~~~~~~~~~~~~~~~~~~~ -# Create a source block on the CPU and memories. - -ipk.create_source_block(object_name="CPU", input_power="25W") -ipk.create_source_block(object_name=["MEMORY1", "MEMORY1_1"], input_power="5W") - -############################################################################### -# Assign boundaries -# ~~~~~~~~~~~~~~~~~ -# Assign the opening and grille. - -region = ipk.modeler["Region"] -ipk.assign_openings(air_faces=region.bottom_face_x.id) -ipk.assign_grille(air_faces=region.top_face_x.id, free_area_ratio=0.8) - -############################################################################### -# Create setup -# ~~~~~~~~~~~~ -# Create the setup. Properties can be set up from the ``setup`` object -# with getters and setters. They don't have to perfectly match the property -# syntax. - -setup1 = ipk.create_setup() -setup1["FlowRegime"] = "Turbulent" -setup1["Max Iterations"] = 5 -setup1["Solver Type Pressure"] = "flex" -setup1["Solver Type Temperature"] = "flex" -ipk.save_project(r"C:\temp\Graphic_card.aedt") - -############################################################################### -# Export project to step file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Export the current project to the step file. - -filename = ipk.design_name -file_path = os.path.join(ipk.working_directory, filename + ".step") -ipk.export_3d_model(file_name=filename, file_path=ipk.working_directory, file_format=".step", assignment_to_export=[], - assignment_to_remove=[]) - -############################################################################### -# Export configuration files -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Export the configuration files. You can optionally disable the export and -# import sections. Supported formats are json and toml files - -conf_file = ipk.configurations.export_config(os.path.join(ipk.working_directory, "config.toml")) -ipk.close_project() - - -############################################################################### -# Create project -# ~~~~~~~~~~~~~~ -# Create an Icepak project and import the step. - -app = ansys.aedt.core.Icepak(project="new_proj_Ipk") -app.modeler.import_3d_cad(file_path) - -############################################################################### -# Import and apply configuration file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Import and apply the configuration file. You can apply all or part of the -# JSON file that you import using options in the ``configurations`` object. - -out = app.configurations.import_config(conf_file) -app.configurations.results.global_import_success - -############################################################################### -# Close project -# ~~~~~~~~~~~~~ -# Close the project. - -app.release_desktop() diff --git a/examples/01-Modeling-Setup/HFSS_CoordinateSystem.py b/examples/01-Modeling-Setup/HFSS_CoordinateSystem.py deleted file mode 100644 index ff28f7ae91c..00000000000 --- a/examples/01-Modeling-Setup/HFSS_CoordinateSystem.py +++ /dev/null @@ -1,281 +0,0 @@ -""" -General: coordinate system creation ------------------------------------ -This example shows how you can use PyAEDT to create and modify coordinate systems in the modeler. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports - -import os - -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Launch AEDT in graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT 2023 R2 in graphical mode. - -d = ansys.aedt.core.launch_desktop(version=aedt_version, non_graphical=non_graphical, new_desktop=True) - -############################################################################### -# Insert HFSS design -# ~~~~~~~~~~~~~~~~~~ -# Insert an HFSS design with the default name. - -hfss = ansys.aedt.core.Hfss(project=ansys.aedt.core.generate_unique_project_name(folder_name="CoordSysDemo")) - -############################################################################### -# Create coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# The coordinate system is centered on the global origin and has the axis -# aligned to the global coordinate system. The new coordinate system is -# saved in the object ``cs1``. - -cs1 = hfss.modeler.create_coordinate_system() - -############################################################################### -# Modify coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# The ``cs1`` object exposes properties and methods to manipulate the -# coordinate system. The origin can be changed. - -cs1["OriginX"] = 10 -cs1.props["OriginY"] = 10 -cs1.props["OriginZ"] = 10 - -# Pointing vectors can be changed - -ypoint = [0, -1, 0] -cs1.props["YAxisXvec"] = ypoint[0] -cs1.props["YAxisYvec"] = ypoint[1] -cs1.props["YAxisZvec"] = ypoint[2] - -############################################################################### -# Rename coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Rename the coordinate system. - -cs1.rename("newCS") - -############################################################################### -# Change coordinate system mode -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Use the ``change_cs_mode`` method to change the mode. Options are ``0`` -# for axis/position, ``1`` for Euler angle ZXZ, and ``2`` for Euler angle ZYZ. -# Here ``1`` sets Euler angle ZXZ as the mode. - -cs1.change_cs_mode(1) - -# In the new mode, these properties can be edited -cs1.props["Phi"] = "10deg" -cs1.props["Theta"] = "22deg" -cs1.props["Psi"] = "30deg" - -############################################################################### -# Delete coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Delete the coordinate system. - -cs1.delete() - -############################################################################### -# Create coordinate system by defining axes -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a coordinate system by defining the axes. During creation, you can -# specify all coordinate system properties. - -cs2 = hfss.modeler.create_coordinate_system( - name="CS2", origin=[1, 2, 3.5], mode="axis", x_pointing=[1, 0, 1], y_pointing=[0, -1, 0] -) - -############################################################################### -# Create coordinate system by defining Euler angles -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a coordinate system by defining Euler angles. - -cs3 = hfss.modeler.create_coordinate_system(name="CS3", origin=[2, 2, 2], mode="zyz", phi=10, theta=20, psi=30) - -############################################################################### -# Create coordinate system by defining view -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a coordinate system by defining the view. Options are ``"iso"``, -# ``"XY"``, ``"XZ"``, and ``"XY"``. Here ``"iso"`` is specified. -# The axes are set automatically. - -cs4 = hfss.modeler.create_coordinate_system(name="CS4", origin=[1, 0, 0], reference_cs="CS3", mode="view", view="iso") - -############################################################################### -# Create coordinate system by defining axis and angle rotation -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a coordinate system by defining the axis and angle rotation. When you -# specify the axis and angle rotation, this data is automatically translated -# to Euler angles. - -cs5 = hfss.modeler.create_coordinate_system(name="CS5", mode="axisrotation", u=[1, 0, 0], theta=123) - -############################################################################### -# Create face coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Face coordinate systems are bound to an object face. -# First create a box and then define the face coordinate system on one of its -# faces. To create the reference face for the face coordinate system, you must -# specify starting and ending points for the axis. - -box = hfss.modeler.create_box([0, 0, 0], [2, 2, 2]) -face = box.faces[0] -fcs1 = hfss.modeler.create_face_coordinate_system( - face=face, origin=face.edges[0], axis_position=face.edges[1], name="FCS1" -) - -############################################################################### -# Create face coordinate system centered on face -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a face coordinate system centered on the face with the X axis pointing -# to the edge vertex. - -fcs2 = hfss.modeler.create_face_coordinate_system( - face=face, origin=face, axis_position=face.edges[0].vertices[0], name="FCS2" -) - -############################################################################### -# Swap X and Y axes of face coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Swap the X axis and Y axis of the face coordinate system. The X axis is the -# pointing ``axis_position`` by default. You can optionally select the Y axis. - -fcs3 = hfss.modeler.create_face_coordinate_system(face=face, origin=face, axis_position=face.edges[0], axis="Y") - -# Axis can also be changed after coordinate system creation -fcs3.props["WhichAxis"] = "X" - -############################################################################### -# Apply a rotation around Z axis -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Apply a rotation around the Z axis. The Z axis of a face coordinate system -# is always orthogonal to the face. A rotation can be applied at definition. -# Rotation is expressed in degrees. - -fcs4 = hfss.modeler.create_face_coordinate_system(face=face, origin=face, axis_position=face.edges[1], rotation=10.3) - -# Rotation can also be changed after coordinate system creation -fcs4.props["ZRotationAngle"] = "3deg" - -############################################################################### -# Apply offset to X and Y axes of face coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Apply an offset to the X axis and Y axis of a face coordinate system. -# The offset is in respect to the face coordinate system itself. - -fcs5 = hfss.modeler.create_face_coordinate_system( - face=face, origin=face, axis_position=face.edges[2], offset=[0.5, 0.3] -) - -# The offset can also be changed after the coordinate system is created. -fcs5.props["XOffset"] = "0.2mm" -fcs5.props["YOffset"] = "0.1mm" - -############################################################################### -# Create coordinate system relative to face coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a coordinate system relative to a face coordinate system. Coordinate -# systems and face coordinate systems interact with each other. - -face = box.faces[1] -fcs6 = hfss.modeler.create_face_coordinate_system(face=face, origin=face, axis_position=face.edges[0]) -cs_fcs = hfss.modeler.create_coordinate_system( - name="CS_FCS", origin=[0, 0, 0], reference_cs=fcs6.name, mode="view", view="iso" -) - -############################################################################### -# Create object coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create object coordinate system with origin on face - -obj_cs = hfss.modeler.create_object_coordinate_system(assignment=box, origin=box.faces[0], x_axis=box.edges[0], - y_axis=[0, 0, 0], name="box_obj_cs") -obj_cs.rename("new_obj_cs") - -############################################################################### -# Create object coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create object coordinate system with origin on edge - -obj_cs_1 = hfss.modeler.create_object_coordinate_system(assignment=box.name, origin=box.edges[0], x_axis=[1, 0, 0], - y_axis=[0, 1, 0], name="obj_cs_1") -obj_cs_1.set_as_working_cs() - -############################################################################### -# Create object coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create object coordinate system with origin specified on point - -obj_cs_2 = hfss.modeler.create_object_coordinate_system(assignment=box.name, origin=[0, 0.8, 0], x_axis=[1, 0, 0], - y_axis=[0, 1, 0], name="obj_cs_2") -new_obj_cs_2 = hfss.modeler.duplicate_coordinate_system_to_global(obj_cs_2) -obj_cs_2.delete() - -############################################################################### -# Create object coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create object coordinate system with origin on vertex - -obj_cs_3 = hfss.modeler.create_object_coordinate_system(assignment=box.name, origin=box.vertices[1], - x_axis=box.faces[2], y_axis=box.faces[4], name="obj_cs_3") -obj_cs_3.props["MoveToEnd"] = False -obj_cs_3.update() - -############################################################################### -# Get all coordinate systems -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Get all coordinate systems. - -css = hfss.modeler.coordinate_systems -names = [i.name for i in css] -print(names) - -############################################################################### -# Select coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Select an existing coordinate system. - -css = hfss.modeler.coordinate_systems -cs_selected = css[0] -cs_selected.delete() - -############################################################################### -# Get point coordinate under another coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Get a point coordinate under another coordinate system. A point coordinate -# can be translated in respect to any coordinate system. - -hfss.modeler.create_box([-10, -10, -10], [20, 20, 20], "Box1") -p = hfss.modeler["Box1"].faces[0].vertices[0].position -print("Global: ", p) -p2 = hfss.modeler.global_to_cs(p, "CS5") -print("CS5 :", p2) - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -# After the simulaton completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.release_desktop` method. -# All methods provide for saving the project before closing. - -d.release_desktop() diff --git a/examples/01-Modeling-Setup/Optimetrics.py b/examples/01-Modeling-Setup/Optimetrics.py deleted file mode 100644 index c6b2247aafe..00000000000 --- a/examples/01-Modeling-Setup/Optimetrics.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -General: optimetrics setup --------------------------- -This example shows how you can use PyAEDT to create a project in HFSS and create all optimetrics setups. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import ansys.aedt.core - -import os - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Initialize object and create variables -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize the ``Hfss`` object and create two needed design variables, -# ``w1`` and ``w2``. - -hfss = ansys.aedt.core.Hfss(version=aedt_version, new_desktop=True, non_graphical=non_graphical, solution_type="Modal") -hfss["w1"] = "1mm" -hfss["w2"] = "100mm" - -############################################################################### -# Create waveguide with sheets on it -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create one of the standard waveguide structures and parametrize it. -# You can also create rectangles of waveguide openings and assign ports later. - -wg1, p1, p2 = hfss.modeler.create_waveguide( - [0, 0, 0], - hfss.AXIS.Y, - "WG17", - wg_thickness="w1", - wg_length="w2", - create_sheets_on_openings=True, -) - -model = hfss.plot(show=False) - -model.show_grid = False -model.plot(os.path.join(hfss.working_directory, "Image.jpg")) - -############################################################################### -# Create wave ports on sheets -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create two wave ports on the sheets. - -hfss.wave_port(p1, integration_line=hfss.AxisDir.ZPos, name="1") -hfss.wave_port(p2, integration_line=hfss.AxisDir.ZPos, name="2") - -############################################################################### -# Create setup and frequency sweep -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a setup and a frequency sweep to use as the base for optimetrics -# setups. - -setup = hfss.create_setup() -hfss.create_linear_step_sweep( - setup=setup.name, - unit="GHz", - start_frequency=1, - stop_frequency=5, - step_size=0.1, - name="Sweep1", - save_fields=True -) - -############################################################################### -# Optimetrics analysis -# ---------------------- -# Create parametrics analysis -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a simple optimetrics parametrics analysis with output calculations. - -sweep = hfss.parametrics.add("w2", 90, 200, 5) -sweep.add_variation("w1", 0.1, 2, 10) -sweep.add_calculation(calculation="dB(S(1,1))", ranges={"Freq": "2.5GHz"}) -sweep.add_calculation(calculation="dB(S(1,1))", ranges={"Freq": "2.6GHz"}) - -############################################################################### -# Create sensitivity analysis -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create an optimetrics sensitivity analysis with output calculations. - -sweep2 = hfss.optimizations.add(calculation="dB(S(1,1))", ranges={"Freq": "2.5GHz"}, optimization_type="Sensitivity") -sweep2.add_variation("w1", 0.1, 3, 0.5) -sweep2.add_calculation(calculation="dB(S(1,1))", ranges={"Freq": "2.6GHz"}) - -############################################################################### -# Create optimization based on goals and calculations -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create an optimization analysis based on goals and calculations. - -sweep3 = hfss.optimizations.add(calculation="dB(S(1,1))", ranges={"Freq": "2.5GHz"}) -sweep3.add_variation("w1", 0.1, 3, 0.5) -sweep3.add_goal(calculation="dB(S(1,1))", ranges={"Freq": "2.6GHz"}) -sweep3.add_goal(calculation="dB(S(1,1))", ranges={"Freq": ("2.6GHz", "5GHz")}) -sweep3.add_goal( - calculation="dB(S(1,1))", - ranges={"Freq": ("2.6GHz", "5GHz")}, - condition="Maximize", -) - -############################################################################### -# Create DX optimization based on a goal and calculation -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a DX (DesignXplorer) optimization based on a goal and a calculation. - -sweep4 = hfss.optimizations.add(calculation="dB(S(1,1))", ranges={"Freq": "2.5GHz"}, optimization_type="DesignExplorer") -sweep4.add_goal(calculation="dB(S(1,1))", ranges={"Freq": "2.6GHz"}) - -############################################################################### -# Create DOE based on a goal and calculation -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a DOE (Design of Experiments) based on a goal and a calculation. - -sweep5 = hfss.optimizations.add(calculation="dB(S(1,1))", ranges={"Freq": "2.5GHz"}, optimization_type="DXDOE") - -############################################################################### -# Create DOE based on a goal and calculation -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a DOE based on a goal and a calculation. - -region = hfss.modeler.create_region() -hfss.assign_radiation_boundary_to_objects(region) -hfss.insert_infinite_sphere(name="Infinite_1") -sweep6 = hfss.optimizations.add(calculation="RealizedGainTotal", - ranges={"Freq": "5GHz", "Theta": ["0deg", "10deg", "20deg"], "Phi": "0deg"}, - solution=hfss.nominal_adaptive, context="Infinite_1") - -############################################################################### -# Close AEDT -# ---------- -# After the simulaton completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.release_desktop` method. -# All methods provide for saving the project before closing. - -hfss.release_desktop() diff --git a/examples/01-Modeling-Setup/Polyline_Primitives.py b/examples/01-Modeling-Setup/Polyline_Primitives.py deleted file mode 100644 index 4ea86d8e708..00000000000 --- a/examples/01-Modeling-Setup/Polyline_Primitives.py +++ /dev/null @@ -1,295 +0,0 @@ -""" -General: polyline creation --------------------------- -This example shows how you can use PyAEDT to create and manipulate polylines. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Create Maxwell 3D object -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a :class:`ansys.aedt.core.maxwell.Maxwell3d` object and set the unit type to ``"mm"``. - -M3D = ansys.aedt.core.Maxwell3d(solution_type="Transient", design="test_polyline_3D", version=aedt_version, - new_desktop=True, non_graphical=non_graphical, ) -M3D.modeler.model_units = "mm" -prim3D = M3D.modeler - -############################################################################### -# Define variables -# ~~~~~~~~~~~~~~~~ -# Define two design variables as parameters for the polyline objects. - -M3D["p1"] = "100mm" -M3D["p2"] = "71mm" - -############################################################################### -# Input data -# ~~~~~~~~~~ -# Input data. All data for the polyline functions can be entered as either floating point -# values or strings. Floating point values are assumed to be in model units -# (``M3D.modeler.model_units``). - -test_points = [["0mm", "p1", "0mm"], ["-p1", "0mm", "0mm"], ["-p1/2", "-p1/2", "0mm"], ["0mm", "0mm", "0mm"]] - -############################################################################### -# Polyline primitives -# ------------------- -# The following examples are for creating polyline primitives. - -# Create line primitive -# ~~~~~~~~~~~~~~~~~~~~~ -# Create a line primitive. The basic polyline command takes a list of positions -# (``[X, Y, Z]`` coordinates) and creates a polyline object with one or more -# segments. The supported segment types are ``Line``, ``Arc`` (3 points), -# ``AngularArc`` (center-point + angle), and ``Spline``. - -P = prim3D.create_polyline(points=test_points[0:2], name="PL01_line") - -print("Created Polyline with name: {}".format(prim3D.objects[P.id].name)) -print("Segment types : {}".format([s.type for s in P.segment_types])) -print("primitive id = {}".format(P.id)) - -############################################################################### -# Create arc primitive -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create an arc primitive. The parameter ``position_list`` must contain at -# least three position values. The first three position values are used. - -P = prim3D.create_polyline(points=test_points[0:3], segment_type="Arc", name="PL02_arc") - -print("Created object with id {} and name {}.".format(P.id, prim3D.objects[P.id].name)) - -############################################################################### -# Create spline primitive -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a spline primitive. Defining the segment using a ``PolylineSegment`` -# object allows you to provide additional input parameters for the spine, such -# as the number of points (in this case 4). The parameter ``position_list`` -# must contain at least four position values. - -P = prim3D.create_polyline(points=test_points, segment_type=prim3D.polyline_segment("Spline", num_points=4), - name="PL03_spline_4pt") - -############################################################################### -# Create center-point arc primitive -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a center-point arc primitive. A center-point arc segment is defined -# by a starting point, a center point, and an angle of rotation around the -# center point. The rotation occurs in a plane parallel to the XY, YZ, or ZX -# plane of the active coordinate system. The starting point and the center point -# must therefore have one coordinate value (X, Y, or Z) with the same value. -# -# Here ``start-point`` and ``center-point`` have a common Z coordinate, ``"0mm"``. -# The curve is therefore rotated in the XY plane with Z = ``"0mm"``. - -start_point = [100, 100, 0] -center_point = [0, 0, 0] -P = prim3D.create_polyline(points=[start_point], - segment_type=prim3D.polyline_segment("AngularArc", arc_center=center_point, - arc_angle="30deg"), name="PL04_center_point_arc") - -############################################################################### -# Here ``start_point`` and ``center_point`` have the same values for the Y and -# Z coordinates, so the plane or rotation could be either XY or ZX. -# For these special cases when the rotation plane is ambiguous, you can specify -# the plane explicitly. - -start_point = [100, 0, 0] -center_point = [0, 0, 0] -P = prim3D.create_polyline(points=[start_point], - segment_type=prim3D.polyline_segment("AngularArc", arc_center=center_point, - arc_angle="30deg", arc_plane="XY"), - name="PL04_center_point_arc_rot_XY") -P = prim3D.create_polyline(points=[start_point], - segment_type=prim3D.polyline_segment("AngularArc", arc_center=center_point, - arc_angle="30deg", arc_plane="ZX"), - name="PL04_center_point_arc_rot_ZX") - -############################################################################### -# Compound polylines -# ------------------ -# You can use a list of points in a single command to create a multi-segment -# polyline. -# -# By default, if no specification of the type of segments is given, all points -# are connected by line segments. - -P = prim3D.create_polyline(points=test_points, name="PL06_segmented_compound_line") - -############################################################################### -# You can specify the segment type with the parameter ``segment_type``. -# In this case, you must specify that the four input points in ``position_list`` -# are to be connected as a line segment followed by a 3-point arc segment. - -P = prim3D.create_polyline(points=test_points, segment_type=["Line", "Arc"], name="PL05_compound_line_arc") - -############################################################################### -# The parameter ``close_surface`` ensures that the polyline starting point and -# ending point are the same. If necessary, you can add an additional line -# segment to achieve this. - -P = prim3D.create_polyline(points=test_points, close_surface=True, name="PL07_segmented_compound_line_closed") - -############################################################################### -# The parameter ``cover_surface=True`` also performs the modeler command -# ``cover_surface``. Note that specifying ``cover_surface=True`` automatically -# results in the polyline being closed. - -P = prim3D.create_polyline(points=test_points, cover_surface=True, name="SPL01_segmented_compound_line") - -############################################################################### -# Compound lines -# -------------- -# The following examples are for inserting compound lines. -# -# Insert line segment -# ~~~~~~~~~~~~~~~~~~~ -# Insert a line segment starting at vertex 1 ``["100mm", "0mm", "0mm"]`` -# of an existing polyline and ending at some new point ``["90mm", "20mm", "0mm"].`` -# By numerical comparison of the starting point with the existing vertices of the -# original polyline object, it is determined automatically that the segment is -# inserted after the first segment of the original polyline. - -P = prim3D.create_polyline(points=test_points, close_surface=True, name="PL08_segmented_compound_insert_segment") - -p2 = P.points[1] -insert_point = ["-100mm", "20mm", "0mm"] - -P.insert_segment(points=[insert_point, p2]) - -############################################################################### -# Insert compound line with insert curve -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Insert a compound line starting a line segment at vertex 1 ``["100mm", "0mm", "0mm"]`` -# of an existing polyline and end at some new point ``["90mm", "20mm", "0mm"]``. -# By numerical comparison of the starting point, it is determined automatically -# that the segment is inserted after the first segment of the original polyline. - -P = prim3D.create_polyline(points=test_points, close_surface=False, name="PL08_segmented_compound_insert_arc") - -start_point = P.vertex_positions[1] -insert_point1 = ["90mm", "20mm", "0mm"] -insert_point2 = [40, 40, 0] - -P.insert_segment(points=[start_point, insert_point1, insert_point2], segment="Arc") - -############################################################################### -# Insert compound line at end of a center-point arc -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Insert a compound line at the end of a center-point arc (``type="AngularArc"``). -# This is a special case. -# -# Step 1: Draw a center-point arc. - -start_point = [2200.0, 0.0, 1200.0] -arc_center_1 = [1400, 0, 800] -arc_angle_1 = "43.47deg" - -P = prim3D.create_polyline(points=[start_point], - segment_type=prim3D.polyline_segment(type="AngularArc", arc_angle=arc_angle_1, - arc_center=arc_center_1), name="First_Arc") - -############################################################################### -# Step 2: Insert a line segment at the end of the arc with a specified end point. - -start_of_line_segment = P.end_point -end_of_line_segment = [3600, 200, 30] - -P.insert_segment(points=[start_of_line_segment, end_of_line_segment]) - -############################################################################### -# Step 3: Append a center-point arc segment to the line object. - -arc_angle_2 = "39.716deg" -arc_center_2 = [3400, 200, 3800] - -P.insert_segment(points=[end_of_line_segment], - segment=prim3D.polyline_segment(type="AngularArc", arc_center=arc_center_2, arc_angle=arc_angle_2)) - -############################################################################### -# You can use the compound polyline definition to complete all three steps in -# a single step. - -prim3D.create_polyline(points=[start_point, end_of_line_segment], segment_type=[ - prim3D.polyline_segment(type="AngularArc", arc_angle="43.47deg", arc_center=arc_center_1), - prim3D.polyline_segment(type="Line"), - prim3D.polyline_segment(type="AngularArc", arc_angle=arc_angle_2, arc_center=arc_center_2), -], name="Compound_Polyline_One_Command") - -######################################################################### -# Insert two 3-point arcs forming a circle and covered -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Insert two 3-point arcs forming a circle and covered. -# Note that the last point of the second arc segment is not defined in -# the position list. - -P = prim3D.create_polyline( - points=[[34.1004, 14.1248, 0], [27.646, 16.7984, 0], [24.9725, 10.3439, 0], [31.4269, 7.6704, 0]], - segment_type=["Arc", "Arc"], cover_surface=True, close_surface=True, name="Rotor_Subtract_25_0", material="vacuum") - -############################################################################### -# Here is an example of a complex polyline where the number of points is -# insufficient to populate the requested segments. This results in an -# ``IndexError`` that PyAEDT catches silently. The return value of the command -# is ``False``, which can be caught at the app level. While this example might -# not be so useful in a Jupyter Notebook, it is important for unit tests. - -MDL_points = [ - ["67.1332mm", "2.9901mm", "0mm"], - ["65.9357mm", "2.9116mm", "0mm"], - ["65.9839mm", "1.4562mm", "0mm"], - ["66mm", "0mm", "0mm"], - ["99mm", "0mm", "0mm"], - ["98.788mm", "6.4749mm", "0mm"], - ["98.153mm", "12.9221mm", "0mm"], - ["97.0977mm", "19.3139mm", "0mm"], -] - - -MDL_segments = ["Line", "Arc", "Line", "Arc", "Line"] -return_value = prim3D.create_polyline(MDL_points, segment_type=MDL_segments, name="MDL_Polyline") -assert return_value # triggers an error at the application error - -############################################################################### -# Here is an example that provides more points than the segment list requires. -# This is valid usage. The remaining points are ignored. - -MDL_segments = ["Line", "Arc", "Line", "Arc"] - -P = prim3D.create_polyline(MDL_points, segment_type=MDL_segments, name="MDL_Polyline") - -############################################################################### -# Save project -# ------------ -# Save the project. - -project_dir = r"C:\temp" -project_name = "Polylines" -project_file = os.path.join(project_dir, project_name + ".aedt") - -M3D.save_project(project_file) - -M3D.release_desktop() diff --git a/examples/01-Modeling-Setup/Readme.txt b/examples/01-Modeling-Setup/Readme.txt deleted file mode 100644 index d9417436357..00000000000 --- a/examples/01-Modeling-Setup/Readme.txt +++ /dev/null @@ -1,4 +0,0 @@ -General model and setup examples -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -These examples use PyAEDT to show some general model and simulation -setup features inside AEDT. diff --git a/examples/02-HFSS/Advanced_Far_Field.py.back b/examples/02-HFSS/Advanced_Far_Field.py.back deleted file mode 100644 index 6b9fc242e14..00000000000 --- a/examples/02-HFSS/Advanced_Far_Field.py.back +++ /dev/null @@ -1,218 +0,0 @@ -""" -HFSS: advanced far field postprocessing ---------------------------------------- -This example shows how you can use advanced postprocessing functions to create plots -using Matplotlib without opening the HFSS user interface. -This examples runs only on Windows using CPython. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import time - -import ansys.aedt.core -from ansys.aedt.core.generic.general_methods import remove_project_lock - -project_name = ansys.aedt.core.downloads.download_antenna_array() - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Import modules for postprocessing -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Import modules for postprocessing. - -import numpy as np -import matplotlib.pyplot as plt - -############################################################################### -# Launch AEDT -# ~~~~~~~~~~~ -# Launch AEDT 2023 R1 in non-graphical mode. - -desktopVersion = "2023.1" -NewThread = True -desktop = ansys.aedt.core.launch_desktop(specified_version=desktopVersion, - non_graphical=non_graphical, - new_desktop_session=NewThread - ) - -############################################################################### -# Open HFSS project -# ~~~~~~~~~~~~~~~~~ -# Open the HFSS project. ``Hfss`` class allows to initialize a new project or -# open an existing project and point to a design name. - -remove_project_lock(project_name) - -hfss = ansys.aedt.core.Hfss(projectname=project_name, - designname="4X4_MultiCell_CA-Array") - -############################################################################### -# Solve HFSS project -# ~~~~~~~~~~~~~~~~~~ -# Solves the HFSS project. -# The solution time is computed. - -hfss.analyze_setup("Setup1") -hfss.save_project() - -####################################### -# Get efields data from solution -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Gets efields data from the solution. - -start = time.time() -ff_data = hfss.post.get_efields_data(ff_setup="3D") -end = time.time() - start -print("Postprocessing Time", end) - - -############################################################################### -# Calculate far field values -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Use Matplotlib to read the solution generated in ``ff_data``. Process -# the field based on Phi and Theta and generate a plot. - -def ff_calc(x=0, y=0, qty="rETotal", dB=True): - array_size = [4, 4] - loc_offset = 2 # if array index is not starting at [1,1] - xphase = float(y) - yphase = float(x) - array_shape = (array_size[0], array_size[1]) - weight = np.zeros(array_shape, dtype=complex) - mag = np.ones(array_shape, dtype="object") - port_names_arranged = np.chararray(array_shape) - all_ports = ff_data.keys() - w_dict = {} - # calculate weights based off of progressive phase shift - for m in range(array_shape[0]): - for n in range(array_shape[1]): - mag_val = mag[m][n] - ang = np.radians(xphase * m) + np.radians(yphase * n) - weight[m][n] = np.sqrt(mag_val) * np.exp(1j * ang) - current_index_str = "[" + str(m + 1 + loc_offset) + "," + str(n + 1 + loc_offset) + "]" - port_name = [y for y in all_ports if current_index_str in y] - w_dict[port_name[0]] = weight[m][n] - - length_of_ff_data = len(ff_data[port_name[0]][2]) - - array_shape = (len(w_dict), length_of_ff_data) - rEtheta_fields = np.zeros(array_shape, dtype=complex) - rEphi_fields = np.zeros(array_shape, dtype=complex) - w = np.zeros((1, array_shape[0]), dtype=complex) - # create port mapping - for n, port in enumerate(ff_data.keys()): - re_theta = ff_data[port][2] - re_phi = ff_data[port][3] - re_theta = re_theta * w_dict[port] - - w[0][n] = w_dict[port] - re_phi = re_phi * w_dict[port] - - rEtheta_fields[n] = re_theta - rEphi_fields[n] = re_phi - - theta_range = ff_data[port][0] - phi_range = ff_data[port][1] - theta = [int(np.min(theta_range)), int(np.max(theta_range)), np.size(theta_range)] - phi = [int(np.min(phi_range)), int(np.max(phi_range)), np.size(phi_range)] - Ntheta = len(theta_range) - Nphi = len(phi_range) - - rEtheta_fields = np.dot(w, rEtheta_fields) - rEtheta_fields = np.reshape(rEtheta_fields, (Ntheta, Nphi)) - - rEphi_fields = np.dot(w, rEphi_fields) - rEphi_fields = np.reshape(rEphi_fields, (Ntheta, Nphi)) - - all_qtys = {} - all_qtys["rEPhi"] = rEphi_fields - all_qtys["rETheta"] = rEtheta_fields - all_qtys["rETotal"] = np.sqrt(np.power(np.abs(rEphi_fields), 2) + np.power(np.abs(rEtheta_fields), 2)) - - pin = np.sum(w) - print(str(pin)) - real_gain = 2 * np.pi * np.abs(np.power(all_qtys["rETotal"], 2)) / pin / 377 - all_qtys["RealizedGain"] = real_gain - - if dB: - if "Gain" in qty: - qty_to_plot = 10 * np.log10(np.abs(all_qtys[qty])) - else: - qty_to_plot = 20 * np.log10(np.abs(all_qtys[qty])) - qty_str = qty + " (dB)" - else: - qty_to_plot = np.abs(all_qtys[qty]) - qty_str = qty + " (mag)" - - plt.figure(figsize=(25, 15)) - plt.title(qty_str) - plt.xlabel("Theta (degree)") - plt.ylabel("Phi (degree)") - - plt.imshow(qty_to_plot, cmap="jet") - plt.colorbar() - - np.max(qty_to_plot) - - -############################################################################### -# Create plot and interact with it -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the plot and interact with it. - -ff_calc() - -# interact(ff_calc, x=widgets.FloatSlider(value=0, min=-180, max=180, step=1), -# y=widgets.FloatSlider(value=0, min=-180, max=180, step=1)) - - -vals = hfss.post.get_far_field_data(setup_sweep_name=hfss.nominal_sweep, - expression="RealizedGainTotal", - domain="Elevation" - ) - -############################################################################### -# Generate polar plot -# ~~~~~~~~~~~~~~~~~~~ -# Generate a polar plot. - -vals.plot(math_formula="db20", is_polar=True) - -############################################################################### -# Generate scalar plot -# ~~~~~~~~~~~~~~~~~~~~ -# Generate a scalar plot. - -vals.plot(math_formula="db20", is_polar=False) - -############################################################################### -# Generate plot using Phi as primary sweep -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Generate the plot using Phi as the primary sweep. - -vals3d = hfss.post.get_far_field_data(setup_sweep_name=hfss.nominal_sweep, - expression="RealizedGainTotal", - domain="Infinite Sphere1" - ) - -vals3d.plot_3d() - -####################################### -# Close HFSS project and AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Close the HFSS project and release AEDT. - -# hfss.close_project() -hfss.save_project() -desktop.release_desktop() diff --git a/examples/02-HFSS/Array.py b/examples/02-HFSS/Array.py deleted file mode 100644 index 268fc00f254..00000000000 --- a/examples/02-HFSS/Array.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -HFSS: component antenna array ------------------------------ -This example shows how you can use PyAEDT to create an example using a 3D component file. It sets up -the analysis, solves it, and uses postprocessing functions to create plots using Matplotlib and -PyVista without opening the HFSS user interface. This examples runs only on Windows using CPython. -""" -########################################################## -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core -from ansys.aedt.core.visualization.advanced.farfield_visualization import FfdSolutionData - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -########################################################## -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -########################################################## -# Download 3D component -# ~~~~~~~~~~~~~~~~~~~~~ -# Download the 3D component that is needed to run the example. -example_path = ansys.aedt.core.downloads.download_3dcomponent() - -########################################################## -# Launch HFSS and save project -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch HFSS and save the project. -project_name = ansys.aedt.core.generate_unique_project_name(project_name="array") -hfss = ansys.aedt.core.Hfss(project=project_name, - version=aedt_version, - design="Array_Simple", - non_graphical=non_graphical, - new_desktop=True) - -print("Project name " + project_name) - -########################################################## -# Read array definition from JSON file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Read the array definition from a JSON file. A JSON file -# can contain all information needed to import and set up a -# full array in HFSS. -# -# If a 3D component is not available in the design, it is loaded -# into the dictionary from the path that you specify. The following -# code edits the dictionary to point to the location of the A3DCOMP file. - -dict_in = ansys.aedt.core.general_methods.read_json(os.path.join(example_path, "array_simple.json")) -dict_in["Circ_Patch_5GHz1"] = os.path.join(example_path, "Circ_Patch_5GHz.a3dcomp") -dict_in["cells"][(3, 3)] = {"name": "Circ_Patch_5GHz1"} -array = hfss.add_3d_component_array_from_json(dict_in) - -########################################################## -# Modify cells -# ~~~~~~~~~~~~ -# Rotate corner elements. - -array.cells[0][0].rotation = 90 -array.cells[0][2].rotation = 90 -array.cells[2][0].rotation = 90 -array.cells[2][2].rotation = 90 - -########################################################## -# Set up simulation -# ~~~~~~~~~~~~~~~~~ -# Set up a simulation and analyze it. - -setup = hfss.create_setup() -setup.props["Frequency"] = "5GHz" -setup.props["MaximumPasses"] = 3 - -hfss.analyze(cores=4) -hfss.save_project() - -########################################################## -# Get far field data -# ~~~~~~~~~~~~~~~~~~ -# Get far field data. After the simulation completes, the far -# field data is generated port by port and stored in a data class. - -ffdata = hfss.get_antenna_data(setup=hfss.nominal_adaptive, sphere="Infinite Sphere1") - -########################################################## -# Generate contour plot -# ~~~~~~~~~~~~~~~~~~~~~ -# Generate a contour plot. You can define the Theta scan -# and Phi scan. - -ffdata.farfield_data.plot_contour(quantity='RealizedGain', - title='Contour at {}Hz'.format(ffdata.farfield_data.frequency)) - -########################################################## -# Release AEDT -# ~~~~~~~~~~~~ -# Release AEDT. -# Far field post-processing can be performed without AEDT because the data is stored. - -eep_file = ffdata.metadata_file -working_directory = hfss.working_directory - -hfss.release_desktop() - -########################################################## -# Load far field data -# ~~~~~~~~~~~~~~~~~~~ -# Load far field data stored. - -ffdata = FfdSolutionData(input_file=eep_file) - -########################################################## -# Generate contour plot -# ~~~~~~~~~~~~~~~~~~~~~ -# Generate a contour plot. You can define the Theta scan -# and Phi scan. - -ffdata.plot_contour(quantity='RealizedGain', title='Contour at {}Hz'.format(ffdata.frequency)) - -########################################################## -# Generate 2D cutout plots -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Generate 2D cutout plots. You can define the Theta scan -# and Phi scan. - -ffdata.plot_cut(quantity='RealizedGain', primary_sweep='theta', secondary_sweep_value=[-180, -75, 75], - title='Azimuth at {}Hz'.format(ffdata.frequency), quantity_format="dB10") - -ffdata.plot_cut(quantity='RealizedGain', primary_sweep="phi", secondary_sweep_value=30, title='Elevation', - quantity_format="dB10") - -########################################################## -# Generate 3D plots -# ~~~~~~~~~~~~~~~~~ -# Generate 3D plots. You can define the Theta scan and Phi scan. - -# ffdata.plot_3d(quantity='RealizedGain', -# output_file=os.path.join(working_directory, "Image.jpg"), -# show=False) diff --git a/examples/02-HFSS/Create_3d_Component_and_use_it.py b/examples/02-HFSS/Create_3d_Component_and_use_it.py deleted file mode 100644 index eac8084ebf8..00000000000 --- a/examples/02-HFSS/Create_3d_Component_and_use_it.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -Create a 3D Component and reuse it ----------------------------------- -Summary of the workflow -1. Create an antenna using PyAEDT and HFSS 3D Modeler (same can be done with EDB and HFSS 3D Layout) -2. Store the object as a 3D Component on the disk -3. Reuse the 3D component in another project -4. Parametrize and optimize target design -""" - -########################################################## -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. -import os -import tempfile -from ansys.aedt.core import Hfss -from ansys.aedt.core.generic.general_methods import generate_unique_name - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -########################################################## -# Launch HFSS -# ~~~~~~~~~~~ -# PyAEDT can initialize a new session of Electronics Desktop or connect to an existing one. -# Once Desktop is connected, a new HFSS session is started and a design is created. - -hfss = Hfss(version=aedt_version, new_desktop=True, close_on_exit=True) - -########################################################## -# Variables -# ~~~~~~~~~ -# PyAEDT can create and store all variables available in AEDT (Design, Project, Post Processing) - -hfss["thick"] = "0.1mm" -hfss["width"] = "1mm" - -########################################################## -# Modeler -# ~~~~~~~~ -# PyAEDT supports all modeler functionalities available in the Desktop. -# Objects can be created, deleted and modified using all available boolean operations. -# History is also fully accessible to PyAEDT. - -substrate = hfss.modeler.create_box(["-width", "-width", "-thick"], ["2*width", "2*width", "thick"], name="sub", - material="FR4_epoxy") - -patch = hfss.modeler.create_rectangle("XY",["-width/2","-width/2","0mm"],["width","width"], name="patch1") - -via1 = hfss.modeler.create_cylinder(2, ["-width/8", "-width/4", "-thick"], "0.01mm", "thick", name="via_inner", - material="copper") - -via_outer = hfss.modeler.create_cylinder(2, ["-width/8", "-width/4", "-thick"], "0.025mm", "thick", name="via_teflon", - material="Teflon_based") - -########################################################## -# Boundaries -# ~~~~~~~~~~ -# Most of HFSS boundaries and excitations are already available in PyAEDT. -# User can assign easily a boundary to a face or to an object by taking benefits of -# Object-Oriented Programming (OOP) available in PyAEDT. - -hfss.assign_perfecte_to_sheets(patch) - -########################################################## -# Advanced Modeler functions -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Thanks to Python capabilities a lot of additional functionalities have been added to the Modeler of PyAEDT. -# in this example there is a property to retrieve automatically top and bottom faces of an objects. - -side_face = [i for i in via_outer.faces if i.id not in [via_outer.top_face_z.id, via_outer.bottom_face_z.id]] - -hfss.assign_perfecte_to_sheets(side_face) -hfss.assign_perfecte_to_sheets(substrate.bottom_face_z) - -########################################################## -# Create Wave Port -# ~~~~~~~~~~~~~~~~ -# Wave port can be assigned to a sheet or to a face of an object. - -hfss.wave_port(via_outer.bottom_face_z, name="P1") - -########################################################## -# Create 3D Component -# ~~~~~~~~~~~~~~~~~~~ -# Once the model is ready a 3D Component can be created. -# Multiple options are available to partially select objects, cs, boundaries and mesh operations. -# Furthermore, encrypted 3d comp can be created too. - -component_path = os.path.join(tempfile.gettempdir(), generate_unique_name("component_test") + ".aedbcomp") -hfss.modeler.create_3dcomponent(component_path, "patch_antenna") - -########################################################## -# Multiple project management -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# PyAEDT allows to control multiple projects, design and solution type at the same time. - -hfss2 = Hfss(project="new_project", design="new_design") - -########################################################## -# Insert of 3d component -# ~~~~~~~~~~~~~~~~~~~~~~ -# The 3d component can be inserted without any additional info. -# All needed info will be read from the file itself. - -hfss2.modeler.insert_3d_component(component_path) - -########################################################## -# 3D Component Parameters -# ~~~~~~~~~~~~~~~~~~~~~~~ -# All 3d Component parameters are available and can be parametrized. - -hfss2.modeler.user_defined_components["patch_antenna1"].parameters - -hfss2["p_thick"] = "1mm" - -hfss2.modeler.user_defined_components["patch_antenna1"].parameters["thick"]="p_thick" - -########################################################## -# Multiple 3d Components -# ~~~~~~~~~~~~~~~~~~~~~~ -# There is no limit to the number of 3D components that can be added on the same design. -# They can be the same or linked to different files. - -hfss2.modeler.create_coordinate_system(origin=[20, 20, 10], name="Second_antenna") - -ant2 = hfss2.modeler.insert_3d_component(component_path, coordinate_system="Second_antenna") - -########################################################## -# Move components -# ~~~~~~~~~~~~~~~ -# The component can be moved by changing is position or moving the relative coordinate system. - -hfss2.modeler.coordinate_systems[0].origin = [10, 10, 3] - -########################################################## -# Boundaries -# ~~~~~~~~~~ -# Most of HFSS boundaries and excitations are already available in PyAEDT. -# User can assign easily a boundary to a face or to an object by taking benefits of - -hfss2.modeler.create_air_region(30, 30, 30, 30, 30, 30) -hfss2.assign_radiation_boundary_to_faces(hfss2.modeler["Region"].faces) - -# Create Setup and Optimetrics -# Once project is ready to be solved, a setup and parametrics analysis can be created with PyAEDT. -# All setup parameters can be edited. - -setup1 = hfss2.create_setup() - -optim = hfss2.parametrics.add("p_thick", "0.2mm", "1.5mm", step=14) - -############################################################################### -# Save project -# ~~~~~~~~~~~~ -# Save the project. - -hfss2.modeler.fit_all() -hfss2.plot(show=False, output_file=os.path.join(hfss.working_directory, "Image.jpg"), plot_air_objects=True) - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.release_desktop` method. -# All methods provide for saving the project before closing AEDT. - -hfss2.save_project(os.path.join(tempfile.gettempdir(), generate_unique_name("parametrized") + ".aedt")) -hfss2.release_desktop() diff --git a/examples/02-HFSS/Flex_CPWG.py b/examples/02-HFSS/Flex_CPWG.py deleted file mode 100644 index c9d3cfb8ac1..00000000000 --- a/examples/02-HFSS/Flex_CPWG.py +++ /dev/null @@ -1,208 +0,0 @@ -""" -HFSS: flex cable CPWG ---------------------- -This example shows how you can use PyAEDT to create a flex cable CPWG (coplanar waveguide with ground). -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -from math import radians, sin, cos, sqrt -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Launch AEDT -# ~~~~~~~~~~~ -# Launch AEDT 2023 R2 in graphical mode. - -hfss = ansys.aedt.core.Hfss(version=aedt_version, - solution_type="DrivenTerminal", - new_desktop=True, - non_graphical=non_graphical) -hfss.change_material_override(True) -hfss.change_automatically_use_causal_materials(True) -hfss.create_open_region("100GHz") -hfss.modeler.model_units = "mil" -hfss.mesh.assign_initial_mesh_from_slider(applycurvilinear=True) - -############################################################################### -# Create variables -# ~~~~~~~~~~~~~~~~ -# Create input variables for creating the flex cable CPWG. - -total_length = 300 -theta = 120 -r = 100 -width = 3 -height = 0.1 -spacing = 1.53 -gnd_width = 10 -gnd_thickness = 2 - -xt = (total_length - r * radians(theta)) / 2 - - -############################################################################### -# Create bend -# ~~~~~~~~~~~ -# Create the bend. The ``create_bending`` method creates a list of points for -# the bend based on the curvature radius and extension. - -def create_bending(radius, extension=0): - position_list = [(-xt, 0, -radius), (0, 0, -radius)] - - for i in [radians(i) for i in range(theta)] + [radians(theta + 0.000000001)]: - position_list.append((radius * sin(i), 0, -radius * cos(i))) - - x1, y1, z1 = position_list[-1] - x0, y0, z0 = position_list[-2] - - scale = (xt + extension) / sqrt((x1 - x0) ** 2 + (z1 - z0) ** 2) - x, y, z = (x1 - x0) * scale + x0, 0, (z1 - z0) * scale + z0 - - position_list[-1] = (x, y, z) - return position_list - - -############################################################################### -# Draw signal line -# ~~~~~~~~~~~~~~~~ -# Draw a signal line to create a bent signal wire. - -position_list = create_bending(r, 1) -line = hfss.modeler.create_polyline(points=position_list, material="copper", xsection_type="Rectangle", - xsection_width=height, xsection_height=width) - -############################################################################### -# Draw ground line -# ~~~~~~~~~~~~~~~~ -# Draw a ground line to create two bent ground wires. - -gnd_r = [(x, spacing + width / 2 + gnd_width / 2, z) for x, y, z in position_list] -gnd_l = [(x, -y, z) for x, y, z in gnd_r] - -gnd_objs = [] -for gnd in [gnd_r, gnd_l]: - x = hfss.modeler.create_polyline(points=gnd, material="copper", xsection_type="Rectangle", xsection_width=height, - xsection_height=gnd_width) - x.color = (255, 0, 0) - gnd_objs.append(x) - -############################################################################### -# Draw dielectric -# ~~~~~~~~~~~~~~~ -# Draw a dielectric to create a dielectric cable. - -position_list = create_bending(r + (height + gnd_thickness) / 2) - -fr4 = hfss.modeler.create_polyline(points=position_list, material="FR4_epoxy", xsection_type="Rectangle", - xsection_width=gnd_thickness, xsection_height=width + 2 * spacing + 2 * gnd_width) - -############################################################################### -# Create bottom metals -# ~~~~~~~~~~~~~~~~~~~~ -# Create the bottom metals. - -position_list = create_bending(r + height + gnd_thickness, 1) - -bot = hfss.modeler.create_polyline(points=position_list, material="copper", xsection_type="Rectangle", - xsection_width=height, xsection_height=width + 2 * spacing + 2 * gnd_width) - -############################################################################### -# Create port interfaces -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create port interfaces (PEC enclosures). - -port_faces = [] -for face, blockname in zip([fr4.top_face_z, fr4.bottom_face_x], ["b1", "b2"]): - xc, yc, zc = face.center - positions = [i.position for i in face.vertices] - - port_sheet_list = [((x - xc) * 10 + xc, (y - yc) + yc, (z - zc) * 10 + zc) for x, y, z in positions] - s = hfss.modeler.create_polyline(port_sheet_list, cover_surface=True, close_surface=True) - center = [round(i, 6) for i in s.faces[0].center] - - port_block = hfss.modeler.thicken_sheet(s.name, -5) - port_block.name = blockname - for f in port_block.faces: - - if [round(i, 6) for i in f.center] == center: - port_faces.append(f) - - port_block.material_name = "PEC" - - for i in [line, bot] + gnd_objs: - i.subtract([port_block], True) - - print(port_faces) - -############################################################################### -# Create boundary condition -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Creates a Perfect E boundary condition. - -boundary = [] -for face in [fr4.top_face_y, fr4.bottom_face_y]: - s = hfss.modeler.create_object_from_face(face) - boundary.append(s) - hfss.assign_perfecte_to_sheets(s) - -############################################################################### -# Create ports -# ~~~~~~~~~~~~ -# Creates ports. - -for s, port_name in zip(port_faces, ["1", "2"]): - reference = [i.name for i in gnd_objs + boundary + [bot]] + ["b1", "b2"] - - hfss.wave_port(s.id, reference=reference, name=port_name) - -############################################################################### -# Create setup and sweep -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create the setup and sweep. - -setup = hfss.create_setup("setup1") -setup["Frequency"] = "2GHz" -setup.props["MaximumPasses"] = 10 -setup.props["MinimumConvergedPasses"] = 2 -hfss.create_linear_count_sweep(setup="setup1", units="GHz", start_frequency=1e-1, stop_frequency=4, - num_of_freq_points=101, name="sweep1", save_fields=False, sweep_type="Interpolating") - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model. - -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"), -) - -############################################################################### -# Analyze and release -# ~~~~~~~~~~~~~~~~~~~~ -# Uncomment the ``hfss.analyze`` command if you want to analyze the -# model and release AEDT. - -hfss.release_desktop() diff --git a/examples/02-HFSS/HFSS_Choke.py b/examples/02-HFSS/HFSS_Choke.py deleted file mode 100644 index cb8a66a1fa4..00000000000 --- a/examples/02-HFSS/HFSS_Choke.py +++ /dev/null @@ -1,228 +0,0 @@ -""" -HFSS: choke ------------ -This example shows how you can use PyAEDT to create a choke setup in HFSS. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import json -import os -import ansys.aedt.core -import tempfile - -########################################################################################### -# Create temporary directory -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create temporary directory. - -temp_dir = tempfile.TemporaryDirectory(suffix=".ansys") - -project_name = ansys.aedt.core.generate_unique_project_name(root_name=temp_dir.name, folder_name="choke", - project_name="choke") - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Launch HFSS -# ~~~~~~~~~~~ -# Launches HFSS 2023 R2 in graphical mode. - -hfss = ansys.aedt.core.Hfss(project=project_name, - version=aedt_version, - non_graphical=non_graphical, - new_desktop=True, - solution_type="Terminal") - -############################################################################### -# Rules and information of use -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# The dictionary values contain the different parameter values of the core and -# the windings that compose the choke. You must not change the main structure of -# the dictionary. The dictionary has many primary keys, including -# ``"Number of Windings"``, ``"Layer"``, and ``"Layer Type"``, that have -# dictionaries as values. The keys of these dictionaries are secondary keys -# of the dictionary values, such as ``"1"``, ``"2"``, ``"3"``, ``"4"``, and -# ``"Simple"``. -# -# You must not modify the primary or secondary keys. You can modify only their values. -# You must not change the data types for these keys. For the dictionaries from -# ``"Number of Windings"`` through ``"Wire Section"``, values must be Boolean. Only -# one value per dictionary can be ``True``. If all values are ``True``, only the first one -# remains set to ``True``. If all values are ``False``, the first value is chosen as the -# correct one by default. For the dictionaries from ``"Core"`` through ``"Inner Winding"``, -# values must be strings, floats, or integers. -# -# Descriptions follow for primary keys: -# -# - ``"Number of Windings"``: Number of windings around the core -# - ``"Layer"``: Number of layers of all windings -# - ``"Layer Type"``: Whether layers of a winding are linked to each other -# - ``"Similar Layer"``: Whether layers of a winding have the same number of turns and same spacing between turns -# - ``"Mode"``: When there are only two windows, whether they are in common or differential mode -# - ``"Wire Section"``: Type of wire section and number of segments -# - ``"Core"``: Design of the core -# - ``"Outer Winding"``: Design of the first layer or outer layer of a winding and the common parameters for all layers -# - ``"Mid Winding"``: Turns and turns spacing ("Coil Pit") for the second or mid layer if it is necessary -# - ``"Inner Winding"``: Turns and turns spacing ("Coil Pit") for the third or inner layer if it is necessary -# - ``"Occupation(%)"``: An informative parameter that is useless to modify -# -# The following parameter values work. You can modify them if you want. - -values = { - "Number of Windings": {"1": False, "2": True, "3": False, "4": False}, - "Layer": {"Simple": False, "Double": True, "Triple": False}, - "Layer Type": {"Separate": False, "Linked": True}, - "Similar Layer": {"Similar": False, "Different": True}, - "Mode": {"Differential": False, "Common": True}, - "Wire Section": {"None": False, "Hexagon": True, "Octagon": False, "Circle": False}, - "Core": { - "Name": "Core", - "Material": "ferrite", - "Inner Radius": 20, - "Outer Radius": 30, - "Height": 10, - "Chamfer": 0.8, - }, - "Outer Winding": { - "Name": "Winding", - "Material": "copper", - "Inner Radius": 20, - "Outer Radius": 30, - "Height": 10, - "Wire Diameter": 1.5, - "Turns": 20, - "Coil Pit(deg)": 0.1, - "Occupation(%)": 0, - }, - "Mid Winding": {"Turns": 25, "Coil Pit(deg)": 0.1, "Occupation(%)": 0}, - "Inner Winding": {"Turns": 4, "Coil Pit(deg)": 0.1, "Occupation(%)": 0}, -} - -############################################################################### -# Convert dictionary to JSON file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Convert a dictionary to a JSON file. You must supply the path of the -# JSON file as an argument. - -json_path = os.path.join(hfss.working_directory, "choke_example.json") - -with open(json_path, "w") as outfile: - json.dump(values, outfile) - -############################################################################### -# Verify parameters of JSON file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Verify parameters of the JSON file. The ``check_choke_values`` method takes -# the JSON file path as an argument and does the following: -# -# - Checks if the JSON file is correctly written (as explained in the rules) -# - Checks in equations on windings parameters to avoid having unintended intersections - -dictionary_values = hfss.modeler.check_choke_values(json_path, create_another_file=False) -print(dictionary_values) - -############################################################################### -# Create choke -# ~~~~~~~~~~~~ -# Create the choke. The ``create_choke`` method takes the JSON file path as an -# argument. - -list_object = hfss.modeler.create_choke(json_path) -print(list_object) -core = list_object[1] -first_winding_list = list_object[2] -second_winding_list = list_object[3] - -############################################################################### -# Create ground -# ~~~~~~~~~~~~~ -# Create a ground. - -ground_radius = 1.2 * dictionary_values[1]["Outer Winding"]["Outer Radius"] -ground_position = [0, 0, first_winding_list[1][0][2] - 2] -ground = hfss.modeler.create_circle("XY", ground_position, ground_radius, name="GND", material="copper") -coat = hfss.assign_coating(ground, is_infinite_ground=True) - -############################################################################### -# Create lumped ports -# ~~~~~~~~~~~~~~~~~~~ -# Create lumped ports. - -port_position_list = [ - [first_winding_list[1][0][0], first_winding_list[1][0][1], first_winding_list[1][0][2] - 1], - [first_winding_list[1][-1][0], first_winding_list[1][-1][1], first_winding_list[1][-1][2] - 1], - [second_winding_list[1][0][0], second_winding_list[1][0][1], second_winding_list[1][0][2] - 1], - [second_winding_list[1][-1][0], second_winding_list[1][-1][1], second_winding_list[1][-1][2] - 1], -] -port_dimension_list = [2, dictionary_values[1]["Outer Winding"]["Wire Diameter"]] -for position in port_position_list: - sheet = hfss.modeler.create_rectangle("XZ", position, port_dimension_list, name="sheet_port") - sheet.move([-dictionary_values[1]["Outer Winding"]["Wire Diameter"] / 2, 0, -1]) - hfss.lumped_port(assignment=sheet.name, reference=[ground], - name="port_" + str(port_position_list.index(position) + 1)) - -############################################################################### -# Create mesh -# ~~~~~~~~~~~ -# Create the mesh. - -cylinder_height = 2.5 * dictionary_values[1]["Outer Winding"]["Height"] -cylinder_position = [0, 0, first_winding_list[1][0][2] - 4] -mesh_operation_cylinder = hfss.modeler.create_cylinder("XY", cylinder_position, ground_radius, cylinder_height, - num_sides=36, name="mesh_cylinder") -hfss.mesh.assign_length_mesh([mesh_operation_cylinder], maximum_length=15, maximum_elements=None, name="choke_mesh") - - -############################################################################### -# Create boundaries -# ~~~~~~~~~~~~~~~~~ -# Create the boundaries. A region with openings is needed to run the analysis. - -region = hfss.modeler.create_region(pad_percent=1000) - - -############################################################################### -# Create setup -# ~~~~~~~~~~~~ -# Create a setup with a sweep to run the simulation. Depending on your machine's -# computing power, the simulation can take some time to run. - -setup = hfss.create_setup("MySetup") -setup.props["Frequency"] = "50MHz" -setup["MaximumPasses"] = 10 -hfss.create_linear_count_sweep(setup=setup.name, units="MHz", start_frequency=0.1, stop_frequency=100, - num_of_freq_points=100, name="sweep1", save_fields=False, sweep_type="Interpolating") - -############################################################################### -# Save project -# ~~~~~~~~~~~~ -# Save the project. - -hfss.modeler.fit_all() -hfss.plot(show=False, output_file=os.path.join(hfss.working_directory, "Image.jpg"), plot_air_objects=True) - - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.release_desktop` method. -# All methods provide for saving the project before closing. - -hfss.release_desktop() diff --git a/examples/02-HFSS/HFSS_Dipole.py b/examples/02-HFSS/HFSS_Dipole.py deleted file mode 100644 index 60e65ae529d..00000000000 --- a/examples/02-HFSS/HFSS_Dipole.py +++ /dev/null @@ -1,220 +0,0 @@ -""" -HFSS: dipole antenna --------------------- -This example shows how you can use PyAEDT to create a dipole antenna in HFSS and postprocess results. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core - -project_name = ansys.aedt.core.generate_unique_project_name(project_name="dipole") - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. ` -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Launch AEDT -# ~~~~~~~~~~~ -# Launch AEDT 2023 R2 in graphical mode. - -d = ansys.aedt.core.launch_desktop(aedt_version, non_graphical=non_graphical, new_desktop=True) - -############################################################################### -# Launch HFSS -# ~~~~~~~~~~~ -# Launch HFSS 2023 R2 in graphical mode. - -hfss = ansys.aedt.core.Hfss(project=project_name, solution_type="Modal") - -############################################################################### -# Define variable -# ~~~~~~~~~~~~~~~ -# Define a variable for the dipole length. - -hfss["l_dipole"] = "13.5cm" - -############################################################################### -# Get 3D component from system library -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Get a 3D component from the ``syslib`` directory. For this example to run -# correctly, you must get all geometry parameters of the 3D component or, in -# case of an encrypted 3D component, create a dictionary of the parameters. - -compfile = hfss.components3d["Dipole_Antenna_DM"] -geometryparams = hfss.get_components3d_vars("Dipole_Antenna_DM") -geometryparams["dipole_length"] = "l_dipole" -hfss.modeler.insert_3d_component(compfile, geometryparams) - -############################################################################### -# Create boundaries -# ~~~~~~~~~~~~~~~~~ -# Create boundaries. A region with openings is needed to run the analysis. - -hfss.create_open_region(frequency="1GHz") - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model. - -my_plot = hfss.plot(show=False, plot_air_objects=False) -my_plot.show_axes = False -my_plot.show_grid = False -my_plot.isometric_view = False -my_plot.plot( - os.path.join(hfss.working_directory, "Image.jpg"), -) - -############################################################################### -# Create setup -# ~~~~~~~~~~~~ -# Create a setup with a sweep to run the simulation. - -setup = hfss.create_setup("MySetup") -setup.props["Frequency"] = "1GHz" -setup.props["MaximumPasses"] = 1 -hfss.create_linear_count_sweep(setup=setup.name, units="GHz", start_frequency=0.5, stop_frequency=1.5, - num_of_freq_points=251, name="sweep1", save_fields=False, sweep_type="Interpolating", - interpolation_tol=3, interpolation_max_solutions=255) - -############################################################################### -# Save and run simulation -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Save and run the simulation. - -hfss.analyze_setup("MySetup") - -############################################################################### -# Create scattering plot and far fields report -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a scattering plot and a far fields report. - -hfss.create_scattering("MyScattering") -variations = hfss.available_variations.nominal_w_values_dict -variations["Freq"] = ["1GHz"] -variations["Theta"] = ["All"] -variations["Phi"] = ["All"] -hfss.post.create_report("db(GainTotal)", hfss.nominal_adaptive, variations, primary_sweep_variable="Theta", - report_category="Far Fields", context="3D") - -############################################################################### -# Create far fields report using report objects -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a far fields report using the ``report_by_category.far field`` method, -# which gives you more freedom. - -new_report = hfss.post.reports_by_category.far_field("db(RealizedGainTotal)", hfss.nominal_adaptive, "3D") -new_report.variations = variations -new_report.primary_sweep = "Theta" -new_report.create("Realized2D") - -############################################################################### -# Generate multiple plots -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Generate multiple plots using the object ``new_report``. This code generates -# 2D and 3D polar plots. - -new_report.report_type = "3D Polar Plot" -new_report.secondary_sweep = "Phi" -new_report.create("Realized3D") - -############################################################################### -# Get solution data -# ~~~~~~~~~~~~~~~~~ -# Get solution data using the object ``new_report``` and postprocess or plot the -# data outside AEDT. - -solution_data = new_report.get_solution_data() -solution_data.plot() - -############################################################################### -# Generate far field plot -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Generate a far field plot by creating a postprocessing variable and assigning -# it to a new coordinate system. You can use the ``post`` prefix to create a -# postprocessing variable directly from a setter, or you can use the ``set_variable`` -# method with an arbitrary name. - -hfss["post_x"] = 2 -hfss.variable_manager.set_variable(name="y_post", expression=1, is_post_processing=True) -hfss.modeler.create_coordinate_system(origin=["post_x", "y_post", 0], name="CS_Post") -hfss.insert_infinite_sphere(custom_coordinate_system="CS_Post", name="Sphere_Custom") - -############################################################################### -# Get solution data -# ~~~~~~~~~~~~~~~~~ -# Get solution data. You can use this code to generate the same plot outside AEDT. - -new_report = hfss.post.reports_by_category.far_field("GainTotal", hfss.nominal_adaptive, "3D") -new_report.primary_sweep = "Theta" -new_report.far_field_sphere = "3D" -solutions = new_report.get_solution_data() - -############################################################################### -# Generate 3D plot using Matplotlib -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Generate a 3D plot using Matplotlib. - -solutions.plot_3d() - -############################################################################### -# Generate 3D far fields plot using Matplotlib -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Generate a far fields plot using Matplotlib. - -new_report.far_field_sphere = "Sphere_Custom" -solutions_custom = new_report.get_solution_data() -solutions_custom.plot_3d() - -############################################################################### -# Generate 2D plot using Matplotlib -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Generate a 2D plot using Matplotlib where you specify whether it is a polar -# plot or a rectangular plot. - -solutions.plot() - -########################################################## -# Get far field data -# ~~~~~~~~~~~~~~~~~~ -# Get far field data. After the simulation completes, the far -# field data is generated port by port and stored in a data class, , user can use this data -# once AEDT is released. - -ffdata = hfss.get_antenna_data(frequencies=["1000MHz"], setup=hfss.nominal_adaptive, - sphere="Sphere_Custom") - -########################################################## -# Generate 2D cutout plot -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Generate 2D cutout plot. You can define the Theta scan -# and Phi scan. - -ffdata.farfield_data.plot_cut(quantity='RealizedGain', primary_sweep="theta", secondary_sweep_value=0, title='FarField', - quantity_format="dB20", is_polar=True) - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.release_desktop` method. -# All methods provide for saving the project before closing. - -d.release_desktop() diff --git a/examples/02-HFSS/HFSS_FSS_unitcell.py b/examples/02-HFSS/HFSS_FSS_unitcell.py deleted file mode 100644 index a06a97de399..00000000000 --- a/examples/02-HFSS/HFSS_FSS_unitcell.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -HFSS: FSS Unitcell Simulation --------------------- -This example shows how you can use PyAEDT to create a FSS unitcell simulations in HFSS and postprocess results. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core - -project_name = ansys.aedt.core.generate_unique_project_name(project_name="FSS") - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. ` -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Launch AEDT -# ~~~~~~~~~~~ -# Launch AEDT 2023 R2 in graphical mode. - -d = ansys.aedt.core.launch_desktop(aedt_version, non_graphical=non_graphical, new_desktop=True) - -############################################################################### -# Launch HFSS -# ~~~~~~~~~~~ -# Launch HFSS 2023 R2 in graphical mode. - -hfss = ansys.aedt.core.Hfss(project=project_name, solution_type="Modal") - -############################################################################### -# Define variable -# ~~~~~~~~~~~~~~~ -# Define a variable for the 3D-component. - -hfss["patch_dim"] = "10mm" - -############################################################################### -# Insert 3D component from system library -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Download the 3D component from the example data and insert the 3D Component. - -unitcell_3d_component_path = ansys.aedt.core.downloads.download_FSS_3dcomponent() -unitcell_path = os.path.join(unitcell_3d_component_path, "FSS_unitcell_23R2.a3dcomp") - -comp = hfss.modeler.insert_3d_component(unitcell_path) - -############################################################################### -# Assign design parameter to 3D Component parameter -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign parameter. - -component_name = hfss.modeler.user_defined_component_names -comp.parameters["a"] = "patch_dim" - -############################################################################### -# Create air region -# ~~~~~~~~~~~~~~~~~ -# Create an open region along +Z direction for unitcell analysis. - -bounding_dimensions = hfss.modeler.get_bounding_dimension() - -periodicity_x = bounding_dimensions[0] -periodicity_y = bounding_dimensions[1] - -region = hfss.modeler.create_air_region( - z_pos=10 * bounding_dimensions[2], - is_percentage=False, - ) - -[x_min, y_min, z_min, x_max, y_max, z_max] = region.bounding_box - -############################################################################### -# Assign Lattice pair boundary -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assigning lattice pair boundary automatically detected. - -hfss.auto_assign_lattice_pairs(assignment=region.name) - -############################################################################### -# Assign Floquet port excitation along +Z direction -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign Floquet port. - -id_z_pos = region.top_face_z -hfss.create_floquet_port(id_z_pos, [0, 0, z_max], [0, y_max, z_max], [x_max, 0, z_max], name='port_z_max', - deembed_distance=10 * bounding_dimensions[2]) - - -############################################################################### -# Create setup -# ~~~~~~~~~~~~ -# Create a setup with a sweep to run the simulation. - -setup = hfss.create_setup("MySetup") -setup.props["Frequency"] = "10GHz" -setup.props["MaximumPasses"] = 10 -hfss.create_linear_count_sweep(setup=setup.name, units="GHz", start_frequency=6, stop_frequency=15, - num_of_freq_points=51, name="sweep1", save_fields=False, sweep_type="Interpolating", - interpolation_tol=6) - -############################################################################### -# Create S-parameter report using report objects -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create S-parameter reports using create report. - -all_quantities = hfss.post.available_report_quantities() -str_mag = [] -str_ang = [] - -variation = {"Freq": ["All"]} - -for i in all_quantities: - str_mag.append("mag(" + i + ")") - str_ang.append("ang_deg(" + i + ")") - -hfss.post.create_report(expressions=str_mag, variations=variation, plot_name="magnitude_plot") -hfss.post.create_report(expressions=str_ang, variations=variation, plot_name="phase_plot") - -############################################################################### -# Save and run simulation -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Save and run the simulation. Uncomment the line following line to run the analysis. - -# hfss.analyze() - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.release_desktop` method. -# All methods provide for saving the project before closing. - -hfss.release_desktop() diff --git a/examples/02-HFSS/HFSS_Spiral.py b/examples/02-HFSS/HFSS_Spiral.py deleted file mode 100644 index 52fbf78d76a..00000000000 --- a/examples/02-HFSS/HFSS_Spiral.py +++ /dev/null @@ -1,205 +0,0 @@ -""" -HFSS: spiral inductor ---------------------- -This example shows how you can use PyAEDT to create a spiral inductor, solve it, and plot results. -""" - -############################################################# -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core - -project_name = ansys.aedt.core.generate_unique_project_name(project_name="spiral") - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################# -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################# -# Launch HFSS -# ~~~~~~~~~~~ -# Launch HFSS 2023 R2 in non-graphical mode and change the -# units to microns. - -hfss = ansys.aedt.core.Hfss(version=aedt_version, non_graphical=non_graphical, design="A1", - new_desktop=True) -hfss.solution_type = "Modal" -hfss.modeler.model_units = "um" -p = hfss.modeler - -############################################################# -# Define variables -# ~~~~~~~~~~~~~~~~ -# Define input variables. You can use the values that follow or edit -# them. - -rin = 10 -width = 2 -spacing = 1 -thickness = 1 -Np = 8 -Nr = 10 -gap = 3 -hfss["Tsub"] = "6" + hfss.modeler.model_units - - -############################################################# -# Standardize polyline -# ~~~~~~~~~~~~~~~~~~~~ -# Standardize the polyline using the ``create_line`` method to fix -# the width, thickness, and material. - -def create_line(pts): - p.create_polyline(pts, material="copper", xsection_type="Rectangle", xsection_width=width, - xsection_height=thickness) - - -################################################################ -# Create spiral inductor -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create the spiral inductor. This spiral inductor is not -# parametric, but you could parametrize it later. - -ind = hfss.modeler.create_spiral( - internal_radius=rin, - width=width, - spacing=spacing, - turns=Nr, - faces=Np, - thickness=thickness, - material="copper", - name="Inductor1", -) - -################################################################ -# Center return path -# ~~~~~~~~~~~~~~~~~~ -# Center the return path. - -x0, y0, z0 = ind.points[0] -x1, y1, z1 = ind.points[-1] -create_line([(x0 - width / 2, y0, -gap), (abs(x1) + 5, y0, -gap)]) -p.create_box([x0 - width / 2, y0 - width / 2, -gap - thickness / 2], [width, width, gap + thickness], material="copper") - -################################################################ -# Create port 1 -# ~~~~~~~~~~~~~ -# Create port 1. - -p.create_rectangle(orientation=ansys.aedt.core.constants.PLANE.YZ, - origin=[abs(x1) + 5, y0 - width / 2, -gap - thickness / 2], - sizes=[width, "-Tsub+{}{}".format(gap, hfss.modeler.model_units)], - name="port1" - ) -hfss.lumped_port(assignment="port1", integration_line=ansys.aedt.core.constants.AXIS.Z) - -################################################################ -# Create port 2 -# ~~~~~~~~~~~~~ -# Create port 2. - -create_line([(x1 + width / 2, y1, 0), (x1 - 5, y1, 0)]) -p.create_rectangle(ansys.aedt.core.constants.PLANE.YZ, [x1 - 5, y1 - width / 2, -thickness / 2], - [width, "-Tsub"], - name="port2") -hfss.lumped_port(assignment="port2", integration_line=ansys.aedt.core.constants.AXIS.Z) - -################################################################ -# Create silicon substrate and ground plane -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the silicon substrate and the ground plane. - -p.create_box([x1 - 20, x1 - 20, "-Tsub-{}{}/2".format(thickness, hfss.modeler.model_units)], - [-2 * x1 + 40, -2 * x1 + 40, "Tsub"], material="silicon") - -p.create_box([x1 - 20, x1 - 20, "-Tsub-{}{}/2".format(thickness, hfss.modeler.model_units)], - [-2 * x1 + 40, -2 * x1 + 40, -0.1], material="PEC") - -################################################################ -# Assign airbox and radiation -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign the airbox and the radiation. - -box = p.create_box( - [x1 - 20, x1 - 20, "-Tsub-{}{}/2 - 0.1{}".format(thickness, hfss.modeler.model_units, hfss.modeler.model_units)], - [-2 * x1 + 40, -2 * x1 + 40, 100], name="airbox", material="air") - -hfss.assign_radiation_boundary_to_objects("airbox") - -################################################################ -# Assign material override -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign a material override so that the validation check does -# not fail. - -hfss.change_material_override() - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model. - -hfss.plot(show=False, output_file=os.path.join(hfss.working_directory, "Image.jpg"), plot_air_objects=False) - -################################################################ -# Create setup -# ~~~~~~~~~~~~ -# Create the setup and define a frequency sweep to solve the project. - -setup1 = hfss.create_setup(name="setup1") -setup1.props["Frequency"] = "10GHz" -hfss.create_linear_count_sweep(setup="setup1", units="GHz", start_frequency=1e-3, stop_frequency=50, - num_of_freq_points=451, sweep_type="Interpolating") -hfss.save_project() -hfss.analyze() - -################################################################ -# Get report data -# ~~~~~~~~~~~~~~~ -# Get report data and use the following formulas to calculate -# the inductance and quality factor. - -L_formula = "1e9*im(1/Y(1,1))/(2*pi*freq)" -Q_formula = "im(Y(1,1))/re(Y(1,1))" - -################################################################ -# Create output variable -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create output variable -hfss.create_output_variable("L", L_formula, solution="setup1 : LastAdaptive") - -################################################################ -# Plot calculated values in Matplotlib -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Plot the calculated values in Matplotlib. - -data = hfss.post.get_solution_data([L_formula, Q_formula]) -data.plot(curves=[L_formula, Q_formula], formula="re", x_label="Freq", y_label="L and Q") - -################################################################ -# Export results to csv file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Export results to csv file -data.export_data_to_csv(os.path.join(hfss.toolkit_directory, "output.csv")) - -################################################################ -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Save the project and close AEDT. - -hfss.save_project(project_name) -hfss.release_desktop() diff --git a/examples/02-HFSS/HFSS_eigenmode.py b/examples/02-HFSS/HFSS_eigenmode.py deleted file mode 100644 index 0c931041605..00000000000 --- a/examples/02-HFSS/HFSS_eigenmode.py +++ /dev/null @@ -1,162 +0,0 @@ -""" -HFSS: Eigenmode filter ----------------------- -This example shows how you can use PyAEDT to automate the eigenmode solver in HFSS. -Eigenmode analysis can be applied to open, radiating structures -using an absorbing boundary condition. This type of analysis is useful for -determining the resonant frequency of a geometry or an antenna and can be used to refine -the mesh at the resonance, even when the resonant frequency of the antenna is not known. - -The challenge posed by this method is to identify and filter the non-physical modes -resulting from reflection from boundaries of the main domain. -Because the Eigenmode solver sorts by frequency and does not filter on the -quality factor, these virtual modes are present when the eigenmode approach is -applied to nominally open structures. -When looking for resonant modes over a wide frequency range for nominally -enclosed structures, several iterations may be required because the minimum frequency -is determined manually and simulations re-run until the complete frequency range is covered -and all important physical modes are calculated. - -The following script finds the physical modes of a model in a wide frequency range by automating the solution setup. -During each simulation, a user-defined number of modes is simulated, and the modes with a Q higher than a user- defined value are filtered. -The next simulation automatically continues to find modes having a frequency higher than the last mode of the previous analysis. -This continues until the maximum frequency in the desired range is achieved. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Run through each cell. This cell imports the required packages. - -import sys -import os -import ansys.aedt.core - -# Create a temporary folder to download the example to. - -temp_folder = ansys.aedt.core.generate_unique_folder_name() -project_path = ansys.aedt.core.downloads.download_file("eigenmode", "emi_PCB_house.aedt", temp_folder) - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Launch AEDT -# ~~~~~~~~~~~ -# Launch AEDT 2023 R2 in graphical mode. - -d = ansys.aedt.core.launch_desktop(aedt_version, non_graphical=non_graphical, new_desktop=True) - -############################################################################### -# Launch HFSS -# ~~~~~~~~~~~ -# Launch HFSS 2023 R2 in graphical mode. - -hfss = ansys.aedt.core.Hfss(project=project_path, non_graphical=non_graphical) - -############################################################################### -# Input parameters for eigenmode solver -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# The geometry and material should be already set. The analyses are generated by the code. -# Number of modes during each analysis, max allowed number is 20. -# Entering a number higher than 10 might need long simulation time as the -# eigenmode solver needs to converge on modes. ``fmin`` is the lowest frequency -# of interest. ``fmax`` is the highest frequency of interest. -# ``limit`` is the parameter limit that determines which modes are ignored. - -num_modes = 6 -fmin = 1 -fmax = 2 -next_fmin = fmin -setup_nr = 1 - -limit = 10 -resonance = {} - - -############################################################################### -# Find the modes -# ~~~~~~~~~~~~~~ -# The following cell is a function. If called, it creates an eigenmode setup and solves it. -# After the solve, each mode, along with its corresponding real frequency and quality factor, -# are saved for further processing. - -def find_resonance(): - # setup creation - next_min_freq = str(next_fmin) + " GHz" - setup_name = "em_setup" + str(setup_nr) - setup = hfss.create_setup(setup_name) - setup.props['MinimumFrequency'] = next_min_freq - setup.props['NumModes'] = num_modes - setup.props['ConvergeOnRealFreq'] = True - setup.props['MaximumPasses'] = 10 - setup.props['MinimumPasses'] = 3 - setup.props['MaxDeltaFreq'] = 5 - # analyzing the eigenmode setup - hfss.analyze_setup(setup_name, cores=8, use_auto_settings=True) - # getting the Q and real frequency of each mode - eigen_q = hfss.post.available_report_quantities(quantities_category="Eigen Q") - eigen_mode = hfss.post.available_report_quantities() - data = {} - cont = 0 - for i in eigen_mode: - eigen_q_value = hfss.post.get_solution_data(expressions=eigen_q[cont], - setup_sweep_name=setup_name + ' : LastAdaptive', - report_category="Eigenmode") - eigen_mode_value = hfss.post.get_solution_data(expressions=eigen_mode[cont], - setup_sweep_name=setup_name + ' : LastAdaptive', - report_category="Eigenmode") - data[cont] = [eigen_q_value.data_real()[0], eigen_mode_value.data_real()[0]] - cont += 1 - - print(data) - return data - - -############################################################################### -# Automate eigenmode solution -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Running the next cell calls the resonance function and saves only those modes with a Q higher than the defined -# limit. The ``find_resonance`` function is called until the complete frequency range is covered. -# When the automation ends, the physical modes in the whole frequency range are reported. - -while next_fmin < fmax: - output = find_resonance() - next_fmin = output[len(output) - 1][1] / 1e9 - setup_nr += 1 - cont_res = len(resonance) - for q in output: - if output[q][0] > limit: - resonance[cont_res] = output[q] - cont_res += 1 - -resonance_frequencies = [f"{resonance[i][1] / 1e9:.5} GHz" for i in resonance] -print(str(resonance_frequencies)) - -############################################################################### -# Save project -# ~~~~~~~~~~~~ -# Save the project. - -hfss.modeler.fit_all() -hfss.plot(show=False, output_file=os.path.join(hfss.working_directory, "Image.jpg"), plot_air_objects=False) - -############################################################################### -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Save the project and close AEDT. - -hfss.save_project() -hfss.release_desktop() diff --git a/examples/02-HFSS/Probe_Fed_Patch.py b/examples/02-HFSS/Probe_Fed_Patch.py deleted file mode 100644 index 57eda273244..00000000000 --- a/examples/02-HFSS/Probe_Fed_Patch.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -HFSS: Probe-fed patch antenna ---------------------------------------------------------- -This example shows how to use the ``Stackup3D`` class -to create and analyze a patch antenna in HFSS. - -Note that the HFSS 3D Layout interface may offer advantages for -laminate structures such as the patch antenna. -""" - -########################### -# Perform imports -# ~~~~~~~~~~~~~~~~~~ - -import os - -import ansys.aedt.core -import tempfile -from ansys.aedt.core.modeler.advanced_cad.stackup_3d import Stackup3D - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. ``"PYAEDT_NON_GRAPHICAL"`` is set to ``False`` -# to create this documentation. -# -# You can set ``non_graphical`` to ``True`` to view -# HFSS while the notebook cells are executed. - -non_graphical = False -length_units = "mm" -freq_units = "GHz" - -######################################################## -# Create temporary working folder -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Use tempfile to create a temporary working folder. Project data -# is deleted after this example is run. -# -# To save the project data in another location, change -# the location of the project directory. -# - -# tmpdir.cleanup() at the end of this notebook removes all -# project files and data. - -tmpdir = tempfile.TemporaryDirectory(suffix="_aedt") -project_folder = tmpdir.name -proj_name = os.path.join(project_folder, "antenna") - -##################### -# Launch HFSS -# ----------- -# - -hfss = ansys.aedt.core.Hfss(project=proj_name, - solution_type="Terminal", - design="patch", - non_graphical=non_graphical, - new_desktop=True, - version=aedt_version) - -hfss.modeler.model_units = length_units - -##################################### -# Create patch -# ------------ -# Create the patch. -# - -stackup = Stackup3D(hfss) -ground = stackup.add_ground_layer("ground", material="copper", thickness=0.035, fill_material="air") -dielectric = stackup.add_dielectric_layer("dielectric", thickness="0.5" + length_units, material="Duroid (tm)") -signal = stackup.add_signal_layer("signal", material="copper", thickness=0.035, fill_material="air") -patch = signal.add_patch(patch_length=9.57, patch_width=9.25, - patch_name="Patch", frequency=1E10) - -stackup.resize_around_element(patch) -pad_length = [3, 3, 3, 3, 3, 3] # Air bounding box buffer in mm. -region = hfss.modeler.create_region(pad_length, is_percentage=False) -hfss.assign_radiation_boundary_to_objects(region.name) - -patch.create_probe_port(ground, rel_x_offset=0.485) -setup = hfss.create_setup(name="Setup1", - setup_type="HFSSDriven", - Frequency="10GHz") - -setup.create_frequency_sweep(unit="GHz", - name="Sweep1", - start_frequency=8, - stop_frequency=12, - sweep_type="Interpolating") - -hfss.save_project() -hfss.analyze() - -############################### -# Plot S11 -# --------- - -plot_data = hfss.get_traces_for_plot() -report = hfss.post.create_report(plot_data) -solution = report.get_solution_data() -plt = solution.plot(solution.expressions) - -############################################################################### -# Release AEDT -# ------------ -# Release AEDT and clean up temporary folders and files. - -hfss.release_desktop() -tmpdir.cleanup() diff --git a/examples/02-HFSS/Readme.txt b/examples/02-HFSS/Readme.txt deleted file mode 100644 index 9f03c21f896..00000000000 --- a/examples/02-HFSS/Readme.txt +++ /dev/null @@ -1,4 +0,0 @@ -HFSS examples -~~~~~~~~~~~~~ -These examples use PyAEDT to show some end-to-end workflows for HFSS 3D. -This includes model generation, setup, meshing, and postprocessing. diff --git a/examples/02-HFSS/Waveguide_Filter.py b/examples/02-HFSS/Waveguide_Filter.py deleted file mode 100644 index 4ac4aa249d2..00000000000 --- a/examples/02-HFSS/Waveguide_Filter.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -HFSS: Inductive Iris waveguide filter -------------------------------------- -This example shows how to build and analyze a 4-pole -X-Band waveguide filter using inductive irises. - -""" - -# sphinx_gallery_thumbnail_path = 'Resources/wgf.png' - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. -# - -import os -import tempfile -import ansys.aedt.core -from ansys.aedt.core import general_methods - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Launch Ansys Electronics Desktop (AEDT) -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# - -############################################################################### -# Define parameters and values for waveguide iris filter -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# l: Length of the cavity from the mid-point of one iris -# to the midpoint of the next iris. -# w: Width of the iris opening. -# a: Long dimension of the waveguide cross-section (X-Band) -# b: Short dimension of the waveguide cross-section. -# t: Metal thickness of the iris insert. - -wgparams = {'l': [0.7428, 0.82188], - 'w': [0.50013, 0.3642, 0.3458], - 'a': 0.4, - 'b': 0.9, - 't': 0.15, - 'units': 'in'} - -non_graphical = False -new_thread = True - -############################################################################### -# Save the project and results in the TEMP folder -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -project_folder = os.path.join(tempfile.gettempdir(), "waveguide_example") -if not os.path.exists(project_folder): - os.mkdir(project_folder) -project_name = os.path.join(project_folder, general_methods.generate_unique_name("wgf", n=2)) - -# Instantiate the HFSS application -hfss = ansys.aedt.core.Hfss(project=project_name + '.aedt', - version=aedt_version, - design="filter", - non_graphical=non_graphical, - new_desktop=True, - close_on_exit=True, - solution_type="Modal") - -# hfss.settings.enable_debug_methods_argument_logger = False # Only for debugging. - -var_mapping = dict() # Used by parse_expr to parse expressions. - -############################################################################### -# Initialize design parameters in HFSS. -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -hfss.modeler.model_units = "in" # Set to inches -for key in wgparams: - if type(wgparams[key]) in [int, float]: - hfss[key] = str(wgparams[key]) + wgparams['units'] - var_mapping[key] = wgparams[key] # Used for expression parsing - elif type(wgparams[key]) == list: - count = 1 - for v in wgparams[key]: - this_key = key + str(count) - hfss[this_key] = str(v) + wgparams['units'] - var_mapping[this_key] = v # Used to parse expressions and generate numerical values. - count += 1 - -if len(wgparams['l']) % 2 == 0: - zstart = "-t/2" # Even number of cavities, odd number of irises. - is_even = True -else: - zstart = "l1/2 - t/2" # Odd number of cavities, even number of irises. - is_even = False - - -############################################################################### -# Draw parametric waveguide filter -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define a function to place each iris at the correct longitudinal (z) position, -# Loop from the largest index (interior of the filter) to 1, which is the first -# iris nearest the waveguide ports. - -def place_iris(zpos, dz, n): - w_str = "w" + str(n) # Iris width parameter as a string. - this_name = "iris_a_" + str(n) # Iris object name in the HFSS project. - iris = [] # Return a list of the two objects that make up the iris. - if this_name in hfss.modeler.object_names: - this_name = this_name.replace("a", "c") - iris.append(hfss.modeler.create_box(origin=['-b/2', '-a/2', zpos], - sizes=['(b - ' + w_str + ')/2', 'a', dz], - name=this_name, - material="silver")) - iris.append(iris[0].mirror([0, 0, 0], [1, 0, 0], duplicate=True)) - return iris - - -############################################################################### -# Place irises -# ~~~~~~~~~~~~ -# Place the irises from inner (highest integer) to outer. - -for count in reversed(range(1, len(wgparams['w']) + 1)): - if count < len(wgparams['w']): # Update zpos - zpos = zpos + "".join([" + l" + str(count) + " + "])[:-3] - iris = place_iris(zpos, "t", count) - iris = place_iris("-(" + zpos + ")", "-t", count) - - else: # Place first iris - zpos = zstart - iris = place_iris(zpos, "t", count) - if not is_even: - iris = place_iris("-(" + zpos + ")", "-t", count) - -############################################################################### -# Draw full waveguide with ports -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Use ``hfss.variable_manager`` which acts like a dict() to return an instance of -# the ``ansys.aedt.core.application.variables.VariableManager`` class for any variable. -# The ``VariableManager`` instance takes the HFSS variable name as a key. -# ``VariableManager`` properties enable access to update, modify and -# evaluate variables. - -var_mapping['port_extension'] = 1.5 * wgparams['l'][0] -hfss['port_extension'] = str(var_mapping['port_extension']) + wgparams['units'] -hfss["wg_z_start"] = "-(" + zpos + ") - port_extension" -hfss["wg_length"] = "2*(" + zpos + " + port_extension )" -wg_z_start = hfss.variable_manager["wg_z_start"] -wg_length = hfss.variable_manager["wg_length"] -hfss["u_start"] = "-a/2" -hfss["u_end"] = "a/2" -hfss.modeler.create_box(origin=["-b/2", "-a/2", "wg_z_start"], - sizes=["b", "a", "wg_length"], - name="waveguide", - material="vacuum") - -############################################################################### -# Draw the whole waveguide. -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# wg_z is the total length of the waveguide, including port extension. -# Note that the ``.evaluated_value`` provides access to the numerical value of -# ``wg_z_start`` which is an expression in HFSS. - -wg_z = [wg_z_start.evaluated_value, hfss.value_with_units(wg_z_start.numeric_value + wg_length.numeric_value, "in")] - -############################################################################### -# Assign wave ports to the end faces of the waveguid -# and define the calibration lines to ensure self-consistent -# polarization between wave ports. -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -count = 0 -ports = [] -for n, z in enumerate(wg_z): - face_id = hfss.modeler.get_faceid_from_position(position=[0, 0, z], assignment="waveguide") - u_start = [0, hfss.variable_manager["u_start"].evaluated_value, z] - u_end = [0, hfss.variable_manager["u_end"].evaluated_value, z] - - ports.append(hfss.wave_port(face_id, integration_line=[u_start, u_end], name="P" + str(n + 1), renormalize=False)) - -############################################################################### -# Insert the mesh adaptation setup using refinement at two frequencies. -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# This approach is useful for resonant structures as the coarse initial -# mesh impacts the resonant frequency and hence, the field propagation through the -# filter. Adaptation at multiple frequencies helps to ensure that energy propagates -# through the resonant structure while the mesh is refined. - -setup = hfss.create_setup("Setup1", setuptype="HFSSDriven", - MultipleAdaptiveFreqsSetup=['9.8GHz', '10.2GHz'], - MaximumPasses=5) - -setup.create_frequency_sweep( - unit="GHz", - name="Sweep1", - start_frequency=9.5, - stop_frequency=10.5, - sweep_type="Interpolating", -) - -################################################################################# -# Solve the project with two tasks. -# Each frequency point is solved simultaneously. - - -setup.analyze(tasks=2) - -############################################################################### -# Generate S-parameter plots -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# The following commands fetch solution data from HFSS for plotting directly -# from the Python interpreter. -# Caution: The syntax for expressions must be identical to that used -# in HFSS. - -traces_to_plot = hfss.get_traces_for_plot(second_element_filter="P1*") -report = hfss.post.create_report(traces_to_plot) # Creates a report in HFSS -solution = report.get_solution_data() - -plt = solution.plot(solution.expressions) # Matplotlib axes object. - -############################################################################### -# Generate E field plot -# ~~~~~~~~~~~~~~~~~~~~~ -# The following command generates a field plot in HFSS and uses PyVista -# to plot the field in Jupyter. - -plot = hfss.post.plot_field(quantity="Mag_E", assignment=["Global:XZ"], plot_type="CutPlane", - setup=hfss.nominal_adaptive, intrinsics={"Freq": "9.8GHz", "Phase": "0deg"}, show=False, - export_path=hfss.working_directory) - -############################################################################### -# Save and close the desktop -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# The following command saves the project to a file and closes the desktop. - -hfss.save_project() -hfss.release_desktop() diff --git a/examples/02-SBR+/Readme.txt b/examples/02-SBR+/Readme.txt deleted file mode 100644 index edb3b62f744..00000000000 --- a/examples/02-SBR+/Readme.txt +++ /dev/null @@ -1,4 +0,0 @@ -SBR+ examples -~~~~~~~~~~~~~ -These examples use PyAEDT to show some end-to-end workflows for HFSS SBR+. -This includes model generation, setup, meshing, and postprocessing. diff --git a/examples/02-SBR+/SBR_City_Import.py b/examples/02-SBR+/SBR_City_Import.py deleted file mode 100644 index 4384fe2ad31..00000000000 --- a/examples/02-SBR+/SBR_City_Import.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -SBR+: Import Geometry from Maps -------------------------------- -This example shows how you can use PyAEDT to create an HFSS SBR+ project from an -OpenStreeMaps. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports and set up the local path to the PyAEDT -# directory path. - -import os -from ansys.aedt.core import Hfss - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Define designs -# ~~~~~~~~~~~~~~ -# Define two designs, one source and one target. -# Each design is connected to a different object. - -app = Hfss( - design="Ansys", - solution_type="SBR+", - version=aedt_version, - new_desktop=True, - non_graphical=non_graphical -) - -############################################################################### -# Define Location to import -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define latitude and longitude to import. -ansys_home = [40.273726, -80.168269] - -############################################################################### -# Generate map and import -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Assign boundaries. - -app.modeler.import_from_openstreet_map(ansys_home, - terrain_radius=250, - road_step=3, - plot_before_importing=False, - import_in_aedt=True) - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model. - -plot_obj = app.plot(show=False, plot_air_objects=True) -plot_obj.background_color = [153,203,255] -plot_obj.zoom = 1.5 -plot_obj.show_grid = False -plot_obj.show_axes = False -plot_obj.bounding_box = False -plot_obj.plot(os.path.join(app.working_directory, "Source.jpg")) - -############################################################################### -# Release AEDT -# ~~~~~~~~~~~~ -# Release AEDT and close the example. - -app.release_desktop() diff --git a/examples/02-SBR+/SBR_Doppler_Example.py b/examples/02-SBR+/SBR_Doppler_Example.py deleted file mode 100644 index f41f1976e35..00000000000 --- a/examples/02-SBR+/SBR_Doppler_Example.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -SBR+: doppler setup -------------------- -This example shows how you can use PyAEDT to create a multipart scenario in HFSS SBR+ -and set up a doppler analysis. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -# Launch AEDT -# ~~~~~~~~~~~ -# Launch AEDT. - -projectname = "MicroDoppler_with_ADP" -designname = "doppler" -library_path = ansys.aedt.core.downloads.download_multiparts() - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Download and open project -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Download and open the project. - -project_name = ansys.aedt.core.generate_unique_project_name(project_name="doppler") - -# Instantiate the application. -app = ansys.aedt.core.Hfss( - version=aedt_version, - solution_type="SBR+", - new_desktop=True, - project=project_name, - close_on_exit=True, - non_graphical=non_graphical -) - -app.autosave_disable() - -############################################################################### -# Save project and rename design -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Save the project to the temporary folder and rename the design. - -app.save_project() -app.rename_design(designname) - -############################################################################### -# Set up library paths -# ~~~~~~~~~~~~~~~~~~~~ -# Set up library paths to 3D components. - -actor_lib = os.path.join(library_path, "actor_library") -env_lib = os.path.join(library_path, "environment_library") -radar_lib = os.path.join(library_path, "radar_modules") -env_folder = os.path.join(env_lib, "road1") -person_folder = os.path.join(actor_lib, "person3") -car_folder = os.path.join(actor_lib, "vehicle1") -bike_folder = os.path.join(actor_lib, "bike1") -bird_folder = os.path.join(actor_lib, "bird1") - -############################################################################### -# Define environment -# ~~~~~~~~~~~~~~~~~~ -# Define the background environment. - -road1 = app.modeler.add_environment(input_dir=env_folder, name="Bari") -prim = app.modeler - -############################################################################### -# Place actors -# ~~~~~~~~~~~~ -# Place actors in the environment. This code places persons, birds, bikes, and cars -# in the environment. - -person1 = app.modeler.add_person(input_dir=person_folder, speed=1.0, global_offset=[25, 1.5, 0], yaw=180, - name="Massimo") -person2 = app.modeler.add_person(input_dir=person_folder, speed=1.0, global_offset=[25, 2.5, 0], yaw=180, name="Devin") -car1 = app.modeler.add_vehicle(input_dir=car_folder, speed=8.7, global_offset=[3, -2.5, 0], name="LuxuryCar") -bike1 = app.modeler.add_vehicle(input_dir=bike_folder, speed=2.1, global_offset=[24, 3.6, 0], yaw=180, - name="Alberto_in_bike") -bird1 = app.modeler.add_bird(input_dir=bird_folder, speed=1.0, global_offset=[19, 4, 3], yaw=120, pitch=-5, - flapping_rate=30, name="Pigeon") -bird2 = app.modeler.add_bird(input_dir=bird_folder, speed=1.0, global_offset=[6, 2, 3], yaw=-60, pitch=10, name="Eagle") - -############################################################################### -# Place radar -# ~~~~~~~~~~~ -# Place radar on the car. The radar is created relative to the car's coordinate -# system. - -radar1 = app.create_sbr_radar_from_json(radar_file=radar_lib, name="Example_1Tx_1Rx", offset=[2.57, 0, 0.54], - use_relative_cs=True, relative_cs_name=car1.cs_name) - -############################################################################### -# Create setup -# ~~~~~~~~~~~~ -# Create setup and validate it. The ``create_sbr_pulse_doppler_setup`` method -# creates a setup and a parametric sweep on the time variable with a -# duration of two seconds. The step is computed automatically from CPI. - -setup, sweep = app.create_sbr_pulse_doppler_setup(sweep_time_duration=2) -app.set_sbr_current_sources_options() -app.validate_simple() - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model. - -app.plot(show=False, output_file=os.path.join(app.working_directory, "Image.jpg"), plot_air_objects=True) - -############################################################################### -# Solve and release AEDT -# ~~~~~~~~~~~~~~~~~~~~~~ -# Solve and release AEDT. To solve, uncomment the ``app.analyze_setup`` command -# to activate the simulation. - -# app.analyze_setup(sweep.name) -app.save_project() -app.release_desktop(close_projects=True, close_desktop=True) diff --git a/examples/02-SBR+/SBR_Example.py b/examples/02-SBR+/SBR_Example.py deleted file mode 100644 index 389ee306719..00000000000 --- a/examples/02-SBR+/SBR_Example.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -SBR+: HFSS to SBR+ coupling ---------------------------- -This example shows how you can use PyAEDT to create an HFSS SBR+ project from an -HFSS antenna and run a simulation. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports and set up the local path to the path for the PyAEDT -# directory. - -import os -import ansys.aedt.core - -project_full_name = ansys.aedt.core.downloads.download_sbr(ansys.aedt.core.generate_unique_project_name(project_name="sbr_freq")) - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Define designs -# ~~~~~~~~~~~~~~ -# Define two designs, one source and one target, with each design connected to -# a different object. - -target = ansys.aedt.core.Hfss( - project=project_full_name, - design="Cassegrain_", - solution_type="SBR+", - version=aedt_version, - new_desktop=True, - non_graphical=non_graphical -) - -source = ansys.aedt.core.Hfss(project=target.project_name, - design="feeder", - version=aedt_version, - ) - -############################################################################### -# Define linked antenna -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Define a linked antenna. This is HFSS far field applied to HFSS SBR+. - -target.create_sbr_linked_antenna(source, target_cs="feederPosition", field_type="farfield") - -############################################################################### -# Assign boundaries -# ~~~~~~~~~~~~~~~~~ -# Assign boundaries. - -target.assign_perfecte_to_sheets(["Reflector", "Subreflector"]) -target.mesh.assign_curvilinear_elements(["Reflector", "Subreflector"]) - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model - -source.plot(show=False, output_file=os.path.join(target.working_directory, "Source.jpg"), plot_air_objects=True) -target.plot(show=False, output_file=os.path.join(target.working_directory, "Target.jpg"), plot_air_objects=False) - -############################################################################### -# Create setup and solve -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a setup and solve it. - -setup1 = target.create_setup() -setup1.props["RadiationSetup"] = "ATK_3D" -setup1.props["ComputeFarFields"] = True -setup1.props["RayDensityPerWavelength"] = 2 -setup1.props["MaxNumberOfBounces"] = 3 -setup1["RangeType"] = "SinglePoints" -setup1["RangeStart"] = "10GHz" -target.analyze() - -############################################################################### -# Plot results -# ~~~~~~~~~~~~ -# Plot results. - -variations = target.available_variations.nominal_w_values_dict -variations["Freq"] = ["10GHz"] -variations["Theta"] = ["All"] -variations["Phi"] = ["All"] -target.post.create_report("db(GainTotal)", target.nominal_adaptive, variations=variations, - primary_sweep_variable="Theta", report_category="Far Fields", context="ATK_3D") - -############################################################################### -# Plot results outside AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Plot results using Matplotlib. - -solution = target.post.get_solution_data( - "GainTotal", - target.nominal_adaptive, - variations=variations, - primary_sweep_variable="Theta", - context="ATK_3D", - report_category="Far Fields", -) -solution.plot() - -############################################################################### -# Release AEDT -# ~~~~~~~~~~~~ -# Release AEDT and close the example. - -target.release_desktop() diff --git a/examples/02-SBR+/SBR_Time_Plot.py b/examples/02-SBR+/SBR_Time_Plot.py deleted file mode 100644 index 01a391164d9..00000000000 --- a/examples/02-SBR+/SBR_Time_Plot.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -SBR+: HFSS to SBR+ time animation ---------------------------------- -This example shows how you can use PyAEDT to create an SBR+ time animation -and save it to a GIF file. This example works only on CPython. -""" - -############################################################################### -# Perform required imports. -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -from ansys.aedt.core import Hfss, downloads - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Launch AEDT and load project -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT and load the project. - -project_file = downloads.download_sbr_time() - -hfss = Hfss(project=project_file, version=aedt_version, non_graphical=non_graphical, new_desktop=True) - -hfss.analyze() - -############################################################################### -# Get solution data -# ~~~~~~~~~~~~~~~~~ -# Get solution data. After simulation is performed, you can load solutions -# in the ``solution_data`` object. - -solution_data = hfss.post.get_solution_data(expressions=["NearEX", "NearEY", "NearEZ"], - variations={"_u": ["All"], "_v": ["All"], "Freq": ["All"]}, - context="Near_Field", - report_category="Near Fields") - -############################################################################### -# Compute IFFT -# ~~~~~~~~~~~~ -# Compute IFFT (Inverse Fast Fourier Transform). - -t_matrix = solution_data.ifft("NearE", window=True) - - -############################################################################### -# Export IFFT to CSV file -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Export IFFT to a CSV file. - -frames_list_file = solution_data.ifft_to_file(coord_system_center=[-0.15, 0, 0], db_val=True, - csv_path=os.path.join(hfss.working_directory, "csv")) - -############################################################################### -# Plot scene -# ~~~~~~~~~~ -# Plot the scene to create the time plot animation - -hfss.post.plot_scene(frames=frames_list_file, gif_path=os.path.join(hfss.working_directory, "animation.gif"), - norm_index=15, dy_rng=35, show=False, view="xy", zoom=1) - -hfss.release_desktop() diff --git a/examples/03-Maxwell/Maxwell2D_DCConduction.py b/examples/03-Maxwell/Maxwell2D_DCConduction.py deleted file mode 100644 index 05e4861fefd..00000000000 --- a/examples/03-Maxwell/Maxwell2D_DCConduction.py +++ /dev/null @@ -1,270 +0,0 @@ -""" -Maxwell 2D: resistance calculation ----------------------------------- -This example uses PyAEDT to set up a resistance calculation -and solve it using the Maxwell 2D DCConduction solver. -Keywords: DXF import, material sweep, expression cache -""" -import os.path - -import ansys.aedt.core - -from ansys.aedt.core.post.pdf import AnsysReport - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -################################################################################## -# Launch AEDT and Maxwell 2D -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT and Maxwell 2D after first setting up the project and design names, -# the solver, and the version. The following code also creates an instance of the -# ``Maxwell2d`` class named ``m2d``. - -m2d = ansys.aedt.core.Maxwell2d( - version=aedt_version, - new_desktop=True, - close_on_exit=True, - solution_type="DCConduction", - project="M2D_DC_Conduction", - design="Ansys_resistor" -) - -########################################################## -# Create results folder -# ~~~~~~~~~~~~~~~~~~~~ -# Create results folder. - -results_folder = os.path.join(m2d.working_directory, "M2D_DC_Conduction") -if not os.path.exists(results_folder): - os.mkdir(results_folder) - -################################################################################## -# Import geometry as a DXF file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# You can test importing a DXF or a Parasolid file by commenting/uncommenting -# the following lines. -# Importing DXF files only works in graphical mode. - -# DXFPath = ansys.aedt.core.downloads.download_file("dxf", "Ansys_logo_2D.dxf") -# dxf_layers = m2d.get_dxf_layers(DXFPath) -# m2d.import_dxf(DXFPath, dxf_layers, scale=1E-05) - -parasolid_path = ansys.aedt.core.downloads.download_file("x_t", "Ansys_logo_2D.x_t") -m2d.modeler.import_3d_cad(parasolid_path) - -################################################################################## -# Define variables -# ~~~~~~~~~~~~~~~~ -# Define conductor thickness in z-direction, material array with 4 materials, -# and MaterialIndex referring to the material array - -m2d["MaterialThickness"] = "5mm" -m2d["ConductorMaterial"] = "[\"Copper\", \"Aluminum\", \"silver\", \"gold\"]" -MaterialIndex = 0 -m2d["MaterialIndex"] = str(MaterialIndex) -no_materials = 4 - - -################################################################################## -# Assign materials -# ~~~~~~~~~~~~~~~~ -# A voltage port is defined as a perfect electric conductor (pec). A conductor -# gets the material defined by the 0th entry of the material array. - -m2d.assign_material(["ANSYS_LOGO_2D_1", "ANSYS_LOGO_2D_2"], "gold") -m2d.modeler["ANSYS_LOGO_2D_3"].material_name = "ConductorMaterial[MaterialIndex]" - -################################################################################## -# Assign voltages -# ~~~~~~~~~~~~~~~ -# 1V and 0V - -m2d.assign_voltage(["ANSYS_LOGO_2D_1"], amplitude=1, name="1V") -m2d.assign_voltage(["ANSYS_LOGO_2D_2"], amplitude=0, name="0V") - -################################################################################## -# Setup conductance calculation -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# 1V is the source, 0V ground - -m2d.assign_matrix(assignment=['1V'], matrix_name="Matrix1", group_sources=['0V']) - -################################################################################## -# Assign mesh operation -# ~~~~~~~~~~~~~~~~~~~~~ -# 3mm on the conductor - -m2d.mesh.assign_length_mesh(["ANSYS_LOGO_2D_3"], maximum_length=3, maximum_elements=None, name="conductor") - -################################################################################## -# Create simulation setup and enable expression cache -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create simulation setup with minimum 4 adaptive passes to ensure convergence. -# Enable expression cache to observe the convergence. - -setup1 = m2d.create_setup(name="Setup1", MinimumPasses=4) -setup1.enable_expression_cache( # doesn't work? - report_type="DCConduction", - expressions="1/Matrix1.G(1V,1V)/MaterialThickness", - isconvergence=True, - conv_criteria=1, - use_cache_for_freq=False) -setup1.analyze() - -################################################################################## -# Create parametric sweep -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Create parametric sweep to sweep all the entries in the material array. -# Save fields and mesh and use the mesh for all the materials. - -param_sweep = m2d.parametrics.add("MaterialIndex", 0, no_materials - 1, 1, "LinearStep", name="MaterialSweep") -param_sweep["SaveFields"] = True -param_sweep["CopyMesh"] = True -param_sweep["SolveWithCopiedMeshOnly"] = True -param_sweep.analyze() - -################################################################################## -# Create resistance report -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Create R. vs. material report - -variations = {"MaterialIndex": ["All"], "MaterialThickness": ["Nominal"]} -report = m2d.post.create_report(expressions="1/Matrix1.G(1V,1V)/MaterialThickness", variations=variations, - primary_sweep_variable="MaterialIndex", report_category="DCConduction", - plot_type="Data Table", plot_name="Resistance vs. Material") - -############################################################################### -# Get solution data -# ~~~~~~~~~~~~~~~~~ -# Get solution data using the object ``report``` to get resistance values -# and plot data outside AEDT. - -d = report.get_solution_data() -resistence = d.data_magnitude() -material_index = d.primary_sweep_values -d.primary_sweep = "MaterialIndex" -d.plot(snapshot_path=os.path.join(results_folder, "M2D_DCConduction.jpg")) - -############################################################################### -# Create material index vs resistance table -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create material index vs resistance table to use in PDF report generator. -# Create ``colors`` table to customize each row of the material index vs resistance table. - -material_index_vs_resistance = [["Material", "Resistance"]] -colors = [[(255, 255, 255), (0, 255, 0)]] -for i in range(len(d.primary_sweep_values)): - material_index_vs_resistance.append([str(d.primary_sweep_values[i]), str(resistence[i])]) - colors.append([None, None]) - -################################################################################## -# Field overlay -# ~~~~~~~~~~~~~ -# Plot electric field and current density on the conductor surface - -conductor_surface = m2d.modeler["ANSYS_LOGO_2D_3"].faces -plot1 = m2d.post.create_fieldplot_surface(conductor_surface, "Mag_E", plot_name="Electric Field") -plot2 = m2d.post.create_fieldplot_surface(conductor_surface, "Mag_J", plot_name="Current Density") - -################################################################################## -# Field overlay -# ~~~~~~~~~~~~~ -# Plot electric field using pyvista and saving to an image - -py_vista_plot = m2d.post.plot_field("Mag_E", conductor_surface, show=False, plot_cad_objs=False) -py_vista_plot.isometric_view = False -py_vista_plot.camera_position = [0, 0, 7] -py_vista_plot.focal_point = [0, 0, 0] -py_vista_plot.roll_angle = 0 -py_vista_plot.elevation_angle = 0 -py_vista_plot.azimuth_angle = 0 -py_vista_plot.plot(os.path.join(results_folder, "mag_E.jpg")) - -################################################################################## -# Field animation -# ~~~~~~~~~~~~~~~ -# Plot current density vs the Material index. - -animated = m2d.post.plot_animated_field(quantity="Mag_J", assignment=conductor_surface, - variation_variable="MaterialIndex", variations=[0, 1, 2, 3], - show=False, log_scale=True, export_gif=False, - export_path=results_folder) -animated.isometric_view = False -animated.camera_position = [0, 0, 7] -animated.focal_point = [0, 0, 0] -animated.roll_angle = 0 -animated.elevation_angle = 0 -animated.azimuth_angle = 0 -animated.animate() - -################################################################################ -# Export model picture -# ~~~~~~~~~~~~~~~~~~~~ -# Export model picture. - -model_picture = m2d.post.export_model_picture() - -################################################################################ -# Generate PDF report -# ~~~~~~~~~~~~~~~~~~~ -# Generate a PDF report with output of simulation. - -pdf_report = AnsysReport(version=aedt_version, design_name=m2d.design_name, project_name=m2d.project_name) - -# Customize text font. - -pdf_report.report_specs.font = "times" -pdf_report.report_specs.text_font_size = 10 - -# Create report - -pdf_report.create() - -# Add project's design info to report. - -pdf_report.add_project_info(m2d) - -# Add model picture in a new chapter and add text. - -pdf_report.add_chapter("Model Picture") -pdf_report.add_text("This section contains the model picture") -pdf_report.add_image(model_picture, "Model Picture", width=80, height=60) - -# Add in a new chapter field overlay plots. - -pdf_report.add_chapter("Field overlay") -pdf_report.add_sub_chapter("Plots") -pdf_report.add_text("This section contains the fields overlay.") -pdf_report.add_image(os.path.join(results_folder, "mag_E.jpg"), "Mag E", width=120, height=80) -pdf_report.add_page_break() - -# Add a new section to display results. - -pdf_report.add_section() -pdf_report.add_chapter("Results") -pdf_report.add_sub_chapter("Resistance vs. Material") -pdf_report.add_text("This section contains resistance vs material data.") -# Aspect ratio is automatically calculated if only width is provided -pdf_report.add_image(os.path.join(results_folder, "M2D_DCConduction.jpg"), width=130) - -# Add a new subchapter to display resistance data from previously created table. - -pdf_report.add_sub_chapter("Resistance data table") -pdf_report.add_text("This section contains Resistance data.") -pdf_report.add_table("Resistance Data", content=material_index_vs_resistance, formatting=colors, col_widths=[75, 100]) - -# Add table of content and save PDF. - -pdf_report.add_toc() -pdf_report.save_pdf(results_folder, "AEDT_Results.pdf") - -################################################################################## -# Release desktop -# ~~~~~~~~~~~~~~~ - -m2d.release_desktop() diff --git a/examples/03-Maxwell/Maxwell2D_Electrostatic.py b/examples/03-Maxwell/Maxwell2D_Electrostatic.py deleted file mode 100644 index be36f0081c6..00000000000 --- a/examples/03-Maxwell/Maxwell2D_Electrostatic.py +++ /dev/null @@ -1,214 +0,0 @@ -""" -Maxwell 2D Electrostatic analysis ---------------------------------- -This example shows how you can use PyAEDT to create a Maxwell 2D electrostatic analysis. -It shows how to create the geometry, load material properties from an Excel file and -set up the mesh settings. Moreover, it focuses on post-processing operations, in particular how to -plot field line traces, relevant for an electrostatic analysis. -""" -################################################################################# -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -################################################################################# -# Initialize Maxwell 2D -# ~~~~~~~~~~~~~~~~~~~~~ -# Initialize Maxwell 2D, providing the version, path to the project, and the design -# name and type. - -setup_name = 'MySetupAuto' -solver = 'Electrostatic' -design_name = 'Design1' -project_name = ansys.aedt.core.generate_unique_project_name() -non_graphical = False - -################################################################################# -# Download .xlsx file -# ~~~~~~~~~~~~~~~~~~~ -# Set local temporary folder to export the .xlsx file to. - -file_name_xlsx = ansys.aedt.core.downloads.download_file("field_line_traces", "my_copper.xlsx") - -################################################################################# -# Initialize dictionaries -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize dictionaries that contain all the definitions for the design variables. - -geom_params_circle = { - 'circle_x0': '-10mm', - 'circle_y0': '0mm', - 'circle_z0': '0mm', - 'circle_axis': 'Z', - 'circle_radius': '1mm' -} - -geom_params_rectangle = { - 'r_x0': '1mm', - 'r_y0': '5mm', - 'r_z0': '0mm', - 'r_axis': 'Z', - 'r_dx': '-1mm', - 'r_dy': '-10mm' -} - -################################################################################## -# Launch Maxwell 2D -# ~~~~~~~~~~~~~~~~~ -# Launch Maxwell 2D and save the project. - -M2D = ansys.aedt.core.Maxwell2d(project=project_name, - version=aedt_version, - design=design_name, - solution_type=solver, - new_desktop=True, - non_graphical=non_graphical - ) - -################################################################################## -# Create object to access 2D modeler -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the object ``mod2D`` to access the 2D modeler easily. - -mod2D = M2D.modeler -mod2D.delete() -mod2D.model_units = "mm" - -################################################################################## -# Define variables from dictionaries -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define design variables from the created dictionaries. - -for k, v in geom_params_circle.items(): - M2D[k] = v -for k, v in geom_params_rectangle.items(): - M2D[k] = v - -################################################################################## -# Read materials from .xslx file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Read materials from .xslx file into and set into design. - -mats = M2D.materials.import_materials_from_excel(file_name_xlsx) - -################################################################################## -# Create design geometries -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Create rectangle and a circle and assign the material read from the .xlsx file. -# Create two new polylines and a region. - -rect = mod2D.create_rectangle(origin=['r_x0', 'r_y0', 'r_z0'], - sizes=['r_dx', 'r_dy', 0], - name='Ground', matname=mats[0]) -rect.color = (0, 0, 255) # rgb -rect.solve_inside = False - -circle = mod2D.create_circle(position=['circle_x0', 'circle_y0', 'circle_z0'], radius='circle_radius', - num_sides='0', is_covered=True, name='Electrode', matname=mats[0]) -circle.color = (0, 0, 255) # rgb -circle.solve_inside = False - -poly1_points = [[-9, 2, 0], [-4, 2, 0], [2, -2, 0],[8, 2, 0]] -poly2_points = [[-9, 0, 0], [9, 0, 0]] -poly1_id = mod2D.create_polyline(points=poly1_points, segment_type='Spline', name='Poly1') -poly2_id = mod2D.create_polyline(points=poly2_points, name='Poly2') -mod2D.split([poly1_id, poly2_id], 'YZ', sides='NegativeOnly') -mod2D.create_region([20, 100, 20, 100]) - -################################################################################## -# Define excitations -# ~~~~~~~~~~~~~~~~~~ -# Assign voltage excitations to rectangle and circle. - -M2D.assign_voltage(rect.id, amplitude=0, name='Ground') -M2D.assign_voltage(circle.id, amplitude=50e6, name='50kV') - -################################################################################## -# Create initial mesh settings -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign a surface mesh to the rectangle. - -M2D.mesh.assign_surface_mesh_manual(assignment=['Ground'], surface_deviation=0.001) - -################################################################################## -# Create, validate and analyze the setup -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create, update, validate and analyze the setup. - -setup = M2D.create_setup(name=setup_name) -setup.props['PercentError'] = 0.5 -setup.update() -M2D.validate_simple() -M2D.analyze_setup(setup_name) - -################################################################################## -# Evaluate the E Field tangential component -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Evaluate the E Field tangential component along the given polylines. -# Add these operations to the Named Expression list in Field Calculator. - -fields = M2D.ofieldsreporter -fields.CalcStack("clear") -fields.EnterQty("E") -fields.EnterEdge("Poly1") -fields.CalcOp("Tangent") -fields.CalcOp("Dot") -fields.AddNamedExpression("e_tan_poly1", "Fields") -fields.EnterQty("E") -fields.EnterEdge("Poly2") -fields.CalcOp("Tangent") -fields.CalcOp("Dot") -fields.AddNamedExpression("e_tan_poly2", "Fields") - -################################################################################## -# Create Field Line Traces Plot -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create Field Line Traces Plot specifying as seeding faces -# the ground, the electrode and the region -# and as ``In surface objects`` only the region. - -plot = M2D.post.create_fieldplot_line_traces(seeding_faces=["Ground", "Electrode", "Region"], - in_volume_tracing_objs="Region", plot_name="LineTracesTest") - -################################################################################### -# Update Field Line Traces Plot -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Update field line traces plot. -# Update seeding points number, line style and line width. - -plot.SeedingPointsNumber = 20 -plot.LineStyle = "Cylinder" -plot.LineWidth = 3 -plot.update() - -################################################################################### -# Export field line traces plot -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Export field line traces plot. -# For field lint traces plot, the export file format is ``.fldplt``. - -M2D.post.export_field_plot(plot_name="LineTracesTest", output_dir=M2D.toolkit_directory, file_format="fldplt") - -########################################################## -# Export the mesh field plot -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Export the mesh in ``aedtplt`` format. - -M2D.post.export_mesh_obj(setup=M2D.nominal_adaptive) - -################################################################################### -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Save the project and close AEDT. - -M2D.save_project() -M2D.release_desktop() diff --git a/examples/03-Maxwell/Maxwell2D_PMSynchronousMotor.py b/examples/03-Maxwell/Maxwell2D_PMSynchronousMotor.py deleted file mode 100644 index d06a1442d0a..00000000000 --- a/examples/03-Maxwell/Maxwell2D_PMSynchronousMotor.py +++ /dev/null @@ -1,759 +0,0 @@ -""" -Maxwell 2D: PM synchronous motor transient analysis ---------------------------------------------------- -This example shows how you can use PyAEDT to create a Maxwell 2D transient analysis for -an interior permanent magnet electric motor. - -""" -################################################################################# -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -from math import sqrt as mysqrt - -import csv -import os -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -################################################################################# -# Initialize Maxwell 2D -# ~~~~~~~~~~~~~~~~~~~~~ -# Initialize Maxwell 2D, providing the version, path to the project, and the design -# name and type. - -setup_name = "MySetupAuto" -solver = "TransientXY" - -project_name = ansys.aedt.core.generate_unique_project_name() -design_name = "Sinusoidal" - -################################################################################# -# Initialize definitions for stator, rotor, and shaft -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize geometry parameter definitions for the stator, rotor, and shaft. -# The naming refers to RMxprt primitives. - -geom_params = { - "DiaGap": "132mm", - "DiaStatorYoke": "198mm", - "DiaStatorInner": "132mm", - "DiaRotorLam": "130mm", - "DiaShaft": "44.45mm", - "DiaOuter": "198mm", - "Airgap": "1mm", - "SlotNumber": "48", - "SlotType": "3" -} - -################################################################################# -# Initialize definitions for stator windings -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize geometry parameter definitions for the stator windings. The naming -# refers to RMxprt primitives. - -wind_params = { - "Layers": "1", - "ParallelPaths": "2", - "R_Phase": "7.5mOhm", - "WdgExt_F": "5mm", - "SpanExt": "30mm", - "SegAngle": "0.25", - "CoilPitch": "5", # coil pitch in slots - "Coil_SetBack": "3.605732823mm", - "SlotWidth": "2.814mm", # RMxprt Bs0 - "Coil_Edge_Short": "3.769235435mm", - "Coil_Edge_Long": "15.37828521mm" -} - -################################################################################# -# Initialize definitions for model setup -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize geometry parameter definitions for the model setup. - -mod_params = { - "NumPoles": "8", - "Model_Length": "80mm", - "SymmetryFactor": "8", - "Magnetic_Axial_Length": "150mm", - "Stator_Lam_Length": "0mm", - "StatorSkewAngle": "0deg", - "NumTorquePointsPerCycle": "30", - "mapping_angle": "0.125*4deg", - "num_m": "16", - "Section_Angle": "360deg/SymmetryFactor" -} - -################################################################################# -# Initialize definitions for operational machine -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize geometry parameter definitions for the operational machine. This -# identifies the operating point for the transient setup. - -oper_params = { - "InitialPositionMD": "180deg/4", - "IPeak": "480A", - "MachineRPM": "3000rpm", - "ElectricFrequency": "MachineRPM/60rpm*NumPoles/2*1Hz", - "ElectricPeriod": "1/ElectricFrequency", - "BandTicksinModel": "360deg/NumPoles/mapping_angle", - "TimeStep": "ElectricPeriod/(2*BandTicksinModel)", - "StopTime": "ElectricPeriod", - "Theta_i": "135deg" -} - -########################################################## -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. ``"PYAEDT_NON_GRAPHICAL"`` is needed to -# generate documentation only. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -########################################################## -# Launch Maxwell 2D -# ~~~~~~~~~~~~~~~~~ -# Launch Maxwell 2D and save the project. - -M2D = ansys.aedt.core.Maxwell2d(project=project_name, - version=aedt_version, - design=design_name, - solution_type=solver, - new_desktop=True, - non_graphical=non_graphical - ) - -########################################################## -# Create object to access 2D modeler -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the object ``mod2D`` to access the 2D modeler easily. - -mod2D = M2D.modeler -mod2D.delete() -mod2D.model_units = "mm" - -########################################################## -# Define variables from dictionaries -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define design variables from the created dictionaries. - -for k, v in geom_params.items(): - M2D[k] = v -for k, v in wind_params.items(): - M2D[k] = v -for k, v in mod_params.items(): - M2D[k] = v -for k, v in oper_params.items(): - M2D[k] = v - -########################################################## -# Define path for non-linear material properties -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define the path for non-linear material properties. -# Materials are stored in text files. - -filename_lam, filename_PM = ansys.aedt.core.downloads.download_leaf() - -########################################################## -# Create first material -# ~~~~~~~~~~~~~~~~~~~~~ -# Create the material ``"Copper (Annealed)_65C"``. - -mat_coils = M2D.materials.add_material("Copper (Annealed)_65C") -mat_coils.update() -mat_coils.conductivity = "49288048.9198" -mat_coils.permeability = "1" - -########################################################## -# Create second material -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create the material ``"Arnold_Magnetics_N30UH_80C"``. -# The BH curve is read from a tabbed CSV file, and a list (``BH_List_PM``) -# is created. This list is passed to the ``mat_PM.permeability.value`` -# method. - -mat_PM = M2D.materials.add_material("Arnold_Magnetics_N30UH_80C_new") -mat_PM.update() -mat_PM.conductivity = "555555.5556" -mat_PM.set_magnetic_coercivity(value=-800146.66287534, x=1, y=0, z=0) -mat_PM.mass_density = "7500" -BH_List_PM = [] -with open(filename_PM) as f: - reader = csv.reader(f, delimiter='\t') - next(reader) - for row in reader: - BH_List_PM.append([float(row[0]), float(row[1])]) -mat_PM.permeability.value = BH_List_PM - -########################################################## -# Create third material -# ~~~~~~~~~~~~~~~~~~~~~ -# Create the laminated material ``30DH_20C_smooth``. -# This material has a BH curve and a core loss model, -# which is set to electrical steel. - -mat_lam = M2D.materials.add_material("30DH_20C_smooth") -mat_lam.update() -mat_lam.conductivity = "1694915.25424" -kh = 71.7180985413 -kc = 0.25092214579 -ke = 12.1625774023 -kdc = 0.001 -eq_depth = 0.001 -mat_lam.set_electrical_steel_coreloss(kh, kc, ke, kdc, eq_depth) -mat_lam.mass_density = "7650" -BH_List_lam = [] -with open(filename_lam) as f: - reader = csv.reader(f, delimiter='\t') - next(reader) - for row in reader: - BH_List_lam.append([float(row[0]), float(row[1])]) -mat_lam.permeability.value = BH_List_lam - -########################################################## -# Create geometry for stator -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the geometry for the stator. It is created via -# the RMxprt user-defined primitive. A list of lists is -# created with the proper UDP parameters. - -udp_par_list_stator = [["DiaGap", "DiaGap"], ["DiaYoke", "DiaStatorYoke"], ["Length", "Stator_Lam_Length"], - ["Skew", "StatorSkewAngle"], ["Slots", "SlotNumber"], ["SlotType", "SlotType"], - ["Hs0", "1.2mm"], ["Hs01", "0mm"], ["Hs1", "0.4834227384999mm"], - ["Hs2", "17.287669825502mm"], - ["Bs0", "2.814mm"], ["Bs1", "4.71154109036mm"], ["Bs2", "6.9777285790998mm"], ["Rs", "2mm"], - ["FilletType", "1"], ["HalfSlot", "0"], ["VentHoles", "0"], ["HoleDiaIn", "0mm"], - ["HoleDiaOut", "0mm"], - ["HoleLocIn", "0mm"], ["HoleLocOut", "0mm"], ["VentDucts", "0"], ["DuctWidth", "0mm"], - ["DuctPitch", "0mm"], - ["SegAngle", "0deg"], ["LenRegion", "Model_Length"], ["InfoCore", "0"]] - -stator_id = mod2D.create_udp(dll="RMxprt/VentSlotCore.dll", parameters=udp_par_list_stator, library='syslib', - name='my_stator') # name not taken - -########################################################## -# Assign properties to stator -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign properties to the stator. The following code assigns -# the material, name, color, and ``solve_inside`` properties. - -M2D.assign_material(assignment=stator_id, material="30DH_20C_smooth") -stator_id.name = "Stator" -stator_id.color = (0, 0, 255) # rgb -stator_id.solve_inside = True # to be reassigned: M2D.assign material puts False if not dielectric - - -##################################################################################### -# Create geometry for PMs -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Create the geometry for the PMs (permanent magnets). In Maxwell 2D, you assign -# magnetization via the coordinate system. Because each PM needs to have a coordinate -# system in the face center, auxiliary functions are created. Here, you use the auxiliary -# function ``find_elements(lst1, lst2)`` to find the elements in list ``lst1`` with indexes -# in list ``lst2``. - -def find_elements(lst1, lst2): - return [lst1[i] for i in lst2] - - -##################################################################################### -# Find largest elements in list -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Use the auxiliary function ``find_n_largest (input_len_list, n_largest_edges)`` -# to find the ``n`` largest elements in the list ``input_len_list``. - -def find_n_largest(input_len_list, n_largest_edges): - tmp = list(input_len_list) - copied = list(input_len_list) - copied.sort() # sort list so that largest elements are on the far right - index_list = [] - for n in range(1, n_largest_edges + 1): # get index of the nth largest element - index_list.append(tmp.index(copied[-n])) - tmp[tmp.index(copied[-n])] = 0 # index can only get the first occurrence that solves the problem - return index_list - - -##################################################################################### -# Create coordinate system for PMs -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the coordinate system for the PMs. The inputs are the object name, coordinate -# system name, and inner or outer magnetization. Find the two longest edges of the magnets -# and get the midpoint of the outer edge. You must have this point to create the face -# coordinate systems in case of outer magnetization. - -def create_cs_magnets(pm_id, cs_name, point_direction): - pm_face_id = mod2D.get_object_faces(pm_id.name)[0] # works with name only - pm_edges = mod2D.get_object_edges(pm_id.name) # gets the edges of the PM object - edge_len_list = list( - map(mod2D.get_edge_length, pm_edges)) # apply method get_edge_length to all elements of list pm_edges - index_2_longest = find_n_largest(edge_len_list, 2) # find the 2 longest edges of the PM - longest_edge_list = find_elements(pm_edges, index_2_longest) - edge_center_list = list(map(mod2D.get_edge_midpoint, - longest_edge_list)) # apply method get_edge_midpoint to all elements of list longest_edge_list - - rad = lambda x: mysqrt(x[0] * x[0] + x[1] * x[1] + x[2] * x[2]) - index_largest_r = find_n_largest(list(map(rad, edge_center_list)), 2) - longest_edge_list2 = [longest_edge_list[i] for i in index_largest_r] # reorder: outer first element of the list - if point_direction == 'outer': - my_axis_pos = longest_edge_list2[0] - elif point_direction == 'inner': - my_axis_pos = longest_edge_list2[1] - - mod2D.create_face_coordinate_system(face=pm_face_id, origin=pm_face_id, axis_position=my_axis_pos, - axis="X", name=cs_name) - pm_id.part_coordinate_system = cs_name - mod2D.set_working_coordinate_system('Global') - - -##################################################################################### -# Create outer and inner PMs -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the outer and inner PMs and assign color to them. - -IM1_points = [[56.70957112, 3.104886585, 0], [40.25081875, 16.67243502, 0], [38.59701538, 14.66621111, 0], - [55.05576774, 1.098662669, 0]] -OM1_points = [[54.37758185, 22.52393189, 0], [59.69688156, 9.68200639, 0], [63.26490432, 11.15992981, 0], - [57.94560461, 24.00185531, 0]] -IPM1_id = mod2D.create_polyline(points=IM1_points, cover_surface=True, name="PM_I1", - material="Arnold_Magnetics_N30UH_80C_new") -IPM1_id.color = (0, 128, 64) -OPM1_id = mod2D.create_polyline(points=OM1_points, cover_surface=True, name="PM_O1", - material="Arnold_Magnetics_N30UH_80C_new") -OPM1_id.color = (0, 128, 64) - -##################################################################################### -# Create coordinate system for PMs in face center -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the coordinate system for PMs in the face center. - -create_cs_magnets(IPM1_id, 'CS_' + IPM1_id.name, 'outer') -create_cs_magnets(OPM1_id, 'CS_' + OPM1_id.name, 'outer') - -##################################################################################### -# Duplicate and mirror PMs -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Duplicate and mirror the PMs along with the local coordinate system. - -mod2D.duplicate_and_mirror([IPM1_id, OPM1_id], origin=[0, 0, 0], - vector=["cos((360deg/SymmetryFactor/2)+90deg)", "sin((360deg/SymmetryFactor/2)+90deg)", 0]) -id_PMs = mod2D.get_objects_w_string("PM", case_sensitive=True) - -########################################################## -# Create coils -# ~~~~~~~~~~~~ -# Create the coils. - -coil_id = mod2D.create_rectangle(origin=['DiaRotorLam/2+Airgap+Coil_SetBack', '-Coil_Edge_Short/2', 0], - sizes=['Coil_Edge_Long', 'Coil_Edge_Short', 0], - name='Coil', material="Copper (Annealed)_65C") -coil_id.color = (255, 128, 0) -M2D.modeler.rotate(assignment=coil_id, axis="Z", angle="360deg/SlotNumber/2") -coil_id.duplicate_around_axis(axis="Z", angle="360deg/SlotNumber", nclones='CoilPitch+1', - create_new_objects=True) -id_coils = mod2D.get_objects_w_string("Coil", case_sensitive=True) - -########################################################## -# Create shaft and region -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Create the shaft and region. - -region_id = mod2D.create_circle(position=[0, 0, 0], radius='DiaOuter/2', - num_sides='SegAngle', is_covered=True, name='Region') -shaft_id = mod2D.create_circle(position=[0, 0, 0], radius='DiaShaft/2', - num_sides='SegAngle', is_covered=True, name='Shaft') - -########################################################## -# Create bands -# ~~~~~~~~~~~~ -# Create the inner band, band, and outer band. - -bandIN_id = mod2D.create_circle(position=[0, 0, 0], radius='(DiaGap - (1.5 * Airgap))/2', - num_sides='mapping_angle', is_covered=True, name='Inner_Band') -bandMID_id = mod2D.create_circle(position=[0, 0, 0], radius='(DiaGap - (1.0 * Airgap))/2', - num_sides='mapping_angle', is_covered=True, name='Band') -bandOUT_id = mod2D.create_circle(position=[0, 0, 0], radius='(DiaGap - (0.5 * Airgap))/2', - num_sides='mapping_angle', is_covered=True, name='Outer_Band') - -########################################################## -# Assign motion setup to object -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign a motion setup to a ``Band`` object named ``RotatingBand_mid``. - -M2D.assign_rotate_motion(assignment='Band', coordinate_system="Global", axis="Z", positive_movement=True, - start_position="InitialPositionMD", angular_velocity="MachineRPM") - -########################################################## -# Create list of vacuum objects -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a list of vacuum objects and assign color. - -vacuum_obj_id = [shaft_id, region_id, bandIN_id, bandMID_id, bandOUT_id] # put shaft first -for item in vacuum_obj_id: - item.color = (128, 255, 255) - -########################################################## -# Create rotor -# ~~~~~~~~~~~~ -# Create the rotor. Holes are specific to the lamination. -# Allocated PMs are created. - -rotor_id = mod2D.create_circle(position=[0, 0, 0], radius='DiaRotorLam/2', - num_sides=0, name="Rotor", material="30DH_20C_smooth") -rotor_id.color = (0, 128, 255) -mod2D.subtract(rotor_id, shaft_id, keep_originals=True) -void_small_1_id = mod2D.create_circle(position=[62, 0, 0], radius="2.55mm", - num_sides=0, name="void1", material="vacuum") -M2D.modeler.duplicate_around_axis(void_small_1_id, axis="Z", angle="360deg/SymmetryFactor", - clones=2, create_new_objects=False) -void_big_1_id = mod2D.create_circle(position=[29.5643, 12.234389332712, 0], radius='9.88mm/2', - num_sides=0, name="void_big", material="vacuum") -mod2D.subtract(rotor_id, [void_small_1_id, void_big_1_id], keep_originals=False) - -slot_IM1_points = [[37.5302872, 15.54555396, 0], [55.05576774, 1.098662669, 0], [57.33637589, 1.25, 0], - [57.28982158, 2.626565019, 0], [40.25081875, 16.67243502, 0]] -slot_OM1_points = [[54.37758185, 22.52393189, 0], [59.69688156, 9.68200639, 0], [63.53825619, 10.5, 0], - [57.94560461, 24.00185531, 0]] -slot_IM_id = mod2D.create_polyline(points=slot_IM1_points, cover_surface=True, name="slot_IM1", material="vacuum") -slot_OM_id = mod2D.create_polyline(points=slot_OM1_points, cover_surface=True, name="slot_OM1", material="vacuum") - -M2D.modeler.duplicate_and_mirror(assignment=[slot_IM_id, slot_OM_id], origin=[0, 0, 0], - vector=["cos((360deg/SymmetryFactor/2)+90deg)", - "sin((360deg/SymmetryFactor/2)+90deg)", 0]) - -id_holes = mod2D.get_objects_w_string("slot_", case_sensitive=True) -M2D.modeler.subtract(rotor_id, id_holes, keep_originals=True) - -########################################################## -# Create section of machine -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a section of the machine. This allows you to take -# advantage of symmetries. - -object_list = [stator_id, rotor_id] + vacuum_obj_id -mod2D.create_coordinate_system(origin=[0, 0, 0], - reference_cs="Global", - name="Section", - mode="axis", - x_pointing=["cos(360deg/SymmetryFactor)", "sin(360deg/SymmetryFactor)", 0], - y_pointing=["-sin(360deg/SymmetryFactor)", "cos(360deg/SymmetryFactor)", 0]) - -mod2D.set_working_coordinate_system("Section") -mod2D.split(object_list, "ZX", sides="NegativeOnly") -mod2D.set_working_coordinate_system("Global") -mod2D.split(object_list, "ZX", sides="PositiveOnly") - -########################################################## -# Create boundary conditions -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create independent and dependent boundary conditions. -# Edges for assignment are picked by position. -# The points for edge picking are in the airgap. - -pos_1 = "((DiaGap - (1.0 * Airgap))/4)" -id_bc_1 = mod2D.get_edgeid_from_position(position=[pos_1, 0, 0], assignment='Region') -id_bc_2 = mod2D.get_edgeid_from_position( - position=[pos_1 + "*cos((360deg/SymmetryFactor))", pos_1 + "*sin((360deg/SymmetryFactor))", 0], assignment='Region') -M2D.assign_master_slave(independent=id_bc_1, dependent=id_bc_2, reverse_master=False, reverse_slave=True, - same_as_master=False, boundary="Matching") - -########################################################## -# Assign vector potential -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Assign a vector potential of ``0`` to the second position. - -pos_2 = "(DiaOuter/2)" -id_bc_az = mod2D.get_edgeid_from_position( - position=[pos_2 + "*cos((360deg/SymmetryFactor/2))", pos_2 + "*sin((360deg/SymmetryFactor)/2)", 0], - assignment='Region') -M2D.assign_vector_potential(id_bc_az, vector_value=0, boundary="VectorPotentialZero") - -########################################################## -# Create excitations -# ~~~~~~~~~~~~~~~~~~ -# Create excitations, defining phase currents for the windings. - -PhA_current = "IPeak * cos(2*pi*ElectricFrequency*time+Theta_i)" -PhB_current = "IPeak * cos(2*pi * ElectricFrequency*time - 120deg+Theta_i)" -PhC_current = "IPeak * cos(2*pi * ElectricFrequency*time - 240deg+Theta_i)" - -########################################################## -# Define windings in phase A -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define windings in phase A. - -M2D.assign_coil(assignment=["Coil"], conductors_number=6, polarity="Positive", name="CT_Ph1_P2_C1_Go") -M2D.assign_coil(assignment=["Coil_5"], conductors_number=6, polarity="Negative", name="CT_Ph1_P2_C1_Ret") -M2D.assign_winding(assignment=None, winding_type="Current", is_solid=False, current=PhA_current, parallel_branches=1, - name="Phase_A") -M2D.add_winding_coils(assignment="Phase_A", coils=["CT_Ph1_P2_C1_Go", "CT_Ph1_P2_C1_Ret"]) - -########################################################## -# Define windings in phase B -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define windings in phase B. - -M2D.assign_coil(assignment="Coil_3", conductors_number=6, polarity="Positive", name="CT_Ph3_P1_C2_Go") -M2D.assign_coil(assignment="Coil_4", conductors_number=6, polarity="Positive", name="CT_Ph3_P1_C1_Go") -M2D.assign_winding(assignment=None, winding_type="Current", is_solid=False, current=PhB_current, parallel_branches=1, - name="Phase_B") -M2D.add_winding_coils(assignment="Phase_B", coils=["CT_Ph3_P1_C2_Go", "CT_Ph3_P1_C1_Go"]) - -########################################################## -# Define windings in phase C -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define windings in phase C. - -M2D.assign_coil(assignment="Coil_1", conductors_number=6, polarity="Negative", name="CT_Ph2_P2_C2_Ret") -M2D.assign_coil(assignment="Coil_2", conductors_number=6, polarity="Negative", name="CT_Ph2_P2_C1_Ret") -M2D.assign_winding(assignment=None, winding_type="Current", is_solid=False, current=PhC_current, parallel_branches=1, - name="Phase_C") -M2D.add_winding_coils(assignment="Phase_C", coils=["CT_Ph2_P2_C2_Ret", "CT_Ph2_P2_C1_Ret"]) - -########################################################## -# Assign total current on PMs -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign a total current of ``0`` on the PMs. - -PM_list = id_PMs -for item in PM_list: - M2D.assign_current(item, amplitude=0, solid=True, name=item + "_I0") - -########################################################## -# Create mesh operations -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create the mesh operations. - -M2D.mesh.assign_length_mesh(id_coils, inside_selection=True, maximum_length=3, maximum_elements=None, name="coils") -M2D.mesh.assign_length_mesh(stator_id, inside_selection=True, maximum_length=3, maximum_elements=None, name="stator") -M2D.mesh.assign_length_mesh(rotor_id, inside_selection=True, maximum_length=3, maximum_elements=None, name="rotor") - -########################################################## -# Turn on eddy effects -# ~~~~~~~~~~~~~~~~~~~~ -# Turn on eddy effects. - -# M2D.eddy_effects_on(eddy_effects_list,activate_eddy_effects=True, activate_displacement_current=False) - -########################################################## -# Turn on core loss -# ~~~~~~~~~~~~~~~~~ -# Turn on core loss. - -core_loss_list = ['Rotor', 'Stator'] -M2D.set_core_losses(core_loss_list) - -########################################################## -# Compute transient inductance -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Compute the transient inductance. - -M2D.change_inductance_computation(compute_transient_inductance=True, incremental_matrix=False) - -########################################################## -# Set model depth -# ~~~~~~~~~~~~~~~ -# Set the model depth. - -M2D.model_depth = "Magnetic_Axial_Length" - -########################################################## -# Set symmetry factor -# ~~~~~~~~~~~~~~~~~~~ -# Set the symmetry factor. - -M2D.change_symmetry_multiplier("SymmetryFactor") - -########################################################## -# Create setup and validate -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the setup and validate it. - -setup = M2D.create_setup(name=setup_name) -setup.props["StopTime"] = "StopTime" -setup.props["TimeStep"] = "TimeStep" -setup.props["SaveFieldsType"] = "None" -setup.props["OutputPerObjectCoreLoss"] = True -setup.props["OutputPerObjectSolidLoss"] = True -setup.props["OutputError"] = True -setup.update() -M2D.validate_simple() - -model = M2D.plot(show=False) -model.plot(os.path.join(M2D.working_directory, "Image.jpg")) - -################################################################################# -# Initialize definitions for output variables -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize the definitions for the output variables. -# These will be used later to generate reports. - -output_vars = { - "Current_A": "InputCurrent(Phase_A)", - "Current_B": "InputCurrent(Phase_B)", - "Current_C": "InputCurrent(Phase_C)", - "Flux_A": "FluxLinkage(Phase_A)", - "Flux_B": "FluxLinkage(Phase_B)", - "Flux_C": "FluxLinkage(Phase_C)", - "pos": "(Moving1.Position -InitialPositionMD) *NumPoles/2", - "cos0": "cos(pos)", - "cos1": "cos(pos-2*PI/3)", - "cos2": "cos(pos-4*PI/3)", - "sin0": "sin(pos)", - "sin1": "sin(pos-2*PI/3)", - "sin2": "sin(pos-4*PI/3)", - "Flux_d": "2/3*(Flux_A*cos0+Flux_B*cos1+Flux_C*cos2)", - "Flux_q": "-2/3*(Flux_A*sin0+Flux_B*sin1+Flux_C*sin2)", - "I_d": "2/3*(Current_A*cos0 + Current_B*cos1 + Current_C*cos2)", - "I_q": "-2/3*(Current_A*sin0 + Current_B*sin1 + Current_C*sin2)", - "Irms": "sqrt(I_d^2+I_q^2)/sqrt(2)", - "ArmatureOhmicLoss_DC": "Irms^2*R_phase", - "Lad": "L(Phase_A,Phase_A)*cos0 + L(Phase_A,Phase_B)*cos1 + L(Phase_A,Phase_C)*cos2", - "Laq": "L(Phase_A,Phase_A)*sin0 + L(Phase_A,Phase_B)*sin1 + L(Phase_A,Phase_C)*sin2", - "Lbd": "L(Phase_B,Phase_A)*cos0 + L(Phase_B,Phase_B)*cos1 + L(Phase_B,Phase_C)*cos2", - "Lbq": "L(Phase_B,Phase_A)*sin0 + L(Phase_B,Phase_B)*sin1 + L(Phase_B,Phase_C)*sin2", - "Lcd": "L(Phase_C,Phase_A)*cos0 + L(Phase_C,Phase_B)*cos1 + L(Phase_C,Phase_C)*cos2", - "Lcq": "L(Phase_C,Phase_A)*sin0 + L(Phase_C,Phase_B)*sin1 + L(Phase_C,Phase_C)*sin2", - "L_d": "(Lad*cos0 + Lbd*cos1 + Lcd*cos2) * 2/3", - "L_q": "(Laq*sin0 + Lbq*sin1 + Lcq*sin2) * 2/3", - "OutputPower": "Moving1.Speed*Moving1.Torque", - "Ui_A": "InducedVoltage(Phase_A)", - "Ui_B": "InducedVoltage(Phase_B)", - "Ui_C": "InducedVoltage(Phase_C)", - "Ui_d": "2/3*(Ui_A*cos0 + Ui_B*cos1 + Ui_C*cos2)", - "Ui_q": "-2/3*(Ui_A*sin0 + Ui_B*sin1 + Ui_C*sin2)", - "U_A": "Ui_A+R_Phase*Current_A", - "U_B": "Ui_B+R_Phase*Current_B", - "U_C": "Ui_C+R_Phase*Current_C", - "U_d": "2/3*(U_A*cos0 + U_B*cos1 + U_C*cos2)", - "U_q": "-2/3*(U_A*sin0 + U_B*sin1 + U_C*sin2)" -} - -########################################################## -# Create output variables for postprocessing -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create output variables for postprocessing. - -for k, v in output_vars.items(): - M2D.create_output_variable(k, v) - -################################################################################# -# Initialize definition for postprocessing plots -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize the definition for postprocessing plots. - -post_params = { - "Moving1.Torque": "TorquePlots" -} -################################################################################# -# Initialize definition for postprocessing multiplots -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize the definition for postprocessing multiplots. - -post_params_multiplot = { # reports - ("U_A", "U_B", "U_C", "Ui_A", "Ui_B", "Ui_C"): "PhaseVoltages", - ("CoreLoss", "SolidLoss", "ArmatureOhmicLoss_DC"): "Losses", - ("InputCurrent(Phase_A)", "InputCurrent(Phase_B)", "InputCurrent(Phase_C)"): "PhaseCurrents", - ("FluxLinkage(Phase_A)", "FluxLinkage(Phase_B)", "FluxLinkage(Phase_C)"): "PhaseFluxes", - ("I_d", "I_q"): "Currents_dq", - ("Flux_d", "Flux_q"): "Fluxes_dq", - ("Ui_d", "Ui_q"): "InducedVoltages_dq", - ("U_d", "U_q"): "Voltages_dq", - ("L(Phase_A,Phase_A)", "L(Phase_B,Phase_B)", "L(Phase_C,Phase_C)", "L(Phase_A,Phase_B)", "L(Phase_A,Phase_C)", - "L(Phase_B,Phase_C)"): "PhaseInductances", - ("L_d", "L_q"): "Inductances_dq", - ("CoreLoss", "CoreLoss(Stator)", "CoreLoss(Rotor)"): "CoreLosses", - ("EddyCurrentLoss", "EddyCurrentLoss(Stator)", "EddyCurrentLoss(Rotor)"): "EddyCurrentLosses (Core)", - ("ExcessLoss", "ExcessLoss(Stator)", "ExcessLoss(Rotor)"): "ExcessLosses (Core)", - ("HysteresisLoss", "HysteresisLoss(Stator)", "HysteresisLoss(Rotor)"): "HysteresisLosses (Core)", - ("SolidLoss", "SolidLoss(IPM1)", "SolidLoss(IPM1_1)", "SolidLoss(OPM1)", "SolidLoss(OPM1_1)"): "SolidLoss" -} - -########################################################## -# Create report -# ~~~~~~~~~~~~~ -# Create a report. - -for k, v in post_params.items(): - M2D.post.create_report(expressions=k, setup_sweep_name="", domain="Sweep", variations=None, - primary_sweep_variable="Time", secondary_sweep_variable=None, report_category=None, - plot_type="Rectangular Plot", context=None, subdesign_id=None, polyline_points=1001, - plot_name=v) - -########################################################## -# Create multiplot report -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Create a multiplot report. - -# for k, v in post_params_multiplot.items(): -# M2D.post.create_report(expressions=list(k), setup_sweep_name="", domain="Sweep", variations=None, -# primary_sweep_variable="Time", secondary_sweep_variable=None, -# report_category=None, plot_type="Rectangular Plot", context=None, subdesign_id=None, -# polyline_points=1001, plotname=v) - - -########################################################## -# Analyze and save project -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Analyze and save the project. - -M2D.save_project() -M2D.analyze_setup(setup_name, use_auto_settings=False) - -########################################################## -# Create flux lines plot on region -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a flux lines plot on a region. The ``object_list`` is -# formerly created when the section is applied. - -faces_reg = mod2D.get_object_faces(object_list[1].name) # Region -plot1 = M2D.post.create_fieldplot_surface(assignment=faces_reg, quantity='Flux_Lines', intrinsics={ - "Time": M2D.variable_manager.variables["StopTime"].evaluated_value}, plot_name="Flux_Lines") - -########################################################## -# Export a field plot to an image file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Export the flux lines plot to an image file using Python PyVista. - -M2D.post.plot_field_from_fieldplot(plot1.name, show=False) - -############################################### -# Get solution data -# ~~~~~~~~~~~~~~~~~ -# Get a simulation result from a solved setup and cast it in a ``SolutionData`` object. -# Plot the desired expression by using Matplotlib plot(). - -solutions = M2D.post.get_solution_data(expressions="Moving1.Torque", - primary_sweep_variable="Time") -#solutions.plot() - -############################################### -# Retrieve the data magnitude of an expression -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# List of shaft torque points and compute average. - -mag = solutions.data_magnitude() -avg = sum(mag) / len(mag) - -############################################### -# Export a report to a file -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Export a 2D Plot data to a .csv file. - -M2D.post.export_report_to_file(output_dir=M2D.toolkit_directory, - plot_name="TorquePlots", - extension=".csv") - -############################################### -# Close AEDT -# ~~~~~~~~~~ -# Close AEDT. - -M2D.release_desktop() diff --git a/examples/03-Maxwell/Maxwell2D_Transformer_LL.py b/examples/03-Maxwell/Maxwell2D_Transformer_LL.py deleted file mode 100644 index 6817586e7eb..00000000000 --- a/examples/03-Maxwell/Maxwell2D_Transformer_LL.py +++ /dev/null @@ -1,274 +0,0 @@ -""" -Transformer leakage inductance calculation in Maxwell 2D Magnetostatic ----------------------------------------------------------------------- -This example shows how you can use pyAEDT to create a Maxwell 2D -magnetostatic analysis to calculate transformer leakage -inductance and reactance. -The analysis based on this document form page 8 on: -https://www.ee.iitb.ac.in/~fclab/FEM/FEM1.pdf -""" - -########################################################## -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ - -import tempfile -from ansys.aedt.core import Maxwell2d - -temp_dir = tempfile.TemporaryDirectory(suffix=".ansys") - -################################## -# Initialize and launch Maxwell 2D -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize and launch Maxwell 2D, providing the version, path to the project, and the design -# name and type. - -non_graphical = False - -project_name = "Transformer_leakage_inductance" -design_name = "1 Magnetostatic" -solver = "MagnetostaticXY" -desktop_version = "2024.2" - -m2d = Maxwell2d(version=desktop_version, - new_desktop=False, - design=design_name, - project=project_name, - solution_type=solver, - non_graphical=non_graphical) - -######################### -# Initialize dictionaries -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize dictionaries that contain all the definitions for the design variables. - -mod = m2d.modeler -mod.model_units = "mm" - -dimensions = { - "core_width": "1097mm", - "core_height": "2880mm", - "core_opening_x1": "270mm", - "core_opening_x2": "557mm", - "core_opening_y1": "540mm", - "core_opening_y2": "2340mm", - "core_opening_width": "core_opening_x2-core_opening_x1", - "core_opening_height": "core_opening_y2-core_opening_y1", - "LV_x1": "293mm", - "LV_x2": "345mm", - "LV_width": "LV_x2-LV_x1", - "LV_mean_radius": "LV_x1+LV_width/2", - "LV_mean_turn_length": "pi*2*LV_mean_radius", - "LV_y1": "620mm", - "LV_y2": "2140mm", - "LV_height": "LV_y2-LV_y1", - "HV_x1": "394mm", - "HV_x2": "459mm", - "HV_width": "HV_x2-HV_x1", - "HV_mean_radius": "HV_x1+HV_width/2", - "HV_mean_turn_length": "pi*2*HV_mean_radius", - "HV_y1": "620mm", - "HV_y2": "2140mm", - "HV_height": "HV_y2-HV_y1", - "HV_LV_gap_radius": "(LV_x2 + HV_x1)/2", - "HV_LV_gap_length": "pi*2*HV_LV_gap_radius", -} - -specifications = { - "Amp_turns": "135024A", - "Frequency": "50Hz", - "HV_turns": "980", - "HV_current": "Amp_turns/HV_turns", -} - -#################################### -# Define variables from dictionaries -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define design variables from the created dictionaries. - -m2d.variable_manager.set_variable(name="Dimensions") - -for k, v in dimensions.items(): - m2d[k] = v - -m2d.variable_manager.set_variable(name="Windings") - -for k, v in specifications.items(): - m2d[k] = v - -########################## -# Create design geometries -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Create transformer core, HV and LV windings, and the region. - -core_id = mod.create_rectangle( - position=[0, 0, 0], - dimension_list=["core_width", "core_height", 0], - name="core", - matname="steel_1008", -) - -core_hole_id = mod.create_rectangle( - position=["core_opening_x1", "core_opening_y1", 0], - dimension_list=["core_opening_width", "core_opening_height", 0], - name="core_hole", -) - -mod.subtract(blank_list=[core_id], tool_list=[core_hole_id], keep_originals=False) - -lv_id = mod.create_rectangle( - position=["LV_x1", "LV_y1", 0], - dimension_list=["LV_width", "LV_height", 0], - name="LV", - matname="copper", -) - -hv_id = mod.create_rectangle( - position=["HV_x1", "HV_y1", 0], - dimension_list=["HV_width", "HV_height", 0], - name="HV", - matname="copper", -) - -# Very small region is enough, because all the flux is concentrated in the core -region_id = mod.create_region( - pad_percent=[20, 10, 0, 10] -) - -########################### -# Assign boundary condition -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign vector potential to zero on all region boundaries. This makes x=0 edge a symmetry boundary. - -region_edges = region_id.edges - -m2d.assign_vector_potential( - input_edge=region_edges, - bound_name="VectorPotential1" -) - -############################## -# Create initial mesh settings -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign a relatively dense mesh to all objects to ensure that the energy is calculated accurately. - -m2d.mesh.assign_length_mesh( - names=["core", "Region", "LV", "HV"], - maxlength=50, - maxel=None, - meshop_name="all_objects" -) - -#################### -# Define excitations -# ~~~~~~~~~~~~~~~~~~ -# Assign the same current in amp-turns but in opposite directions to HV and LV windings. - -m2d.assign_current( - object_list=lv_id, - amplitude="Amp_turns", - name="LV" -) -m2d.assign_current( - object_list=hv_id, - amplitude="Amp_turns", - name="HV", - swap_direction=True -) - -############################## -# Create and analyze the setup -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create and analyze the setup. Setu no. of minimum passes to 3 to ensure accuracy. - -m2d.create_setup( - setupname="Setup1", - MinimumPasses=3 -) -m2d.analyze_setup() - - -######################################################## -# Calculate transformer leakage inductance and reactance -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Calculate transformer leakage inductance from the magnetic energy. - -field_calculator = m2d.ofieldsreporter - -field_calculator.EnterQty("Energy") -field_calculator.EnterSurf("HV") -field_calculator.CalcOp("Integrate") -field_calculator.EnterScalarFunc("HV_mean_turn_length") -field_calculator.CalcOp("*") - -field_calculator.EnterQty("Energy") -field_calculator.EnterSurf("LV") -field_calculator.CalcOp("Integrate") -field_calculator.EnterScalarFunc("LV_mean_turn_length") -field_calculator.CalcOp("*") - -field_calculator.EnterQty("Energy") -field_calculator.EnterSurf("Region") -field_calculator.CalcOp("Integrate") -field_calculator.EnterScalarFunc("HV_LV_gap_length") -field_calculator.CalcOp("*") - -field_calculator.CalcOp("+") -field_calculator.CalcOp("+") - -field_calculator.EnterScalar(2) -field_calculator.CalcOp("*") -field_calculator.EnterScalarFunc("HV_current") -field_calculator.EnterScalarFunc("HV_current") -field_calculator.CalcOp("*") -field_calculator.CalcOp("/") -field_calculator.AddNamedExpression("Leakage_inductance", "Fields") - -field_calculator.CopyNamedExprToStack("Leakage_inductance") -field_calculator.EnterScalar(2) -field_calculator.EnterScalar(3.14159265358979) -field_calculator.EnterScalarFunc("Frequency") -field_calculator.CalcOp("*") -field_calculator.CalcOp("*") -field_calculator.CalcOp("*") -field_calculator.AddNamedExpression("Leakage_reactance", "Fields") - -m2d.post.create_report( - expressions=["Leakage_inductance", "Leakage_reactance"], - report_category="Fields", - primary_sweep_variable="core_width", - plot_type="Data Table", - plotname="Transformer Leakage Inductance", -) - -###################################################################### -# Print leakage inductance and reactance values in the Message Manager -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Print leakage inductance and reactance values in the Message Manager - -m2d.logger.clear_messages() -m2d.logger.info( - "Leakage_inductance = {:.4f}H".format(m2d.post.get_scalar_field_value(quantity_name="Leakage_inductance")) -) -m2d.logger.info( - "Leakage_reactance = {:.2f}Ohm".format(m2d.post.get_scalar_field_value(quantity_name="Leakage_reactance")) -) - -###################################### -# Plot energy in the simulation domain -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Most of the energy is confined in the air between the HV and LV windings. - -object_faces = [] -for name in mod.object_names: - object_faces.extend(m2d.modeler.get_object_faces(name)) - -energy_field_overlay = m2d.post.create_fieldplot_surface( - objlist=object_faces, - quantityName="energy", - plot_name="Energy", -) - -m2d.save_project() -m2d.release_desktop() -temp_dir.cleanup() diff --git a/examples/03-Maxwell/Maxwell2D_Transient.py b/examples/03-Maxwell/Maxwell2D_Transient.py deleted file mode 100644 index db0f2f7bd68..00000000000 --- a/examples/03-Maxwell/Maxwell2D_Transient.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -Maxwell 2D: transient winding analysis --------------------------------------- -This example shows how you can use PyAEDT to create a project in Maxwell 2D -and run a transient simulation. It runs only on Windows using CPython. - -The following libraries are required for the advanced postprocessing features -used in this example: - -- `Matplotlib `_ -- `Numpty `_ -- `PyVista `_ - -Install these libraries with: - -.. code:: - - pip install numpy pyvista matplotlib - -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core -import tempfile - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -########################################################################################### -# Create temporary directory -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create temporary directory. - -temp_dir = tempfile.TemporaryDirectory(suffix=".ansys") - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Insert Maxwell 2D design and save project -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Insert a Maxwell 2D design and save the project. - -maxwell_2d = ansys.aedt.core.Maxwell2d(solution_type="TransientXY", version=aedt_version, non_graphical=non_graphical, - new_desktop=True, project=ansys.aedt.core.generate_unique_project_name()) - -############################################################################### -# Create rectangle and duplicate it -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a rectangle and duplicate it. - -rect1 = maxwell_2d.modeler.create_rectangle([0, 0, 0], [10, 20], name="winding", material="copper") -added = rect1.duplicate_along_line([14, 0, 0]) -rect2 = maxwell_2d.modeler[added[0]] - -############################################################################### -# Create air region -# ~~~~~~~~~~~~~~~~~ -# Create an air region. - -region = maxwell_2d.modeler.create_region([100, 100, 100, 100]) - -############################################################################### -# Assign windings and balloon -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assigns windings to the sheets and a balloon to the air region. - -maxwell_2d.assign_winding([rect1.name, rect2.name], name="PHA") -maxwell_2d.assign_balloon(region.edges) - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model. - -maxwell_2d.plot(show=False, output_file=os.path.join(temp_dir.name, "Image.jpg"), plot_air_objects=True) - -############################################################################### -# Create setup -# ~~~~~~~~~~~~ -# Create the transient setup. - -setup = maxwell_2d.create_setup() -setup.props["StopTime"] = "0.02s" -setup.props["TimeStep"] = "0.0002s" -setup.props["SaveFieldsType"] = "Every N Steps" -setup.props["N Steps"] = "1" -setup.props["Steps From"] = "0s" -setup.props["Steps To"] = "0.002s" - -############################################################################### -# Create rectangular plot -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Create a rectangular plot. - -maxwell_2d.post.create_report("InputCurrent(PHA)", domain="Time", primary_sweep_variable="Time", - plot_name="Winding Plot 1") - -############################################################################### -# Solve model -# ~~~~~~~~~~~ -# Solve the model. - -maxwell_2d.analyze(use_auto_settings=False) - -############################################################################### -# Create output and plot using PyVista -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the output and plot it using PyVista. - -cutlist = ["Global:XY"] -face_lists = rect1.faces -face_lists += rect2.faces -timesteps = [str(i * 2e-4) + "s" for i in range(11)] -id_list = [f.id for f in face_lists] - -gif = maxwell_2d.post.plot_animated_field(quantity="Mag_B", assignment=id_list, plot_type="Surface", - intrinsics={"Time": "0s"}, variation_variable="Time", - variations=timesteps, show=False, export_gif=False) -gif.isometric_view = False -gif.camera_position = [15, 15, 80] -gif.focal_point = [15, 15, 0] -gif.roll_angle = 0 -gif.elevation_angle = 0 -gif.azimuth_angle = 0 -# Set off_screen to False to visualize the animation. -# gif.off_screen = False -gif.animate() - -############################################################################### -# Generate plot outside AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Generate the same plot outside AEDT. - -solutions = maxwell_2d.post.get_solution_data("InputCurrent(PHA)", primary_sweep_variable="Time") -solutions.plot() - -############################################### -# Close AEDT -# ~~~~~~~~~~ -# Close AEDT. - -maxwell_2d.release_desktop() -temp_dir.cleanup() diff --git a/examples/03-Maxwell/Maxwell3DTeam7.py b/examples/03-Maxwell/Maxwell3DTeam7.py deleted file mode 100644 index 14e8fe3a139..00000000000 --- a/examples/03-Maxwell/Maxwell3DTeam7.py +++ /dev/null @@ -1,429 +0,0 @@ -""" -Maxwell 3D: asymmetric conductor analysis ------------------------------------------ -This example uses PyAEDT to set up the TEAM 7 problem for an asymmetric -conductor with a hole and solve it using the Maxwell 3D Eddy Current solver. -https://www.compumag.org/wp/wp-content/uploads/2018/06/problem7.pdf -""" -########################################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import numpy as np -import os -import tempfile - -from ansys.aedt.core import Maxwell3d -from ansys.aedt.core.generic.general_methods import write_csv - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -########################################################################################### -# Create temporary directory -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create temporary directory. - -temp_dir = tempfile.TemporaryDirectory(suffix=".ansys") - -########################################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -########################################################################################### -# Launch AEDT and Maxwell 3D -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT and Maxwell 3D. The following code sets up the project and design names, the solver, and -# the version. It also creates an instance of the ``Maxwell3d`` class named ``m3d``. - -project_name = "COMPUMAG" -design_name = "TEAM 7 Asymmetric Conductor" -solver = "EddyCurrent" - -m3d = Maxwell3d( - project=project_name, - design=design_name, - solution_type=solver, - version=aedt_version, - non_graphical=non_graphical, - new_desktop=True -) -m3d.modeler.model_units = "mm" - -########################################################################################### -# Add Maxwell 3D setup -# ~~~~~~~~~~~~~~~~~~~~ -# Add a Maxwell 3D setup with frequency points at DC, 50 Hz, and 200Hz. -# Otherwise, the default PyAEDT setup values are used. To approximate a DC field in the -# Eddy Current solver, use a low frequency value. Second-order shape functions improve -# the smoothness of the induced currents in the plate. - -dc_freq = 0.1 -stop_freq = 50 - -setup = m3d.create_setup(name="Setup1") -setup.props["Frequency"] = "200Hz" -setup.props["HasSweepSetup"] = True -setup.add_eddy_current_sweep("LinearStep", dc_freq, stop_freq, stop_freq - dc_freq, clear=True) -setup.props["UseHighOrderShapeFunc"] = True -setup.props["PercentError"] = 0.4 -setup.update() - -########################################################################################### -# Define coil dimensions -# ~~~~~~~~~~~~~~~~~~~~~~ -# Define coil dimensions as shown on the TEAM7 drawing of the coil. - -coil_external = 150 + 25 + 25 -coil_internal = 150 -coil_r1 = 25 -coil_r2 = 50 -coil_thk = coil_r2 - coil_r1 -coil_height = 100 -coil_centre = [294 - 25 - 150 / 2, 25 + 150 / 2, 19 + 30 + 100 / 2] - -# Use expressions to construct the three dimensions needed to describe the midpoints of -# the coil. - -dim1 = coil_internal / 2 + (coil_external - coil_internal) / 4 -dim2 = coil_internal / 2 - coil_r1 -dim3 = dim2 + np.sqrt(((coil_r1 + (coil_r2 - coil_r1) / 2) ** 2) / 2) - -# Use coordinates to draw a polyline along which to sweep the coil cross sections. -P1 = [dim1, -dim2, 0] -P2 = [dim1, dim2, 0] -P3 = [dim3, dim3, 0] -P4 = [dim2, dim1, 0] - -########################################################################################### -# Create coordinate system for positioning coil -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a coordinate system for positioning the coil. - -m3d.modeler.create_coordinate_system(origin=coil_centre, mode="view", view="XY", name="Coil_CS") - -########################################################################################### -# Create polyline -# ~~~~~~~~~~~~~~~ -# Create a polyline. One quarter of the coil is modeled by sweeping a 2D sheet along a polyline. - -test = m3d.modeler.create_polyline(points=[P1, P2, P3, P4], segment_type=["Line", "Arc"], name="Coil") -test.set_crosssection_properties(type="Rectangle", width=coil_thk, height=coil_height) - -########################################################################################### -# Duplicate and unite polyline to create full coil -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Duplicate and unit the polyline to create a full coil. - -m3d.modeler.duplicate_around_axis( - "Coil", axis="Global", angle=90, clones=4, create_new_objects=True, is_3d_comp=False -) -m3d.modeler.unite("Coil, Coil_1, Coil_2") -m3d.modeler.unite("Coil, Coil_3") -m3d.modeler.fit_all() - -########################################################################################### -# Assign material and if solution is allowed inside coil -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign the material ``Cooper`` from the Maxwell internal library to the coil and -# allow a solution inside the coil. - -m3d.assign_material("Coil", "Copper") -m3d.solve_inside("Coil") - -########################################################################################### -# Create terminal -# ~~~~~~~~~~~~~~~ -# Create a terminal for the coil from a cross-section that is split and one half deleted. - -m3d.modeler.section("Coil", "YZ") -m3d.modeler.separate_bodies("Coil_Section1") -m3d.modeler.delete("Coil_Section1_Separate1") - -# Add variable for coil excitation -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Add a design variable for coil excitation. The NB units here are AmpereTurns. - -Coil_Excitation = 2742 -m3d["Coil_Excitation"] = str(Coil_Excitation) + "A" -m3d.assign_current(assignment="Coil_Section1", amplitude="Coil_Excitation", solid=False) -m3d.modeler.set_working_coordinate_system("Global") - -########################################################################################### -# Add a material -# ~~~~~~~~~~~~~~ -# Add a material named ``team3_aluminium``. - -mat = m3d.materials.add_material("team7_aluminium") -mat.conductivity = 3.526e7 - -########################################################################################### -# Model aluminium plate with a hole -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Model the aluminium plate with a hole by subtracting two rectangular cuboids. - -plate = m3d.modeler.create_box(origin=[0, 0, 0], sizes=[294, 294, 19], name="Plate", material="team7_aluminium") -m3d.modeler.fit_all() -m3d.modeler.create_box(origin=[18, 18, 0], sizes=[108, 108, 19], name="Hole") -m3d.modeler.subtract(blank_list="Plate", tool_list=["Hole"], keep_originals=False) - -########################################################################################### -# Draw a background region -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Draw a background region that uses the default properties for an air region. - -m3d.modeler.create_air_region(x_pos=100, y_pos=100, z_pos=100, x_neg=100, y_neg=100, z_neg=100) - -################################################################################ -# Adjust eddy effects for plate and coil -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Adjust the eddy effects for the plate and coil by turning off displacement currents -# for all parts. The setting for eddy effect is ignored for the stranded conductor type -# used in the coil. - -m3d.eddy_effects_on(assignment="Plate") -m3d.eddy_effects_on(assignment=["Coil", "Region", "Line_A1_B1mesh", "Line_A2_B2mesh"], enable_eddy_effects=False, - enable_displacement_current=False) - -################################################################################ -# Create expression for Z component of B in Gauss -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create an expression for the Z component of B in Gauss using the fields calculator. - -Fields = m3d.ofieldsreporter -Fields.CalcStack("clear") -Fields.EnterQty("B") -Fields.CalcOp("ScalarZ") -Fields.EnterScalarFunc("Phase") -Fields.CalcOp("AtPhase") -Fields.EnterScalar(10000) -Fields.CalcOp("*") -Fields.CalcOp("Smooth") -Fields.AddNamedExpression("Bz", "Fields") - -################################################################################ -# Draw two lines along which to plot Bz -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Draw two lines along which to plot Bz. The following code also adds a small cylinder -# to refine the mesh locally around each line. - -lines = ["Line_A1_B1", "Line_A2_B2"] -mesh_diameter = "2mm" - -line_points_1 = [["0mm", "72mm", "34mm"], ["288mm", "72mm", "34mm"]] -polyline = m3d.modeler.create_polyline(points=line_points_1, name=lines[0]) -l1_mesh = m3d.modeler.create_polyline(points=line_points_1, name=lines[0] + "mesh") -l1_mesh.set_crosssection_properties(type="Circle", width=mesh_diameter) - -line_points_2 = [["0mm", "144mm", "34mm"], ["288mm", "144mm", "34mm"]] -polyline2 = m3d.modeler.create_polyline(points=line_points_2, name=lines[1]) -l2_mesh = m3d.modeler.create_polyline(points=line_points_2, name=lines[1] + "mesh") -l2_mesh.set_crosssection_properties(type="Circle", width=mesh_diameter) - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model. - -m3d.plot(show=False, output_file=os.path.join(temp_dir.name, "model.jpg"), plot_air_objects=False) - -################################################################################ -# Published measurement results are included with this script via the list below. -# Test results are used to create text files for import into a rectangular plot -# and to overlay simulation results. - -dataset = [ - "Bz A1_B1 000 0", - "Bz A1_B1 050 0", - "Bz A1_B1 050 90", - "Bz A1_B1 200 0", - "Bz A1_B1 200 90", - "Bz A2_B2 050 0", - "Bz A2_B2 050 90", - "Bz A2_B2 200 0", - "Bz A2_B2 200 90", -] -header = ["Distance [mm]", "Bz [Tesla]"] - -line_length = [0, 18, 36, 54, 72, 90, 108, 126, 144, 162, 180, 198, 216, 234, 252, 270, 288] -data = [ - [ - -6.667, - -7.764, - -8.707, - -8.812, - -5.870, - 8.713, - 50.40, - 88.47, - 100.9, - 104.0, - 104.8, - 104.9, - 104.6, - 103.1, - 97.32, - 75.19, - 29.04, - ], - [ - 4.90, - -17.88, - -22.13, - -20.19, - -15.67, - 0.36, - 43.64, - 78.11, - 71.55, - 60.44, - 53.91, - 52.62, - 53.81, - 56.91, - 59.24, - 52.78, - 27.61, - ], - [-1.16, 2.84, 4.15, 4.00, 3.07, 2.31, 1.89, 4.97, 12.61, 14.15, 13.04, 12.40, 12.05, 12.27, 12.66, 9.96, 2.36], - [ - -3.63, - -18.46, - -23.62, - -21.59, - -16.09, - 0.23, - 44.35, - 75.53, - 63.42, - 53.20, - 48.66, - 47.31, - 48.31, - 51.26, - 53.61, - 46.11, - 24.96, - ], - [-1.38, 1.20, 2.15, 1.63, 1.10, 0.27, -2.28, -1.40, 4.17, 3.94, 4.86, 4.09, 3.69, 4.60, 3.48, 4.10, 0.98], - [ - -1.83, - -8.50, - -13.60, - -15.21, - -14.48, - -5.62, - 28.77, - 60.34, - 61.84, - 56.64, - 53.40, - 52.36, - 53.93, - 56.82, - 59.48, - 52.08, - 26.56, - ], - [-1.63, -0.60, -0.43, 0.11, 1.26, 3.40, 6.53, 10.25, 11.83, 11.83, 11.01, 10.58, 10.80, 10.54, 10.62, 9.03, 1.79], - [ - -0.86, - -7.00, - -11.58, - -13.36, - -13.77, - -6.74, - 24.63, - 53.19, - 54.89, - 50.72, - 48.03, - 47.13, - 48.25, - 51.35, - 53.35, - 45.37, - 24.01, - ], - [-1.35, -0.71, -0.81, -0.67, 0.15, 1.39, 2.67, 3.00, 4.01, 3.80, 4.00, 3.02, 2.20, 2.78, 1.58, 1.37, 0.93], -] - -################################################################################ -# Write dataset values in a CSV file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Dataset details are used to encode test parameters in the text files. -# For example, ``Bz A1_B1 050 0`` is the Z component of flux density ``B``. -# along line ``A1_B1`` at 50 Hz and 0 deg. - -line_length.insert(0, header[0]) -for i in range(len(dataset)): - data[i].insert(0, header[1]) - ziplist = zip(line_length, data[i]) - file_path = os.path.join(temp_dir.name, str(dataset[i]) + ".csv") - write_csv(output_file=file_path, list_data=ziplist) - -################################################################################ -# Create rectangular plots and import test data into report -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create rectangular plots, using text file encoding to control their formatting. -# Import test data into correct plot and overlay with simulation results. -# Variations for a DC plot must have different frequency and phase than the other plots. - -for item in range(len(dataset)): - if item % 2 == 0: - t = dataset[item] - plot_name = t[0:3] + "Along the Line" + t[2:9] + ", " + t[9:12] + "Hz" - if t[9:12] == "000": - variations = { - "Distance": ["All"], - "Freq": [str(dc_freq) + "Hz"], - "Phase": ["0deg"], - "Coil_Excitation": ["All"], - } - else: - variations = { - "Distance": ["All"], - "Freq": [t[9:12] + "Hz"], - "Phase": ["0deg", "90deg"], - "Coil_Excitation": ["All"], - } - report = m3d.post.create_report(expressions=t[0:2], variations=variations, primary_sweep_variable="Distance", - report_category="Fields", context="Line_" + t[3:8], plot_name=plot_name) - file_path = os.path.join(temp_dir.name, str(dataset[i]) + ".csv") - report.import_traces(file_path, plot_name) - -################################################################################################### -# Analyze project -# ~~~~~~~~~~~~~~~ -# Analyze the project. - -m3d.analyze() - -################################################################################################### -# Create plots of induced current and flux density on surface of plate -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create two plots of the induced current (``Mag_J``) and the flux density (``Mag_B``) on the -# surface of the plate. - -surf_list = m3d.modeler.get_object_faces("Plate") -intrinsic_dict = {"Freq": "200Hz", "Phase": "0deg"} -m3d.post.create_fieldplot_surface(surf_list, "Mag_J", intrinsics=intrinsic_dict, plot_name="Mag_J") -m3d.post.create_fieldplot_surface(surf_list, "Mag_B", intrinsics=intrinsic_dict, plot_name="Mag_B") -m3d.post.create_fieldplot_surface(surf_list, "Mesh", intrinsics=intrinsic_dict, plot_name="Mesh") - -#################################################################################################### -# Release AEDT and clean up temporary directory -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Release AEDT and remove both the project and temporary directories. - -m3d.release_desktop(True, True) -temp_dir.cleanup() diff --git a/examples/03-Maxwell/Maxwell3D_Choke.py b/examples/03-Maxwell/Maxwell3D_Choke.py deleted file mode 100644 index 4d8c60ed9ab..00000000000 --- a/examples/03-Maxwell/Maxwell3D_Choke.py +++ /dev/null @@ -1,222 +0,0 @@ -""" -Maxwell 3D: choke setup ------------------------ -This example shows how you can use PyAEDT to create a choke setup in Maxwell 3D. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import json -import os -import ansys.aedt.core -import tempfile - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -########################################################################################### -# Create temporary directory -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create temporary directory. - -temp_dir = tempfile.TemporaryDirectory(suffix=".ansys") - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can define ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Launch Maxwell3D -# ~~~~~~~~~~~~~~~~ -# Launch Maxwell 3D 2023 R2 in graphical mode. - -m3d = ansys.aedt.core.Maxwell3d(project=ansys.aedt.core.generate_unique_project_name(), - solution_type="EddyCurrent", - version=aedt_version, - non_graphical=non_graphical, - new_desktop=True - ) - -############################################################################### -# Rules and information of use -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# The dictionary values containing the different parameters of the core and -# the windings that compose the choke. You must not change the main structure of -# the dictionary. The dictionary has many primary keys, including -# ``"Number of Windings"``, ``"Layer"``, and ``"Layer Type"``, that have -# dictionaries as values. The keys of these dictionaries are secondary keys -# of the dictionary values, such as ``"1"``, ``"2"``, ``"3"``, ``"4"``, and -# ``"Simple"``. -# -# You must not modify the primary or secondary keys. You can modify only their values. -# You must not change the data types for these keys. For the dictionaries from -# ``"Number of Windings"`` through ``"Wire Section"``, values must be Boolean. Only -# one value per dictionary can be ``"True"``. If all values are ``True``, only the first one -# remains set to ``True``. If all values are ``False``, the first value is chosen as the -# correct one by default. For the dictionaries from ``"Core"`` through ``"Inner Winding"``, -# values must be strings, floats, or integers. -# -# Descriptions follow for primary keys: -# -# - ``"Number of Windings"``: Number of windings around the core -# - ``"Layer"``: Number of layers of all windings -# - ``"Layer Type"``: Whether layers of a winding are linked to each other -# - ``"Similar Layer"``: Whether layers of a winding have the same number of turns and same spacing between turns -# - ``"Mode"``: When there are only two windows, whether they are in common or differential mode -# - ``"Wire Section"``: Type of wire section and number of segments -# - ``"Core"``: Design of the core -# - ``"Outer Winding"``: Design of the first layer or outer layer of a winding and the common parameters for all layers -# - ``"Mid Winding"``: Turns and turns spacing ("Coil Pit") for the second or mid layer if it is necessary -# - ``"Inner Winding"``: Turns and turns spacing ("Coil Pit") for the third or inner layer if it is necessary -# - ``"Occupation(%)"``: An informative parameter that is useless to modify -# -# The following parameter values work. You can modify them if you want. - -values = { - "Number of Windings": {"1": False, "2": False, "3": True, "4": False}, - "Layer": {"Simple": False, "Double": False, "Triple": True}, - "Layer Type": {"Separate": False, "Linked": True}, - "Similar Layer": {"Similar": False, "Different": True}, - "Mode": {"Differential": True, "Common": False}, - "Wire Section": {"None": False, "Hexagon": False, "Octagon": True, "Circle": False}, - "Core": { - "Name": "Core", - "Material": "ferrite", - "Inner Radius": 100, - "Outer Radius": 143, - "Height": 25, - "Chamfer": 0.8, - }, - "Outer Winding": { - "Name": "Winding", - "Material": "copper", - "Inner Radius": 100, - "Outer Radius": 143, - "Height": 25, - "Wire Diameter": 5, - "Turns": 2, - "Coil Pit(deg)": 4, - "Occupation(%)": 0, - }, - "Mid Winding": {"Turns": 7, "Coil Pit(deg)": 4, "Occupation(%)": 0}, - "Inner Winding": {"Turns": 10, "Coil Pit(deg)": 4, "Occupation(%)": 0}, -} - -############################################################################### -# Convert dictionary to JSON file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Covert a dictionary to a JSON file. PyAEDT methods ask for the path of the -# JSON file as an argument. You can convert a dictionary to a JSON file. - -json_path = os.path.join(temp_dir.name, "choke_example.json") - -with open(json_path, "w") as outfile: - json.dump(values, outfile) - -############################################################################### -# Verify parameters of JSON file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Verify parameters of the JSON file. The ``check_choke_values`` method takes -# the JSON file path as an argument and does the following: -# -# - Checks if the JSON file is correctly written (as explained in the rules) -# - Checks inequations on windings parameters to avoid having unintended intersections - -dictionary_values = m3d.modeler.check_choke_values(json_path, create_another_file=False) -print(dictionary_values) - -############################################################################### -# Create choke -# ~~~~~~~~~~~~ -# Create the choke. The ``create_choke`` method takes the JSON file path as an -# argument. - -list_object = m3d.modeler.create_choke(json_path) -print(list_object) -core = list_object[1] -first_winding_list = list_object[2] -second_winding_list = list_object[3] -third_winding_list = list_object[4] - -############################################################################### -# Assign excitations -# ~~~~~~~~~~~~~~~~~~ -# Assign excitations. - -first_winding_faces = m3d.modeler.get_object_faces(first_winding_list[0].name) -second_winding_faces = m3d.modeler.get_object_faces(second_winding_list[0].name) -third_winding_faces = m3d.modeler.get_object_faces(third_winding_list[0].name) -m3d.assign_current([first_winding_faces[-1]], amplitude=1000, phase="0deg", swap_direction=False, name="phase_1_in") -m3d.assign_current([first_winding_faces[-2]], amplitude=1000, phase="0deg", swap_direction=True, name="phase_1_out") -m3d.assign_current([second_winding_faces[-1]], amplitude=1000, phase="120deg", swap_direction=False, name="phase_2_in") -m3d.assign_current([second_winding_faces[-2]], amplitude=1000, phase="120deg", swap_direction=True, name="phase_2_out") -m3d.assign_current([third_winding_faces[-1]], amplitude=1000, phase="240deg", swap_direction=False, name="phase_3_in") -m3d.assign_current([third_winding_faces[-2]], amplitude=1000, phase="240deg", swap_direction=True, name="phase_3_out") - -############################################################################### -# Assign matrix -# ~~~~~~~~~~~~~ -# Assign the matrix. - -m3d.assign_matrix(["phase_1_in", "phase_2_in", "phase_3_in"], matrix_name="current_matrix") - -############################################################################### -# Create mesh operation -# ~~~~~~~~~~~~~~~~~~~~~ -# Create the mesh operation. - -mesh = m3d.mesh -mesh.assign_skin_depth(assignment=[first_winding_list[0], second_winding_list[0], third_winding_list[0]], - skin_depth=0.20, triangulation_max_length="10mm", name="skin_depth") -mesh.assign_surface_mesh_manual([first_winding_list[0], second_winding_list[0], third_winding_list[0]], - surface_deviation=None, normal_dev="30deg", name="surface_approx") - -############################################################################### -# Create boundaries -# ~~~~~~~~~~~~~~~~~ -# Create the boundaries. A region with openings is needed to run the analysis. - -region = m3d.modeler.create_air_region(x_pos=100, y_pos=100, z_pos=100, x_neg=100, y_neg=100, z_neg=0) - -############################################################################### -# Create setup -# ~~~~~~~~~~~~ -# Create a setup with a sweep to run the simulation. Depending on your machine's -# computing power, the simulation can take some time to run. - -setup = m3d.create_setup("MySetup") -print(setup.props) -setup.props["Frequency"] = "100kHz" -setup.props["PercentRefinement"] = 15 -setup.props["MaximumPasses"] = 10 -setup.props["HasSweepSetup"] = True -setup.add_eddy_current_sweep(range_type="LinearCount", start=100, end=1000, count=12, units="kHz", clear=True) - -############################################################################### -# Save project -# ~~~~~~~~~~~~ -# Save the project. - -m3d.save_project() -m3d.modeler.fit_all() -m3d.plot(show=False, output_file=os.path.join(temp_dir.name, "Image.jpg"), plot_air_objects=True) - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.release_desktop` method. -# All methods provide for saving the project before closing. - -m3d.release_desktop() -temp_dir.cleanup() diff --git a/examples/03-Maxwell/Maxwell3D_Segmentation.py b/examples/03-Maxwell/Maxwell3D_Segmentation.py deleted file mode 100644 index 5d25a48dfc6..00000000000 --- a/examples/03-Maxwell/Maxwell3D_Segmentation.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -Maxwell 3D: magnet segmentation -------------------------------- -This example shows how you can use PyAEDT to segment magnets of an electric motor. -The method is valid and usable for any object the user would like to segment. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -from ansys.aedt.core import downloads -from ansys.aedt.core import Maxwell3d - -import tempfile - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -########################################################################################### -# Create temporary directory -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create temporary directory. - -temp_dir = tempfile.TemporaryDirectory(suffix=".ansys") - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -################################################################################# -# Download .aedt file example -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set local temporary folder to export the .aedt file to. - -aedt_file = downloads.download_file("object_segmentation", "Motor3D_obj_segments.aedt", temp_dir.name) - -################################################################################## -# Launch Maxwell 3D -# ~~~~~~~~~~~~~~~~~ -# Launch Maxwell 3D. - -m3d = Maxwell3d(project=aedt_file, - version=aedt_version, - new_desktop=True, - non_graphical=non_graphical) - -################################################################################## -# Create object to access 3D modeler -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the object ``mod3D`` to access the 3D modeler easily. - -modeler = m3d.modeler - -################################################################################## -# Segment first magnet by specifying the number of segments -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Select first magnet to segment by specifying the number of segments. -# The method accepts in input either the list of magnets names to segment or -# magnets ids or the magnet object :class:`ansys.aedt.core.modeler.cad.object_3d.Object3d`. -# ``apply_mesh_sheets`` is enabled which means that the mesh sheets will -# be applied in the geometry too. -# In this specific case we give as input the name of the magnet. - -segments_number = 5 -object_name = "PM_I1" -sheets_1 = modeler.objects_segmentation(object_name, segments=segments_number, apply_mesh_sheets=True) - -################################################################################## -# Segment second magnet by specifying the number of segments -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Select second magnet to segment by specifying the number of segments. -# In this specific case we give as input the id of the magnet. - -segments_number = 4 -object_name = "PM_I1_1" -magnet_id = [obj.id for obj in modeler.object_list if obj.name == object_name][0] -sheets_2 = modeler.objects_segmentation(magnet_id, segments=segments_number, apply_mesh_sheets=True) - -################################################################################## -# Segment third magnet by specifying the segmentation thickness -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Select third magnet to segment by specifying the segmentation thickness. -# In this specific case we give as input the magnet object of type :class:`ansys.aedt.core.modeler.cad.object_3d.Object3d`. - -segmentation_thickness = 1 -object_name = "PM_O1" -magnet = [obj for obj in modeler.object_list if obj.name == object_name][0] -sheets_3 = modeler.objects_segmentation(magnet, segmentation_thickness=segmentation_thickness, apply_mesh_sheets=True) - -################################################################################## -# Segment fourth magnet by specifying the number of segments -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Select fourth magnet to segment by specifying the number of segments. -# In this specific case we give as input the name of the magnet and we disable the mesh sheets. - -object_name = "PM_O1_1" -segments_number = 10 -sheets_4 = modeler.objects_segmentation(object_name, segments=segments_number) - -################################################################################### -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Save the project and close AEDT. - -m3d.save_project() -m3d.release_desktop() -temp_dir.cleanup() diff --git a/examples/03-Maxwell/Maxwell3D_Team3_bath_plate.py b/examples/03-Maxwell/Maxwell3D_Team3_bath_plate.py deleted file mode 100644 index c70eb5f6881..00000000000 --- a/examples/03-Maxwell/Maxwell3D_Team3_bath_plate.py +++ /dev/null @@ -1,240 +0,0 @@ -""" -Maxwell 3D: bath plate analysis -------------------------------- -This example uses PyAEDT to set up the TEAM 3 bath plate problem and -solve it using the Maxwell 3D Eddy Current solver. -https://www.compumag.org/wp/wp-content/uploads/2018/06/problem3.pdf -""" -################################################################################## -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core -import tempfile - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -########################################################################################### -# Create temporary directory -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create temporary directory. - -temp_dir = tempfile.TemporaryDirectory(suffix=".ansys") - -################################################################################## -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -################################################################################## -# Launch AEDT and Maxwell 3D -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT and Maxwell 3D after first setting up the project and design names, -# the solver, and the version. The following code also creates an instance of the -# ``Maxwell3d`` class named ``M3D``. - -project_name = os.path.join(temp_dir.name, "COMPUMAG.aedt") -design_name = "TEAM 3 Bath Plate" -solver = "EddyCurrent" - -m3d = ansys.aedt.core.Maxwell3d( - project=project_name, - design=design_name, - solution_type=solver, - version=aedt_version, - non_graphical=non_graphical, - new_desktop=True, -) -m3d.modeler.model_units = "mm" - -############################################################################### -# Add variable -# ~~~~~~~~~~~~ -# Add a design variable named ``Coil_Position`` that you use later to adjust the -# position of the coil. - -Coil_Position = -20 -m3d["Coil_Position"] = str(Coil_Position) + m3d.modeler.model_units - -################################################################################ -# Add material -# ~~~~~~~~~~~~ -# Add a material named ``team3_aluminium`` for the ladder plate. - -mat = m3d.materials.add_material("team3_aluminium") -mat.conductivity = 32780000 - -############################################################################### -# Draw background region -# ~~~~~~~~~~~~~~~~~~~~~~ -# Draw a background region that uses the default properties for an air region. - -m3d.modeler.create_air_region(x_pos=100, y_pos=100, z_pos=100, x_neg=100, y_neg=100, z_neg=100) - -################################################################################ -# Draw ladder plate and assign material -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Draw a ladder plate and assign it the newly created material ``team3_aluminium``. - -m3d.modeler.create_box(origin=[-30, -55, 0], sizes=[60, 110, -6.35], name="LadderPlate", material="team3_aluminium") -m3d.modeler.create_box(origin=[-20, -35, 0], sizes=[40, 30, -6.35], name="CutoutTool1") -m3d.modeler.create_box(origin=[-20, 5, 0], sizes=[40, 30, -6.35], name="CutoutTool2") -m3d.modeler.subtract("LadderPlate", ["CutoutTool1", "CutoutTool2"], keep_originals=False) - -################################################################################ -# Add mesh refinement to ladder plate -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Add a mesh refinement to the ladder plate. - -m3d.mesh.assign_length_mesh("LadderPlate", maximum_length=3, maximum_elements=None, name="Ladder_Mesh") - -################################################################################ -# Draw search coil and assign excitation -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Draw a search coil and assign it a ``stranded`` current excitation. -# The stranded type forces the current density to be constant in the coil. - -m3d.modeler.create_cylinder(orientation="Z", origin=[0, "Coil_Position", 15], radius=40, height=20, name="SearchCoil", - material="copper") -m3d.modeler.create_cylinder(orientation="Z", origin=[0, "Coil_Position", 15], radius=20, height=20, name="Bore", - material="copper") -m3d.modeler.subtract("SearchCoil", "Bore", keep_originals=False) -m3d.modeler.section("SearchCoil", "YZ") -m3d.modeler.separate_bodies("SearchCoil_Section1") -m3d.modeler.delete("SearchCoil_Section1_Separate1") -m3d.assign_current(assignment=["SearchCoil_Section1"], amplitude=1260, solid=False, name="SearchCoil_Excitation") - -################################################################################ -# Draw a line for plotting Bz -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Draw a line for plotting Bz later. Bz is the Z component of the flux -# density. The following code also adds a small diameter cylinder to refine the -# mesh locally around the line. - -line_points = [["0mm", "-55mm", "0.5mm"], ["0mm", "55mm", "0.5mm"]] -m3d.modeler.create_polyline(points=line_points, name="Line_AB") -poly = m3d.modeler.create_polyline(points=line_points, name="Line_AB_MeshRefinement") -poly.set_crosssection_properties(type="Circle", width="0.5mm") - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model. - -m3d.plot(show=False, output_file=os.path.join(temp_dir.name, "Image.jpg"), plot_air_objects=False) - -############################################################################### -# Add Maxwell 3D setup -# ~~~~~~~~~~~~~~~~~~~~ -# Add a Maxwell 3D setup with frequency points at 50 Hz and 200 Hz. - -setup = m3d.create_setup(name="Setup1") -setup.props["Frequency"] = "200Hz" -setup.props["HasSweepSetup"] = True -setup.add_eddy_current_sweep(range_type="LinearStep", start=50, end=200, count=150, clear=True) - -################################################################################ -# Adjust eddy effects -# ~~~~~~~~~~~~~~~~~~~ -# Adjust eddy effects for the ladder plate and the search coil. The setting for -# eddy effect is ignored for the stranded conductor type used in the search coil. - -m3d.eddy_effects_on(assignment=["LadderPlate"], enable_eddy_effects=True, enable_displacement_current=True) -m3d.eddy_effects_on(assignment=["SearchCoil"], enable_eddy_effects=False, enable_displacement_current=True) - -################################################################################ -# Add linear parametric sweep -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Add a linear parametric sweep for the two coil positions. - -sweep_name = "CoilSweep" -param = m3d.parametrics.add("Coil_Position", -20, 0, 20, "LinearStep", name=sweep_name) -param["SaveFields"] = True -param["CopyMesh"] = False -param["SolveWithCopiedMeshOnly"] = True - -################################################################################ -# Solve parametric sweep -# ~~~~~~~~~~~~~~~~~~~~~~ -# Solve the parametric sweep directly so that results of all variations are available. - -m3d.analyze_setup(sweep_name) - -############################################################################### -# Create expression for Bz -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Create an expression for Bz using the fields calculator. - -Fields = m3d.ofieldsreporter -Fields.EnterQty("B") -Fields.CalcOp("ScalarZ") -Fields.EnterScalar(1000) -Fields.CalcOp("*") -Fields.CalcOp("Smooth") -Fields.AddNamedExpression("Bz", "Fields") - -############################################################################### -# Plot mag(Bz) as a function of frequency -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Plot mag(Bz) as a function of frequency for both coil positions. - -variations = {"Distance": ["All"], "Freq": ["All"], "Phase": ["0deg"], "Coil_Position": ["All"]} -m3d.post.create_report(expressions="mag(Bz)", variations=variations, primary_sweep_variable="Distance", - report_category="Fields", context="Line_AB", plot_name="mag(Bz) Along 'Line_AB' Coil") - -############################################################################### -# Get simulation results from a solved setup -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Get simulation results from a solved setup as a ``SolutionData`` object. - -solutions = m3d.post.get_solution_data( - expressions="mag(Bz)", - report_category="Fields", - context="Line_AB", - variations=variations, - primary_sweep_variable="Distance", -) - -############################################################################### -# Set up sweep value and plot solution -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set up a sweep value and plot the solution. - -solutions.active_variation["Coil_Position"] = -0.02 -solutions.plot() - -############################################################################### -# Change sweep value and plot solution -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Change the sweep value and plot the solution again. - -solutions.active_variation["Coil_Position"] = 0 -solutions.plot() - -############################################################################### -# Plot induced current density on surface of ladder plate -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Plot the induced current density, ``"Mag_J"``, on the surface of the ladder plate. - -ladder_plate = m3d.modeler.objects_by_name["LadderPlate"] -intrinsic_dict = {"Freq": "50Hz", "Phase": "0deg"} -m3d.post.create_fieldplot_surface(ladder_plate.faces, "Mag_J", intrinsics=intrinsic_dict, plot_name="Mag_J") - -############################################################################### -# Release AEDT -# ~~~~~~~~~~~~ -# Release AEDT from the script engine, leaving both AEDT and the project open. - -m3d.release_desktop() - -temp_dir.cleanup() diff --git a/examples/03-Maxwell/Maxwell_Control_Program.py b/examples/03-Maxwell/Maxwell_Control_Program.py deleted file mode 100644 index 81ed5306ad0..00000000000 --- a/examples/03-Maxwell/Maxwell_Control_Program.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -Enabling Control Program in a Maxwell 2D Project ------------------------------------------------- -This example shows how you can use PyAEDT to enable control program in a Maxwell 2D project. -It shows how to create the geometry, load material properties from an Excel file and -set up the mesh settings. Moreover, it focuses on post-processing operations, in particular how to -plot field line traces, relevant for an electrostatic analysis. - -""" -################################################################################# -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -from ansys.aedt.core import downloads -from ansys.aedt.core.generic.general_methods import generate_unique_folder_name -from ansys.aedt.core import Maxwell2d - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -################################################################################# -# Download .aedt file example -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set local temporary folder to export the .aedt file to. -temp_folder = generate_unique_folder_name() -aedt_file = downloads.download_file("maxwell_ctrl_prg", "ControlProgramDemo.aedt", temp_folder) -ctrl_prg_file = downloads.download_file("maxwell_ctrl_prg", "timestep_only.py", temp_folder) - -################################################################################## -# Launch Maxwell 2D -# ~~~~~~~~~~~~~~~~~ -# Launch Maxwell 2D. - -m2d = Maxwell2d(project=aedt_file, - version=aedt_version, - new_desktop=True, - non_graphical=non_graphical) - -################################################################################## -# Set active design -# ~~~~~~~~~~~~~~~~~ -# Set active design. - -m2d.set_active_design("1 time step control") - -################################################################################## -# Get design setup -# ~~~~~~~~~~~~~~~~ -# Get design setup to enable the control program to. - -setup = m2d.setups[0] - -################################################################################## -# Enable control program -# ~~~~~~~~~~~~~~~~~~~~~~ -# Enable control program by giving the path to the file. - -setup.enable_control_program(control_program_path=ctrl_prg_file) - -################################################################################## -# Analyze setup -# ~~~~~~~~~~~~~ -# Analyze setup. - -setup.analyze() - - -################################################################################## -# Plot results -# ~~~~~~~~~~~~ -# Plot Solved Results. - -sols = m2d.post.get_solution_data("FluxLinkage(Winding1)", variations={"Time":["All"]}, primary_sweep_variable="Time") -sols.plot() - -################################################################################### -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Save the project and close AEDT. - -m2d.save_project() -m2d.release_desktop() diff --git a/examples/03-Maxwell/Maxwell_Magnet.py b/examples/03-Maxwell/Maxwell_Magnet.py deleted file mode 100644 index c4e5aa68756..00000000000 --- a/examples/03-Maxwell/Maxwell_Magnet.py +++ /dev/null @@ -1,138 +0,0 @@ -""" -Maxwell 3D: magnet DC analysis ------------------------------- -This example shows how you can use PyAEDT to create a Maxwell DC analysis, -compute mass center, and move coordinate systems. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -from ansys.aedt.core import Maxwell3d -from ansys.aedt.core import generate_unique_project_name -import os -import tempfile - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -########################################################################################### -# Create temporary directory -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create temporary directory. - -temp_dir = tempfile.TemporaryDirectory(suffix=".ansys") - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Launch AEDT -# ~~~~~~~~~~~ -# Launch AEDT in graphical mode. - -m3d = Maxwell3d(project=generate_unique_project_name(), - version=aedt_version, - new_desktop=True, - non_graphical=non_graphical) - -############################################################################### -# Set up Maxwell solution -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Set up the Maxwell solution to DC. - -m3d.solution_type = m3d.SOLUTIONS.Maxwell3d.ElectroDCConduction - -############################################################################### -# Create magnet -# ~~~~~~~~~~~~~ -# Create a magnet. - -magnet = m3d.modeler.create_box(origin=[7, 4, 22], sizes=[10, 5, 30], name="Magnet", material="copper") - -############################################################################### -# Create setup and assign voltage -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the setup and assign a voltage. - -m3d.assign_voltage(magnet.faces, 0) -m3d.create_setup() - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model. - -m3d.plot(show=False, output_file=os.path.join(temp_dir.name, "Image.jpg"), plot_air_objects=True) - -############################################################################### -# Solve setup -# ~~~~~~~~~~~ -# Solve the setup. - -m3d.analyze() - -############################################################################### -# Compute mass center -# ~~~~~~~~~~~~~~~~~~~ -# Compute mass center using the fields calculator. - -m3d.post.ofieldsreporter.EnterScalarFunc("X") -m3d.post.ofieldsreporter.EnterVol(magnet.name) -m3d.post.ofieldsreporter.CalcOp("Mean") -m3d.post.ofieldsreporter.AddNamedExpression("CM_X", "Fields") -m3d.post.ofieldsreporter.EnterScalarFunc("Y") -m3d.post.ofieldsreporter.EnterVol(magnet.name) -m3d.post.ofieldsreporter.CalcOp("Mean") -m3d.post.ofieldsreporter.AddNamedExpression("CM_Y", "Fields") -m3d.post.ofieldsreporter.EnterScalarFunc("Z") -m3d.post.ofieldsreporter.EnterVol(magnet.name) -m3d.post.ofieldsreporter.CalcOp("Mean") -m3d.post.ofieldsreporter.AddNamedExpression("CM_Z", "Fields") -m3d.post.ofieldsreporter.CalcStack("clear") - -############################################################################### -# Get mass center -# ~~~~~~~~~~~~~~~ -# Get mass center using the fields calculator. - -xval = m3d.post.get_scalar_field_value("CM_X") -yval = m3d.post.get_scalar_field_value("CM_Y") -zval = m3d.post.get_scalar_field_value("CM_Z") - -############################################################################### -# Create variables -# ~~~~~~~~~~~~~~~~ -# Create variables with mass center values. - -m3d[magnet.name + "x"] = str(xval * 1e3) + "mm" -m3d[magnet.name + "y"] = str(yval * 1e3) + "mm" -m3d[magnet.name + "z"] = str(zval * 1e3) + "mm" - -############################################################################### -# Create coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a parametric coordinate system. - -cs1 = m3d.modeler.create_coordinate_system( - [magnet.name + "x", magnet.name + "y", magnet.name + "z"], reference_cs="Global", name=magnet.name + "CS" -) - -############################################################################### -# Save and close -# ~~~~~~~~~~~~~~ -# Save the project and close AEDT. - -m3d.save_project() -m3d.release_desktop(close_projects=True, close_desktop=True) -temp_dir.cleanup() diff --git a/examples/03-Maxwell/Maxwell_Transformer_Coreloss.py b/examples/03-Maxwell/Maxwell_Transformer_Coreloss.py deleted file mode 100644 index 7befa8f3478..00000000000 --- a/examples/03-Maxwell/Maxwell_Transformer_Coreloss.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Maxwell 3D: Transformer ------------------------ -This example shows how you can use PyAEDT to set core loss given a set -of Power-Volume [kw/m^3] curves at different frequencies. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -from ansys.aedt.core import downloads -from ansys.aedt.core.generic.general_methods import generate_unique_folder_name -from ansys.aedt.core import Maxwell3d -from ansys.aedt.core.generic.constants import unit_converter -from ansys.aedt.core.generic.general_methods import read_csv_pandas - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -################################################################################# -# Download .aedt file example -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set local temporary folder to export the .aedt file to. - -temp_folder = generate_unique_folder_name() -aedt_file = downloads.download_file("core_loss_transformer", "Ex2-PlanarTransformer_2023R2.aedtz", temp_folder) -freq_curve_csv_25kHz = downloads.download_file("core_loss_transformer", "mf3_25kHz.csv", temp_folder) -freq_curve_csv_100kHz = downloads.download_file("core_loss_transformer", "mf3_100kHz.csv", temp_folder) -freq_curve_csv_200kHz = downloads.download_file("core_loss_transformer", "mf3_200kHz.csv", temp_folder) -freq_curve_csv_400kHz = downloads.download_file("core_loss_transformer", "mf3_400kHz.csv", temp_folder) -freq_curve_csv_700kHz = downloads.download_file("core_loss_transformer", "mf3_700kHz.csv", temp_folder) -freq_curve_csv_1MHz = downloads.download_file("core_loss_transformer", "mf3_1MHz.csv", temp_folder) - -data = read_csv_pandas(input_file=freq_curve_csv_25kHz) -curves_csv_25kHz = list(zip(data[data.columns[0]], - data[data.columns[1]])) -data = read_csv_pandas(input_file=freq_curve_csv_100kHz) -curves_csv_100kHz = list(zip(data[data.columns[0]], - data[data.columns[1]])) -data = read_csv_pandas(input_file=freq_curve_csv_200kHz) -curves_csv_200kHz = list(zip(data[data.columns[0]], - data[data.columns[1]])) -data = read_csv_pandas(input_file=freq_curve_csv_400kHz) -curves_csv_400kHz = list(zip(data[data.columns[0]], - data[data.columns[1]])) -data = read_csv_pandas(input_file=freq_curve_csv_700kHz) -curves_csv_700kHz = list(zip(data[data.columns[0]], - data[data.columns[1]])) -data = read_csv_pandas(input_file=freq_curve_csv_1MHz) -curves_csv_1MHz = list(zip(data[data.columns[0]], - data[data.columns[1]])) - -############################################################################### -# Launch AEDT -# ~~~~~~~~~~~ -# Launch AEDT in graphical mode. - -m3d = Maxwell3d(project=aedt_file, - design="02_3D eddycurrent_CmXY_for_thermal", - version=aedt_version, - new_desktop=True, - non_graphical=False) - -############################################################################### -# Set core loss at frequencies -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a new material, create a dictionary of Power-Volume [kw/m^3] points for a set of frequencies -# retrieved from datasheet provided by supplier and finally set Power-Ferrite core loss model. - -mat = m3d.materials.add_material("newmat") -freq_25kHz = unit_converter(25, unit_system="Freq", input_units="kHz", output_units="Hz") -freq_100kHz = unit_converter(100, unit_system="Freq", input_units="kHz", output_units="Hz") -freq_200kHz = unit_converter(200, unit_system="Freq", input_units="kHz", output_units="Hz") -freq_400kHz = unit_converter(400, unit_system="Freq", input_units="kHz", output_units="Hz") -freq_700kHz = unit_converter(700, unit_system="Freq", input_units="kHz", output_units="Hz") -pv = {freq_25kHz: curves_csv_25kHz, - freq_100kHz: curves_csv_100kHz, - freq_200kHz: curves_csv_200kHz, - freq_400kHz: curves_csv_400kHz, - freq_700kHz: curves_csv_700kHz} -m3d.materials[mat.name].set_coreloss_at_frequency(points_list_at_freq=pv, - coefficient_setup="kw_per_cubic_meter", - core_loss_model_type="Power Ferrite") -coefficients = m3d.materials[mat.name].get_core_loss_coefficients(points_at_frequency=pv, - coefficient_setup="kw_per_cubic_meter") - -################################################################################### -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Save the project and close AEDT. - -m3d.save_project() -m3d.release_desktop() diff --git a/examples/03-Maxwell/RMxprt.py b/examples/03-Maxwell/RMxprt.py deleted file mode 100644 index 08d59b61cd8..00000000000 --- a/examples/03-Maxwell/RMxprt.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -Rmxprt: Create and export motor -------------------------------- -This example uses PyAEDT to create an Rmxprt project and export to Maxwell 2D -Keywords: Rmxprt, Maxwell2D -""" -import os.path -import tempfile - -import ansys.aedt.core - - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" -temp_dir = tempfile.TemporaryDirectory(suffix=".ansys") - -################################################################################## -# Launch AEDT and Rmxprt -# ~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT and Rmxprt after first setting up the project name. -# As solution type we will use ASSM (Adjust-Speed Syncronous Machine). - - -app = ansys.aedt.core.Rmxprt( - version=aedt_version, - new_desktop=True, - close_on_exit=True, - solution_type="ASSM", - project=os.path.join(temp_dir.name, "ASSM.aedt"), -) - -################################################################################## -# Define Machine settings -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Define global machine settings. - -app.general["Number of Poles"] = 4 -app.general["Rotor Position"] = "Inner Rotor" -app.general["Frictional Loss"] = "12W" -app.general["Windage Loss"] = "0W" -app.general["Reference Speed"] = "1500rpm" -app.general["Control Type"] = "DC" -app.general["Circuit Type"] = "Y3" - - -################################################################################## -# Define circuit settings -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Define circuit settings. - -app.circuit["Trigger Pulse Width"] = "120deg" -app.circuit["Transistor Drop"] = "2V" -app.circuit["Diode Drop"] = "2V" - - -################################################################################## -# Stator -# ~~~~~~ -# Define stator, slot and windings settings. - -app.stator["Outer Diameter"] = "122mm" -app.stator["Inner Diameter"] = "75mm" -app.stator["Length"] = "65mm" -app.stator["Stacking Factor"] = 0.95 -app.stator["Steel Type"] = "steel_1008" -app.stator["Number of Slots"] = 24 -app.stator["Slot Type"] = 2 - -app.stator.properties.children["Slot"].props["Auto Design"] = False -app.stator.properties.children["Slot"].props["Hs0"] = "0.5mm" -app.stator.properties.children["Slot"].props["Hs1"] = "1.2mm" -app.stator.properties.children["Slot"].props["Hs2"] = "8.2mm" -app.stator.properties.children["Slot"].props["Bs0"] = "2.5mm" -app.stator.properties.children["Slot"].props["Bs1"] = "5.6mm" -app.stator.properties.children["Slot"].props["Bs2"] = "7.6mm" - -app.stator.properties.children["Winding"].props["Winding Layers"] = 2 -app.stator.properties.children["Winding"].props["Parallel Branches"] = 1 -app.stator.properties.children["Winding"].props["Conductors per Slot"] = 52 -app.stator.properties.children["Winding"].props["Coil Pitch"] = 5 -app.stator.properties.children["Winding"].props["Number of Strands"] = 1 - -################################################################################## -# Rotor -# ~~~~~ -# Define rotor and pole settings. - -app.rotor["Outer Diameter"] = "74mm" -app.rotor["Inner Diameter"] = "26mm" -app.rotor["Length"] = "65mm" -app.rotor["Stacking Factor"] = 0.95 -app.rotor["Steel Type"] = "steel_1008" -app.rotor["Pole Type"] = 1 - -app.rotor.properties.children["Pole"].props["Embrace"] = 0.7 -app.rotor.properties.children["Pole"].props["Offset"] = 0 -app.rotor.properties.children["Pole"].props["Magnet Type"] = ["Material:=", "Alnico9"] -app.rotor.properties.children["Pole"].props["Magnet Thickness"] = "3.5mm" - -################################################################################## -# Setup -# ~~~~~ -# Create a setup and define main settings. - -setup = app.create_setup() -setup.props["RatedVoltage"] = "220V" -setup.props["RatedOutputPower"] = "550W" -setup.props["RatedSpeed"] = "1500rpm" -setup.props["OperatingTemperature"] = "75cel" - - -setup.analyze() - -################################################################################## -# Export to Maxwell -# ~~~~~~~~~~~~~~~~~ -# After the project is solved we can export in Maxwell 2D or Maxwell 3D. - -m2d = app.create_maxwell_design(setup_name=setup.name, maxwell_2d=True) - -m2d.plot(show=False, output_file=os.path.join(temp_dir.name, "Image.jpg"), plot_air_objects=True) - - -################################################################################## -# Rmxprt settings export -# ~~~~~~~~~~~~~~~~~~~~~~ -# All Rmxprt settings can be exported in a json file and reused for another -# project with import function. - -config = app.export_configuration(os.path.join(temp_dir.name, "assm.json")) -app2 = ansys.aedt.core.Rmxprt(project="assm_test2",solution_type=app.solution_type, design="from_configuration") -app2.import_configuration(config) - - - -################################################################################## -# Save and Close Desktop -# ~~~~~~~~~~~~~~~~~~~~~~ -# Save and Close Desktop. - -m2d.save_project(os.path.join(temp_dir.name, "Maxwell_project.aedt")) - -m2d.release_desktop() - -########################## -# Cleanup -# ~~~~~~~ -# -# All project files are saved in the folder ``temp_dir.name``. If you've run this example as a Jupyter notebook you -# can retrieve those project files. The following cell removes all temporary files, including the project folder. - -temp_dir.cleanup() \ No newline at end of file diff --git a/examples/03-Maxwell/Readme.txt b/examples/03-Maxwell/Readme.txt deleted file mode 100644 index 6eb8131b407..00000000000 --- a/examples/03-Maxwell/Readme.txt +++ /dev/null @@ -1,6 +0,0 @@ -Maxwell examples -~~~~~~~~~~~~~~~~ -These examples use PyAEDT to show some end-to-end workflows for Maxwell 2D and -Maxwell 3D. This includes model generation, setup, meshing, and postprocessing. -Examples cover different Maxwell solution types (Eddy Current, Magnetostatic, -and Transient). diff --git a/examples/04-Icepak/Icepak_3DComponents_Example.py b/examples/04-Icepak/Icepak_3DComponents_Example.py deleted file mode 100644 index 4b6bcf2a865..00000000000 --- a/examples/04-Icepak/Icepak_3DComponents_Example.py +++ /dev/null @@ -1,180 +0,0 @@ -""" -Icepak: thermal analysis with 3D components -------------------------------------------- -This example shows how to create a thermal analysis of an electronic package by taking advantage of 3D components and -features added by PyAEDT. -""" - -############################################################################### -# Import PyAEDT and download files -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform import of required classes from the ``pyaedt`` package and import the ``os`` package. - -from ansys.aedt.core import Icepak, generate_unique_folder_name, downloads, settings -import os - -# Download needed files - -temp_folder = generate_unique_folder_name() -package_temp_name, qfp_temp_name = downloads.download_icepak_3d_component(temp_folder) - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Create heatsink -# ~~~~~~~~~~~~~~~ -# Open a new project in non-graphical mode. - -ipk = Icepak(project=os.path.join(temp_folder, "Heatsink.aedt"), - version=aedt_version, - non_graphical=non_graphical, - close_on_exit=True, - new_desktop=True) - -# Remove air region created by default because it is not needed as the heatsink will be exported as a 3DComponent. - -ipk.modeler.get_object_from_name("Region").delete() - -# Definition of heatsink with boxes -hs_base = ipk.modeler.create_box(origin=[0, 0, 0], sizes=[37.5, 37.5, 2], name="HS_Base") -hs_base.material_name = "Al-Extruded" -hs_fin = ipk.modeler.create_box(origin=[0, 0, 2], sizes=[37.5, 1, 18], name="HS_Fin1") -hs_fin.material_name = "Al-Extruded" -hs_fin.duplicate_along_line([0, 3.65, 0], nclones=11) - -ipk.plot(show=False, output_file=os.path.join(temp_folder, "Heatsink.jpg")) - -# Definition of a mesh region. First a non-model box is created, then the mesh region is assigned -mesh_box = ipk.modeler.create_box(origin=[-2, -2, -3], sizes=[41.5, 41.5, 24]) -mesh_box.model = False -mesh_region = ipk.mesh.assign_mesh_region([mesh_box.name]) -mesh_region.UserSpecifiedSettings = True -mesh_region.MaxElementSizeX = "5mm" -mesh_region.MaxElementSizeY = "5mm" -mesh_region.MaxElementSizeZ = "1mm" -mesh_region.MinElementsInGap = "4" -mesh_region.MaxLevels = "2" -mesh_region.BufferLayers = "1" -mesh_region.update() - -# Assignment of monitor objects. -hs_fin5_object = ipk.modeler.get_object_from_name("HS_Fin1_5") -point_monitor_position = [0.5 * (hs_base.bounding_box[i] + hs_base.bounding_box[i + 3]) for i in range(2)] + [ - hs_fin5_object.bounding_box[-1]] # average x,y, top z - -ipk.monitor.assign_point_monitor(point_monitor_position, monitor_quantity=["Temperature", "HeatFlux"], - monitor_name="TopPoint") -ipk.monitor.assign_face_monitor(hs_base.bottom_face_z.id, monitor_quantity="Temperature", monitor_name="Bottom") -ipk.monitor.assign_point_monitor_in_object("HS_Fin1_5", monitor_quantity="Temperature", monitor_name="Fin5Center") - -# Export the heatsink 3D component and close project. auxiliary_dict is set to true in order to export the -# monitor objects along with the .a3dcomp file. -os.mkdir(os.path.join(temp_folder, "componentLibrary")) -ipk.modeler.create_3dcomponent(os.path.join(temp_folder, "componentLibrary", "Heatsink.a3dcomp"), name="Heatsink", - export_auxiliary=True) -ipk.close_project(save=False) - -############################################################################### -# Create QFP -# ~~~~~~~~~~ -# Download and open a project containing a QPF. -ipk = Icepak(project=qfp_temp_name) -ipk.plot(show=False, output_file=os.path.join(temp_folder, "QFP2.jpg")) - -# Create dataset for power dissipation. -x_datalist = [45, 53, 60, 70] -y_datalist = [0.5, 3, 6, 9] -ipk.create_dataset("PowerDissipationDataset", x_datalist, y_datalist, z=None, v=None, is_project_dataset=False, - x_unit="cel", y_unit="W", v_unit="") - -# Assign source power condition to the die. -ipk.create_source_power( - "DieSource", - thermal_dependent_dataset="PowerDissipationDataset", - source_name="DieSource" -) - -# Assign thickness to the die attach surface. -ipk.create_conduting_plate(face_id="Die_Attach", - thermal_specification="Thickness", - shell_conduction=True, - thickness="0.05mm", - solid_material="Epoxy Resin-Typical", - bc_name="Die_Attach", - ) - -# Assign monitor objects. -ipk.monitor.assign_point_monitor_in_object("QFP2_die", monitor_quantity="Temperature", monitor_name="DieCenter") -ipk.monitor.assign_surface_monitor("Die_Attach", monitor_quantity="Temperature", monitor_name="DieAttach") - -# Export the QFP 3D component and close project. Here the auxiliary dictionary allows exporting not only the monitor -# objects but also the dataset used for the power source assignment. -ipk.modeler.create_3dcomponent(os.path.join(temp_folder, "componentLibrary", "QFP.a3dcomp"), name="QFP", - export_auxiliary=True, datasets=["PowerDissipationDataset"]) -ipk.release_desktop(False, False) - -############################################################################### -# Create electronic package -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Download and open a project containing the electronic package. -ipk = Icepak(project=package_temp_name, - version=aedt_version, - non_graphical=non_graphical) -ipk.plot(show=False, output_file=os.path.join(temp_folder, "electronic_package_missing_obj.jpg")) - -# The heatsink and the QFP are missing. They can be inserted as 3d components. The auxiliary files are needed since -# the aim is to import also monitor objects and datasets. Also, a coordinate system is created for the heatsink so -# that it is placed on top of the AGP. -agp = ipk.modeler.get_object_from_name("AGP_IDF") -cs = ipk.modeler.create_coordinate_system( - origin=[agp.bounding_box[0], agp.bounding_box[1], agp.bounding_box[-1]], - name="HeatsinkCS", - reference_cs="Global", - x_pointing=[1, 0, 0], - y_pointing=[0, 1, 0], -) -heatsink_obj = ipk.modeler.insert_3d_component( - input_file=os.path.join(temp_folder, "componentLibrary", "Heatsink.a3dcomp"), coordinate_system="HeatsinkCS", - auxiliary_parameters=True) - -QFP2_obj = ipk.modeler.insert_3d_component(input_file=os.path.join(temp_folder, "componentLibrary", "QFP.a3dcomp"), - coordinate_system="Global", auxiliary_parameters=True) -ipk.plot(show=False, output_file=os.path.join(temp_folder, "electronic_package.jpg")) - -# Create a coordinate system at the xmin, ymin, zmin of the model -bounding_box = ipk.modeler.get_model_bounding_box() -cs_pcb_assembly = ipk.modeler.create_coordinate_system( - origin=[bounding_box[0], bounding_box[1], bounding_box[2]], - name="PCB_Assembly", - reference_cs="Global", - x_pointing=[1, 0, 0], - y_pointing=[0, 1, 0], -) - -# Export of the whole assembly as 3d component and close project. First, a flattening is needed because nested 3d -# components are not natively supported. Then it is possible to export the whole package as 3d component. Here the -# auxiliary dictionary is needed to export monitor objects, datasets and native components. -ipk.flatten_3d_components() -ipk.modeler.create_3dcomponent(input_file=os.path.join(temp_folder, "componentLibrary", "PCBAssembly.a3dcomp"), - name="PCBAssembly", coordinate_systems=["Global", "HeatsinkCS", "PCB_Assembly"], - reference_coordinate_system="PCB_Assembly", export_auxiliary=True) - -############################################################################### -# Release AEDT -# ~~~~~~~~~~~~ -# Release AEDT. - -ipk.release_desktop(True, True) diff --git a/examples/04-Icepak/Icepak_CSV_Import.py b/examples/04-Icepak/Icepak_CSV_Import.py deleted file mode 100644 index 5b4bd223e10..00000000000 --- a/examples/04-Icepak/Icepak_CSV_Import.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -Icepak: Creating blocks and assigning materials and power -------------------------------------- -This example shows how to create different types of blocks and assign power and material to them using -a *.csv input file -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports including the operating system, regular expression, csv, Ansys PyAEDT -# and its boundary objects. - -import os -import re -import csv -import ansys.aedt.core -from ansys.aedt.core.modules.boundary import BoundaryObject - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Download and open project -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Download the project, open it, and save it to the temporary folder. - -temp_folder = ansys.aedt.core.generate_unique_folder_name() - -ipk = ansys.aedt.core.Icepak(project=os.path.join(temp_folder, "Icepak_CSV_Import.aedt"), - version=aedt_version, - new_desktop=True, - non_graphical=non_graphical - ) - -ipk.autosave_disable() - -# Create the PCB as a simple block. -board = ipk.modeler.create_box([-30.48, -27.305, 0], [146.685, 71.755, 0.4064], "board_outline", material="FR-4_Ref") - -############################################################################### -# Blocks creation with a CSV file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# The CSV file lists the name of blocks, their type and material properties. -# Block types (solid, network, hollow), block name, block starting and end points, solid material, and power are listed. -# Hollow and network blocks do not need the material name. -# Network blocks must have Rjb and Rjc values. -# Monitor points can be created for any types of block if the last column is assigned to be 1 (0 and 1 are the only options). -# -# The following image does not show the entire rows and data and only serves as a sample. -# -# .. image:: ../../_static/CSV_Import.png -# :width: 400 -# :alt: CSV Screenshot. -# -# -# In this step the code will loop over the csv file lines and creates the blocks. -# It will create solid blocks and assign BCs. -# Every row of the csv has information of a particular block. - -filename = ansys.aedt.core.downloads.download_file('icepak', 'blocks-list.csv', destination=temp_folder) - -with open(filename, 'r') as csv_file: - csv_reader = csv.DictReader(csv_file) - for row in csv_reader: - origin = [float(row["xs"]), float(row["ys"]), float(row["zs"])] # block starting point - dimensions = [float(row["xd"]), float(row["yd"]), float(row["zd"])] # block lengths in 3 dimensions - block_name = row["name"] # block name - - # Define material name - if row["matname"]: - material_name = row["matname"] - else: - material_name = "copper" - - # creates the block with the given name, coordinates, material, and type - block = ipk.modeler.create_box(origin, dimensions, name=block_name, material=material_name) - - # Assign boundary conditions - if row["block_type"] == "solid": - ipk.assign_solid_block(block_name, row["power"] + "W", block_name) - elif row["block_type"] == "network": - ipk.create_two_resistor_network_block(block_name, - board.name, - row["power"] + "W", - row["Rjb"], - row["Rjc"]) - else: - ipk.modeler[block.name].solve_inside = False - ipk.assign_hollow_block(block_name, assignment_type="Total Power", assignment_value=row["power"] + "W", - boundary_name=block_name) - - # Create temperature monitor points if assigned value is 1 in the last column of the csv file - if row["Monitor_point"] == '1': - ipk.monitor.assign_point_monitor_in_object( - row["name"], - monitor_quantity="Temperature", - monitor_name=row["name"]) - -############################################################################### -# Release AEDT -# ~~~~~~~~~~~~ -# Release AEDT. - -ipk.release_desktop(True, True) diff --git a/examples/04-Icepak/Icepak_ECAD_Import.py b/examples/04-Icepak/Icepak_ECAD_Import.py deleted file mode 100644 index b51476a40cc..00000000000 --- a/examples/04-Icepak/Icepak_ECAD_Import.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -Icepak: Importing a PCB and its components via IDF and EDB -------------------------------------- -This example shows how to import a PCB and its components using IDF files (*.ldb/*.bdf). -The *.emn/*.emp combination can also be used in a similar way. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports including the operating system, Ansys PyAEDT packages. - - -# Generic Python packages - -import os - -# PyAEDT Packages -import ansys.aedt.core -from ansys.aedt.core import Hfss3dLayout -from ansys.aedt.core import settings -settings.enable_debug_grpc_api_logger = True -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Download and open project -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Download the project, open it, and save it to the temporary folder. - -temp_folder = ansys.aedt.core.generate_unique_folder_name() - -ipk = ansys.aedt.core.Icepak(project=os.path.join(temp_folder, "Icepak_ECAD_Import.aedt"), - version=aedt_version, - new_desktop=True, - non_graphical=non_graphical - ) - -ipk.autosave_disable() # Saves the project - -############################################################################### -# Import the IDF files -# ~~~~~~~~~~~~~~~~~~~~ -# Sample *.bdf and *.ldf files are presented here. -# -# -# -# -# Imports the idf files with several filtering options including caps, resistors, inductors, power, size, ... -# There are also options for the PCB creation (number o flayers, copper percentages, layer sizes). -# In this example, the default values are used for the PCB. -# The imported PCB here will be deleted later and replaced by a PCB that has the trace information for higher accuracy. - - -def_path = ansys.aedt.core.downloads.download_file('icepak/Icepak_ECAD_Import/A1_uprev.aedb','edb.def',temp_folder) -board_path = ansys.aedt.core.downloads.download_file('icepak/Icepak_ECAD_Import/','A1.bdf',temp_folder) -library_path = ansys.aedt.core.downloads.download_file('icepak/Icepak_ECAD_Import/','A1.ldf',temp_folder) - -ipk.import_idf(board_path, library_path=None, control_path=None, - filter_cap=False, filter_ind=False, filter_res=False, - filter_height_under=None, filter_height_exclude_2d=False, - power_under=None, create_filtered_as_non_model=False, - high_surface_thick='0.07mm', low_surface_thick='0.07mm', - internal_thick='0.07mm', internal_layer_number=2, - high_surface_coverage=30, low_surface_coverage=30, - internal_layer_coverage=30, trace_material='Cu-Pure', - substrate_material='FR-4', create_board=True, - model_board_as_rect=False, model_device_as_rect=True, - cutoff_height='5mm', component_lib='') - -############################################################################### -# Fit to scale, save the project -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -ipk.modeler.fit_all() # scales to fit all objects in AEDT -ipk.save_project() # saves the project - -############################################################################### -# Add an HFSS 3D Layout design with the layout information of the PCB -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Layout_name = 'A1_uprev' # 3D layout name available for import, the extension of .aedb should not be listed here - -hfss3dLO = Hfss3dLayout('Icepak_ECAD_Import', 'PCB_temp') # adding a dummy HFSS 3D layout to the current project - -#edb_full_path = os.path.join(os.getcwd(), Layout_name+'.aedb\edb.def') # path to the EDB file -hfss3dLO.import_edb(def_path) # importing the EDB file -hfss3dLO.save_project() # save the new project so files are stored in the path - -ipk.delete_design(name='PCB_temp', fallback_design=None) # deleting the dummy layout from the original project - -# This part creates a 3D component PCB in Icepak from the imported EDB file -# 1 watt is assigned to the PCB as power input - -component_name = "PCB_ECAD" - -odb_path = os.path.join(temp_folder, 'icepak/Icepak_ECAD_Import/'+Layout_name+'.aedt') -ipk.create_pcb_from_3dlayout( - component_name, odb_path, Layout_name, resolution=2, extenttype="Polygon", outlinepolygon='poly_0', - custom_x_resolution=None, custom_y_resolution=None, power_in=1) - -############################################################################### -# Delete PCB objects -# ~~~~~~~~~~~~~~~~~~ -# Delete the PCB object from IDF import. - -ipk.modeler.delete_objects_containing("IDF_BoardOutline", False) - -############################################################################### -# Compute power budget -# ~~~~~~~~~~~~~~~~~~~~ - -# Creates a setup to be able to calculate the power -ipk.create_setup("setup1") - -power_budget, total = ipk.post.power_budget("W") -print(total) - -############################################################################### -# Release AEDT -# ~~~~~~~~~~~~ -# Release AEDT. - -ipk.release_desktop(True, True) \ No newline at end of file diff --git a/examples/04-Icepak/Icepak_Example.py b/examples/04-Icepak/Icepak_Example.py deleted file mode 100644 index 28cfa5bc3de..00000000000 --- a/examples/04-Icepak/Icepak_Example.py +++ /dev/null @@ -1,117 +0,0 @@ -""" -Icepak: graphic card thermal analysis -------------------------------------- -This example shows how you can use PyAEDT to create a graphic card setup in Icepak and postprocess results. -The example file is an Icepak project with a model that is already created and has materials assigned. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Download and open project -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Download the project, open it, and save it to the temporary folder. - -temp_folder = ansys.aedt.core.generate_unique_folder_name() -project_temp_name = ansys.aedt.core.downloads.download_icepak(temp_folder) - -ipk = ansys.aedt.core.Icepak(project=project_temp_name, - version=aedt_version, - new_desktop=True, - non_graphical=non_graphical - ) - -ipk.autosave_disable() - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model. - -ipk.plot(show=False, output_file=os.path.join(temp_folder, "Graphics_card.jpg"), plot_air_objects=False) - -############################################################################### -# Create source blocks -# ~~~~~~~~~~~~~~~~~~~~ -# Create source blocks on the CPU and memories. - -ipk.create_source_block("CPU", "25W") -ipk.create_source_block(["MEMORY1", "MEMORY1_1"], "5W") - -############################################################################### -# Assign openings and grille -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign openings and a grille. - -region = ipk.modeler["Region"] -ipk.assign_openings(air_faces=region.bottom_face_x.id) -ipk.assign_grille(air_faces=region.top_face_x.id, free_area_ratio=0.8) - -############################################################################### -# Assign mesh regions -# ~~~~~~~~~~~~~~~~~~~ -# Assign a mesh region to the heat sink and CPU. - -mesh_region = ipk.mesh.assign_mesh_region(assignment=["HEAT_SINK", "CPU"]) -mesh_region.UserSpecifiedSettings = True -mesh_region.MaxElementSizeX = "3.35mm" -mesh_region.MaxElementSizeY = "1.75mm" -mesh_region.MaxElementSizeZ = "2.65mm" -mesh_region.MaxLevels = "2" -mesh_region.update() - -############################################################################### -# Assign point monitor -# ~~~~~~~~~~~~~~~~~~~~ -# Assign a point monitor and set it up. - -ipk.assign_point_monitor(point_position=["-35mm", "3.6mm", "-86mm"], monitor_name="TemperatureMonitor1") -ipk.assign_point_monitor(point_position=["80mm", "14.243mm", "-55mm"], monitor_type="Speed") -setup1 = ipk.create_setup() -setup1.props["Flow Regime"] = "Turbulent" -setup1.props["Convergence Criteria - Max Iterations"] = 5 -setup1.props["Linear Solver Type - Pressure"] = "flex" -setup1.props["Linear Solver Type - Temperature"] = "flex" -ipk.save_project() - -############################################################################### -# Solve project and postprocess -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Solve the project and plot temperatures. - -quantity_name = "SurfTemperature" -surflist = [i.id for i in ipk.modeler["CPU"].faces] -surflist += [i.id for i in ipk.modeler["MEMORY1"].faces] -surflist += [i.id for i in ipk.modeler["MEMORY1_1"].faces] -surflist += [i.id for i in ipk.modeler["ALPHA_MAIN_PCB"].faces] - -plot5 = ipk.post.create_fieldplot_surface(surflist, "SurfTemperature") - -ipk.analyze() - -############################################################################### -# Release AEDT -# ~~~~~~~~~~~~ -# Release AEDT. - -ipk.release_desktop(True, True) diff --git a/examples/04-Icepak/Readme.txt b/examples/04-Icepak/Readme.txt deleted file mode 100644 index f7bc280fad1..00000000000 --- a/examples/04-Icepak/Readme.txt +++ /dev/null @@ -1,5 +0,0 @@ -Icepak examples -~~~~~~~~~~~~~~~ -These examples use PyAEDT to show end-to-end workflows for Icepak. This includes -schematic generation, setup, and thermal postprocessing. - diff --git a/examples/04-Icepak/Sherlock_Example.py b/examples/04-Icepak/Sherlock_Example.py deleted file mode 100644 index a55c2eead1e..00000000000 --- a/examples/04-Icepak/Sherlock_Example.py +++ /dev/null @@ -1,207 +0,0 @@ -""" -Icepak: setup from Sherlock inputs ------------------------------------ -This example shows how you can create an Icepak project from Sherlock -files (STEP and CSV) and an AEDB board. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports and set paths. - -import time -import os -import ansys.aedt.core -import datetime - -# Set paths -project_folder = ansys.aedt.core.generate_unique_folder_name() -input_dir = ansys.aedt.core.downloads.download_sherlock(destination=project_folder) - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` value either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Define variables -# ~~~~~~~~~~~~~~~~ -# Define input variables. The following code creates all input variables that are needed -# to run this example. - -material_name = "MaterialExport.csv" -component_properties = "TutorialBoardPartsList.csv" -component_step = "TutorialBoard.stp" -aedt_odb_project = "SherlockTutorial.aedt" -aedt_odb_design_name = "PCB" - -############################################################################### -# Launch AEDT -# ~~~~~~~~~~~ -# Launch AEDT 2024 R2 in graphical mode. - -d = ansys.aedt.core.launch_desktop(version=aedt_version, non_graphical=non_graphical, new_desktop=True) - -start = time.time() -material_list = os.path.join(input_dir, material_name) -component_list = os.path.join(input_dir, component_properties) -validation = os.path.join(project_folder, "validation.log") -file_path = os.path.join(input_dir, component_step) -project_name = os.path.join(project_folder, component_step[:-3] + "aedt") -component_name = "from_ODB" - -############################################################################### -# Create Icepak project -# ~~~~~~~~~~~~~~~~~~~~~ -# Create an Icepak project. - -ipk = ansys.aedt.core.Icepak(project=project_name) - -############################################################################### -# Disable autosave to speed up import -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Disable autosave to speed up the import. - -d.disable_autosave() - -############################################################################### -# Import PCB from AEDB file -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Import a PCB from an AEDB file. - -odb_path = os.path.join(input_dir, aedt_odb_project) -ipk.create_pcb_from_3dlayout(component_name=component_name, project_name=odb_path, design_name=aedt_odb_design_name, - extenttype="Polygon") - -############################################################################### -# Create offset coordinate system -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create an offset coordinate system to match ODB++ with the -# Sherlock STEP file. - -bb = ipk.modeler.user_defined_components[component_name+"1"].bounding_box -stackup_thickness = bb[-1] - bb[2] -ipk.modeler.create_coordinate_system( - origin=[0, 0, stackup_thickness / 2], mode="view", view="XY" -) - -############################################################################### -# Import CAD file -# ~~~~~~~~~~~~~~~ -# Import a CAD file and delete the CAD "pcb" object as the ECAD is already in the design. - -ipk.modeler.import_3d_cad(file_path, refresh_all_ids=False) -ipk.modeler.delete_objects_containing("pcb", False) - -############################################################################### -# Modify air region -# ~~~~~~~~~~~~~ -# Modify air region dimensions. - -ipk.mesh.global_mesh_region.global_region.padding_values = [20, 20, 20, 20, 300, 300] - -############################################################################### -# Assign materials -# ~~~~~~~~~~~~~~~~ -# Assign materials from Sherlock file. - -ipk.assignmaterial_from_sherlock_files( - component_file=component_list, material_file=material_list -) - -############################################################################### -# Delete objects with no material assignments -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Delete objects with no materials assignments. - -no_material_objs = ipk.modeler.get_objects_by_material("") -ipk.modeler.delete(no_material_objs) -ipk.save_project() - -############################################################################### -# Assign power to component blocks -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign power to component blocks. - -all_objects = ipk.modeler.object_names - -############################################################################### -# Assign power blocks -# ~~~~~~~~~~~~~~~~~~~ -# Assign power blocks from the Sherlock file. - -total_power = ipk.assign_block_from_sherlock_file(csv_name=component_list) - -############################################################################### -# Assign openings -# ~~~~~~~~~~~~~~~~~~~ -# Assign opening boundary condition to all the faces of the region. -ipk.assign_openings(ipk.modeler.get_object_faces("Region")) - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model again now that materials are assigned. - -ipk.plot( - show=False, - output_file=os.path.join(project_folder, "Sherlock_Example.jpg"), - plot_air_objects=False, - plot_as_separate_objects=False -) - -############################################################################### -# Set up mesh settings -# ~~~~~~~~~~~~~~~~~ -# Mesh settings that is tailored for PCB - -ipk.globalMeshSettings(3, gap_min_elements='1', noOgrids=True, MLM_en=True, - MLM_Type='2D', edge_min_elements='2', object='Region') - -############################################################################### -# Numerical settings -# ~~~~~~~~~~~~~~~~~ - -setup1 = ipk.create_setup() -setup1.props["Solution Initialization - Y Velocity"] = "1m_per_sec" -setup1.props["Radiation Model"] = "Discrete Ordinates Model" -setup1.props["Include Gravity"] = True -setup1.props["Secondary Gradient"] = True -setup1.props["Convergence Criteria - Max Iterations"] = 100 - -############################################################################### -# Check for intersections -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Check for intersections using validation and fix them by -# assigning priorities. - -ipk.assign_priority_on_intersections() - -############################################################################### -# Compute power budget -# ~~~~~~~~~~~~~~~~~~~~ - -power_budget, total = ipk.post.power_budget("W") -print(total) - -############################################################################### -# Save project and release AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Save the project and release AEDT. - -ipk.save_project() - -end = time.time() - start -print("Elapsed time: {}".format(datetime.timedelta(seconds=end))) -print("Project Saved in {} ".format(ipk.project_file)) -ipk.release_desktop() diff --git a/examples/05-Q3D/Q2D_Armoured_Cable.py b/examples/05-Q3D/Q2D_Armoured_Cable.py deleted file mode 100644 index 71a959717ac..00000000000 --- a/examples/05-Q3D/Q2D_Armoured_Cable.py +++ /dev/null @@ -1,255 +0,0 @@ -""" -Q2D: Cable parameter identification ---------------------------------------------------- -This example shows how you can use PyAEDT to perform these tasks: - - - Create a Q2D design using the Modeler primitives and importing part of the geometry. - - Set up the entire simulation. - - Link the solution to a Simplorer design. - - For cable information, see `4 Core Armoured Power Cable `_ - -""" -################################################################################# -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ - -import ansys.aedt.core -import math - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -################################################################################# -# Initialize core strand dimensions and positions -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize cable sizing - radii in mm. - -c_strand_radius = 2.575 -cable_n_cores = 4 -core_n_strands = 6 -core_xlpe_ins_thickness = 0.5 -core_xy_coord = math.ceil(3 * c_strand_radius + 2 * core_xlpe_ins_thickness) - -################################################################################# -# Initialize filling and sheath dimensions -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize radii of further structures incrementally adding thicknesses. - -filling_radius = 1.4142 * (core_xy_coord + 3 * c_strand_radius + core_xlpe_ins_thickness + 0.5) -inner_sheath_radius = filling_radius + 0.75 -armour_thickness = 3 -armour_radius = inner_sheath_radius + armour_thickness -outer_sheath_radius = armour_radius + 2 - -################################################################################# -# Initialize armature strand dimensions -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize radii. - -armour_centre_pos = inner_sheath_radius + armour_thickness / 2.0 -arm_strand_rad = armour_thickness / 2.0 - 0.2 -n_arm_strands = 30 - -################################################################################# -# Initialize dictionaries -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize dictionaries that contain all the definitions for the design -# variables and output variables. - -core_params = { - "n_cores": str(cable_n_cores), - "n_strands_core": str(core_n_strands), - "c_strand_radius": str(c_strand_radius) + 'mm', - "c_strand_xy_coord": str(core_xy_coord) + 'mm' -} -outer_params = { - "filling_radius": str(filling_radius) + 'mm', - "inner_sheath_radius": str(inner_sheath_radius) + 'mm', - "armour_radius": str(armour_radius) + 'mm', - "outer_sheath_radius": str(outer_sheath_radius) + 'mm' -} -armour_params = { - "armour_centre_pos": str(armour_centre_pos) + 'mm', - "arm_strand_rad": str(arm_strand_rad) + 'mm', - "n_arm_strands": str(n_arm_strands) -} - -################################################################################# -# Initialize Q2D -# ~~~~~~~~~~~~~~ -# Initialize Q2D, providing the version, path to the project, and the design -# name and type. - -project_name = 'Q2D_ArmouredCableExample' -q2d_design_name = '2D_Extractor_Cable' -setup_name = "MySetupAuto" -sweep_name = "sweep1" -tb_design_name = 'CableSystem' -q2d = ansys.aedt.core.Q2d(project=project_name, design=q2d_design_name, version=aedt_version) - -########################################################## -# Define variables from dictionaries -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define design variables from the created dictionaries. - -for k, v in core_params.items(): - q2d[k] = v -for k, v in outer_params.items(): - q2d[k] = v -for k, v in armour_params.items(): - q2d[k] = v - -########################################################## -# Create object to access 2D modeler -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the ``mod2D`` object to access the 2D modeler easily. - -mod2D = q2d.modeler -mod2D.delete() -mod2D.model_units = "mm" - -################################################################################# -# Initialize required material properties -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Cable insulators require the definition of specific materials since they are not included in the Sys Library. -# Plastic, PE (cross-linked, wire, and cable grade) - -mat_pe_cable_grade = q2d.materials.add_material("plastic_pe_cable_grade") -mat_pe_cable_grade.conductivity = "1.40573e-16" -mat_pe_cable_grade.permittivity = "2.09762" -mat_pe_cable_grade.dielectric_loss_tangent = "0.000264575" -mat_pe_cable_grade.update() -# Plastic, PP (10% carbon fiber) -mat_pp = q2d.materials.add_material("plastic_pp_carbon_fiber") -mat_pp.conductivity = "0.0003161" -mat_pp.update() - -##################################################################################### -# Create geometry for core strands, filling, and XLPE insulation -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -mod2D.create_coordinate_system(['c_strand_xy_coord', 'c_strand_xy_coord', '0mm'], name='CS_c_strand_1') -mod2D.set_working_coordinate_system('CS_c_strand_1') -c1_id = mod2D.create_circle(['0mm', '0mm', '0mm'], 'c_strand_radius', name='c_strand_1', material='copper') -c2_id = c1_id.duplicate_along_line(vector=['0mm', '2.0*c_strand_radius', '0mm'], nclones=2) -mod2D.duplicate_around_axis(c2_id, axis="Z", angle=360 / core_n_strands, clones=6) -c_unite_name = mod2D.unite(q2d.get_all_conductors_names()) - -fill_id = mod2D.create_circle(['0mm', '0mm', '0mm'], '3*c_strand_radius', name='c_strand_fill', - material='plastic_pp_carbon_fiber') -fill_id.color = (255, 255, 0) -xlpe_id = mod2D.create_circle(['0mm', '0mm', '0mm'], '3*c_strand_radius+' + str(core_xlpe_ins_thickness) + 'mm', - name='c_strand_xlpe', - material='plastic_pe_cable_grade') -xlpe_id.color = (0, 128, 128) - -mod2D.set_working_coordinate_system('Global') -all_obj_names = q2d.get_all_conductors_names() + q2d.get_all_dielectrics_names() -mod2D.duplicate_around_axis(all_obj_names, axis="Z", angle=360 / cable_n_cores, clones=4) -cond_names = q2d.get_all_conductors_names() - -##################################################################################### -# Create geometry for filling object -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -filling_id = mod2D.create_circle(['0mm', '0mm', '0mm'], 'filling_radius', name='Filling', - material='plastic_pp_carbon_fiber') -filling_id.color = (255, 255, 180) - -##################################################################################### -# Create geometry for inner sheath object -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -inner_sheath_id = mod2D.create_circle(['0mm', '0mm', '0mm'], 'inner_sheath_radius', name='InnerSheath', - material='PVC plastic') -inner_sheath_id.color = (0, 0, 0) - -##################################################################################### -# Create geometry for armature fill -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -arm_fill_id = mod2D.create_circle(['0mm', '0mm', '0mm'], 'armour_radius', name='ArmourFilling', - material='plastic_pp_carbon_fiber') -arm_fill_id.color = (255, 255, 255) - -##################################################################################### -# Create geometry for outer sheath -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -outer_sheath_id = mod2D.create_circle(['0mm', '0mm', '0mm'], 'outer_sheath_radius', name='OuterSheath', - material='PVC plastic') -outer_sheath_id.color = (0, 0, 0) - -##################################################################################### -# Create geometry for armature steel strands -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -arm_strand_1_id = mod2D.create_circle(['0mm', 'armour_centre_pos', '0mm'], '1.1mm', name='arm_strand_1', - material='steel_stainless') -arm_strand_1_id.color = (128, 128, 64) -arm_strand_1_id.duplicate_around_axis('Z', '360deg/n_arm_strands', clones='n_arm_strands') -arm_strand_names = mod2D.get_objects_w_string('arm_strand') - -##################################################################################### -# Create region -# ~~~~~~~~~~~~~ - -region = q2d.modeler.create_region([500, 500, 500, 500]) -region.material_name = "vacuum" - -########################################################## -# Assign conductors and reference ground -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -obj = [q2d.modeler.get_object_from_name(i) for i in cond_names] -[q2d.assign_single_conductor(assignment=i, name='C1' + str(obj.index(i) + 1), conductor_type='SignalLine') for i - in obj] -obj = [q2d.modeler.get_object_from_name(i) for i in arm_strand_names] -q2d.assign_single_conductor(assignment=obj, name="gnd", conductor_type="ReferenceGround") -mod2D.fit_all() - -########################################################## -# Assign design settings -# ~~~~~~~~~~~~~~~~~~~~~~ - -lumped_length = "100m" -q2d_des_settings = q2d.design_settings -q2d_des_settings['LumpedLength'] = lumped_length - -########################################################## -# Insert setup and frequency sweep -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -q2d_setup = q2d.create_setup(name=setup_name) -q2d_sweep = q2d_setup.add_sweep(name=sweep_name) - -########################################################## -# Analyze setup -# ~~~~~~~~~~~~~ - -# q2d.analyze(name=name) - -################################################################### -# Add a Simplorer/Twin Builder design and the Q3D dynamic component -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -tb = ansys.aedt.core.TwinBuilder(design=tb_design_name) - -########################################################## -# Add a Q3D dynamic component -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -tb.add_q3d_dynamic_component(project_name, q2d_design_name, setup_name, sweep_name, coupling_matrix_name="Original", - model_depth=lumped_length) - -########################################################## -# Save project and release desktop -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -tb.save_project() -tb.release_desktop(True, True) diff --git a/examples/05-Q3D/Q2D_Example_CPWG.py b/examples/05-Q3D/Q2D_Example_CPWG.py deleted file mode 100644 index fa13f1d4763..00000000000 --- a/examples/05-Q3D/Q2D_Example_CPWG.py +++ /dev/null @@ -1,214 +0,0 @@ -""" -2D Extractor: CPWG analysis ---------------------------- -This example shows how you can use PyAEDT to create a CPWG (coplanar waveguide with ground) design -in 2D Extractor and run a simulation. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Launch AEDT and 2D Extractor -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT 2023 R2 in graphical mode and launch 2D Extractor. This example -# uses SI units. - -q = ansys.aedt.core.Q2d(version=aedt_version, - non_graphical=non_graphical, - new_desktop=True, - project=ansys.aedt.core.generate_unique_name("pyaedt_q2d_example"), - design="coplanar_waveguide") - -############################################################################### -# Define variables -# ~~~~~~~~~~~~~~~~ -# Define variables. - -e_factor = "e_factor" -sig_bot_w = "sig_bot_w" -co_gnd_w = "gnd_w" -clearance = "clearance" -cond_h = "cond_h" -d_h = "d_h" -sm_h = "sm_h" - -for var_name, var_value in { - "sig_bot_w": "150um", - "e_factor": "2", - "gnd_w": "500um", - "clearance": "150um", - "cond_h": "50um", - "d_h": "150um", - "sm_h": "20um", -}.items(): - q[var_name] = var_value - -delta_w_half = "({0}/{1})".format(cond_h, e_factor) -sig_top_w = "({1}-{0}*2)".format(delta_w_half, sig_bot_w) -co_gnd_top_w = "({1}-{0}*2)".format(delta_w_half, co_gnd_w) -model_w = "{}*2+{}*2+{}".format(co_gnd_w, clearance, sig_bot_w) - -############################################################################### -# Create primitives -# ~~~~~~~~~~~~~~~~~ -# Create primitives and define the layer heights. - -layer_1_lh = 0 -layer_1_uh = cond_h -layer_2_lh = layer_1_uh + "+" + d_h -layer_2_uh = layer_2_lh + "+" + cond_h - -############################################################################### -# Create signal -# ~~~~~~~~~~~~~ -# Create a signal. - -base_line_obj = q.modeler.create_polyline(points=[[0, layer_2_lh, 0], [sig_bot_w, layer_2_lh, 0]], name="signal") -top_line_obj = q.modeler.create_polyline(points=[[0, layer_2_uh, 0], [sig_top_w, layer_2_uh, 0]]) -q.modeler.move(assignment=[top_line_obj], vector=[delta_w_half, 0, 0]) -q.modeler.connect([base_line_obj, top_line_obj]) -q.modeler.move(assignment=[base_line_obj], vector=["{}+{}".format(co_gnd_w, clearance), 0, 0]) - -############################################################################### -# Create coplanar ground -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create a coplanar ground. - -base_line_obj = q.modeler.create_polyline(points=[[0, layer_2_lh, 0], [co_gnd_w, layer_2_lh, 0]], name="co_gnd_left") -top_line_obj = q.modeler.create_polyline(points=[[0, layer_2_uh, 0], [co_gnd_top_w, layer_2_uh, 0]]) -q.modeler.move(assignment=[top_line_obj], vector=[delta_w_half, 0, 0]) -q.modeler.connect([base_line_obj, top_line_obj]) - -base_line_obj = q.modeler.create_polyline(points=[[0, layer_2_lh, 0], [co_gnd_w, layer_2_lh, 0]], name="co_gnd_right") -top_line_obj = q.modeler.create_polyline(points=[[0, layer_2_uh, 0], [co_gnd_top_w, layer_2_uh, 0]]) -q.modeler.move(assignment=[top_line_obj], vector=[delta_w_half, 0, 0]) -q.modeler.connect([base_line_obj, top_line_obj]) -q.modeler.move(assignment=[base_line_obj], vector=["{}+{}*2+{}".format(co_gnd_w, clearance, sig_bot_w), 0, 0]) - -############################################################################### -# Create reference ground plane -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a reference ground plane. - -q.modeler.create_rectangle(origin=[0, layer_1_lh, 0], sizes=[model_w, cond_h], name="ref_gnd") - -############################################################################### -# Create dielectric -# ~~~~~~~~~~~~~~~~~ -# Create a dielectric. - -q.modeler.create_rectangle( - origin=[0, layer_1_uh, 0], sizes=[model_w, d_h], name="Dielectric", material="FR4_epoxy" -) - -############################################################################### -# Create conformal coating -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a conformal coating. - -sm_obj_list = [] -ids = [1, 2, 3] -if aedt_version >= "2023.1": - ids = [0, 1, 2] - -for obj_name in ["signal", "co_gnd_left", "co_gnd_right"]: - obj = q.modeler.get_object_from_name(obj_name) - e_obj_list = [] - for i in ids: - e_obj = q.modeler.create_object_from_edge(obj.edges[i]) - e_obj_list.append(e_obj) - e_obj_1 = e_obj_list[0] - q.modeler.unite(e_obj_list) - new_obj = q.modeler.sweep_along_vector(e_obj_1.id, [0, sm_h, 0]) - sm_obj_list.append(e_obj_1) - -new_obj = q.modeler.create_rectangle(origin=[co_gnd_w, layer_2_lh, 0], sizes=[clearance, sm_h]) -sm_obj_list.append(new_obj) - -new_obj = q.modeler.create_rectangle(origin=[co_gnd_w, layer_2_lh, 0], sizes=[clearance, sm_h]) -q.modeler.move([new_obj], [sig_bot_w + "+" + clearance, 0, 0]) -sm_obj_list.append(new_obj) - -sm_obj = sm_obj_list[0] -q.modeler.unite(sm_obj_list) -sm_obj.material_name = "SolderMask" -sm_obj.color = (0, 150, 100) -sm_obj.name = "solder_mask" - -############################################################################### -# Assign conductor -# ~~~~~~~~~~~~~~~~ -# Assign a conductor to the signal. - -obj = q.modeler.get_object_from_name("signal") -q.assign_single_conductor(assignment=[obj], name=obj.name, conductor_type="SignalLine", solve_option="SolveOnBoundary", - units="mm") - -############################################################################### -# Create reference ground -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Create a reference ground. - -obj = [q.modeler.get_object_from_name(i) for i in ["co_gnd_left", "co_gnd_right", "ref_gnd"]] -q.assign_single_conductor(assignment=obj, name="gnd", conductor_type="ReferenceGround", solve_option="SolveOnBoundary", - units="mm") - -############################################################################### -# Assign Huray model on signal -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign the Huray model on the signal. - -obj = q.modeler.get_object_from_name("signal") -q.assign_huray_finitecond_to_edges(obj.edges, radius="0.5um", ratio=3, name="b_" + obj.name) - -############################################################################### -# Create setup, analyze, and plot -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the setup, analyze it, and plot solution data. - -setup = q.create_setup(name="new_setup") - -sweep = setup.add_sweep(name="sweep1", sweep_type="Discrete") -sweep.props["RangeType"] = "LinearStep" -sweep.props["RangeStart"] = "1GHz" -sweep.props["RangeStep"] = "100MHz" -sweep.props["RangeEnd"] = "5GHz" -sweep.props["SaveFields"] = False -sweep.props["SaveRadFields"] = False -sweep.props["Type"] = "Interpolating" - -sweep.update() - -q.analyze() - -a = q.post.get_solution_data(expressions="Z0(signal,signal)", context="Original") -a.plot() - -############################################################################### -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Save the project and close AEDT. - -home = os.path.expanduser("~") -q.save_project(os.path.join(home, "Downloads", "pyaedt_example", q.project_name + ".aedt")) -q.release_desktop() diff --git a/examples/05-Q3D/Q2D_Example_Stripline.py b/examples/05-Q3D/Q2D_Example_Stripline.py deleted file mode 100644 index 864c2828722..00000000000 --- a/examples/05-Q3D/Q2D_Example_Stripline.py +++ /dev/null @@ -1,225 +0,0 @@ -""" -2D Extractor: stripline analysis --------------------------------- -This example shows how you can use PyAEDT to create a differential stripline design in -2D Extractor and run a simulation. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False -project_path = ansys.aedt.core.generate_unique_project_name() - -############################################################################### -# Launch AEDT and 2D Extractor -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT 2023 R2 in graphical mode and launch 2D Extractor. This example -# uses SI units. - -q = ansys.aedt.core.Q2d(project=project_path, - design="differential_stripline", - version=aedt_version, - non_graphical=non_graphical, - new_desktop=True - ) - -############################################################################### -# Define variables -# ~~~~~~~~~~~~~~~~ -# Define variables. - -e_factor = "e_factor" -sig_w = "sig_bot_w" -sig_gap = "sig_gap" -co_gnd_w = "gnd_w" -clearance = "clearance" -cond_h = "cond_h" -core_h = "core_h" -pp_h = "pp_h" - -for var_name, var_value in { - "e_factor": "2", - "sig_bot_w": "150um", - "sig_gap": "150um", - "gnd_w": "500um", - "clearance": "150um", - "cond_h": "17um", - "core_h": "150um", - "pp_h": "150um", - -}.items(): - q[var_name] = var_value - -delta_w_half = "({0}/{1})".format(cond_h, e_factor) -sig_top_w = "({1}-{0}*2)".format(delta_w_half, sig_w) -co_gnd_top_w = "({1}-{0}*2)".format(delta_w_half, co_gnd_w) -model_w = "{}*2+{}*2+{}*2+{}".format(co_gnd_w, clearance, sig_w, sig_gap) - -############################################################################### -# Create primitives -# ~~~~~~~~~~~~~~~~~ -# Create primitives and define the layer heights. - -layer_1_lh = 0 -layer_1_uh = cond_h -layer_2_lh = layer_1_uh + "+" + core_h -layer_2_uh = layer_2_lh + "+" + cond_h -layer_3_lh = layer_2_uh + "+" + pp_h -layer_3_uh = layer_3_lh + "+" + cond_h - -############################################################################### -# Create positive signal -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create a positive signal. - -base_line_obj = q.modeler.create_polyline([[0, layer_2_lh, 0], [sig_w, layer_2_lh, 0]], name="signal_p") -top_line_obj = q.modeler.create_polyline([[0, layer_2_uh, 0], [sig_top_w, layer_2_uh, 0]]) -q.modeler.move([top_line_obj], [delta_w_half, 0, 0]) -q.modeler.connect([base_line_obj, top_line_obj]) -q.modeler.move([base_line_obj], ["{}+{}".format(co_gnd_w, clearance), 0, 0]) - -# Create negative signal -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create a negative signal. - -base_line_obj = q.modeler.create_polyline(points=[[0, layer_2_lh, 0], [sig_w, layer_2_lh, 0]], name="signal_n") -top_line_obj = q.modeler.create_polyline(points=[[0, layer_2_uh, 0], [sig_top_w, layer_2_uh, 0]]) -q.modeler.move(assignment=[top_line_obj], vector=[delta_w_half, 0, 0]) -q.modeler.connect([base_line_obj, top_line_obj]) -q.modeler.move(assignment=[base_line_obj], vector=["{}+{}+{}+{}".format(co_gnd_w, clearance, sig_w, sig_gap), 0, 0]) - -############################################################################### -# Create coplanar ground -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create a coplanar ground. - -base_line_obj = q.modeler.create_polyline(points=[[0, layer_2_lh, 0], [co_gnd_w, layer_2_lh, 0]], name="co_gnd_left") -top_line_obj = q.modeler.create_polyline(points=[[0, layer_2_uh, 0], [co_gnd_top_w, layer_2_uh, 0]]) -q.modeler.move([top_line_obj], [delta_w_half, 0, 0]) -q.modeler.connect([base_line_obj, top_line_obj]) - -base_line_obj = q.modeler.create_polyline(points=[[0, layer_2_lh, 0], [co_gnd_w, layer_2_lh, 0]], name="co_gnd_right") -top_line_obj = q.modeler.create_polyline(points=[[0, layer_2_uh, 0], [co_gnd_top_w, layer_2_uh, 0]]) -q.modeler.move(assignment=[top_line_obj], vector=[delta_w_half, 0, 0]) -q.modeler.connect([base_line_obj, top_line_obj]) -q.modeler.move(assignment=[base_line_obj], vector=["{}+{}*2+{}*2+{}".format(co_gnd_w, clearance, sig_w, sig_gap), 0, 0]) - -############################################################################### -# Create reference ground plane -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a reference ground plane. - -q.modeler.create_rectangle(origin=[0, layer_1_lh, 0], sizes=[model_w, cond_h], name="ref_gnd_u") -q.modeler.create_rectangle(origin=[0, layer_3_lh, 0], sizes=[model_w, cond_h], name="ref_gnd_l") - -############################################################################### -# Create dielectric -# ~~~~~~~~~~~~~~~~~ -# Create a dielectric. - -q.modeler.create_rectangle( - origin=[0, layer_1_uh, 0], sizes=[model_w, core_h], name="Core", material="FR4_epoxy" -) -q.modeler.create_rectangle( - origin=[0, layer_2_uh, 0], sizes=[model_w, pp_h], name="Prepreg", material="FR4_epoxy" -) -q.modeler.create_rectangle( - origin=[0, layer_2_lh, 0], sizes=[model_w, cond_h], name="Filling", material="FR4_epoxy" -) - -############################################################################### -# Assign conductors -# ~~~~~~~~~~~~~~~~~ -# Assign conductors to the signal. - -obj = q.modeler.get_object_from_name("signal_p") -q.assign_single_conductor(assignment=[obj], name=obj.name, conductor_type="SignalLine", solve_option="SolveOnBoundary", - units="mm") - -obj = q.modeler.get_object_from_name("signal_n") -q.assign_single_conductor(assignment=[obj], name=obj.name, conductor_type="SignalLine", solve_option="SolveOnBoundary", - units="mm") - -############################################################################### -# Create reference ground -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Create a reference ground. - -obj = [q.modeler.get_object_from_name(i) for i in ["co_gnd_left", "co_gnd_right", "ref_gnd_u", "ref_gnd_l"]] -q.assign_single_conductor(assignment=obj, name="gnd", conductor_type="ReferenceGround", solve_option="SolveOnBoundary", - units="mm") - -############################################################################### -# Assign Huray model on signals -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Assign the Huray model on the signals. - -obj = q.modeler.get_object_from_name("signal_p") -q.assign_huray_finitecond_to_edges(obj.edges, radius="0.5um", ratio=3, name="b_" + obj.name) - -obj = q.modeler.get_object_from_name("signal_n") -q.assign_huray_finitecond_to_edges(obj.edges, radius="0.5um", ratio=3, name="b_" + obj.name) - -############################################################################### -# Define differential pair -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Define the differential pair. - -matrix = q.insert_reduced_matrix(operation_name=q.MATRIXOPERATIONS.DiffPair, assignment=["signal_p", "signal_n"], - reduced_matrix="diff_pair") - -############################################################################### -# Create setup, analyze, and plot -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a setup, analyze, and plot solution data. - -# Create a setup. -setup = q.create_setup(name="new_setup") - -# Add a sweep. -sweep = setup.add_sweep(name="sweep1", sweep_type="Discrete") -sweep.props["RangeType"] = "LinearStep" -sweep.props["RangeStart"] = "1GHz" -sweep.props["RangeStep"] = "100MHz" -sweep.props["RangeEnd"] = "5GHz" -sweep.props["SaveFields"] = False -sweep.props["SaveRadFields"] = False -sweep.props["Type"] = "Interpolating" -sweep.update() - -# Analyze the nominal design and plot characteristic impedance. -q.analyze() -plot_sources = matrix.get_sources_for_plot(category="Z0") -a = q.post.get_solution_data(expressions=plot_sources, context=matrix.name) -a.plot(snapshot_path=os.path.join(q.working_directory, "plot.jpg")) # Save plot as jpg - -# Add a parametric sweep and analyze. -parametric = q.parametrics.add(variable="sig_bot_w", start_point=75, end_point=100, step=5, variation_type="LinearStep") -parametric.add_variation(sweep_variable="sig_gap", start_point="100um", end_point="200um", step=5, - variation_type="LinearCount") -q.analyze_setup(name=parametric.name) - -############################################################################### -# Save project and release AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Save the project and release AEDT. -q.save_project() -q.release_desktop() diff --git a/examples/05-Q3D/Q3D_DC_IR.py b/examples/05-Q3D/Q3D_DC_IR.py deleted file mode 100644 index e18d9a949be..00000000000 --- a/examples/05-Q3D/Q3D_DC_IR.py +++ /dev/null @@ -1,258 +0,0 @@ -""" -Q3D Extractor: PCB DCIR analysis --------------------------------- -This example shows how you can use PyAEDT to create a design in -Q3D Extractor and run a DC IR Drop simulation starting from an EDB Project. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core -from pyedb import Edb -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set up project files and path -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Download needed project file and set up temporary project directory. - -project_dir = ansys.aedt.core.generate_unique_folder_name() -aedb_project = ansys.aedt.core.downloads.download_file('edb/ANSYS-HSD_V1.aedb', destination=project_dir) -coil = ansys.aedt.core.downloads.download_file('inductance_3d_component', 'air_coil.a3dcomp') -res = ansys.aedt.core.downloads.download_file('resistors', 'Res_0402.a3dcomp') -project_name = ansys.aedt.core.generate_unique_name("HSD") -output_edb = os.path.join(project_dir, project_name + '_out.aedb') -output_q3d = os.path.join(project_dir, project_name + '_q3d.aedt') - -############################################################################### -# Open EDB -# ~~~~~~~~ -# Open the EDB project and create a cutout on the selected nets -# before exporting to Q3D. - -edb = Edb(aedb_project, edbversion=aedt_version) -edb.cutout(["1.2V_AVDLL_PLL", "1.2V_AVDDL", "1.2V_DVDDL", "NetR106_1"], - ["GND"], - output_aedb_path=output_edb, - use_pyaedt_extent_computing=True, - ) -edb.layout_validation.disjoint_nets("GND", keep_only_main_net=True) -############################################################################### -# Identify pin positions -# ~~~~~~~~~~~~~~~~~~~~~~ -# Identify [x,y] pin locations on the components to define where to assign sources -# and sinks for Q3D. - -pin_u11_scl = [i for i in edb.components["U11"].pins.values() if i.net_name == "1.2V_AVDLL_PLL"] -pin_u9_1 = [i for i in edb.components["U9"].pins.values() if i.net_name == "1.2V_AVDDL"] -pin_u9_2 = [i for i in edb.components["U9"].pins.values() if i.net_name == "1.2V_DVDDL"] -pin_u11_r106 = [i for i in edb.components["U11"].pins.values() if i.net_name == "NetR106_1"] - -############################################################################### -# Append Z Positions -# ~~~~~~~~~~~~~~~~~~ -# Compute Q3D 3D position. The factor 1000 converts from "meters" to "mm". - -location_u11_scl = [i * 1000 for i in pin_u11_scl[0].position] -location_u11_scl.append(edb.components["U11"].upper_elevation * 1000) - -location_u9_1_scl = [i * 1000 for i in pin_u9_1[0].position] -location_u9_1_scl.append(edb.components["U9"].upper_elevation * 1000) - -location_u9_2_scl = [i * 1000 for i in pin_u9_2[0].position] -location_u9_2_scl.append(edb.components["U9"].upper_elevation * 1000) - -location_u11_r106 = [i * 1000 for i in pin_u11_r106[0].position] -location_u11_r106.append(edb.components["U11"].upper_elevation * 1000) - -############################################################################### -# Identify pin positions for 3D components -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Identify the pin positions where 3D components of passives are to be added. - -location_l2_1 = [i * 1000 for i in edb.components["L2"].pins["1"].position] -location_l2_1.append(edb.components["L2"].upper_elevation * 1000) -location_l4_1 = [i * 1000 for i in edb.components["L4"].pins["1"].position] -location_l4_1.append(edb.components["L4"].upper_elevation * 1000) - -location_r106_1 = [i * 1000 for i in edb.components["R106"].pins["1"].position] -location_r106_1.append(edb.components["R106"].upper_elevation * 1000) - -############################################################################### -# Save and close EDB -# ~~~~~~~~~~~~~~~~~~ -# Save and close EDB. Then, open EDT in HFSS 3D Layout to generate the 3D model. -edb.save_edb_as(output_edb) -edb.close_edb() - -h3d = ansys.aedt.core.Hfss3dLayout(output_edb, version=aedt_version, non_graphical=False, new_desktop=True) - -############################################################################### -# Export to Q3D -# ~~~~~~~~~~~~~ -# Create a dummy setup and export the layout in Q3D. -# The ``keep_net_name`` parameter reassigns Q3D net names from HFSS 3D Layout. -setup = h3d.create_setup() -setup.export_to_q3d(output_q3d, keep_net_name=True) -h3d.close_project() - -############################################################################### -# Open Q3D -# ~~~~~~~~ -# Launch the newly created q3d project. - -q3d = ansys.aedt.core.Q3d(output_q3d) -q3d.modeler.delete("GND") -q3d.delete_all_nets() - -############################################################################### -# Insert inductors -# ~~~~~~~~~~~~~~~~ -# Create new coordinate systems and place 3D component inductors. - -q3d.modeler.create_coordinate_system(location_l2_1, name="L2") -comp = q3d.modeler.insert_3d_component(coil, coordinate_system="L2") -comp.rotate(q3d.AXIS.Z, -90) -comp.parameters["n_turns"] = "3" -comp.parameters["d_wire"] = "100um" -q3d.modeler.set_working_coordinate_system("Global") -q3d.modeler.create_coordinate_system(location_l4_1, name="L4") -comp2 = q3d.modeler.insert_3d_component(coil, coordinate_system="L4") -comp2.rotate(q3d.AXIS.Z, -90) -comp2.parameters["n_turns"] = "3" -comp2.parameters["d_wire"] = "100um" -q3d.modeler.set_working_coordinate_system("Global") - -q3d.modeler.set_working_coordinate_system("Global") -q3d.modeler.create_coordinate_system(location_r106_1, name="R106") -comp3 = q3d.modeler.insert_3d_component(res, geometry_parameters={'$Resistance': 2000}, coordinate_system="R106") -comp3.rotate(q3d.AXIS.Z, -90) - -q3d.modeler.set_working_coordinate_system("Global") - -############################################################################### -# Delete dielectrics -# ~~~~~~~~~~~~~~~~~~ -# Delete all dielectric objects since not needed in DC analysis. - -q3d.modeler.delete(q3d.modeler.get_objects_by_material("Megtron4")) -q3d.modeler.delete(q3d.modeler.get_objects_by_material("Megtron4_2")) -q3d.modeler.delete(q3d.modeler.get_objects_by_material("Megtron4_3")) -q3d.modeler.delete(q3d.modeler.get_objects_by_material("Solder Resist")) - -objs_copper = q3d.modeler.get_objects_by_material("copper") -objs_copper_names = [i.name for i in objs_copper] -q3d.plot(assignment=objs_copper_names, show=False, output_file=os.path.join(q3d.working_directory, "Q3D.jpg"), - plot_as_separate_objects=False, plot_air_objects=False) - -############################################################################### -# Assign source and sink -# ~~~~~~~~~~~~~~~~~~~~~~ -# Use previously calculated positions to identify faces, -# select the net "1_Top" and -# assign sources and sinks on nets. - -sink_f = q3d.modeler.create_circle(q3d.PLANE.XY, location_u11_scl, 0.1) -source_f1 = q3d.modeler.create_circle(q3d.PLANE.XY, location_u9_1_scl, 0.1) -source_f2 = q3d.modeler.create_circle(q3d.PLANE.XY, location_u9_2_scl, 0.1) -source_f3= q3d.modeler.create_circle(q3d.PLANE.XY, location_u11_r106, 0.1) -sources_objs = [source_f1, source_f2, source_f3] -q3d.auto_identify_nets() - -identified_net = q3d.nets[0] - -q3d.sink(sink_f, net_name=identified_net) - -source1 = q3d.source(source_f1, net_name=identified_net) - -source2 = q3d.source(source_f2, net_name=identified_net) -source3 = q3d.source(source_f3, net_name=identified_net) -sources_bounds = [source1, source2, source3] - -q3d.edit_sources(dcrl={"{}:{}".format(source1.props["Net"], source1.name): "-1.0A", - "{}:{}".format(source2.props["Net"], source2.name): "-1.0A", - "{}:{}".format(source2.props["Net"], source3.name): "-1.0A"}) - -############################################################################### -# Create setup -# ~~~~~~~~~~~~ -# Create a setup and a frequency sweep from DC to 2GHz. -# Analyze project. - -setup = q3d.create_setup() -setup.dc_enabled = True -setup.capacitance_enabled = False -setup.ac_rl_enabled = False -setup.props["SaveFields"] = True -setup.props["DC"]["Cond"]["MaxPass"]=3 -setup.analyze() - -############################################################################### -# Field Calculator -# ~~~~~~~~~~~~~~~~ -# We will create a named expression using field calculator. - -drop_name = "Vdrop3_3" -fields = q3d.ofieldsreporter -q3d.ofieldsreporter.CalcStack("clear") -q3d.ofieldsreporter.EnterQty("Phidc") -q3d.ofieldsreporter.EnterScalar(3.3) -q3d.ofieldsreporter.CalcOp("+") -q3d.ofieldsreporter.AddNamedExpression(drop_name, "DC R/L Fields") - -############################################################################### -# Phi plot -# ~~~~~~~~ -# Compute ACL solutions and plot them. - -plot1 = q3d.post.create_fieldplot_surface(q3d.modeler.get_objects_by_material("copper"), - quantity=drop_name, - intrinsics={"Freq": "1GHz"}) - -q3d.post.plot_field_from_fieldplot(plot1.name, project_path=q3d.working_directory, mesh_plot=False, image_format="jpg", - view="isometric", show=False, plot_cad_objs=False, log_scale=False) - -############################################################################### -# Computing Voltage on Source Circles -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Using Field Calculator we can compute the voltage on source circles and get the value -# using get_solution_data method. - -curves = [] -for source_circle, source_bound in zip(sources_objs, sources_bounds): - source_sheet_name = source_circle.name - - curves.append("V{}".format(source_bound.name)) - - q3d.ofieldsreporter.CalcStack("clear") - q3d.ofieldsreporter.CopyNamedExprToStack(drop_name) - q3d.ofieldsreporter.EnterSurf(source_sheet_name) - q3d.ofieldsreporter.CalcOp("Maximum") - q3d.ofieldsreporter.AddNamedExpression("V{}".format(source_bound.name), "DC R/L Fields") - - -data = q3d.post.get_solution_data( - curves, - q3d.nominal_adaptive, - variations={"Freq": "1GHz"}, - report_category="DC R/L Fields", - ) -for curve in curves: - print(data.data_real(curve)) - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# ``release_desktop`` method. All methods provide for saving projects before closing. - -q3d.save_project() -q3d.release_desktop() diff --git a/examples/05-Q3D/Q3D_Example.py b/examples/05-Q3D/Q3D_Example.py deleted file mode 100644 index 2c3d2ea48d9..00000000000 --- a/examples/05-Q3D/Q3D_Example.py +++ /dev/null @@ -1,158 +0,0 @@ -""" -Q3D Extractor: busbar analysis ------------------------------- -This example shows how you can use PyAEDT to create a busbar design in -Q3D Extractor and run a simulation. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Set debugger mode -# ~~~~~~~~~~~~~~~~~ -# PyAEDT allows to enable a debug logger which logs all methods called and argument passed. -# This example shows how to enable it. - -ansys.aedt.core.settings.enable_debug_logger = True -ansys.aedt.core.settings.enable_debug_methods_argument_logger = True -ansys.aedt.core.settings.enable_debug_internal_methods_logger = False - - -############################################################################### -# Launch AEDT and Q3D Extractor -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT 2023 R2 in graphical mode and launch Q3D Extractor. -# This example uses SI units. - -q = ansys.aedt.core.Q3d(project=ansys.aedt.core.generate_unique_project_name(), - version=aedt_version, - non_graphical=non_graphical, - new_desktop=True) - -############################################################################### -# Create primitives -# ~~~~~~~~~~~~~~~~~ -# Create polylines for three busbars and a box for the substrate. - -b1 = q.modeler.create_polyline([[0, 0, 0], [-100, 0, 0]], name="Bar1", material="copper", xsection_type="Rectangle", - xsection_width="5mm", xsection_height="1mm") -q.modeler["Bar1"].color = (255, 0, 0) - -q.modeler.create_polyline([[0, -15, 0], [-150, -15, 0]], name="Bar2", material="aluminum", xsection_type="Rectangle", - xsection_width="5mm", xsection_height="1mm") -q.modeler["Bar2"].color = (0, 255, 0) - -q.modeler.create_polyline([[0, -30, 0], [-175, -30, 0], [-175, -10, 0]], name="Bar3", material="copper", - xsection_type="Rectangle", xsection_width="5mm", xsection_height="1mm") -q.modeler["Bar3"].color = (0, 0, 255) - -q.modeler.create_box([50, 30, -0.5], [-250, -100, -3], name="substrate", material="FR4_epoxy") -q.modeler["substrate"].color = (128, 128, 128) -q.modeler["substrate"].transparency = 0.8 - -q.plot(show=False, output_file=os.path.join(q.working_directory, "Q3D.jpg"), plot_air_objects=False) - -############################################################################### -# Set up boundaries -# ~~~~~~~~~~~~~~~~~ -# Identify nets and assign sources and sinks to all nets. -# There is a source and sink for each busbar. - -q.auto_identify_nets() - -q.source("Bar1", direction=q.AxisDir.XPos, name="Source1") -q.sink("Bar1", direction=q.AxisDir.XNeg, name="Sink1") - -q.source("Bar2", direction=q.AxisDir.XPos, name="Source2") -q.sink("Bar2", direction=q.AxisDir.XNeg, name="Sink2") -q.source("Bar3", direction=q.AxisDir.XPos, name="Source3") -bar3_sink = q.sink("Bar3", direction=q.AxisDir.YPos) -bar3_sink.name = "Sink3" - -############################################################################### -# Print information -# ~~~~~~~~~~~~~~~~~ -# Use the different methods available to print net and terminal information. - -print(q.nets) -print(q.net_sinks("Bar1")) -print(q.net_sinks("Bar2")) -print(q.net_sinks("Bar3")) -print(q.net_sources("Bar1")) -print(q.net_sources("Bar2")) -print(q.net_sources("Bar3")) - -############################################################################### -# Create setup -# ~~~~~~~~~~~~ -# Create a setup for Q3D Extractor and add a sweep that defines the adaptive -# frequency value. - -setup1 = q.create_setup(props={"AdaptiveFreq": "100MHz"}) -sw1 = setup1.add_sweep() -sw1.props["RangeStart"] = "1MHz" -sw1.props["RangeEnd"] = "100MHz" -sw1.props["RangeStep"] = "5MHz" -sw1.update() - -############################################################################### -# Get curves to plot -# ~~~~~~~~~~~~~~~~~~ -# Get the curves to plot. The following code simplifies the way to get curves. - -data_plot_self = q.matrices[0].get_sources_for_plot(get_self_terms=True, get_mutual_terms=False) -data_plot_mutual = q.get_traces_for_plot(get_self_terms=False, get_mutual_terms=True, category="C") - -############################################################################### -# Create rectangular plot -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Create a rectangular plot and a data table. - -q.post.create_report(expressions=data_plot_self) -q.post.create_report(expressions=data_plot_mutual, plot_type="Data Table", context="Original") - -############################################################################### -# Solve setup -# ~~~~~~~~~~~ -# Solve the setup. - -q.analyze() -q.save_project() - -############################################################################### -# Get report data -# ~~~~~~~~~~~~~~~ -# Get the report data into a data structure that allows you to manipulate it. - -a = q.post.get_solution_data(expressions=data_plot_self, context="Original") -a.plot() - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# ``release_desktop`` method. All methods provide for saving projects before closing. - -ansys.aedt.core.settings.enable_debug_logger = False -ansys.aedt.core.settings.enable_debug_methods_argument_logger = False -q.release_desktop(close_projects=True, close_desktop=True) diff --git a/examples/05-Q3D/Q3D_from_EDB.py b/examples/05-Q3D/Q3D_from_EDB.py deleted file mode 100644 index ae4eb2a297f..00000000000 --- a/examples/05-Q3D/Q3D_from_EDB.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Q3D Extractor: PCB analysis ---------------------------- -This example shows how you can use PyAEDT to create a design in -Q3D Extractor and run a simulation starting from an EDB Project. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Setup project files and path -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Download of needed project file and setup of temporary project directory. - -project_dir = ansys.aedt.core.generate_unique_folder_name() -aedb_project = ansys.aedt.core.downloads.download_file('edb/ANSYS-HSD_V1.aedb',destination=project_dir) - -project_name = ansys.aedt.core.generate_unique_name("HSD") -output_edb = os.path.join(project_dir, project_name + '.aedb') -output_q3d = os.path.join(project_dir, project_name + '_q3d.aedt') - -############################################################################### -# Open EDB -# ~~~~~~~~ -# Open the edb project and created a cutout on the selected nets -# before exporting to Q3D. - -edb = ansys.aedt.core.Edb(aedb_project, edbversion=aedt_version) -edb.cutout(["CLOCK_I2C_SCL", "CLOCK_I2C_SDA"], ["GND"], output_aedb_path=output_edb, - use_pyaedt_extent_computing=True, ) - -############################################################################### -# Identify pins position -# ~~~~~~~~~~~~~~~~~~~~~~ -# Identify [x,y] pin locations on the components to define where to assign sources -# and sinks for Q3D and append Z elevation. - -pin_u13_scl = [i for i in edb.components["U13"].pins.values() if i.net_name == "CLOCK_I2C_SCL"] -pin_u1_scl = [i for i in edb.components["U1"].pins.values() if i.net_name == "CLOCK_I2C_SCL"] -pin_u13_sda = [i for i in edb.components["U13"].pins.values() if i.net_name == "CLOCK_I2C_SDA"] -pin_u1_sda = [i for i in edb.components["U1"].pins.values() if i.net_name == "CLOCK_I2C_SDA"] - -############################################################################### -# Append Z Positions -# ~~~~~~~~~~~~~~~~~~ -# Note: The factor 100 converts from "meters" to "mm" - -location_u13_scl = [i * 1000 for i in pin_u13_scl[0].position] -location_u13_scl.append(edb.components["U13"].upper_elevation * 1000) - -location_u1_scl = [i * 1000 for i in pin_u1_scl[0].position] -location_u1_scl.append(edb.components["U1"].upper_elevation * 1000) - -location_u13_sda = [i * 1000 for i in pin_u13_sda[0].position] -location_u13_sda.append(edb.components["U13"].upper_elevation * 1000) - -location_u1_sda = [i * 1000 for i in pin_u1_sda[0].position] -location_u1_sda.append(edb.components["U1"].upper_elevation * 1000) - -############################################################################### -# Save and close Edb -# ~~~~~~~~~~~~~~~~~~ -# Save, close Edb and open it in Hfss 3D Layout to generate the 3D model. - -edb.save_edb() -edb.close_edb() - -h3d = ansys.aedt.core.Hfss3dLayout(output_edb, version=aedt_version, non_graphical=True, new_desktop=True) - -############################################################################### -# Export to Q3D -# ~~~~~~~~~~~~~ -# Create a dummy setup and export the layout in Q3D. -# keep_net_name will reassign Q3D nets names from Hfss 3D Layout. - -setup = h3d.create_setup() -setup.export_to_q3d(output_q3d, keep_net_name=True) -h3d.close_project() - -############################################################################### -# Open Q3D -# ~~~~~~~~ -# Launch the newly created q3d project and plot it. - -q3d = ansys.aedt.core.Q3d(output_q3d) -q3d.plot(assignment=["CLOCK_I2C_SCL", "CLOCK_I2C_SDA"], show=False, - output_file=os.path.join(q3d.working_directory, "Q3D.jpg"), plot_air_objects=False) - -############################################################################### -# Assign Source and Sink -# ~~~~~~~~~~~~~~~~~~~~~~ -# Use previously calculated position to identify faces and -# assign sources and sinks on nets. - -f1 = q3d.modeler.get_faceid_from_position(location_u13_scl, assignment="CLOCK_I2C_SCL") -q3d.source(f1, net_name="CLOCK_I2C_SCL") -f1 = q3d.modeler.get_faceid_from_position(location_u13_sda, assignment="CLOCK_I2C_SDA") -q3d.source(f1, net_name="CLOCK_I2C_SDA") -f1 = q3d.modeler.get_faceid_from_position(location_u1_scl, assignment="CLOCK_I2C_SCL") -q3d.sink(f1, net_name="CLOCK_I2C_SCL") -f1 = q3d.modeler.get_faceid_from_position(location_u1_sda, assignment="CLOCK_I2C_SDA") -q3d.sink(f1, net_name="CLOCK_I2C_SDA") - -############################################################################### -# Create Setup -# ~~~~~~~~~~~~ -# Create a setup and a frequency sweep from DC to 2GHz. -# Analyze project. - -setup = q3d.create_setup() -setup.dc_enabled = True -setup.capacitance_enabled = False -sweep = setup.add_sweep() -sweep.add_subrange("LinearStep", 0, end=2, count=0.05, unit="GHz", save_single_fields=False, clear=True) -setup.analyze() - -############################################################################### -# ACL Report -# ~~~~~~~~~~ -# Compute ACL solutions and plot them. - -traces_acl = q3d.post.available_report_quantities(quantities_category="ACL Matrix") -solution = q3d.post.get_solution_data(traces_acl) -solution.plot() - -############################################################################### -# ACR Report -# ~~~~~~~~~~ -# Compute ACR solutions and plot them. - -traces_acr = q3d.post.available_report_quantities(quantities_category="ACR Matrix") -solution2 = q3d.post.get_solution_data(traces_acr) -solution2.plot() - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# ``release_desktop`` method. All methods provide for saving projects before closing. - -q3d.release_desktop() diff --git a/examples/05-Q3D/Readme.txt b/examples/05-Q3D/Readme.txt deleted file mode 100644 index 1d4f3398872..00000000000 --- a/examples/05-Q3D/Readme.txt +++ /dev/null @@ -1,5 +0,0 @@ -2D Extractor and Q3D Extractor examples -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -These examples use PyAEDT to show some end-to-end workflows for 2D Extractor and -Q3D Extractor. This includes model generation, setup, and thermal postprocessing. - diff --git a/examples/06-Multiphysics/Circuit-HFSS-Icepak-coupling.py b/examples/06-Multiphysics/Circuit-HFSS-Icepak-coupling.py deleted file mode 100644 index 8ec5fcd37e4..00000000000 --- a/examples/06-Multiphysics/Circuit-HFSS-Icepak-coupling.py +++ /dev/null @@ -1,319 +0,0 @@ -""" -Multiphysics: Circuit-HFSS-Icepak coupling workflow ---------------------------------------------------- -This example demonstrates how to create a two-way coupling between Circuit-HFSS designs and Icepak. - -Let's consider a design where some components are simulated in HFSS with a full 3D model, -while others are simulated in Circuit as lumped elements. The electrical simulation is done by -placing the HFSS design into a Circuit design as a subcomponent and by connecting the lumped components to -its ports. - -The purpose of the workflow is to perform a thermal simulation of the Circuit-HFSS design, -creating a two-way coupling with Icepak that allows running multiple iterations. -The losses from both designs are accounted for: EM losses are evaluated by the HFSS solver -and fed into Icepak via a direct link, while losses from the lumped components in the Circuit -design are evaluated analytically and must be manually set into the Icepak boundary. - -On the way back of the coupling, temperature information is handled differently for HFSS and Circuit. -For HFSS, a temperature map is exported from the Icepak design and used to create a 3D dataset; -then the material properties in the HFSS design are updated based on this dataset. -For Circuit, the average temperature of the lumped components is extracted from the Icepak design -and used to update the temperature-dependent characteristics of the lumped components in Circuit. - -In this example, the Circuit design contains only a resistor component, -with temperature-dependent resistance described by this formula: 0.162*(1+0.004*(TempE-TempE0)), -where TempE is the current temperature and TempE0 is the ambient temperature. -The HFSS design includes only a cylinder with temperature-dependent material conductivity, -defined by a 2D dataset. The resistor and the cylinder have matching resistances. - - -The workflow steps are as follows: - -1. Solve the HFSS design. -2. Refresh the dynamic link and solve the Circuit design. -3. Push excitations (HFSS design results are scaled automatically). -4. Extract the resistor's power loss value from the Circuit design. -5. Set the resistor's power loss value in the Icepak design (block thermal condition). -6. Solve the Icepak design. -7. Export the temperature map from the Icepak design and create a new 3D dataset with it. -8. Update material properties in the HFSS design based on the new dataset. -9. Extract the average temperature of the resistor from the Icepak design. -10. Update the resistance value in the Circuit design based on the new resistor average temperature. - -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. -from ansys.aedt.core import Circuit -from ansys.aedt.core import Hfss -from ansys.aedt.core import Icepak -from ansys.aedt.core import downloads -import os - - -############################################################################### -# Download and open project -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Download the project archive. Save it to the temporary folder. -project_path = downloads.download_file("circuit_hfss_icepak", "Circuit-HFSS-Icepak-workflow.aedtz") - -############################################################################### -# Open the project and get the Circuit design. -circuit = Circuit( - project=project_path, - new_desktop_session=True, - specified_version=242, - non_graphical=False -) - -############################################################################### -# Set the name of the resistor in Circuit. -resistor_body_name = "Circuit_Component" - -############################################################################### -# Set the name of the cylinder body in HFSS. -device3D_body_name = "Device_3D" - - -############################################################################### -# Get the Hfss design and prepare the material for the thermal link -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Get the Hfss design. -hfss = Hfss(project=circuit.project_name) - -############################################################################### -# Create a new material that will be used to set the temperature map on it. -# The material is created by duplicating the material assigned to the cylinder. -material_name = hfss.modeler.objects_by_name[device3D_body_name].material_name -new_material_name = material_name + "_dataset" -new_material = hfss.materials.duplicate_material(material=material_name, name=new_material_name) - -############################################################################### -# Save the conductivity value. It will be used later in the iterations. -old_conductivity = new_material.conductivity.value - -############################################################################### -# Assign the new material to the cylinder object in HFSS. -hfss.modeler.objects_by_name[device3D_body_name].material_name = new_material_name - -############################################################################### -# Since this material has a high conductivity, HFSS automatically deactivate "Solve Inside". -# It needs to be set back on as we need to evaluate the losses inside the cylinder. -hfss.modeler.objects_by_name[device3D_body_name].solve_inside = True - - -############################################################################### -# Get the Icepak design -# ~~~~~~~~~~~~~~~~~~~~~ -# Get the Icepak design. -icepak = Icepak(project=circuit.project_name) - - -############################################################################### -# Set the parameters for the iterations -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set the initial temperature to a value closer to the final one, to speed up the convergence. -circuit["TempE"] = "300cel" - -############################################################################### -# Set the maximum number of iterations. -max_iter = 5 - -############################################################################### -# Set the residual convergence criteria to stop the iterations. -temp_residual_limit = 0.02 -loss_residual_limit = 0.02 - -############################################################################### -# This variable will contain the iteration statistics. -stats = {} - - -############################################################################### -# Start the iterations -# ~~~~~~~~~~~~~~~~~~~~ -# Each for loop is a complete two-way iteration. -# The code is thoroughly commented. -# Please read the inline comments carefully for a full understanding. -for cp_iter in range(1, max_iter + 1): - stats[cp_iter] = {} - - - # Step 1: Solve the Hfss design - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Solve the Hfss design. - hfss.analyze() - - - # Step 2: Refresh the dynamic link and solve the Circuit design - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Find the HFSS subcomponent in Circuit. - # This information is required by refresh_dynamic_link and push_excitations methods. - hfss_component_name = "" - hfss_instance_name = "" - for component in circuit.modeler.components.components.values(): - if ( - component.model_name is not None - and hfss.design_name in component.model_name - ): - hfss_component_name = component.model_name - hfss_instance_name = component.refdes - break - if not hfss_component_name or not hfss_instance_name: - raise "Hfss component not found in Circuit design" - - # Refresh the dynamic link. - circuit.modeler.schematic.refresh_dynamic_link(name=hfss_component_name) - - # Solve the Circuit design. - circuit.analyze() - - - # Step 3: Push excitations (HFSS design results are scaled automatically) - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Push excitations. - circuit.push_excitations(instance=hfss_instance_name) - - - # Step 4: Extract the resistor's power loss value from the Circuit design - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Evaluate power loss on resistor. - r_losses = circuit.post.get_solution_data(expressions="0.5*mag(I(I1)*V(V1))").data_magnitude()[0] - - # Save the losses in the stats. - stats[cp_iter]["losses"] = r_losses - - - # Step 5: Set the resistor's power loss value in the Icepak design (block thermal condition) - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Find the Solid Block boundary in Icepak. - boundaries = icepak.boundaries - boundary = None - for bb in boundaries: - if bb.name == "Block1": - boundary = bb - break - if not boundary: - raise "Block boundary not defined in Icepak design." - - # Set the resistor's power loss in the Block Boundary. - boundary.props["Total Power"] = str(r_losses) + "W" - - - # Step 6: Solve the Icepak design - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Clear linked data, otherwise Icepak continues to run simulation with the initial losses. - icepak.clear_linked_data() - - # Solve the Icepak design. - icepak.analyze() - - - # Step 7: Export the temperature map from the Icepak and create a new 3D dataset with it - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Export the temperature map into a file. - fld_filename = os.path.join( - icepak.working_directory, f"temperature_map_{cp_iter}.fld" - ) - icepak.post.export_field_file( - quantity="Temp", output_file=fld_filename, assignment="AllObjects", objects_type="Vol" - ) - - # Convert the fld file format into a dataset tab file compatible with dataset import. - # The existing header lines must be removed and replaced with a single header line - # containing the value unit. - with open(fld_filename, "r") as f: - lines = f.readlines() - - _ = lines.pop(0) - _ = lines.pop(0) - lines.insert(0, '"X" "Y" "Z" "cel"\n') - - basename, _ = os.path.splitext(fld_filename) - tab_filename = basename + "_dataset.tab" - - with open(tab_filename, "w") as f: - f.writelines(lines) - - # Import the 3D dataset. - dataset_name = f"temp_map_step_{cp_iter}" - hfss.import_dataset3d( - input_file=tab_filename, name=dataset_name, is_project_dataset=True - ) - - - # Step 8: Update material properties in the HFSS design based on the new dataset - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Set the new conductivity value. - new_material.conductivity.value = ( - f"{old_conductivity}*Pwl($TempDepCond,clp(${dataset_name},X,Y,Z))" - ) - - # Switch off the thermal modifier of the material, if any. - new_material.conductivity.thermalmodifier = None - - - # Step 9: Extract the average temperature of the resistor from the Icepak design - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Get the mean temp value on the high resistivity object. - mean_temp = icepak.post.get_scalar_field_value( - quantity="Temp", scalar_function="Mean", object_name=resistor_body_name - ) - - # Save the temperature in the iteration stats. - stats[cp_iter]["temp"] = mean_temp - - - # Step 10: Update the resistance value in the Circuit design - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Set this temperature in circuit in variable TempE. - circuit["TempE"] = f"{mean_temp}cel" - - # Save the project - circuit.save_project() - - - # Check the convergence of the iteration - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Evaluate the relative residuals on temperature and losses. - # If the residuals are smaller than the threshold, set the convergence flag to `True`. - # Residuals are calculated starting from the second iteration. - converged = False - stats[cp_iter]["converged"] = converged - if cp_iter > 1: - delta_temp = abs(stats[cp_iter]["temp"] - stats[cp_iter-1]["temp"]) / abs(stats[cp_iter-1]["temp"]) - delta_losses = abs(stats[cp_iter]["losses"] - stats[cp_iter-1]["losses"]) / abs(stats[cp_iter-1]["losses"]) - if delta_temp <= temp_residual_limit and delta_losses <= loss_residual_limit: - converged = True - stats[cp_iter]["converged"] = converged - else: - delta_temp = None - delta_losses = None - - # Save the relative residuals in the iteration stats. - stats[cp_iter]["delta_temp"] = delta_temp - stats[cp_iter]["delta_losses"] = delta_losses - - # Exit from the loop if the convergence is reached. - if converged: - break - -############################################################################### -# Print the overall statistics -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Print the overall statistics for the multiphysic loop. -for i in stats: - txt = "yes" if stats[i]["converged"] else "no" - delta_temp = f"{stats[i]['delta_temp']:.4f}" if stats[i]['delta_temp'] is not None else "None" - delta_losses = f"{stats[i]['delta_losses']:.4f}" if stats[i]['delta_losses'] is not None else "None" - print( - f"Step {i}: temp={stats[i]['temp']:.3f}, losses={stats[i]['losses']:.3f}, " - f"delta_temp={delta_temp}, delta_losses={delta_losses}, " - f"converged={txt}" - ) - -############################################################################### -# Close Electronics Desktop -circuit.release_desktop() diff --git a/examples/06-Multiphysics/Hfss_Icepak_Coupling.py b/examples/06-Multiphysics/Hfss_Icepak_Coupling.py deleted file mode 100644 index 556bcc5217f..00000000000 --- a/examples/06-Multiphysics/Hfss_Icepak_Coupling.py +++ /dev/null @@ -1,335 +0,0 @@ -""" -Multiphysics: HFSS-Icepak multiphysics analysis ------------------------------------------------- -This example shows how you can create a project from scratch in HFSS and Icepak (linked to HFSS). -This includes creating a setup, solving it, and creating postprocessing outputs. - -To provide the advanced postprocessing features needed for this example, the ``numpy``, -``matplotlib``, and ``pyvista`` packages must be installed on the machine. - -This examples runs only on Windows using CPython. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core -from ansys.aedt.core.post.pdf import AnsysReport - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Open project -# ~~~~~~~~~~~~ -# Open the project. - -NewThread = True - -project_file = ansys.aedt.core.generate_unique_project_name() - -############################################################################### -# Launch AEDT and initialize HFSS -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT and initialize HFSS. If there is an active HFSS design, the ``aedtapp`` -# object is linked to it. Otherwise, a new design is created. - -aedtapp = ansys.aedt.core.Hfss(project=project_file, - version=aedt_version, - non_graphical=non_graphical, - new_desktop=NewThread - ) - -############################################################################### -# Initialize variable settings -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize variable settings. You can initialize a variable simply by creating -# it as a list object. If you enter the prefix ``$``, the variable is created for -# the project. Otherwise, the variable is created for the design. - -aedtapp["$coax_dimension"] = "100mm" -udp = aedtapp.modeler.Position(0, 0, 0) -aedtapp["inner"] = "3mm" - -############################################################################### -# Create coaxial and cylinders -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a coaxial and three cylinders. You can apply parameters -# directly using the :func:`ansys.aedt.core.modeler.cad.primitives_3d.Primitives3D.create_cylinder` -# method. You can assign a material directly to the object creation action. -# Optionally, you can assign a material using the :func:`assign_material` method. - -# TODO: How does this work when two truesurfaces are defined? -o1 = aedtapp.modeler.create_cylinder(orientation=aedtapp.PLANE.ZX, origin=udp, radius="inner", height="$coax_dimension", - num_sides=0, name="inner") -o2 = aedtapp.modeler.create_cylinder(orientation=aedtapp.PLANE.ZX, origin=udp, radius=8, height="$coax_dimension", - num_sides=0, material="teflon_based") -o3 = aedtapp.modeler.create_cylinder(orientation=aedtapp.PLANE.ZX, origin=udp, radius=10, height="$coax_dimension", - num_sides=0, name="outer") - -############################################################################### -# Assign colors -# ~~~~~~~~~~~~~ -# Assign colors to each primitive. - -o1.color = (255, 0, 0) -o2.color = (0, 255, 0) -o3.color = (255, 0, 0) -o3.transparency = 0.8 -aedtapp.modeler.fit_all() - -############################################################################### -# Assign materials -# ~~~~~~~~~~~~~~~~ -# Assign materials. You can assign materials either directly when creating the primitive, -# which was done for ``id2``, or after the object is created. - -o1.material_name = "Copper" -o3.material_name = "Copper" - -############################################################################### -# Perform modeler operations -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform modeler operations. You can subtract, add, and perform other operations -# using either the object ID or object name. - -aedtapp.modeler.subtract(o3, o2, True) -aedtapp.modeler.subtract(o2, o1, True) - -############################################################################### -# Perform mesh operations -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Perform mesh operations. Most mesh operations are available. -# After a mesh is created, you can access a mesh operation to -# edit or review parameter values. - -aedtapp.mesh.assign_initial_mesh_from_slider(level=6) -aedtapp.mesh.assign_model_resolution(assignment=[o1.name, o3.name], defeature_length=None) -aedtapp.mesh.assign_length_mesh(assignment=o2.faces, inside_selection=False, maximum_length=1, maximum_elements=2000) - -############################################################################### -# Create excitations -# ~~~~~~~~~~~~~~~~~~ -# Create excitations. The ``create_wave_port_between_objects`` method automatically -# identifies the closest faces on a predefined direction and creates a sheet to cover -# the faces. It also assigns a port to this face. If ``add_pec_cap=True``, the method -# creates a PEC cap. - -aedtapp.wave_port(assignment="inner", reference="outer", create_port_sheet=True, create_pec_cap=True, - integration_line=1, name="P1") -aedtapp.wave_port(assignment="inner", reference="outer", create_port_sheet=True, create_pec_cap=True, - integration_line=4, name="P2") - -port_names = aedtapp.get_all_sources() -aedtapp.modeler.fit_all() - -############################################################################### -# Create setup -# ~~~~~~~~~~~~ -# Create a setup. A setup is created with default values. After its creation, -# you can change values and update the setup. The ``update`` method returns a Boolean -# value. - -aedtapp.set_active_design(aedtapp.design_name) -setup = aedtapp.create_setup("MySetup") -setup.props["Frequency"] = "1GHz" -setup.props["BasisOrder"] = 2 -setup.props["MaximumPasses"] = 1 - -############################################################################### -# Create sweep -# ~~~~~~~~~~~~ -# Create a sweep. A sweep is created with default values. - -sweepname = aedtapp.create_linear_count_sweep(setup="MySetup", units="GHz", start_frequency=0.8, stop_frequency=1.2, - num_of_freq_points=401, sweep_type="Interpolating") - -################################################################################ -# Create Icepak model -# ~~~~~~~~~~~~~~~~~~~ -# Create an Icepak model. After an HFSS setup is ready, link this model to an Icepak -# project and run a coupled physics analysis. The :func:`FieldAnalysis3D.copy_solid_bodies_from` -# method imports a model from HFSS with all material settings. - -ipkapp = ansys.aedt.core.Icepak() -ipkapp.copy_solid_bodies_from(aedtapp) - -################################################################################ -# Link sources to EM losses -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Link sources to the EM losses. - -surfaceobj = ["inner", "outer"] -ipkapp.assign_em_losses(design=aedtapp.design_name, setup="MySetup", sweep="LastAdaptive", map_frequency="1GHz", - surface_objects=surfaceobj, parameters=["$coax_dimension", "inner"]) - -################################################################################# -# Edit gravity setting -# ~~~~~~~~~~~~~~~~~~~~ -# Edit the gravity setting if necessary because it is important for a fluid analysis. - -ipkapp.edit_design_settings(aedtapp.GRAVITY.ZNeg) - -################################################################################ -# Set up Icepak project -# ~~~~~~~~~~~~~~~~~~~~~ -# Set up the Icepak project. When you create a setup, default settings are applied. -# When you need to change a property of the setup, you can use the ``props`` -# command to pass the correct value to the property. The ``update`` function -# applies the settings to the setup. The setup creation process is identical -# for all tools. - -setup_ipk = ipkapp.create_setup("SetupIPK") -setup_ipk.props["Convergence Criteria - Max Iterations"] = 3 - -################################################################################ -# Edit or review mesh parameters -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Edit or review the mesh parameters. After a mesh is created, you can access -# a mesh operation to edit or review parameter values. - -airbox = ipkapp.modeler.get_obj_id("Region") -ipkapp.modeler[airbox].display_wireframe = True -airfaces = ipkapp.modeler.get_object_faces(airbox) -ipkapp.assign_openings(airfaces) - -################################################################################ -# Close and open projects -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Close and open the projects to ensure that the HFSS - Icepak coupling works -# correctly in AEDT versions 2019 R3 through 2021 R1. Closing and opening projects -# can be helpful when performing operations on multiple projects. - -aedtapp.save_project() -aedtapp.close_project(aedtapp.project_name) -aedtapp = ansys.aedt.core.Hfss(project_file) -ipkapp = ansys.aedt.core.Icepak() -ipkapp.solution_type = ipkapp.SOLUTIONS.Icepak.SteadyTemperatureAndFlow -ipkapp.modeler.fit_all() - -################################################################################ -# Solve Icepak project -# ~~~~~~~~~~~~~~~~~~~~ -# Solve the Icepak project and the HFSS sweep. - -setup1 = ipkapp.analyze_setup("SetupIPK") -aedtapp.save_project() -aedtapp.modeler.fit_all() -aedtapp.analyze_setup("MySetup") - -################################################################################ -# Generate field plots and export -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Generate field plots on the HFSS project and export them as images. - -cutlist = [ansys.aedt.core.constants.GLOBALCS.XY, ansys.aedt.core.constants.GLOBALCS.ZX, ansys.aedt.core.constants.GLOBALCS.YZ] -vollist = [o2.name] -setup_name = "MySetup : LastAdaptive" -quantity_name = "ComplexMag_E" -quantity_name2 = "ComplexMag_H" -intrinsic = {"Freq": "1GHz", "Phase": "0deg"} -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.working_directory, "Coaxial_Results_NG") -if not os.path.exists(results_folder): - os.mkdir(results_folder) - -aedtapp.post.plot_field_from_fieldplot(plot1.name, project_path=results_folder, mesh_plot=False, image_format="jpg", - view="isometric", show=False, plot_cad_objs=False, log_scale=False) - -################################################################################ -# Generate animation from field plots -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Generate an animation from field plots using PyVista. - -import time - -start = time.time() -cutlist = ["Global:XY"] -phases = [str(i * 5) + "deg" for i in range(18)] - -animated = aedtapp.post.plot_animated_field(quantity="Mag_E", assignment=cutlist, plot_type="CutPlane", - setup=aedtapp.nominal_adaptive, - intrinsics={"Freq": "1GHz", "Phase": "0deg"}, variation_variable="Phase", - variations=phases, show=False, log_scale=True, export_gif=False, - export_path=results_folder) -animated.gif_file = os.path.join(aedtapp.working_directory, "animate.gif") -# animated.camera_position = [0, 0, 300] -# animated.focal_point = [0, 0, 0] -# Set off_screen to False to visualize the animation. -# animated.off_screen = False -animated.animate() - -endtime = time.time() - start -print("Total Time", endtime) - -################################################################################ -# Create Icepak plots and export -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create Icepak plots and export them as images using the same functions that -# were used early. Only the quantity is different. - -quantity_name = "Temperature" -setup_name = ipkapp.existing_analysis_sweeps[0] -intrinsic = "" -surflist = ipkapp.modeler.get_object_faces("inner") + ipkapp.modeler.get_object_faces("outer") -plot5 = ipkapp.post.create_fieldplot_surface(surflist, "SurfTemperature") - -aedtapp.save_project() - -################################################################################ -# Generate plots outside of AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Generate plots outside of AEDT using Matplotlib and NumPy. - -trace_names = aedtapp.get_traces_for_plot(category="S") -cxt = ["Domain:=", "Sweep"] -families = ["Freq:=", ["All"]] -my_data = aedtapp.post.get_solution_data(expressions=trace_names) -my_data.plot(trace_names, "db20", x_label="Frequency (Ghz)", y_label="SParameters(dB)", title="Scattering Chart", - snapshot_path=os.path.join(results_folder, "Touchstone_from_matplotlib.jpg")) - -################################################################################ -# Generate pdf report -# ~~~~~~~~~~~~~~~~~~~ -# Generate a pdf report with output of simultion. -report = AnsysReport(version=aedt_version, design_name=aedtapp.design_name, project_name=aedtapp.project_name) -report.create() -report.add_section() -report.add_chapter("Hfss Results") -report.add_sub_chapter("Field Plot") -report.add_text("This section contains Field plots of Hfss Coaxial.") -report.add_image(os.path.join(results_folder, plot1.name + ".jpg"), "Coaxial Cable") -report.add_page_break() -report.add_sub_chapter("S Parameters") -report.add_chart(my_data.intrinsics["Freq"], my_data.data_db20(), "Freq", trace_names[0], "S-Parameters") -report.add_image(os.path.join(results_folder, "Touchstone_from_matplotlib.jpg"), "Touchstone from Matplotlib") -report.add_section() -report.add_chapter("Icepak Results") -report.add_sub_chapter("Temperature Plot") -report.add_text("This section contains Multiphysics temperature plot.") -report.add_toc() -# report.add_image(os.path.join(results_folder, plot5.name+".jpg"), "Coaxial Cable Temperatures") -report.save_pdf(results_folder, "AEDT_Results.pdf") - -################################################################################ -# Close project and release AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Close the project and release AEDT. - -aedtapp.release_desktop() diff --git a/examples/06-Multiphysics/Hfss_Mechanical.py b/examples/06-Multiphysics/Hfss_Mechanical.py deleted file mode 100644 index 0de9512d2b7..00000000000 --- a/examples/06-Multiphysics/Hfss_Mechanical.py +++ /dev/null @@ -1,163 +0,0 @@ -""" -Multiphysics: HFSS-Mechanical multiphysics analysis ---------------------------------------------------- -This example shows how you can use PyAEDT to create a multiphysics workflow that -includes Circuit, HFSS, and Mechanical. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Download and open project -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Download and open the project. Save it to the temporary folder. - -project_temp_name = ansys.aedt.core.downloads.download_via_wizard(ansys.aedt.core.generate_unique_folder_name()) - -############################################################################### -# Start HFSS -# ~~~~~~~~~~ -# Start HFSS and initialize the PyAEDT object. - -hfss = ansys.aedt.core.Hfss(project=project_temp_name, version=aedt_version, non_graphical=non_graphical, - new_desktop=True) -pin_names = hfss.excitations -hfss.change_material_override(True) - -############################################################################### -# Start Circuit -# ~~~~~~~~~~~~~ -# Start Circuit and add the HFSS dynamic link component to it. - -circuit = ansys.aedt.core.Circuit() -hfss_comp = circuit.modeler.schematic.add_subcircuit_dynamic_link(hfss) - -############################################################################### -# Set up dynamic link options -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set up dynamic link options. The argument for the ``set_sim_option_on_hfss_subcircuit`` -# method can be the component name, component ID, or component object. - -circuit.modeler.schematic.refresh_dynamic_link(hfss_comp.composed_name) -circuit.modeler.schematic.set_sim_option_on_hfss_subcircuit(hfss_comp) -hfss_setup_name = hfss.setups[0].name + " : " + hfss.setups[0].sweeps[0].name -circuit.modeler.schematic.set_sim_solution_on_hfss_subcircuit(hfss_comp.composed_name, hfss_setup_name) - -############################################################################### -# Create ports and excitations -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create ports and excitations. Find component pin locations and create interface -# ports on them. Define the voltage source on the input port. - -circuit.modeler.schematic.create_interface_port( - name="Excitation_1", location=[hfss_comp.pins[0].location[0], hfss_comp.pins[0].location[1]] -) -circuit.modeler.schematic.create_interface_port( - name="Excitation_2", location=[hfss_comp.pins[1].location[0], hfss_comp.pins[1].location[1]] -) -circuit.modeler.schematic.create_interface_port( - name="Port_1", location=[hfss_comp.pins[2].location[0], hfss_comp.pins[2].location[1]] -) -circuit.modeler.schematic.create_interface_port( - name="Port_2", location=[hfss_comp.pins[3].location[0], hfss_comp.pins[3].location[1]] -) - -voltage = 1 -phase = 0 -ports_list = ["Excitation_1", "Excitation_2"] -source = circuit.assign_voltage_sinusoidal_excitation_to_ports(ports_list) -source.ac_magnitude = voltage -source.phase = phase - -############################################################################### -# Create setup -# ~~~~~~~~~~~~ -# Create a setup. - -setup_name = "MySetup" -LNA_setup = circuit.create_setup(name=setup_name) -bw_start = 4.3 -bw_stop = 4.4 -n_points = 1001 -unit = "GHz" -sweep_list = ["LINC", str(bw_start) + unit, str(bw_stop) + unit, str(n_points)] -LNA_setup.props["SweepDefinition"]["Data"] = " ".join(sweep_list) - -############################################################################### -# Solve and push excitations -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Solve the circuit and push excitations to the HFSS model to calculate the -# correct value of losses. - -circuit.analyze() -circuit.push_excitations(instance="S1", setup=setup_name) - - -############################################################################### -# Start Mechanical -# ~~~~~~~~~~~~~~~~ -# Start Mechanical and copy bodies from the HFSS project. - -mech = ansys.aedt.core.Mechanical() -mech.copy_solid_bodies_from(hfss) -mech.change_material_override(True) - -############################################################################### -# Get losses from HFSS and assign convection to Mechanical -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Get losses from HFSS and assign the convection to Mechanical. - -mech.assign_em_losses(design=hfss.design_name, setup=hfss.setups[0].name, sweep="LastAdaptive", - map_frequency=hfss.setups[0].props["Frequency"], surface_objects=hfss.get_all_conductors_names()) -diels = ["1_pd", "2_pd", "3_pd", "4_pd", "5_pd"] -for el in diels: - mech.assign_uniform_convection(assignment=[mech.modeler[el].top_face_y, mech.modeler[el].bottom_face_y], - convection_value=3) - -############################################################################### -# Plot model -# ~~~~~~~~~~ -# Plot the model. - -mech.plot(show=False, output_file=os.path.join(mech.working_directory, "Mech.jpg"), plot_air_objects=False) - -############################################################################### -# Solve and plot thermal results -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Solve and plot the thermal results. - -mech.create_setup() -mech.save_project() -mech.analyze() -surfaces = [] -for name in mech.get_all_conductors_names(): - surfaces.extend(mech.modeler.get_object_faces(name)) -mech.post.create_fieldplot_surface(assignment=surfaces, quantity="Temperature") - -############################################################################### -# Release AEDT -# ~~~~~~~~~~~~ -# Release AEDT. - -mech.release_desktop(True, True) diff --git a/examples/06-Multiphysics/MRI.py b/examples/06-Multiphysics/MRI.py deleted file mode 100644 index 03cbb09e3ac..00000000000 --- a/examples/06-Multiphysics/MRI.py +++ /dev/null @@ -1,293 +0,0 @@ -""" -Multiphysics: HFSS-Mechanical MRI analysis ---------------------------------------------------- -The goal of this workshop is to use a coil tuned to 63.8 MHz to determine the temperature -rise in a gel phantom near an implant given a background SAR of 1 W/kg. - -Steps to follow -Step 1: Simulate coil loaded by empty phantom: -Scale input to coil ports to produce desired background SAR of 1 W/kg at location that will later contain the implant. -Step 2: Simulate coil loaded by phantom containing implant in proper location: -View SAR in tissue surrounding implant. -Step 3: Thermal simulation: -Link HFSS to transient thermal solver to find temperature rise in tissue near implant vs. time. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. -import os.path - -from ansys.aedt.core import Hfss, Mechanical, Icepak, downloads - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. ` -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Project load -# ~~~~~~~~~~~~ -# Open the ANSYS Electronics Desktop 2018.2 -# Open project background_SAR.aedt -# Project contains phantom and airbox -# Phantom consists of two objects: phantom and implant_box -# Separate objects are used to selectively assign mesh operations -# Material properties defined in this project already contain #electrical and thermal properties. - -project_path = downloads.download_file(directory="mri") -hfss = Hfss(os.path.join(project_path, "background_SAR.aedt"), version=aedt_version, non_graphical=non_graphical, - new_desktop=True) - -############################################################################### -# Insert 3D component -# ~~~~~~~~~~~~~~~~~~~ -# The MRI Coil is saved as a separate 3D Component -# ‒ 3D Components store geometry (including parameters), -# material properties, boundary conditions, mesh assignments, -# and excitations -# ‒ 3D Components make it easy to reuse and share parts of a simulation - -hfss.modeler.insert_3d_component(os.path.join(project_path, "coil.a3dcomp")) - -############################################################################### -# Expression Cache -# ~~~~~~~~~~~~~~~~~ -# On the expression cache tab, define additional convergence criteria for self impedance of the four coil -# ports -# ‒ Set each of these convergence criteria to 2.5 ohm -# For this demo number of passes is limited to 2 to reduce simulation time. - -im_traces = hfss.get_traces_for_plot(get_mutual_terms=False, category="im(Z", first_element_filter="Coil1_p*") - -hfss.setups[0].enable_expression_cache( - report_type="Modal Solution Data", - expressions=im_traces, - isconvergence=True, - isrelativeconvergence=False, - conv_criteria=2.5, - use_cache_for_freq=False) -hfss.setups[0].props["MaximumPasses"] = 2 - -############################################################################### -# Edit Sources -# ~~~~~~~~~~~~ -# The 3D Component of the MRI Coil contains all the ports, -# but the sources for these ports are not yet defined. -# Browse to and select sources.csv. -# These sources were determined by tuning this coil at 63.8 MHz. -# Notice the “*input_scale” multiplier to allow quick adjustment of the coil excitation power. - -hfss.edit_sources_from_file(os.path.join(project_path, "sources.csv")) - -############################################################################### -# Run Simulation -# ~~~~~~~~~~~~~~ -# Save and analyze the project. - -hfss.save_project(os.path.join(project_path, "solved.aedt")) -hfss.analyze(cores=6) - -############################################################################### -# Plot SAR on cut plane in phantom -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Ensure that the SAR averaging method is set to Gridless -# Plot averagedSAR on GlobalYZ plane -# Draw Point1 at origin of the implant coordinate system - -hfss.sar_setup(-1, tissue_mass=1, material_density=1, average_sar_method=1) -hfss.post.create_fieldplot_cutplane(assignment="implant:YZ", quantity="Average_SAR", filter_objects=["implant_box"]) - -hfss.modeler.set_working_coordinate_system("implant") -hfss.modeler.create_point([0, 0, 0], name="Point1") - -hfss.post.plot_field(quantity="Average_SAR", assignment="implant:YZ", plot_type="CutPlane", show=False, - show_legend=False, filter_objects=["implant_box"]) - -############################################################################### -# Adjust Input Power to MRI Coil -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# The goal is to adjust the MRI coil’s input power, so that the averageSAR at Point1 is 1 W/kg -# Note that SAR and input power are linearly related -# To determine required input, calculate -# input_scale = 1/AverageSAR at Point1 - -sol_data = hfss.post.get_solution_data(expressions="Average_SAR", - primary_sweep_variable="Freq", - context="Point1", - report_category="Fields") -sol_data.data_real() - -hfss["input_scale"] = 1 / sol_data.data_real()[0] - -############################################################################### -# Phantom with Implant -# ~~~~~~~~~~~~~~~~~~~~ -# Import implant geometry. -# Subtract rod from implant_box. -# Assign titanium to the imported object rod. -# Analyze the project. - -hfss.modeler.import_3d_cad(os.path.join(project_path, "implant_rod.sat")) - -hfss.modeler["implant_box"].subtract("rod", keep_originals=True) -hfss.modeler["rod"].material_name = "titanium" -hfss.analyze(cores=6) -hfss.save_project() - -############################################################################### -# Thermal Simulation -# ~~~~~~~~~~~~~~~~~~ -# Initialize a new Mechanical Transient Thermal analysis. -# Mechanical Transient Thermal is available in AEDT from 2023 R2 as a Beta feature. - -mech = Mechanical(solution_type="Transient Thermal", version=aedt_version) - -############################################################################### -# Copy geometries -# ~~~~~~~~~~~~~~~ -# Copy bodies from the HFSS project. 3D Component will not be copied. - -mech.copy_solid_bodies_from(hfss) - -################################################################################ -# Link sources to EM losses -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Link sources to the EM losses. -# Assign external convection. - -exc = mech.assign_em_losses(design=hfss.design_name, setup=hfss.setups[0].name, sweep="LastAdaptive", - map_frequency=hfss.setups[0].props["Frequency"], - surface_objects=mech.get_all_conductors_names()) -mech.assign_uniform_convection(mech.modeler["Region"].faces, convection_value=1) - -################################################################################ -# Create Setup -# ~~~~~~~~~~~~ -# Create a new setup and edit properties. -# Simulation will be for 60 seconds. - -setup = mech.create_setup() -# setup.add_mesh_link("backgroundSAR") -# mech.create_dataset1d_design("PowerMap", [0, 239, 240, 360], [1, 1, 0, 0]) -# exc.props["LossMultiplier"] = "pwl(PowerMap,Time)" - -mech.modeler.set_working_coordinate_system("implant") -mech.modeler.create_point([0, 0, 0], name="Point1") -setup.props["Stop Time"] = 60 -setup.props["Time Step"] = "10s" -setup.props["SaveFieldsType"] = "Every N Steps" -setup.props["N Steps"] = "2" - -############################################################################### -# Analyze Mechanical -# ~~~~~~~~~~~~~~~~~~ -# Analyze the project. - -mech.analyze(cores=6) - -############################################################################### -# Plot fields -# ~~~~~~~~~~~ -# Plot Temperature on cut plane. -# Plot Temperature on point. - -mech.post.create_fieldplot_cutplane("implant:YZ", "Temperature", filter_objects=["implant_box"]) -mech.save_project() - -data = mech.post.get_solution_data("Temperature", primary_sweep_variable="Time", context="Point1", - report_category="Fields") -#data.plot() - -mech.post.plot_animated_field(quantity="Temperature", assignment="implant:YZ", plot_type="CutPlane", - intrinsics={"Time": "10s"}, variation_variable="Time", - variations=["10s", "20s", "30s", "40s", "50s", "60s"], - show=False, filter_objects=["implant_box"]) - -############################################################################### -# Thermal Simulation -# ~~~~~~~~~~~~~~~~~~ -# Initialize a new Icepak Transient Thermal analysis. - -ipk = Icepak(solution_type="Transient", version=aedt_version) -ipk.design_solutions.problem_type = "TemperatureOnly" - -############################################################################### -# Copy geometries -# ~~~~~~~~~~~~~~~ -# Copy bodies from the HFSS project. 3D Component will not be copied. - -ipk.modeler.delete("Region") -ipk.copy_solid_bodies_from(hfss) - -################################################################################ -# Link sources to EM losses -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Link sources to the EM losses. -# Assign external convection. - -exc = ipk.assign_em_losses(design=hfss.design_name, setup=hfss.setups[0].name, sweep="LastAdaptive", - map_frequency=hfss.setups[0].props["Frequency"], - surface_objects=ipk.get_all_conductors_names()) - -################################################################################ -# Create Setup -# ~~~~~~~~~~~~ -# Create a new setup and edit properties. -# Simulation will be for 60 seconds. - -setup = ipk.create_setup() - -setup.props["Stop Time"] = 60 -setup.props["N Steps"] = 2 -setup.props["Time Step"] = 5 -setup.props['Convergence Criteria - Energy'] = 1e-12 - -################################################################################ -# Mesh Region -# ~~~~~~~~~~~ -# Create a new mesh region and change accuracy level to 4. - -bound = ipk.modeler["implant_box"].bounding_box -mesh_box = ipk.modeler.create_box(bound[:3], [bound[3] - bound[0], bound[4] - bound[1], bound[5] - bound[2]]) -mesh_box.model = False -mesh_region = ipk.mesh.assign_mesh_region([mesh_box.name]) -mesh_region.UserSpecifiedSettings = False -mesh_region.Level = 4 -mesh_region.update() - -################################################################################ -# Point Monitor -# ~~~~~~~~~~~~~ -# Create a new point monitor. - -ipk.modeler.set_working_coordinate_system("implant") -ipk.monitor.assign_point_monitor([0, 0, 0], monitor_name="Point1") -ipk.assign_openings(ipk.modeler["Region"].top_face_z) - -############################################################################### -# Analyze and plot fields -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Analyze the project. -# Plot temperature on cut plane. -# Plot temperature on monitor point. - -ipk.analyze(cores=4, tasks=4) -ipk.post.create_fieldplot_cutplane("implant:YZ", "Temperature", filter_objects=["implant_box"]) -ipk.save_project() - -data = ipk.post.get_solution_data("Point1.Temperature", primary_sweep_variable="Time", report_category="Monitor") -#data.plot() - -ipk.release_desktop(True, True) diff --git a/examples/06-Multiphysics/Maxwell3D_Icepak_2Way_Coupling.py b/examples/06-Multiphysics/Maxwell3D_Icepak_2Way_Coupling.py deleted file mode 100644 index 17919c5f64b..00000000000 --- a/examples/06-Multiphysics/Maxwell3D_Icepak_2Way_Coupling.py +++ /dev/null @@ -1,281 +0,0 @@ -""" -Multiphysics: Maxwell 3D - Icepak electrothermal analysis ---------------------------------------------------------- -This example uses PyAEDT to set up a simple Maxwell design consisting of a coil and a ferrite core. -Coil current is set to 100A, and coil resistance and ohmic loss are analyzed. -Ohmic loss is mapped to Icepak, and a thermal analysis is performed. Icepak calculates a temperature distribution, -and it is mapped back to Maxwell (2-way coupling). Coil resistance and ohmic loss are analyzed again in Maxwell. -Results are printed in AEDT Message Manager. -""" -########################################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import ansys.aedt.core -from ansys.aedt.core.generic.constants import AXIS - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -########################################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -########################################################################################### -# Launch AEDT and Maxwell 3D -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT and Maxwell 3D. The following code sets up the project and design names, the solver, and -# the version. It also creates an instance of the ``Maxwell3d`` class named ``m3d``. - -project_name = "Maxwell-Icepak-2way-Coupling" -maxwell_design_name = "1 Maxwell" -icepak_design_name = "2 Icepak" - -m3d = ansys.aedt.core.Maxwell3d( - project=project_name, - design=maxwell_design_name, - solution_type="EddyCurrent", - version=aedt_version, - non_graphical=non_graphical, -) - -############################################################################### -# Create geometry in Maxwell -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the coil, coil terminal, core, and region. - -coil = m3d.modeler.create_rectangle( - orientation="XZ", origin=[70, 0, -11], sizes=[11, 110], name="Coil" -) - -coil.sweep_around_axis(axis=AXIS.Z) - -coil_terminal = m3d.modeler.create_rectangle( - orientation="XZ", origin=[70, 0, -11], sizes=[11, 110], name="Coil_terminal" -) - -core = m3d.modeler.create_rectangle( - orientation="XZ", origin=[45, 0, -18], sizes=[7, 160], name="Core" -) -core.sweep_around_axis(axis=AXIS.Z) - -# Magnetic flux is not concentrated by the core in +z-direction. Therefore, more padding is needed in that direction. -region = m3d.modeler.create_region(pad_percent=[20, 20, 500, 20, 20, 100]) - -############################################################################### -# Assign materials -# ~~~~~~~~~~~~~~~~ -# Create a material: Copper AWG40 Litz wire, strand diameter = 0.08mm, 24 parallel strands. -# Assign materials: Assign Coil to AWG40 copper, core to ferrite, and region to vacuum. - -no_strands = 24 -strand_diameter = 0.08 - -cu_litz = m3d.materials.duplicate_material("copper", "copper_litz") -cu_litz.stacking_type = "Litz Wire" -cu_litz.wire_diameter = str(strand_diameter) + "mm" -cu_litz.wire_type = "Round" -cu_litz.strand_number = no_strands - -m3d.assign_material(region.name, "vacuum") -m3d.assign_material(coil.name, "copper_litz") -m3d.assign_material(core.name, "ferrite") - -############################################################################### -# Assign excitation -# ~~~~~~~~~~~~~~~~~ -# Assign coil current, coil consists of 20 turns, total current 10A. -# Note that each coil turn consists of 24 parallel Litz strands, see above. - -no_turns = 20 -coil_current = 10 -m3d.assign_coil(["Coil_terminal"], conductors_number=no_turns, name="Coil_terminal") -m3d.assign_winding(is_solid=False, current=coil_current, name="Winding1") - -m3d.add_winding_coils(assignment="Winding1", coils=["Coil_terminal"]) - -############################################################################### -# Assign mesh operations -# ~~~~~~~~~~~~~~~~~~~~~~ -# Mesh operations are not necessary in eddy current solver because of auto-adaptive meshing. -# However, with appropriate mesh operations, less adaptive passes are needed. - -m3d.mesh.assign_length_mesh(["Core"], maximum_length=15, maximum_elements=None, name="Inside_Core") -m3d.mesh.assign_length_mesh(["Coil"], maximum_length=30, maximum_elements=None, name="Inside_Coil") - -############################################################################### -# Set conductivity temperature coefficient -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set conductivity as a function of temperature. Resistivity increases by 0.393% per K. - -cu_resistivity_temp_coefficient = 0.00393 -cu_litz.conductivity.add_thermal_modifier_free_form("1.0/(1.0+{}*(Temp-20))".format(cu_resistivity_temp_coefficient)) - -############################################################################### -# Set object temperature and enable feedback -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set the temperature of the objects to default temperature (22deg C) -# and enable temperature feedback for two-way coupling. - -m3d.modeler.set_objects_temperature(["Coil"]) - -############################################################################### -# Assign matrix -# ~~~~~~~~~~~~~ -# Resistance and inductance calculation. - -m3d.assign_matrix(["Winding1"], matrix_name="Matrix1") - -############################################################################### -# Create and analyze simulation setup -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Simulation frequency 150kHz. - -setup = m3d.create_setup(name="Setup1") -setup.props["Frequency"] = "150kHz" -m3d.analyze_setup("Setup1") - -############################################################################### -# Postprocessing -# ~~~~~~~~~~~~~~ -# Calculate analytical DC resistance and compare it with the simulated coil resistance, -# print them in the message manager, as well as ohmic loss in coil before temperature feedback. - -report = m3d.post.create_report(expressions="Matrix1.R(Winding1,Winding1)") -solution = report.get_solution_data() -resistance = solution.data_magnitude()[0] - -report_loss = m3d.post.create_report(expressions="StrandedLossAC") -solution_loss = report_loss.get_solution_data() -em_loss = solution_loss.data_magnitude()[0] - -# Analytical calculation of the DC resistance of the coil -cu_cond = float(cu_litz.conductivity.value) -# average radius of a coil turn = 0.125m -l_conductor = no_turns*2*0.125*3.1415 -# R = resistivity * length / area / no_strand -r_analytical_DC = (1.0 / cu_cond) * l_conductor / (3.1415 * (strand_diameter / 1000 / 2) ** 2) / no_strands - -# Print results in the Message Manager -m3d.logger.info("*******Coil analytical DC resistance = {:.2f}Ohm".format(r_analytical_DC)) -m3d.logger.info("*******Coil resistance at 150kHz BEFORE temperature feedback = {:.2f}Ohm".format(resistance)) -m3d.logger.info("*******Ohmic loss in coil BEFORE temperature feedback = {:.2f}W".format(em_loss / 1000)) - -############################################################################### -# Icepak design -# ~~~~~~~~~~~~~ -# Insert Icepak design, copy solid objects from Maxwell, and modify region dimensions. - -ipk = ansys.aedt.core.Icepak(design=icepak_design_name) -ipk.copy_solid_bodies_from(m3d, no_pec=False) - -# Set domain dimensions suitable for natural convection using the diameter of the coil -ipk.modeler["Region"].delete() -coil_dim = coil.bounding_dimension[0] -ipk.modeler.create_region(0, False) -ipk.modeler.edit_region_dimensions([coil_dim / 2, coil_dim / 2, coil_dim / 2, coil_dim / 2, coil_dim * 2, coil_dim]) - -############################################################################### -# Map coil losses -# ~~~~~~~~~~~~~~~ -# Map ohmic losses from Maxwell to the Icepak design. - -ipk.assign_em_losses(design="1 Maxwell", setup=m3d.setups[0].name, sweep="LastAdaptive", assignment=["Coil"]) - -############################################################################### -# Boundary conditions -# ~~~~~~~~~~~~~~~~~~~ -# Assign opening. - -faces = ipk.modeler["Region"].faces -face_names = [face.id for face in faces] -ipk.assign_free_opening(face_names, boundary_name="Opening1") - -############################################################################### -# Assign monitor -# ~~~~~~~~~~~~~~ -# Temperature monitor on the coil surface - -temp_monitor = ipk.assign_point_monitor([70, 0, 0], monitor_name="PointMonitor1") - -############################################################################### -# Icepak solution setup -# ~~~~~~~~~~~~~~~~~~~~~ - -solution_setup = ipk.create_setup() -solution_setup.props["Convergence Criteria - Max Iterations"] = 50 -solution_setup.props["Flow Regime"] = "Turbulent" -solution_setup.props["Turbulent Model Eqn"] = "ZeroEquation" -solution_setup.props["Radiation Model"] = "Discrete Ordinates Model" -solution_setup.props["Include Flow"] = True -solution_setup.props["Include Gravity"] = True -solution_setup.props["Solution Initialization - Z Velocity"] = "0.0005m_per_sec" -solution_setup.props["Convergence Criteria - Flow"] = 0.0005 -solution_setup.props["Flow Iteration Per Radiation Iteration"] = "5" - - -############################################################################### -# Add 2-way coupling and solve the project -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Enable mapping temperature distribution back to Maxwell. -# Default number Maxwell <--> Icepak iterations is 2, -# but for increased accuracy it can be increased (number_of_iterations). - -ipk.assign_2way_coupling() -ipk.analyze_setup(name=solution_setup.name) - -############################################################################### -# Postprocessing -# ~~~~~~~~~~~~~~ -# Plot temperature on the object surfaces. - -surface_list = [] -for name in ["Coil", "Core"]: - surface_list.extend(ipk.modeler.get_object_faces(name)) - -surf_temperature = ipk.post.create_fieldplot_surface(surface_list, quantity="SurfTemperature", - plot_name="Surface Temperature") - -velocity_cutplane = ipk.post.create_fieldplot_cutplane(assignment=["Global:XZ"], quantity="Velocity Vectors", - plot_name="Velocity Vectors") - -surf_temperature.export_image() -velocity_cutplane.export_image(orientation="right") - -report_temp = ipk.post.create_report(expressions="PointMonitor1.Temperature", primary_sweep_variable="X") -solution_temp = report_temp.get_solution_data() -temp = solution_temp.data_magnitude()[0] -m3d.logger.info("*******Coil temperature = {:.2f}deg C".format(temp)) - -############################################################################### -# Get new resistance from Maxwell -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Temperature of the coil increases, and consequently also coil resistance increases. - -report_new = m3d.post.create_report(expressions="Matrix1.R(Winding1,Winding1)") -solution_new = report_new.get_solution_data() -resistance_new = solution_new.data_magnitude()[0] -resistance_increase = (resistance_new - resistance)/resistance * 100 - -report_loss_new = m3d.post.create_report(expressions="StrandedLossAC") -solution_loss_new = report_loss_new.get_solution_data() -em_loss_new = solution_loss_new.data_magnitude()[0] - -m3d.logger.info("*******Coil resistance at 150kHz AFTER temperature feedback = {:.2f}Ohm".format(resistance_new)) -m3d.logger.info("*******Coil resistance increased by {:.2f}%".format(resistance_increase)) -m3d.logger.info("*******Ohmic loss in coil AFTER temperature feedback = {:.2f}W".format(em_loss_new/1000)) - -################################################################################## -# Release desktop -# ~~~~~~~~~~~~~~~ - -ipk.release_desktop(True, True) diff --git a/examples/06-Multiphysics/Readme.txt b/examples/06-Multiphysics/Readme.txt deleted file mode 100644 index d35019b7dbd..00000000000 --- a/examples/06-Multiphysics/Readme.txt +++ /dev/null @@ -1,5 +0,0 @@ -Multiphysics examples -~~~~~~~~~~~~~~~~~~~~~ -These examples use PyAEDT to create some multiphysics workflows. They might use -an electromagnetic tool like HFSS or Maxwell and a thermal or structural tool -like Icepak or Mechanical. diff --git a/examples/07-Circuit/Circuit_AMI.py b/examples/07-Circuit/Circuit_AMI.py deleted file mode 100644 index 11ee2ce73ee..00000000000 --- a/examples/07-Circuit/Circuit_AMI.py +++ /dev/null @@ -1,271 +0,0 @@ -""" -Circuit: AMI PostProcessing ----------------------------------- -This example shows how you can use PyAEDT to perform advanced postprocessing of AMI simulations. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports and set the local path to the path for PyAEDT. - -# sphinx_gallery_thumbnail_path = 'Resources/spectrum_plot.png' - -import os -from matplotlib import pyplot as plt -import numpy as np - -import ansys.aedt.core - -# Set local path to path for PyAEDT -temp_folder = ansys.aedt.core.generate_unique_folder_name() -project_path = ansys.aedt.core.downloads.download_file("ami", "ami_usb.aedtz", temp_folder) - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -########################################################## -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. -# The Boolean parameter ``new_thread`` defines whether to create a new instance -# of AEDT or try to connect to an existing instance of it. - -non_graphical = False -NewThread = True - -############################################################################### -# Launch AEDT with Circuit and enable Pandas as the output format -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# All outputs obtained with the `get_solution_data` method will have the Pandas format. -# Launch AEDT with Circuit. The :class:`ansys.aedt.core.Desktop` class initializes AEDT -# and starts the specified version in the specified mode. - -ansys.aedt.core.settings.enable_pandas_output = True -cir = ansys.aedt.core.Circuit(project=os.path.join(project_path), non_graphical=non_graphical, - version=aedt_version, new_desktop=NewThread) - -############################################################################### -# Solve AMI setup -# ~~~~~~~~~~~~~~~ -# Solve the transient setup. - -cir.analyze() - -############################################################################### -# Get AMI report -# ~~~~~~~~~~~~~~ -# Get AMI report data - -plot_name = "WaveAfterProbe" -cir.solution_type = "NexximAMI" -original_data = cir.post.get_solution_data(expressions=plot_name, domain="Time", - variations=cir.available_variations.nominal) -original_data_value = original_data.full_matrix_real_imag[0] -original_data_sweep = original_data.primary_sweep_values -print(original_data_value) - -############################################################################### -# Plot data -# ~~~~~~~~~ -# Create a plot based on solution data. - -fig = original_data.plot() - -############################################################################### -# Sample WaveAfterProbe waveform using receiver clock -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Extract waveform at specific clock time plus half unit interval - -probe_name = "b_input_43" -source_name = "b_output4_42" -plot_type = "WaveAfterProbe" -setup_name = "AMIAnalysis" -ignore_bits = 100 -unit_interval = 0.1e-9 -sample_waveform = cir.post.sample_ami_waveform(setup=setup_name, probe=probe_name, source=source_name, - variation_list_w_value=cir.available_variations.nominal, - unit_interval=unit_interval, ignore_bits=ignore_bits, - plot_type=plot_type) - -############################################################################### -# Plot waveform and samples -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the plot from a start time to stop time in seconds - -tstop = 55e-9 -tstart = 50e-9 -scale_time = ansys.aedt.core.constants.unit_converter(1, unit_system="Time", input_units="s", - output_units=original_data.units_sweeps["Time"]) -scale_data = ansys.aedt.core.constants.unit_converter(1, unit_system="Voltage", input_units="V", - output_units=original_data.units_data[plot_name]) - -tstop_ns = scale_time * tstop -tstart_ns = scale_time * tstart - -for time in original_data_value[plot_name].index: - if tstart_ns <= time[0]: - start_index_original_data = time[0] - break -for time in original_data_value[plot_name][start_index_original_data:].index: - if time[0] >= tstop_ns: - stop_index_original_data = time[0] - break -for time in sample_waveform[0].index: - if tstart <= time: - sample_index = sample_waveform[0].index == time - start_index_waveform = sample_index.tolist().index(True) - break -for time in sample_waveform[0].index: - if time >= tstop: - sample_index = sample_waveform[0].index == time - stop_index_waveform = sample_index.tolist().index(True) - break - -original_data_zoom = original_data_value[start_index_original_data:stop_index_original_data] -sampled_data_zoom = sample_waveform[0].values[start_index_waveform:stop_index_waveform] * scale_data -sampled_time_zoom = sample_waveform[0].index[start_index_waveform:stop_index_waveform] * scale_time - -fig, ax = plt.subplots() -ax.plot(sampled_time_zoom, sampled_data_zoom, "r*") -ax.plot(np.array(list(original_data_zoom.index.values)), original_data_zoom.values) -ax.set_title('WaveAfterProbe') -ax.set_xlabel(original_data.units_sweeps["Time"]) -ax.set_ylabel(original_data.units_data[plot_name]) -plt.show() - -############################################################################### -# Plot Slicer Scatter -# ~~~~~~~~~~~~~~~~~~~ -# Create the plot from a start time to stop time in seconds - -fig, ax2 = plt.subplots() -ax2.plot(sample_waveform[0].index, sample_waveform[0].values, "r*") -ax2.set_title('Slicer Scatter: WaveAfterProbe') -ax2.set_xlabel("s") -ax2.set_ylabel("V") -plt.show() - -############################################################################### -# Plot scatter histogram -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create the plot from a start time to stop time in seconds. - -fig, ax4 = plt.subplots() -ax4.set_title('Slicer Histogram: WaveAfterProbe') -ax4.hist(sample_waveform[0].values, orientation='horizontal') -ax4.set_ylabel("V") -ax4.grid() -plt.show() - -############################################################################### -# Get Transient report -# ~~~~~~~~~~~~~~~~~~~~ -# Get Transient report data - -plot_name = "V(b_input_43.int_ami_rx.eye_probe.out)" -cir.solution_type = "NexximTransient" -original_data = cir.post.get_solution_data(expressions=plot_name, - domain="Time", - setup_sweep_name="NexximTransient", - variations=cir.available_variations.nominal) - -############################################################################### -# Sample waveform using a user-defined clock -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Extract waveform at specific clock time plus half unit interval. - -original_data.enable_pandas_output = False -original_data_value = original_data.data_real() -original_data_sweep = original_data.primary_sweep_values -waveform_unit = original_data.units_data[plot_name] -waveform_sweep_unit = original_data.units_sweeps["Time"] -tics = np.arange(20e-9, 100e-9, 1e-10, dtype=float) - -sample_waveform = cir.post.sample_waveform( - waveform_data=original_data_value, - waveform_sweep=original_data_sweep, - waveform_unit=waveform_unit, - waveform_sweep_unit=waveform_sweep_unit, - unit_interval=unit_interval, - clock_tics=tics, - pandas_enabled=False, -) - -############################################################################### -# Plot waveform and samples -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the plot from a start time to stop time in seconds. - -tstop = 40.0e-9 -tstart = 25.0e-9 -scale_time = ansys.aedt.core.constants.unit_converter(1, unit_system="Time", input_units="s", - output_units=waveform_sweep_unit) -scale_data = ansys.aedt.core.constants.unit_converter(1, unit_system="Voltage", input_units="V", - output_units=waveform_unit) - -tstop_ns = scale_time * tstop -tstart_ns = scale_time * tstart - -for time in original_data_sweep: - if tstart_ns <= time: - start_index_original_data = original_data_sweep.index(time) - break -for time in original_data_sweep[start_index_original_data:]: - if time >= tstop_ns: - stop_index_original_data = original_data_sweep.index(time) - break -cont = 0 -for frame in sample_waveform: - if tstart <= frame[0]: - start_index_waveform = cont - break - cont += 1 -for frame in sample_waveform[start_index_waveform:]: - if frame[0] >= tstop: - stop_index_waveform = cont - break - cont += 1 - -original_data_zoom = original_data_value[start_index_original_data:stop_index_original_data] -original_sweep_zoom = original_data_sweep[start_index_original_data:stop_index_original_data] -original_data_zoom_array = np.array(list(map(list, zip(original_sweep_zoom, original_data_zoom)))) -original_data_zoom_array[:, 0] *= 1 -sampled_data_zoom_array = np.array(sample_waveform[start_index_waveform:stop_index_waveform]) -sampled_data_zoom_array[:, 0] *= scale_time -sampled_data_zoom_array[:, 1] *= scale_data - -fig, ax = plt.subplots() -ax.plot(sampled_data_zoom_array[:, 0], sampled_data_zoom_array[:, 1], "r*") -ax.plot(original_sweep_zoom, original_data_zoom_array[:, 1]) -ax.set_title(plot_name) -ax.set_xlabel(waveform_sweep_unit) -ax.set_ylabel(waveform_unit) -plt.show() - -############################################################################### -# Plot slicer scatter -# ~~~~~~~~~~~~~~~~~~~ -# Create the plot from a start time to stop time in seconds. - -sample_waveform_array = np.array(sample_waveform) -fig, ax2 = plt.subplots() -ax2.plot(sample_waveform_array[:, 0], sample_waveform_array[:, 1], "r*") -ax2.set_title('Slicer Scatter: ' + plot_name) -ax2.set_xlabel("s") -ax2.set_ylabel("V") -plt.show() - -############################################################################### -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Save the project and close AEDT. - -cir.save_project() -print("Project Saved in {}".format(cir.project_path)) -cir.release_desktop() diff --git a/examples/07-Circuit/Circuit_Example.py b/examples/07-Circuit/Circuit_Example.py deleted file mode 100644 index 9ea3b8e4aaa..00000000000 --- a/examples/07-Circuit/Circuit_Example.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -Circuit: schematic creation and analysis ----------------------------------------- -This example shows how you can use PyAEDT to create a circuit design -and run a Nexxim time-domain simulation. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -# sphinx_gallery_thumbnail_path = 'Resources/circuit.png' - -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. -# The Boolean parameter ``new_thread`` defines whether to create a new instance -# of AEDT or try to connect to an existing instance of it. - -non_graphical = False -new_thread = True - -############################################################################### -# Launch AEDT and Circuit -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT and Circuit. The :class:`ansys.aedt.core.Desktop` class initializes AEDT and -# starts the specified version in the specified mode. - -desktop = ansys.aedt.core.launch_desktop(aedt_version, non_graphical, new_thread) -aedt_app = ansys.aedt.core.Circuit(project=ansys.aedt.core.generate_unique_project_name()) -aedt_app.modeler.schematic.schematic_units = "mil" -############################################################################### -# Create circuit setup -# ~~~~~~~~~~~~~~~~~~~~ -# Create and customize an LNA (linear network analysis) setup. - -setup1 = aedt_app.create_setup("MyLNA") -setup1.props["SweepDefinition"]["Data"] = "LINC 0GHz 4GHz 10001" - -############################################################################### -# Create components -# ~~~~~~~~~~~~~~~~~ -# Create components, such as an inductor, resistor, and capacitor. - -inductor = aedt_app.modeler.schematic.create_inductor(name="L1", value=1e-9, location=[0, 0]) -resistor = aedt_app.modeler.schematic.create_resistor(name="R1", value=50, location=[500, 0]) -capacitor = aedt_app.modeler.schematic.create_capacitor(name="C1", value=1e-12, location=[1000, 0]) - -############################################################################### -# Get all pins -# ~~~~~~~~~~~~ -# Get all pins of a specified component. - -pins_resistor = resistor.pins - -############################################################################### -# Create port and ground -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create a port and a ground, which are needed for the circuit analysis. - -port = aedt_app.modeler.components.create_interface_port(name="myport", location=[-200, 0] ) -gnd = aedt_app.modeler.components.create_gnd(location=[1200, -100]) - -############################################################################### -# Connect components -# ~~~~~~~~~~~~~~~~~~ -# Connect components with wires. - -port.pins[0].connect_to_component(assignment=inductor.pins[0], use_wire=True) -inductor.pins[1].connect_to_component(assignment=resistor.pins[1], use_wire=True) -resistor.pins[0].connect_to_component(assignment=capacitor.pins[0], use_wire=True) -capacitor.pins[1].connect_to_component(assignment=gnd.pins[0], use_wire=True) - -############################################################################### -# Create transient setup -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create a transient setup. - -setup2 = aedt_app.create_setup(name="MyTransient", setup_type=aedt_app.SETUPS.NexximTransient) -setup2.props["TransientData"] = ["0.01ns", "200ns"] -setup3 = aedt_app.create_setup(name="MyDC", setup_type=aedt_app.SETUPS.NexximDC) - -############################################################################### -# Solve transient setup -# ~~~~~~~~~~~~~~~~~~~~~ -# Solve the transient setup. - -aedt_app.analyze_setup("MyLNA") -aedt_app.export_fullwave_spice() - -############################################################################### -# Create report -# ~~~~~~~~~~~~~ -# Create a report that plots solution data. - -solutions = aedt_app.post.get_solution_data(expressions=aedt_app.get_traces_for_plot(category="S")) -solutions.enable_pandas_output = True -real, imag = solutions.full_matrix_real_imag -print(real) - -############################################################################### -# Plot data -# ~~~~~~~~~ -# Create a plot based on solution data. - -fig = solutions.plot() - -############################################################################### -# Close AEDT -# ~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.force_close_desktop` method. -# All methods provide for saving the project before closing. - -desktop.release_desktop() diff --git a/examples/07-Circuit/Circuit_Siwave_Multizones.py b/examples/07-Circuit/Circuit_Siwave_Multizones.py deleted file mode 100644 index 8f9387ae690..00000000000 --- a/examples/07-Circuit/Circuit_Siwave_Multizones.py +++ /dev/null @@ -1,108 +0,0 @@ -""" -Circuit: Simulate multi-zones layout with Siwave ------------------------------------------------- -This example shows how you can use PyAEDT simulate multi-zones with Siwave. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports, which includes importing a section. - -from ansys.aedt.core import Edb, Circuit -import os.path -import ansys.aedt.core - -############################################################################### -# Download file -# ~~~~~~~~~~~~~ -# Download the AEDB file and copy it in the temporary folder. - -temp_folder = ansys.aedt.core.generate_unique_folder_name() -edb_file = ansys.aedt.core.downloads.download_file(destination=temp_folder, directory="edb/siwave_multi_zones.aedb") -working_directory = os.path.join(temp_folder, "workdir") -aedt_file = os.path.splitext(edb_file)[0] + ".aedt" -circuit_project_file = os.path.join(working_directory, os.path.splitext(os.path.basename(edb_file))[0] + - "multizone_clipped_circuit.aedt") -print(edb_file) - - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -##################################################################################### -# Ground net -# ~~~~~~~~~~ -# Common reference net used across all sub-designs, Mandatory for this work flow. - -common_reference_net = "GND" - -######################################################################################## -# Project load -# ~~~~~~~~~~~~ -# Load initial Edb file, checking if aedt file exists and remove to allow Edb loading. - -if os.path.isfile(aedt_file): - os.remove(aedt_file) -edb = Edb(edbversion=aedt_version, edbpath=edb_file) - -############################################################################### -# Project zones -# ~~~~~~~~~~~~~ -# Copy project zone into sub project. - -edb_zones = edb.copy_zones(working_directory=working_directory) - -############################################################################### -# Split zones -# ~~~~~~~~~~~ -# Clip sub-designs along with corresponding zone definition -# and create port of clipped signal traces. -defined_ports, project_connexions = edb.cutout_multizone_layout(edb_zones, common_reference_net) - -############################################################################################################# -# Circuit -# ~~~~~~~ -# Create circuit design, import all sub-project as EM model and connect all corresponding pins in circuit. - -circuit = Circuit(version=aedt_version, project=circuit_project_file) -circuit.connect_circuit_models_from_multi_zone_cutout(project_connections=project_connexions, - edb_zones_dict=edb_zones, ports=defined_ports, - model_inc=70) -############################################################################### -# Setup -# ~~~~~ -# Add Nexxim LNA simulation setup. -circuit_setup= circuit.create_setup("Pyedt_LNA") - -############################################################################### -# Frequency sweep -# ~~~~~~~~~~~~~~~ -# Add frequency sweep from 0GHt to 20GHz with 10NHz frequency step. -circuit_setup.props["SweepDefinition"]["Data"] = "LIN {} {} {}".format("0GHz", "20GHz", "10MHz") - -############################################################################### -# Start simulation -# ~~~~~~~~~~~~~~~~ -# Analyze all siwave projects and solves the circuit. -circuit.analyze() - -############################################################################### -# Define differential pairs -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -circuit.set_differential_pair(assignment="U0.via_38.B2B_SIGP", reference="U0.via_39.B2B_SIGN", differential_mode="U0") -circuit.set_differential_pair(assignment="U1.via_32.B2B_SIGP", reference="U1.via_33.B2B_SIGN", differential_mode="U1") - -############################################################################### -# Plot results -# ~~~~~~~~~~~~ -circuit.post.create_report(expressions=["dB(S(U0,U0))", "dB(S(U1,U0))"], context="Differential Pairs") - -############################################################################### -# Release AEDT desktop -# ~~~~~~~~~~~~~~~~~~~~ -circuit.release_desktop() \ No newline at end of file diff --git a/examples/07-Circuit/Circuit_Subcircuit_Example.py b/examples/07-Circuit/Circuit_Subcircuit_Example.py deleted file mode 100644 index a06fa063381..00000000000 --- a/examples/07-Circuit/Circuit_Subcircuit_Example.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Circuit: schematic subcircuit management ----------------------------------------- -This example shows how you can use PyAEDT to add a subcircuit to a circuit design. -It pushes down the child subcircuit and pops up to the parent design. -""" -########################################################## -# Perform required import -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Perform the required import. - -import os -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -########################################################## -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Launch AEDT with Circuit -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT 2023 R2 in graphical mode with Circuit. - -circuit = ansys.aedt.core.Circuit(project=ansys.aedt.core.generate_unique_project_name(), - version=aedt_version, - non_graphical=non_graphical, - new_desktop=True - ) -circuit.modeler.schematic_units = "mil" - -############################################################################### -# Add subcircuit -# ~~~~~~~~~~~~~~ -# Add a new subcircuit to the previously created circuit design, creating a -# child circuit. Push this child circuit down into the child subcircuit. - -subcircuit = circuit.modeler.schematic.create_subcircuit(location=[0.0, 0.0]) -subcircuit_name = subcircuit.composed_name -circuit.push_down(subcircuit) - -############################################################################### -# Parametrize subcircuit -# ~~~~~~~~~~~~~~~~~~~~~~ -# Parametrize the subcircuit and add a resistor, inductor, and a capacitor with -# the parameter values in the following code example. Connect them in series -# and then use the ``pop_up`` # method to get back to the parent design. - -circuit.variable_manager.set_variable(name="R_val", expression="35ohm") -circuit.variable_manager.set_variable(name="L_val", expression="1e-7H") -circuit.variable_manager.set_variable(name="C_val", expression="5e-10F") -p1 = circuit.modeler.schematic.create_interface_port(name="In") -r1 = circuit.modeler.schematic.create_resistor(value="R_val") -l1 = circuit.modeler.schematic.create_inductor(value="L_val") -c1 = circuit.modeler.schematic.create_capacitor(value="C_val") -p2 = circuit.modeler.schematic.create_interface_port(name="Out") -circuit.modeler.schematic.connect_components_in_series(assignment=[p1, r1, l1, c1, p2], use_wire=True) -circuit.pop_up() - - -############################################################################### -# Release AEDT -# ~~~~~~~~~~~~ -# Release AEDT. - -circuit.release_desktop(True, True) diff --git a/examples/07-Circuit/Circuit_Transient.py b/examples/07-Circuit/Circuit_Transient.py deleted file mode 100644 index eec35728cb6..00000000000 --- a/examples/07-Circuit/Circuit_Transient.py +++ /dev/null @@ -1,183 +0,0 @@ -""" -Circuit: transient analysis and eye plot ----------------------------------------- -This example shows how you can use PyAEDT to create a circuit design, -run a Nexxim time-domain simulation, and create an eye diagram. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -from matplotlib import pyplot as plt -import numpy as np -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode, ``"PYAEDT_NON_GRAPHICAL"`` is needed to generate -# documentation only. -# You can set ``non_graphical`` either to ``True`` or ``False``. - -non_graphical = False - -############################################################################### -# Launch AEDT with Circuit -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT 2023 R2 in graphical mode with Circuit. - -cir = ansys.aedt.core.Circuit(project=ansys.aedt.core.generate_unique_project_name(), - version=aedt_version, - new_desktop=True, - non_graphical=non_graphical - ) - -############################################################################### -# Read IBIS file -# ~~~~~~~~~~~~~~ -# Read an IBIS file and place a buffer in the schematic. - -ibis = cir.get_ibis_model_from_file(os.path.join(cir.desktop_install_dir, 'buflib', 'IBIS', 'u26a_800.ibs')) -ibs = ibis.buffers["DQ_u26a_800"].insert(0, 0) - -############################################################################### -# Place ideal transmission line -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Place an ideal transmission line in the schematic and parametrize it. - -tr1 = cir.modeler.components.components_catalog["Ideal Distributed:TRLK_NX"].place("tr1") -tr1.parameters["P"] = "50mm" - -############################################################################### -# Create resistor and ground -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a resistor and ground in the schematic. - -res = cir.modeler.components.create_resistor(name="R1", value="1Meg") -gnd1 = cir.modeler.components.create_gnd() - -############################################################################### -# Connect elements -# ~~~~~~~~~~~~~~~~ -# Connect elements in the schematic. - -tr1.pins[0].connect_to_component(ibs.pins[0]) -tr1.pins[1].connect_to_component(res.pins[0]) -res.pins[1].connect_to_component(gnd1.pins[0]) - -############################################################################### -# Place probe -# ~~~~~~~~~~~ -# Place a probe and rename it to ``Vout``. - -pr1 = cir.modeler.components.components_catalog["Probes:VPROBE"].place("vout") -pr1.parameters["Name"] = "Vout" -pr1.pins[0].connect_to_component(res.pins[0]) -pr2 = cir.modeler.components.components_catalog["Probes:VPROBE"].place("Vin") -pr2.parameters["Name"] = "Vin" -pr2.pins[0].connect_to_component(ibs.pins[0]) - -############################################################################### -# Create setup and analyze -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a transient analysis setup and analyze it. - -trans_setup = cir.create_setup(name="TransientRun", setup_type="NexximTransient") -trans_setup.props["TransientData"] = ["0.01ns", "200ns"] -cir.analyze_setup("TransientRun") - -############################################################################### -# Create report outside AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a report outside AEDT using the ``get_solution_data`` method. This -# method allows you to get solution data and plot it outside AEDT without needing -# a UI. - -report = cir.post.create_report("V(Vout)", domain="Time") -if not non_graphical: - report.add_cartesian_y_marker(0) -solutions = cir.post.get_solution_data(domain="Time") -solutions.plot("V(Vout)") - -############################################################################### -# Create report inside AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a report inside AEDT using the ``new_report`` object. This object is -# fully customizable and usable with most of the reports available in AEDT. -# The standard report is the main one used in Circuit and Twin Builder. - -new_report = cir.post.reports_by_category.standard("V(Vout)") -new_report.domain = "Time" -new_report.create() -if not non_graphical: - new_report.add_limit_line_from_points([60, 80], [1, 1], "ns", "V") - vout = new_report.traces[0] - vout.set_trace_properties(style=vout.LINESTYLE.Dot, width=2, trace_type=vout.TRACETYPE.Continuous, - color=(0, 0, 255)) - vout.set_symbol_properties(style=vout.SYMBOLSTYLE.Circle, fill=True, color=(255, 255, 0)) - ll = new_report.limit_lines[0] - ll.set_line_properties(style=ll.LINESTYLE.Solid, width=4, hatch_above=True, violation_emphasis=True, hatch_pixels=2, - color=(0, 0, 255)) -new_report.time_start = "20ns" -new_report.time_stop = "100ns" -new_report.create() -sol = new_report.get_solution_data() -sol.plot() - -############################################################################### -# Create eye diagram inside AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create an eye diagram inside AEDT using the ``new_eye`` object. - -new_eye = cir.post.reports_by_category.eye_diagram("V(Vout)") -new_eye.unit_interval = "1e-9s" -new_eye.time_stop = "100ns" -new_eye.create() - -############################################################################### -# Create eye diagram outside AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create the same eye diagram outside AEDT using Matplotlib and the -# ``get_solution_data`` method. - -unit_interval = 1 -offset = 0.25 -tstop = 200 -tstart = 0 -t_steps = [] -i = tstart + offset -while i < tstop: - i += 2 * unit_interval - t_steps.append(i) - -t = [[i for i in solutions.intrinsics["Time"] if k - 2 * unit_interval < i <= k] for k in - t_steps] -ys = [[i / 1000 for i, j in zip(solutions.data_real(), solutions.intrinsics["Time"]) if - k - 2 * unit_interval < j <= k] for k in t_steps] -fig, ax = plt.subplots(sharex=True) -cellst = np.array([]) -cellsv = np.array([]) -for a, b in zip(t, ys): - an = np.array(a) - an = an - an.mean() - bn = np.array(b) - cellst = np.append(cellst, an) - cellsv = np.append(cellsv, bn) -plt.plot(cellst.T, cellsv.T, zorder=0) -plt.show() - -############################################################################### -# Release AEDT -# ~~~~~~~~~~~~ -# Release AEDT. -cir.save_project() -cir.release_desktop() diff --git a/examples/07-Circuit/Create_Netlist.py b/examples/07-Circuit/Create_Netlist.py deleted file mode 100644 index b63607f7ef5..00000000000 --- a/examples/07-Circuit/Create_Netlist.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Circuit: netlist to schematic import ------------------------------------- -This example shows how you can import netlist data into a circuit design. -HSPICE files are fully supported. Mentor files are partially supported. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports and set paths. - -import os - -import ansys.aedt.core - -netlist = ansys.aedt.core.downloads.download_netlist() - -project_name = ansys.aedt.core.generate_unique_project_name() -print(project_name) - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. -# The Boolean parameter ``NewThread`` defines whether to create a new instance -# of AEDT or try to connect to an existing instance of it. - -non_graphical = False -NewThread = True - -############################################################################### -# Launch AEDT with Circuit -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT with Circuit. The :class:`ansys.aedt.core.Desktop` class initializes AEDT -# and starts it on the specified version in the specified graphical mode. - -desktop = ansys.aedt.core.launch_desktop(aedt_version, non_graphical, NewThread) -aedtapp = ansys.aedt.core.Circuit(project=project_name) - -############################################################################### -# Define variable -# ~~~~~~~~~~~~~~~ -# Define a design variable by using a ``$`` prefix. - -aedtapp["Voltage"] = "5" - -############################################################################### -# Create schematic from netlist file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a schematic from a netlist file. The ``create_schematic_from_netlist`` -# method reads the netlist file and parses it. All components are parsed -# but only these categories are mapped: R, L, C, Q, U, J, V, and I. - -aedtapp.create_schematic_from_netlist(netlist) - -############################################################################### -# Close project and release AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# After adding any other desired functionalities, close the project and release -# AEDT. - -desktop.release_desktop() diff --git a/examples/07-Circuit/Readme.txt b/examples/07-Circuit/Readme.txt deleted file mode 100644 index 85222d203e1..00000000000 --- a/examples/07-Circuit/Readme.txt +++ /dev/null @@ -1,5 +0,0 @@ -Circuit examples -~~~~~~~~~~~~~~~~ -These examples use PyAEDT to show some end-to-end workflows for Circuit. -This includes schematic generation, setup, and postprocessing. - diff --git a/examples/07-Circuit/Reports.py b/examples/07-Circuit/Reports.py deleted file mode 100644 index 15fbe039666..00000000000 --- a/examples/07-Circuit/Reports.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -Circuit: automatic report creation ----------------------------------- -This example shows how you can use PyAEDT to create reports automatically using a JSON file. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports and set the local path to the path for PyAEDT. - -import os -from IPython.display import Image -import ansys.aedt.core - -# Set local path to path for PyAEDT -temp_folder = ansys.aedt.core.generate_unique_folder_name() -project_path = ansys.aedt.core.downloads.download_custom_reports(destination=temp_folder) - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -########################################################## -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. -# The Boolean parameter ``new_thread`` defines whether to create a new instance -# of AEDT or try to connect to an existing instance of it. - -non_graphical = False -NewThread = True - -############################################################################### -# Launch AEDT with Circuit -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT with Circuit. The :class:`ansys.aedt.core.Desktop` class initializes AEDT -# and starts the specified version in the specified mode. - -cir = ansys.aedt.core.Circuit(project=os.path.join(project_path, 'CISPR25_Radiated_Emissions_Example23R1.aedtz'), - non_graphical=non_graphical, - version=aedt_version, - new_desktop=True - ) -cir.analyze() - -############################################################################### -# Create spectrum report -# ~~~~~~~~~~~~~~~~~~~~~~ -# Create a spectrum report. You can use a JSON file to create a simple setup -# or a fully customized one. The following code creates a simple setup and changes -# the JSON file to customize it. In a spectrum report, you can add limitilines and -# notes and edit axes, the grid, and the legend. You can create custom reports -# in non-graphical mode in AEDT 2023 R2 and later. - -report1 = cir.post.create_report_from_configuration(os.path.join(project_path, 'Spectrum_CISPR_Basic.json')) -out = cir.post.export_report_to_jpg(cir.working_directory, report1.plot_name) -Image(out) - -############################################################################### -# Create spectrum report -# ~~~~~~~~~~~~~~~~~~~~~~ -# Every aspect of the report can be customized. - -report1_full = cir.post.create_report_from_configuration(os.path.join(project_path, 'Spectrum_CISPR_Custom.json')) -out = cir.post.export_report_to_jpg(cir.working_directory, report1_full.plot_name) -Image(out) -############################################################################### -# Create transient report -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Create a transient report. You can read and modify the JSON file -# before running the script. The following code modifies the traces -# before generating the report. You can create custom reports in non-graphical -# mode in AEDT 2023 R2 and later. - - -props = ansys.aedt.core.general_methods.read_json(os.path.join(project_path, 'Transient_CISPR_Custom.json')) - -report2 = cir.post.create_report_from_configuration(report_settings=props, solution_name="NexximTransient") -out = cir.post.export_report_to_jpg(cir.working_directory, report2.plot_name) -Image(out) - -############################################################################### -# Create transient report -# ~~~~~~~~~~~~~~~~~~~~~~~ -# You can customize any aspect of the property dictionary and easily create a new report. -# The following code customizes the curve name. -# The expressions key can be a list of expressions as follows or a dictionary containing the expressions to plot and line properties. -# props["expressions"] = { "V(Battery)" : -# {"color": [0, 255, 0], "trace_style": "Solid", "width": 1, "trace_type": "Continuous"}} - - -props["expressions"] = ["V(Battery)", "V(U1_VDD)"] -props["plot_name"] = "Battery Voltage" -report3 = cir.post.create_report_from_configuration(report_settings=props, solution_name="NexximTransient") -out = cir.post.export_report_to_jpg(cir.working_directory, report3.plot_name) -Image(out) - -############################################################################### -# Create eye diagram -# ~~~~~~~~~~~~~~~~~~ -# Create an eye diagram. If the JSON file contains an eye mask, you can create -# an eye diagram and fully customize it. - -report4 = cir.post.create_report_from_configuration(os.path.join(project_path, 'EyeDiagram_CISPR_Basic.json')) -out = cir.post.export_report_to_jpg(cir.working_directory, report4.plot_name) -Image(out) - -############################################################################### -# Create eye diagram -# ~~~~~~~~~~~~~~~~~~ -# You can create custom reports in -# non-graphical mode in AEDT 2023 R2 and later. - -report4_full = cir.post.create_report_from_configuration(os.path.join(project_path, 'EyeDiagram_CISPR_Custom.json')) - -out = cir.post.export_report_to_jpg(cir.working_directory, report4_full.plot_name) -Image(out) -################################################ -# This is how the spectrum looks like -# .. image:: Resources/spectrum_plot.png - -############################################################################### -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Save the project and close AEDT. - -cir.save_project() -print("Project Saved in {}".format(cir.project_path)) -cir.release_desktop() diff --git a/examples/07-Circuit/Touchstone_Management.py b/examples/07-Circuit/Touchstone_Management.py deleted file mode 100644 index bfabb7f8a73..00000000000 --- a/examples/07-Circuit/Touchstone_Management.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Circuit: Touchstone file management ------------------------------------ -This example shows how you can use objects in a Touchstone file without opening AEDT. - -To provide the advanced postprocessing features needed for this example, Matplotlib and NumPy -must be installed on your machine. - -This example runs only on Windows using CPython. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports and set the local path to the path for PyAEDT. - -from ansys.aedt.core import downloads - -example_path = downloads.download_touchstone() - -############################################################################### -# Import libraries and Touchstone file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Import Matplotlib, NumPy, and the Touchstone file. - -from ansys.aedt.core.post.touchstone_parser import read_touchstone - -############################################################################### -# Read Touchstone file -# ~~~~~~~~~~~~~~~~~~~~ -# Read the Touchstone file. - -data = read_touchstone(example_path) - -############################################################################### -# Get curve plot -# ~~~~~~~~~~~~~~ -# Get the curve plot by category. The following code shows how to plot lists of the return losses, -# insertion losses, fext, and next based on a few inputs and port names. - -data.plot_return_losses() - -data.plot_insertion_losses() - -data.plot_next_xtalk_losses("U1") - -data.plot_fext_xtalk_losses(tx_prefix="U1", rx_prefix="U7") - - -############################################################################### -# Get curve worst cases -# ~~~~~~~~~~~~~~~~~~~~~ -# Get curve worst cases. - -worst_rl, global_mean = data.get_worst_curve( - freq_min=1, freq_max=20, worst_is_higher=True, curve_list=data.get_return_loss_index() -) -worst_il, mean2 = data.get_worst_curve(freq_min=1, - freq_max=20, - worst_is_higher=False, - curve_list=data.get_insertion_loss_index() - ) -worst_fext, mean3 = data.get_worst_curve(freq_min=1, - freq_max=20, - worst_is_higher=True, - curve_list=data.get_fext_xtalk_index_from_prefix(tx_prefix="U1", - rx_prefix="U7") - ) -worst_next, mean4 = data.get_worst_curve( - freq_min=1, freq_max=20, worst_is_higher=True, curve_list=data.get_next_xtalk_index("U1") -) diff --git a/examples/07-Circuit/Virtual_Compliance.py b/examples/07-Circuit/Virtual_Compliance.py deleted file mode 100644 index 47d8b9c669b..00000000000 --- a/examples/07-Circuit/Virtual_Compliance.py +++ /dev/null @@ -1,217 +0,0 @@ -""" -Circuit: PCIE virtual compliance --------------------------------- -This example shows how to generate a compliance report in PyAEDT using -the ``VirtualCompliance`` class. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports and set paths. - -import os.path -import ansys.aedt.core -from ansys.aedt.core.visualization.post.compliance import VirtualCompliance - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. -# The Boolean parameter ``new_thread`` defines whether to create a new instance -# of AEDT or try to connect to an existing instance of it. - -non_graphical = True -new_thread = True - -############################################################################### -# Download example files -# ~~~~~~~~~~~~~~~~~~~~~~ -# Download the project and files needed to run the example. -workdir = ansys.aedt.core.downloads.download_file('pcie_compliance') - -projectdir = os.path.join(workdir, "project") - -############################################################################### -# Launch AEDT -# ~~~~~~~~~~~ -# Launch AEDT. - -d = ansys.aedt.core.Desktop(aedt_version, new_desktop=new_thread, non_graphical=non_graphical) - -############################################################################### -# Open and solve layout -# ~~~~~~~~~~~~~~~~~~~~~ -# Open the HFSS 3D Layout project and analyze it using the SIwave solver. -# Before solving, this code ensures that the model is solved from DC to 70GHz and that -# causality and passivity are enforced. - -h3d = ansys.aedt.core.Hfss3dLayout(os.path.join(projectdir, "PCIE_GEN5_only_layout.aedtz"), version=242) -h3d.remove_all_unused_definitions() -h3d.edit_cosim_options(simulate_missing_solution=False) -h3d.setups[0].sweeps[0].props["EnforcePassivity"] = True -h3d.setups[0].sweeps[0].props["Sweeps"]["Data"] = 'LIN 0MHz 70GHz 0.1GHz' -h3d.setups[0].sweeps[0].props["EnforceCausality"] = True -h3d.setups[0].sweeps[0].update() -h3d.analyze() -h3d = ansys.aedt.core.Hfss3dLayout(version=242) -touchstone_path = h3d.export_touchstone() - -############################################################################### -# Create LNA project -# ~~~~~~~~~~~~~~~~~~ -# Use the LNA setup to retrieve Touchstone files -# and generate frequency domain reports. - -cir = ansys.aedt.core.Circuit(project=h3d.project_name, design="Touchstone") -status, diff_pairs, comm_pairs = cir.create_lna_schematic_from_snp(input_file=touchstone_path, start_frequency=0, - stop_frequency=70, auto_assign_diff_pairs=True, - separation=".", pattern=["component", "pin", "net"], - analyze=True) - -insertion = cir.get_all_insertion_loss_list(drivers=diff_pairs, receivers=diff_pairs, drivers_prefix_name="X1", - receivers_prefix_name="U1", math_formula="dB", - nets=["RX0", "RX1", "RX2", "RX3"]) -return_diff = cir.get_all_return_loss_list(excitations=diff_pairs, excitation_name_prefix="X1", math_formula="dB", - nets=["RX0", "RX1", "RX2", "RX3"]) -return_comm = cir.get_all_return_loss_list(excitations=comm_pairs, excitation_name_prefix="COMMON_X1", - math_formula="dB", nets=["RX0", "RX1", "RX2", "RX3"]) -############################################################################### -# Create TDR project -# ~~~~~~~~~~~~~~~~~~ -# Create a TDR project to compute transient simulation and retrieve -# the TDR measurement on a differential pair. -# The original circuit schematic is duplicated and modified to achieve this target. - -result, tdr_probe_name = cir.create_tdr_schematic_from_snp(input_file=touchstone_path, - tx_schematic_pins=["X1.A2.PCIe_Gen4_RX0_P"], - tx_schematic_differential_pins=["X1.A3.PCIe_Gen4_RX0_N"], - termination_pins=["U1.AP26.PCIe_Gen4_RX0_P", - "U1.AN26.PCIe_Gen4_RX0_N"], - differential=True, rise_time=35, use_convolution=True, - analyze=True, design_name="TDR") - -############################################################################### -# Create AMI project -# ~~~~~~~~~~~~~~~~~~ -# Create an Ibis AMI project to compute an eye diagram simulation and retrieve -# eye mask violations. -result, eye_curve_tx, eye_curve_rx = cir.create_ami_schematic_from_snp(input_file=touchstone_path, - ibis_tx_file=os.path.join(projectdir, "models", - "pcieg5_32gt.ibs"), - tx_buffer_name="1p", rx_buffer_name="2p", - tx_schematic_pins=[ - "U1.AM25.PCIe_Gen4_TX0_CAP_P"], - rx_schematic_pins=[ - "X1.B2.PCIe_Gen4_TX0_P"], - tx_schematic_differential_pins=[ - "U1.AL25.PCIe_Gen4_TX0_CAP_N"], - rx_schematic_differentialial_pins=[ - "X1.B3.PCIe_Gen4_TX0_N"], - ibis_tx_component_name="Spec_Model", - use_ibis_buffer=False, differential=True, - bit_pattern="random_bit_count=2.5e3 random_seed=1", - unit_interval="31.25ps", use_convolution=True, - analyze=True, design_name="AMI") - -cir.save_project() - -############################################################################### -# Create virtual compliance report -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Initialize the ``VirtualCompliance`` class -# and set up the main project information needed to generate the report. -# -# -# .. image:: ../../_static/virtual_compliance_class.png -# :width: 400 -# :alt: Virtual compliance class description. -# -# -# .. image:: ../../_static/virtual_compliance_configs.png -# :width: 400 -# :alt: Virtual compliance configuration files hierarchy. -# -# - -template = os.path.join(workdir, "pcie_gen5_templates", "main.json") - -v = VirtualCompliance(cir.desktop_class, str(template)) - -############################################################################### -# Customize project and design -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define the path to the project file and the -# design names to be used in each report generation. -# -# -# .. image:: ../../_static/virtual_compliance_usage.png -# :width: 400 -# :alt: Virtual compliance configuration usage example. -# -# - -v.project_file = cir.project_file -v.reports["insertion losses"].design_name = "LNA" -v.reports["return losses"].design_name = "LNA" -v.reports["common mode return losses"].design_name = "LNA" -v.reports["tdr from circuit"].design_name = "TDR" -v.reports["eye1"].design_name = "AMI" -v.reports["eye3"].design_name = "AMI" -v.parameters["erl"].design_name = "LNA" -v.specs_folder = os.path.join(workdir, 'readme_pictures') - -############################################################################### -# Define trace names -# ~~~~~~~~~~~~~~~~~~ -# Change the trace name with projects and users. -# Reuse the compliance template and update traces accordingly. - - -v.reports["insertion losses"].traces = insertion - -v.reports["return losses"].traces = return_diff - -v.reports["common mode return losses"].traces = return_comm - -v.reports["eye1"].traces = eye_curve_tx -v.reports["eye3"].traces = eye_curve_tx -v.reports["tdr from circuit"].traces = tdr_probe_name -v.parameters = {} -v.parameters["erl"].trace_pins = [ - ["X1.A5.PCIe_Gen4_RX1_P", "X1.A6.PCIe_Gen4_RX1_N", "U1.AR25.PCIe_Gen4_RX1_P", "U1.AP25.PCIe_Gen4_RX1_N"], - [7, 8, 18, 17]] - -############################################################################### -# Generate PDF report -# ~~~~~~~~~~~~~~~~~~~~ -# Generate the reports and produce a PDF report. -# -# -# .. image:: ../../_static/virtual_compliance_scattering1.png -# :width: 400 -# :alt: Insertion loss output. -# -# -# .. image:: ../../_static/virtual_compliance_scattering2.png -# :width: 400 -# :alt: Return loss output. -# -# -# .. image:: ../../_static/virtual_compliance_eye.png -# :width: 400 -# :alt: Eye diagram example. -# -# - -v.create_compliance_report() - -d.release_desktop(True, True) diff --git a/examples/07-EMIT/ComputeInterferenceType.py b/examples/07-EMIT/ComputeInterferenceType.py deleted file mode 100644 index 81608d52a40..00000000000 --- a/examples/07-EMIT/ComputeInterferenceType.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -EMIT: Classify interference type --------------------------------- -This example shows how you can use PyAEDT to load an existing AEDT -project with an EMIT design and analyze the results to classify the -worst-case interference. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. -import sys -from ansys.aedt.core.emit_core.emit_constants import InterfererType, ResultType, TxRxMode -from ansys.aedt.core import Emit -import ansys.aedt.core -import os -import ansys.aedt.core.generic.constants as consts -import subprocess - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -# Check to see which Python libraries have been installed -reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']) -installed_packages = [r.decode().split('==')[0] for r in reqs.split()] - - -# Install required packages if they are not installed -def install(package): - subprocess.check_call([sys.executable, '-m', 'pip', 'install', package]) - - -# Install plotly library (if needed) to display legend and scenario matrix results (internet connection needed) -required_packages = ['plotly'] -for package in required_packages: - if package not in installed_packages: - install(package) - -# Import plotly library -import plotly.graph_objects as go - -# Define colors for tables -table_colors = {"green": '#7d73ca', "yellow": '#d359a2', "orange": '#ff6361', "red": '#ffa600', "white": '#ffffff'} -header_color = 'grey' - -# Check for if emit version is compatible -if aedt_version <= "2023.1": - print("Warning: this example requires AEDT 2023.2 or later.") - sys.exit() - -############################################################################### -# Launch AEDT with EMIT -# ~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT with EMIT. The ``Desktop`` class initializes AEDT and starts it -# on the specified version and in the specified graphical mode. - -non_graphical = False -new_thread = True -desktop = ansys.aedt.core.launch_desktop(aedt_version, non_graphical=non_graphical, new_desktop=new_thread) - -path_to_desktop_project = ansys.aedt.core.downloads.download_file("emit", "interference.aedtz") -emitapp = Emit(non_graphical=False, new_desktop=False, project=path_to_desktop_project) - -# Get all the radios in the project -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Get lists of all transmitters and receivers in the project. -rev = emitapp.results.analyze() -tx_interferer = InterfererType().TRANSMITTERS -rx_radios = rev.get_receiver_names() -tx_radios = rev.get_interferer_names(tx_interferer) -domain = emitapp.results.interaction_domain() - -if tx_radios is None or rx_radios is None: - print("No receivers or transmitters are in the design.") - sys.exit() - -############################################################################### -# Classify the interference -# ~~~~~~~~~~~~~~~~~~~~~~~~~ -# Iterate over all the transmitters and receivers and compute the power -# at the input to each receiver due to each of the transmitters. Computes -# which, if any, type of interference occurred. - -power_matrix = [] -all_colors = [] -all_colors, power_matrix = rev.interference_type_classification(domain, use_filter=False, filter_list=[]) - -############################################################################### -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.force_close_desktop` method. -# All methods provide for saving the project before closing. - -emitapp.save_project() -emitapp.release_desktop() - - -############################################################################### -# Create a scenario matrix view -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a scenario matrix view with the transmitters defined across the top -# and receivers down the left-most column. The power at the input to each -# receiver is shown in each cell of the matrix and color-coded based on the -# interference type. - -def create_scenario_view(emis, colors, tx_radios, rx_radios): - """Create a scenario matrix-like table with the higher received - power for each Tx-Rx radio combination. The colors - used for the scenario matrix view are based on the interference type.""" - - all_colors = [] - for color in colors: - col = [] - for cell in color: - col.append(table_colors[cell]) - all_colors.append(col) - - fig = go.Figure(data=[go.Table( - header=dict( - values=['Tx/Rx', '{}'.format(tx_radios[0]), '{}'.format(tx_radios[1])], - line_color='darkslategray', - fill_color='grey', - align=['left', 'center'], - font=dict(color='white', size=16) - ), - cells=dict( - values=[ - rx_radios, - emis[0], - emis[1]], - line_color='darkslategray', - fill_color=['white', all_colors[0], all_colors[1]], - align=['left', 'center'], - height=25, - font=dict( - color=['darkslategray', 'black'], - size=15) - ) - )]) - fig.update_layout( - title=dict( - text='Interference Type Classification', - font=dict(color='darkslategray', size=20), - x=0.5 - ), - width=600 - ) - fig.show() - - -############################################################################### -# Generate a legend -# ~~~~~~~~~~~~~~~~~ -# Define the interference types and colors used to display the results of -# the analysis. - -def create_legend_table(): - """Create a table showing the interference types.""" - classifications = ['In-band/In-band', 'Out-of-band/In-band', - 'In-band/Out-of-band', 'Out-of-band/Out-of-band'] - fig = go.Figure(data=[go.Table( - header=dict( - values=['Interference Type (Source/Victim)'], - line_color='darkslategray', - fill_color=header_color, - align=['center'], - font=dict(color='white', size=16) - ), - cells=dict( - values=[classifications], - line_color='darkslategray', - fill_color=[[table_colors['red'], table_colors['orange'], table_colors['yellow'], table_colors['green']]], - align=['center'], - height=25, - font=dict( - color=['darkslategray', 'black'], - size=15) - ) - )]) - fig.update_layout( - title=dict( - text='Interference Type Classification', - font=dict(color='darkslategray', size=20), - x=0.5 - ), - width=600 - ) - fig.show() - - -if os.getenv("PYAEDT_DOC_GENERATION", "False") != "1": - # Create a scenario view for all the interference types - create_scenario_view(power_matrix, all_colors, tx_radios, rx_radios) - - # Create a legend for the interference types - create_legend_table() diff --git a/examples/07-EMIT/ComputeProtectionLevels.py b/examples/07-EMIT/ComputeProtectionLevels.py deleted file mode 100644 index fcfb58ef368..00000000000 --- a/examples/07-EMIT/ComputeProtectionLevels.py +++ /dev/null @@ -1,266 +0,0 @@ -""" -EMIT: Compute receiver protection levels ----------------------------------------- -This example shows how you can use PyAEDT to open an AEDT project with -an EMIT design and analyze the results to determine if the received -power at the input to each receiver exceeds the specified protection -levels. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. -# -# sphinx_gallery_thumbnail_path = "Resources/emit_protection_levels.png" -import os -import sys -import subprocess -import ansys.aedt.core -from ansys.aedt.core import Emit -from ansys.aedt.core.emit_core.emit_constants import TxRxMode, ResultType, InterfererType - -# Check to see which Python libraries have been installed -reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']) -installed_packages = [r.decode().split('==')[0] for r in reqs.split()] - -# Install required packages if they are not installed -def install(package): - subprocess.check_call([sys.executable, '-m', 'pip', 'install', package]) - -# Install any missing libraries -required_packages = ['plotly'] -for package in required_packages: - if package not in installed_packages: - install(package) - -# Import required modules -import plotly.graph_objects as go - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. ``"PYAEDT_NON_GRAPHICAL"``` is needed to generate -# documentation only. -# You can set ``non_graphical`` either to ``True`` or ``False``. -# The ``new_thread`` Boolean variable defines whether to create a new instance -# of AEDT or try to connect to existing instance of it if one is available. - -non_graphical = False -new_thread = True - -############################################################################### -# Launch AEDT with EMIT -# ~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT with EMIT. The ``Desktop`` class initializes AEDT and starts it -# on the specified version and in the specified graphical mode. - -if aedt_version <= "2023.1": - print("Warning: this example requires AEDT 2023.2 or later.") - sys.exit() - -d = ansys.aedt.core.launch_desktop(aedt_version, non_graphical, new_thread) -emitapp = Emit(ansys.aedt.core.generate_unique_project_name()) - -############################################################################### -# Specify the protection levels -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# The protection levels are specified in dBm. -# If the damage threshold is exceeded, permanent damage to the receiver front -# end may occur. -# Exceeding the overload threshold severely densensitizes the receiver. -# Exceeding the intermod threshold can drive the victim receiver into non- -# linear operation, where it operates as a mixer. -# Exceeding the desense threshold reduces the signal-to-noise ratio and can -# reduce the maximum range, maximum bandwidth, and/or the overall link quality. - -header_color = 'grey' -damage_threshold = 30 -overload_threshold = -4 -intermod_threshold = -30 -desense_threshold = -104 - -protection_levels = [damage_threshold, overload_threshold, intermod_threshold, desense_threshold] - -############################################################################### -# Create and connect EMIT components -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Set up the scenario with radios connected to antennas. - -bluetooth, blue_ant = emitapp.modeler.components.create_radio_antenna("Bluetooth Low Energy (LE)", "Bluetooth") -gps, gps_ant = emitapp.modeler.components.create_radio_antenna("GPS Receiver", "GPS") -wifi, wifi_ant = emitapp.modeler.components.create_radio_antenna("WiFi - 802.11-2012", "WiFi") - -############################################################################### -# Configure the radios -# ~~~~~~~~~~~~~~~~~~~~ -# Enable the HR-DSSS bands for the Wi-Fi radio and set the power level -# for all transmit bands to -20 dBm. - -bands = wifi.bands() -for band in bands: - if "HR-DSSS" in band.node_name: - if "Ch 1-13" in band.node_name: - band.enabled=True - band.set_band_power_level(-20) - -# Reduce the bluetooth transmit power -bands = bluetooth.bands() -for band in bands: - band.set_band_power_level(-20) - -def get_radio_node(radio_name): - """Get the radio node that matches the - given radio name. - Arguments: - radio_name: String name of the radio. - Returns: Instance of the radio. - """ - if gps.name == radio_name: - radio = gps - elif bluetooth.name == radio_name: - radio = bluetooth - else: - radio = wifi - return radio - -bands = gps.bands() -for band in bands: - for child in band.children: - if "L2 P(Y)" in band.node_name: - band.enabled=True - else: - band.enabled=False - -############################################################################### -# Load the results set -# ~~~~~~~~~~~~~~~~~~~~ -# Create a results revision and load it for analysis. - -rev = emitapp.results.analyze() - -############################################################################### -# Generate a legend -# ~~~~~~~~~~~~~~~~~ -# Define the thresholds and colors used to display the results of -# the protection level analysis. - -def create_legend_table(): - """Create a table showing the defined protection levels.""" - protectionLevels = ['>{} dBm'.format(damage_threshold), '>{} dBm'.format(overload_threshold), - '>{} dBm'.format(intermod_threshold), '>{} dBm'.format(desense_threshold)] - fig = go.Figure(data=[go.Table( - header=dict( - values=['Interference','Power Level Threshold'], - line_color='darkslategray', - fill_color=header_color, - align=['left','center'], - font=dict(color='white',size=16) - ), - cells=dict( - values=[['Damage','Overload','Intermodulation','Clear'], protectionLevels], - line_color='darkslategray', - fill_color=['white',['red','orange','yellow','green']], - align = ['left', 'center'], - font = dict( - color = ['darkslategray','black'], - size = 15) - ) - )]) - fig.update_layout( - title=dict( - text='Protection Levels (dBm)', - font=dict(color='darkslategray',size=20), - x = 0.5 - ), - width = 600 - ) - fig.show() - -############################################################################### -# Create a scenario matrix view -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a scenario matrix view with the transmitters defined across the top -# and receivers down the left-most column. The power at the input to each -# receiver is shown in each cell of the matrix and color-coded based on the -# protection level thresholds defined. - -def create_scenario_view(emis, colors, tx_radios, rx_radios): - """Create a scenario matrix-like table with the higher received - power for each Tx-Rx radio combination. The colors - used for the scenario matrix view are based on the highest - protection level that the received power exceeds.""" - fig = go.Figure(data=[go.Table( - header=dict( - values=['Tx/Rx','{}'.format(tx_radios[0]),'{}'.format(tx_radios[1])], - line_color='darkslategray', - fill_color=header_color, - align=['left','center'], - font=dict(color='white',size=16) - ), - cells=dict( - values=[ - rx_radios, - emis[0], - emis[1]], - line_color='darkslategray', - fill_color=['white',colors[0], colors[1]], - align = ['left', 'center'], - font = dict( - color = ['darkslategray','black'], - size = 15) - ) - )]) - fig.update_layout( - title=dict( - text='Protection Levels (dBm)', - font=dict(color='darkslategray',size=20), - x = 0.5 - ), - width = 600 - ) - fig.show() - -############################################################################### -# Get all the radios in the project -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Get lists of all transmitters and receivers in the project. -if os.getenv("PYAEDT_DOC_GENERATION", "False") != "1": - rev = emitapp.results.current_revision - rx_radios = rev.get_receiver_names() - tx_radios = rev.get_interferer_names(InterfererType.TRANSMITTERS) - domain = emitapp.results.interaction_domain() - -############################################################################### -# Classify the results -# ~~~~~~~~~~~~~~~~~~~~ -# Iterate over all the transmitters and receivers and compute the power -# at the input to each receiver due to each of the transmitters. Computes -# which, if any, protection levels are exceeded by these power levels. -if os.getenv("PYAEDT_DOC_GENERATION", "False") != "1": - power_matrix=[] - all_colors=[] - - all_colors, power_matrix = rev.protection_level_classification(domain, global_levels = protection_levels) - - # Create a scenario matrix-like view for the protection levels - create_scenario_view(power_matrix, all_colors, tx_radios, rx_radios) - - # Create a legend for the protection levels - create_legend_table() - -############################################################################### -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.force_close_desktop` method. -# All methods provide for saving the project before closing. - -emitapp.save_project() -emitapp.release_desktop(close_projects=True, close_desktop=True) diff --git a/examples/07-EMIT/EMIT_Example.py b/examples/07-EMIT/EMIT_Example.py deleted file mode 100644 index cd70777db69..00000000000 --- a/examples/07-EMIT/EMIT_Example.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -EMIT: antenna ---------------------- -This example shows how you can use PyAEDT to create a project in EMIT for -the simulation of an antenna. -""" -############################################################################### -# Perform required inputs -# ~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. -# -# sphinx_gallery_thumbnail_path = "Resources/emit_simple_cosite.png" - -import os -import ansys.aedt.core -from ansys.aedt.core.emit_core.emit_constants import TxRxMode, ResultType - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. -# The ``NewThread`` Boolean variable defines whether to create a new instance -# of AEDT or try to connect to existing instance of it if one is available. - -non_graphical = False -NewThread = True - -############################################################################### -# Launch AEDT with EMIT -# ~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT with EMIT. The ``Desktop`` class initializes AEDT and starts it -# on the specified version and in the specified graphical mode. - -d = ansys.aedt.core.launch_desktop(aedt_version, non_graphical, NewThread) -aedtapp = ansys.aedt.core.Emit(ansys.aedt.core.generate_unique_project_name()) - - -############################################################################### -# Create and connect EMIT components -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create three radios and connect an antenna to each one. - -rad1 = aedtapp.modeler.components.create_component("New Radio") -ant1 = aedtapp.modeler.components.create_component("Antenna") -if rad1 and ant1: - ant1.move_and_connect_to(rad1) - -# Convenience method to create a radio and antenna connected together -rad2, ant2 = aedtapp.modeler.components.create_radio_antenna("GPS Receiver") -rad3, ant3 = aedtapp.modeler.components.create_radio_antenna("Bluetooth Low Energy (LE)", "Bluetooth") - -############################################################################### -# Define coupling among RF systems -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define the coupling among the RF systems. This portion of the EMIT API is not -# yet implemented. - - -############################################################################### -# Run EMIT simulation -# ~~~~~~~~~~~~~~~~~~~ -# Run the EMIT simulation. -# -# This part of the example requires Ansys AEDT 2023 R2. - -if aedt_version > "2023.1" and os.getenv("PYAEDT_DOC_GENERATION", "False") != "1": - rev = aedtapp.results.analyze() - rx_bands = rev.get_band_names(rad2.name, TxRxMode.RX) - tx_bands = rev.get_band_names(rad3.name, TxRxMode.TX) - domain = aedtapp.results.interaction_domain() - domain.set_receiver(rad2.name, rx_bands[0], -1) - domain.set_interferer(rad3.name,tx_bands[0]) - interaction = rev.run(domain) - worst = interaction.get_worst_instance(ResultType.EMI) - if worst.has_valid_values(): - emi = worst.get_value(ResultType.EMI) - print("Worst case interference is: {} dB".format(emi)) - -############################################################################### -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.force_close_desktop` method. -# All methods provide for saving the project before closing. - -aedtapp.save_project() -aedtapp.release_desktop(close_projects=True, close_desktop=True) diff --git a/examples/07-EMIT/EMIT_HFSS_Example.py b/examples/07-EMIT/EMIT_HFSS_Example.py deleted file mode 100644 index ecf63b7c92e..00000000000 --- a/examples/07-EMIT/EMIT_HFSS_Example.py +++ /dev/null @@ -1,145 +0,0 @@ -""" -EMIT: HFSS to EMIT coupling ---------------------------- -This example shows how you can use PyAEDT to open an AEDT project with -an HFSS design, create an EMIT design in the project, and link the HFSS design -as a coupling link in the EMIT design. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. -# -# sphinx_gallery_thumbnail_path = "Resources/emit_hfss.png" - -import os - -# Import required modules -import ansys.aedt.core -from ansys.aedt.core.generic.filesystem import Scratch -from ansys.aedt.core.emit_core.emit_constants import TxRxMode, ResultType - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Set non-graphical mode -# ~~~~~~~~~~~~~~~~~~~~~~ -# Set non-graphical mode. -# You can set ``non_graphical`` either to ``True`` or ``False``. -# The Boolean parameter ``new_thread`` defines whether to create a new instance -# of AEDT or try to connect to an existing instance of it. -# -# The following code uses AEDT 2023 R2. - -non_graphical = False -NewThread = True -scratch_path = ansys.aedt.core.generate_unique_folder_name() - -############################################################################### -# Launch AEDT with EMIT -# ~~~~~~~~~~~~~~~~~~~~~ -# Launch AEDT with EMIT. The ``Desktop`` class initializes AEDT and starts it -# on the specified version and in the specified graphical mode. - -d = ansys.aedt.core.launch_desktop(aedt_version, non_graphical, NewThread) - -temp_folder = os.path.join(scratch_path, ("EmitHFSSExample")) -if not os.path.exists(temp_folder): - os.mkdir(temp_folder) - -example_name = "Cell Phone RFI Desense" -example_aedt = example_name + ".aedt" -example_results = example_name + ".aedtresults" -example_lock = example_aedt + ".lock" -example_pdf_file = example_name + " Example.pdf" - -example_dir = os.path.join(d.install_path, "Examples\\EMIT") -example_project = os.path.join(example_dir, example_aedt) -example_results_folder = os.path.join(example_dir, example_results) -example_pdf = os.path.join(example_dir, example_pdf_file) - -######################################################################################################## -# If the ``Cell Phone RFT Defense`` example is not -# in the installation directory, exit from this example. - -if not os.path.exists(example_project): - msg = """ - Cell phone RFT Desense example file is not in the - Examples/EMIT directory under the EDT installation. You cannot run this example. - """ - print(msg) - d.release_desktop(True, True) - exit() - -my_project = os.path.join(temp_folder, example_aedt) -my_results_folder = os.path.join(temp_folder, example_results) -my_project_lock = os.path.join(temp_folder, example_lock) -my_project_pdf = os.path.join(temp_folder, example_pdf_file) - -if os.path.exists(my_project): - os.remove(my_project) - -if os.path.exists(my_project_lock): - os.remove(my_project_lock) - -with Scratch(scratch_path) as local_scratch: - local_scratch.copyfile(example_project, my_project) - local_scratch.copyfolder(example_results_folder, my_results_folder) - if os.path.exists(example_pdf): - local_scratch.copyfile(example_pdf, my_project_pdf) - -aedtapp = ansys.aedt.core.Emit(my_project) - -############################################################################### -# Create and connect EMIT components -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create two radios with antennas connected to each one. - -rad1, ant1 = aedtapp.modeler.components.create_radio_antenna("Bluetooth Low Energy (LE)") -rad2, ant2 = aedtapp.modeler.components.create_radio_antenna("Bluetooth Low Energy (LE)") - -############################################################################### -# Define coupling among RF systems -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Define coupling among the RF systems. - -for link in aedtapp.couplings.linkable_design_names: - aedtapp.couplings.add_link(link) - -for link in aedtapp.couplings.coupling_names: - aedtapp.couplings.update_link(link) - -############################################################################### -# Run EMIT simulation -# ~~~~~~~~~~~~~~~~~~~ -# Run the EMIT simulation. -# -# This part of the example requires Ansys AEDT 2023 R2. - -if aedt_version > "2023.1": - rev = aedtapp.results.analyze() - rx_bands = rev.get_band_names(rad1.name, TxRxMode.RX) - tx_bands = rev.get_band_names(rad2.name, TxRxMode.TX) - domain = aedtapp.results.interaction_domain() - domain.set_receiver(rad1.name, rx_bands[0], -1) - domain.set_interferer(rad2.name,tx_bands[0]) - interaction = rev.run(domain) - worst = interaction.get_worst_instance(ResultType.EMI) - if worst.has_valid_values(): - emi = worst.get_value(ResultType.EMI) - print("Worst case interference is: {} dB".format(emi)) - -############################################################################### -# Save project and close AEDT -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# After the simulation completes, you can close AEDT or release it using the -# :func:`ansys.aedt.core.Desktop.force_close_desktop` method. -# All methods provide for saving the project before closing. - -aedtapp.save_project() -aedtapp.release_desktop(close_projects=True, close_desktop=True) diff --git a/examples/07-EMIT/Readme.txt b/examples/07-EMIT/Readme.txt deleted file mode 100644 index 35b36f1cd11..00000000000 --- a/examples/07-EMIT/Readme.txt +++ /dev/null @@ -1,5 +0,0 @@ -EMIT examples -~~~~~~~~~~~~~ -These examples use PyAEDT to show some end-to-end workflows for EMIT. -This includes schematic generation, setup, and postprocessing. - diff --git a/examples/07-EMIT/interference_gui.py b/examples/07-EMIT/interference_gui.py deleted file mode 100644 index c47efd4d88d..00000000000 --- a/examples/07-EMIT/interference_gui.py +++ /dev/null @@ -1,621 +0,0 @@ -""" -EMIT: Classify interference type GUI ----------------------------------------- -This example uses a GUI to open an AEDT project with -an EMIT design and analyze the results to classify the -worst-case interference. -""" -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import sys -from ansys.aedt.core.emit_core.emit_constants import InterfererType, ResultType, TxRxMode -from ansys.aedt.core import Emit -from ansys.aedt.core import get_pyaedt_app -import ansys.aedt.core -import os -import subprocess -import ansys.aedt.core.generic.constants as consts - -# Check that emit is a compatible version -aedt_version = "2024.2" -if aedt_version < "2023.2": - print("Must have v2023.2 or later") - sys.exit() - -# Check to see which Python libraries have been installed -reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']) -installed_packages = [r.decode().split('==')[0] for r in reqs.split()] - -# Install required packages if they are not installed -def install(package): - subprocess.check_call([sys.executable, '-m', 'pip', 'install', package]) - -# Install required libraries for GUI and Excel exporting (internet connection needed) -required_packages = ['PySide6', 'openpyxl'] -for package in required_packages: - if package not in installed_packages: - install(package) - -# Import PySide6 and openpyxl libraries -from PySide6 import QtWidgets, QtUiTools, QtGui, QtCore -from openpyxl.styles import PatternFill -import openpyxl - -# Uncomment if there are Qt plugin errors -# import PySide6 -# dirname = os.path.dirname(PySide6.__file__) -# plugin_path = os.path.join(dirname, 'plugins', 'platforms') -# os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path - -# Launch EMIT -non_graphical = False -new_thread = True -desktop = ansys.aedt.core.launch_desktop(aedt_version, non_graphical, new_thread) - -# Add emitapi to system path -emit_path = os.path.join(desktop.install_path, "Delcross") -sys.path.insert(0,emit_path) -import EmitApiPython -api = EmitApiPython.EmitApi() - -# Define .ui file for GUI -ui_file = ansys.aedt.core.downloads.download_file("emit", "interference_gui.ui") -Ui_MainWindow, _ = QtUiTools.loadUiType(ui_file) - -class DoubleDelegate(QtWidgets.QStyledItemDelegate): - def __init__(self, decimals, values, max_power, min_power): - super().__init__() - self.decimals = decimals - self.values = values - self.max_power = max_power - self.min_power = min_power - - def createEditor(self, parent, option, index): - editor = super().createEditor(parent, option, index) - if isinstance(editor, QtWidgets.QLineEdit): - validator = QtGui.QDoubleValidator(parent) - num_rows = len(self.values) - cur_row = index.row() - if cur_row == 0: - min_val = self.values[1] - max_val = self.max_power - elif cur_row == num_rows - 1: - min_val = self.min_power - max_val = self.values[cur_row-1] - else: - min_val = self.values[cur_row + 1] - max_val = self.values[cur_row - 1] - validator.setRange(min_val, max_val, self.decimals) - validator.setNotation(QtGui.QDoubleValidator.Notation.StandardNotation) - editor.setValidator(validator) - return editor - - def update_values(self, values): - self.values = values - -class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): - def __init__(self): - super(MainWindow, self).__init__() - self.emitapp = None - self.populating_dropdown = False - self.setupUi(self) - self.setup_widgets() - - ############################################################################### - # Setup widgets - # ~~~~~~~~~~~~~ - # Define all widgets from the UI file, connect the widgets to functions, define - # table colors, and format table settings. - - def setup_widgets(self): - # Widget definitions for file selection/tab management - self.file_select_btn = self.findChild(QtWidgets.QToolButton, "file_select_btn") - self.file_path_box = self.findChild(QtWidgets.QLineEdit, "file_path_box") - self.design_name_dropdown = self.findChild(QtWidgets.QComboBox, "design_name_dropdown") - self.design_name_dropdown.setEnabled(True) - self.tab_widget = self.findChild(QtWidgets.QTabWidget, "tab_widget") - - # Widget definitions for protection level classification - self.protection_results_btn = self.findChild(QtWidgets.QPushButton, "protection_results_btn") - self.protection_matrix = self.findChild(QtWidgets.QTableWidget, "protection_matrix") - self.protection_legend_table = self.findChild(QtWidgets.QTableWidget, "protection_legend_table") - - self.damage_check = self.findChild(QtWidgets.QCheckBox, "damage_check") - self.overload_check = self.findChild(QtWidgets.QCheckBox, "overload_check") - self.intermodulation_check = self.findChild(QtWidgets.QCheckBox, "intermodulation_check") - self.desensitization_check = self.findChild(QtWidgets.QCheckBox, "desensitization_check") - self.protection_export_btn = self.findChild(QtWidgets.QPushButton, "protection_export_btn") - self.radio_specific_levels = self.findChild(QtWidgets.QCheckBox, "radio_specific_levels") - self.radio_dropdown = self.findChild(QtWidgets.QComboBox, "radio_dropdown") - self.protection_save_img_btn = self.findChild(QtWidgets.QPushButton, 'protection_save_img_btn') - - # warning label - self.warning_label = self.findChild(QtWidgets.QLabel, "warnings") - myFont = QtGui.QFont() - myFont.setBold(True) - self.warning_label.setFont(myFont) - self.warning_label.setHidden(True) - self.design_name_dropdown.currentIndexChanged.connect(self.design_dropdown_changed) - - # Setup for protection level buttons and table - self.protection_results_btn.setEnabled(False) - self.protection_export_btn.setEnabled(False) - self.protection_save_img_btn.setEnabled(False) - self.file_select_btn.clicked.connect(self.open_file_dialog) - self.protection_export_btn.clicked.connect(self.save_results_excel) - self.protection_results_btn.clicked.connect(self.protection_results) - self.protection_legend_table.resizeRowsToContents() - self.protection_legend_table.resizeColumnsToContents() - self.damage_check.stateChanged.connect(self.protection_results) - self.overload_check.stateChanged.connect(self.protection_results) - self.intermodulation_check.stateChanged.connect(self.protection_results) - self.desensitization_check.stateChanged.connect(self.protection_results) - self.protection_legend_table.setEditTriggers(QtWidgets.QTableWidget.DoubleClicked) - self.global_protection_level = True - self.protection_levels = {} - values = [float(self.protection_legend_table.item(row, 0).text()) for row in range(self.protection_legend_table.rowCount())] - self.protection_levels['Global'] = values - self.changing = False - self.radio_dropdown.currentIndexChanged.connect(self.radio_dropdown_changed) - self.protection_legend_table.itemChanged.connect(self.table_changed) - self.protection_save_img_btn.clicked.connect(self.save_image) - - # Widget definitions for interference type - self.interference_results_btn = self.findChild(QtWidgets.QPushButton, "interference_results_btn") - self.interference_matrix = self.findChild(QtWidgets.QTableWidget, "interference_matrix") - self.interference_legend_table = self.findChild(QtWidgets.QTableWidget, "interference_legend_table") - - # set the items read only - for i in range(0, self.interference_legend_table.rowCount()): - item = self.interference_legend_table.item(i, 0) - item.setFlags(QtCore.Qt.ItemIsEnabled) - self.interference_legend_table.setItem(i, 0, item) - - self.in_in_check = self.findChild(QtWidgets.QCheckBox, "in_in_check") - self.in_out_check = self.findChild(QtWidgets.QCheckBox, "in_out_check") - self.out_in_check = self.findChild(QtWidgets.QCheckBox, "out_in_check") - self.out_out_check = self.findChild(QtWidgets.QCheckBox, "out_out_check") - self.interference_export_btn = self.findChild(QtWidgets.QPushButton, "interference_export_btn") - self.interference_save_img_btn = self.findChild(QtWidgets.QPushButton, 'interference_save_img_btn') - - # Setup for interference type buttons and table - self.interference_results_btn.setEnabled(False) - self.interference_export_btn.setEnabled(False) - self.interference_save_img_btn.setEnabled(False) - self.interference_export_btn.clicked.connect(self.save_results_excel) - self.interference_results_btn.clicked.connect(self.interference_results) - self.interference_legend_table.resizeRowsToContents() - self.interference_legend_table.resizeColumnsToContents() - self.in_in_check.stateChanged.connect(self.interference_results) - self.in_out_check.stateChanged.connect(self.interference_results) - self.out_in_check.stateChanged.connect(self.interference_results) - self.out_out_check.stateChanged.connect(self.interference_results) - self.radio_specific_levels.stateChanged.connect(self.radio_specific) - self.interference_save_img_btn.clicked.connect(self.save_image) - - # Color definition dictionary and previous project/design names - self.color_dict = {"green": [QtGui.QColor(125, 115, 202),'#7d73ca'], - "yellow":[QtGui.QColor(211, 89, 162), '#d359a2'], - "orange": [QtGui.QColor(255, 99, 97), '#ff6361'], - "red": [QtGui.QColor(255, 166, 0), '#ffa600'], - "white": [QtGui.QColor("white"),'#ffffff']} - self.previous_design = '' - self.previous_project = '' - - # Set the legend tables to strech resize mode - header = self.protection_legend_table.horizontalHeader() - v_header = self.protection_legend_table.verticalHeader() - - header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch) - v_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch) - v_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Stretch) - v_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.Stretch) - v_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeMode.Stretch) - - header = self.interference_legend_table.horizontalHeader() - v_header = self.interference_legend_table.verticalHeader() - - header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch) - v_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch) - v_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.Stretch) - v_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.Stretch) - v_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeMode.Stretch) - - # Input validation for protection level legend table - self.delegate = DoubleDelegate(decimals=2, values=values, - max_power=1000, min_power=-200) - self.protection_legend_table.setItemDelegateForColumn(0, self.delegate) - self.open_file_dialog() - - ############################################################################### - # Open file dialog and select project - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Open the file dialog for project selection and populate the design dropdown - # with all EMIT designs in the project. - - def open_file_dialog(self): - fname, _filter = QtWidgets.QFileDialog.getOpenFileName(self, "Select EMIT Project", "", "Ansys Electronics Desktop Files (*.aedt)", ) - if fname: - self.file_path_box.setText(fname) - - # Close previous project and open specified one - if self.emitapp is not None: - self.emitapp.close_project() - self.emitapp = None - desktop_proj = desktop.load_project(self.file_path_box.text()) - - # check for an empty project (i.e. no designs) - if isinstance(desktop_proj, bool): - self.file_path_box.setText("") - msg = QtWidgets.QMessageBox() - msg.setWindowTitle("Error: Project missing designs.") - msg.setText( - "The selected project has no designs. Projects must have at least " - "one EMIT design. See AEDT log for more information.") - x = msg.exec() - return - - # Check if project is already open - if desktop_proj.lock_file == None: - msg = QtWidgets.QMessageBox() - msg.setWindowTitle("Error: Project already open") - msg.setText("Project is locked. Close or remove the lock before proceeding. See AEDT log for more information.") - x = msg.exec() - return - - # Populate design dropdown with all design names - designs = desktop_proj.design_list - emit_designs = [] - self.populating_dropdown = True - self.design_name_dropdown.clear() - self.populating_dropdown = False - for d in designs: - design_type = desktop.design_type(desktop_proj.project_name, d) - if design_type == "EMIT": - emit_designs.append(d) - - # add warning if no EMIT design - # Note: this should never happen since loading a project without an EMIT design - # should add a blank EMIT design - self.warning_label.setHidden(True) - if len(emit_designs) == 0: - self.warning_label.setText("Warning: The project must contain at least one EMIT design.") - self.warning_label.setHidden(False) - return - - self.populating_dropdown = True - self.design_name_dropdown.addItems(emit_designs) - self.populating_dropdown = False - self.emitapp = get_pyaedt_app(desktop_proj.project_name, emit_designs[0]) - self.design_name_dropdown.setCurrentIndex(0) - - # check for at least 2 radios - radios = self.emitapp.modeler.components.get_radios() - self.warning_label.setHidden(True) - if len(radios) < 2: - self.warning_label.setText("Warning: The selected design must contain at least two radios.") - self.warning_label.setHidden(False) - - if self.radio_specific_levels.isEnabled(): - self.radio_specific_levels.setChecked(False) - self.radio_dropdown.clear() - self.radio_dropdown.setEnabled(False) - self.protection_levels = {} - values = [float(self.protection_legend_table.item(row, 0).text()) for row in range(self.protection_legend_table.rowCount())] - self.protection_levels['Global'] = values - - self.radio_specific_levels.setEnabled(True) - self.protection_results_btn.setEnabled(True) - self.interference_results_btn.setEnabled(True) - - ############################################################################### - # Change design selection - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Refresh the warning messages when the selected design changes - - def design_dropdown_changed(self): - if self.populating_dropdown: - # don't load design's on initial project load - return - design_name = self.design_name_dropdown.currentText() - self.emitapp = get_pyaedt_app(self.emitapp.project_name, design_name) - # check for at least 2 radios - radios = self.emitapp.modeler.components.get_radios() - self.warning_label.setHidden(True) - if len(radios) < 2: - self.warning_label.setText("Warning: The selected design must contain at least two radios.") - self.warning_label.setHidden(False) - - # clear the table if the design is changed - self.clear_table() - - ############################################################################### - # Enable radio specific protection levels - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Activate radio selection dropdown and initialize dictionary to store protection levels - # when the radio-specific level dropdown is checked. - - def radio_specific(self): - self.radio_dropdown.setEnabled(self.radio_specific_levels.isChecked()) - self.radio_dropdown.clear() - if self.radio_dropdown.isEnabled(): - self.emitapp.set_active_design(self.design_name_dropdown.currentText()) - radios = self.emitapp.modeler.components.get_radios() - values = [float(self.protection_legend_table.item(row, 0).text()) for row in range(self.protection_legend_table.rowCount())] - for radio in radios: - if radios[radio].has_rx_channels(): - self.protection_levels[radio] = values - self.radio_dropdown.addItem(radio) - else: - self.radio_dropdown.clear() - values = [float(self.protection_legend_table.item(row, 0).text()) for row in range(self.protection_legend_table.rowCount())] - self.protection_levels['Global'] = values - self.global_protection_level = not self.radio_specific_levels.isChecked() - - ############################################################################### - # Update legend table - # ~~~~~~~~~~~~~~~~~~~ - # Update shown legend table values when the radio dropdown value changes. - - def radio_dropdown_changed(self): - if self.radio_dropdown.isEnabled(): - self.changing = True - for row in range(self.protection_legend_table.rowCount()): - item = self.protection_legend_table.item(row, 0) - item.setText(str(self.protection_levels[self.radio_dropdown.currentText()][row])) - self.changing = False - # update the validator so min/max for each row is properly set - values = [float(self.protection_legend_table.item(row, 0).text()) for row in - range(self.protection_legend_table.rowCount())] - self.delegate.update_values(values) - - ############################################################################### - # Save legend table values - # ~~~~~~~~~~~~~~~~~~~~~~~~ - # Save inputted radio protection level threshold values every time one is changed - # in the legend table. - - def table_changed(self): - if self.changing == False: - values = [float(self.protection_legend_table.item(row, 0).text()) for row in - range(self.protection_legend_table.rowCount())] - if self.radio_dropdown.currentText() == '': - index = 'Global' - else: - index = self.radio_dropdown.currentText() - self.protection_levels[index] = values - self.delegate.update_values(values) - - ############################################################################### - # Save scenario matrix to as PNG file - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Save the scenario matrix table as a PNG file. - - def save_image(self): - if self.tab_widget.currentIndex() == 0: - table = self.protection_matrix - else: - table = self.interference_matrix - - fname, _filter = QtWidgets.QFileDialog.getSaveFileName(self, "Save Scenario Matrix", "Scenario Matrix", "png (*.png)") - if fname: - image = QtGui.QImage(table.size(), QtGui.QImage.Format_ARGB32) - table.render(image) - image.save(fname) - - ############################################################################### - # Save scenario matrix to Excel file - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Write the scenario matrix results to an Excel file with color coding. - - def save_results_excel(self): - defaultName = "" - if self.tab_widget.currentIndex() == 0: - table = self.protection_matrix - defaultName = "Protection Level Classification" - else: - table = self.interference_matrix - defaultName = "Interference Type Classification" - - fname, _filter = QtWidgets.QFileDialog.getSaveFileName(self, "Save Scenario Matrix", defaultName, "xlsx (*.xlsx)") - - if fname: - workbook = openpyxl.Workbook() - worksheet = workbook.active - header = self.tx_radios[:] - header.insert(0, "Tx/Rx") - worksheet.append(header) - for row in range(2, table.rowCount()+2): - worksheet.cell(row = row, column = 1, value = str(self.rx_radios[row-2])) - for col in range(2, table.columnCount()+2): - text = str(table.item(row-2, col-2).text()) - worksheet.cell(row = row, column = col, value = text) - cell = worksheet.cell(row, col) - cell.fill = PatternFill(start_color = self.color_dict[self.all_colors[col-2][row-2]][1][1:], - end_color = self.color_dict[self.all_colors[col-2][row-2]][1][1:], - fill_type = "solid") - workbook.save(fname) - - ############################################################################### - # Run interference type simulation - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Run interference type simulation and classify results. - - def interference_results(self): - # Initialize filter check marks and expected filter results - self.interference_checks = [self.in_in_check.isChecked(), self.out_in_check.isChecked(), - self.in_out_check.isChecked(), self.out_out_check.isChecked()] - - self.interference_filters =["TxFundamental:In-band", ["TxHarmonic/Spurious:In-band","Intermod:In-band", "Broadband:In-band"], - "TxFundamental:Out-of-band", ["TxHarmonic/Spurious:Out-of-band","Intermod:Out-of-band", "Broadband:Out-of-band"]] - - # Create list of problem types to analyze according to inputted filters - filter = [i for (i,v) in zip(self.interference_filters, self.interference_checks) if v] - - if self.file_path_box.text() != "" and self.design_name_dropdown.currentText() != "": - if self.previous_design != self.design_name_dropdown.currentText() or self.previous_project != self.file_path_box.text(): - self.previous_design = self.design_name_dropdown.currentText() - self.previous_project = self.file_path_box.text() - self.emitapp.set_active_design(self.design_name_dropdown.currentText()) - - # Check if file is read-only - if self.emitapp.save_project() == False: - msg = QtWidgets.QMessageBox() - msg.setWindowTitle("Writing Error") - msg.setText("An error occurred while writing to the file. Is it readonly? Disk full? See AEDT log for more information.") - x = msg.exec() - return - - # Get results and radios - self.rev = self.emitapp.results.analyze() - self.tx_interferer = InterfererType().TRANSMITTERS - self.rx_radios = self.rev.get_receiver_names() - self.tx_radios = self.rev.get_interferer_names(self.tx_interferer) - - # Check if design is valid - if self.tx_radios is None or self.rx_radios is None: - return - - # Classify the interference - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Iterate over all the transmitters and receivers and compute the power - # at the input to each receiver due to each of the transmitters. Compute - # which, if any, type of interference occurred. - domain = self.emitapp.results.interaction_domain() - self.all_colors, self.power_matrix = self.rev.interference_type_classification(domain, use_filter = True, filter_list = filter) - - # Save project and plot results on table widget - self.emitapp.save_project() - self.populate_table() - - ############################################################################### - # Run protection level simulation - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Run protection level simulation and classify results accroding to inputted - # threshold levels. - - def protection_results(self): - # Initialize filter check marks and expected filter results - self.protection_checks = [self.damage_check.isChecked(), self.overload_check.isChecked(), - self.intermodulation_check.isChecked(), self.desensitization_check.isChecked()] - - self.protection_filters = ['damage', 'overload', 'intermodulation', 'desensitization'] - - filter = [i for (i,v) in zip(self.protection_filters, self.protection_checks) if v] - - if self.file_path_box.text() != "" and self.design_name_dropdown.currentText() != "": - if self.previous_design != self.design_name_dropdown.currentText() or self.previous_project != self.file_path_box.text(): - self.previous_design = self.design_name_dropdown.currentText() - self.previous_project = self.file_path_box.text() - self.emitapp.set_active_design(self.design_name_dropdown.currentText()) - - # Check if file is read-only - if self.emitapp.save_project() == False: - msg = QtWidgets.QMessageBox() - msg.setWindowTitle("Writing Error") - msg.setText("An error occurred while writing to the file. Is it readonly? Disk full? See AEDT log for more information.") - x = msg.exec() - return - - # Get results and design radios - self.tx_interferer = InterfererType().TRANSMITTERS - self.rev = self.emitapp.results.analyze() - self.rx_radios = self.rev.get_receiver_names() - self.tx_radios = self.rev.get_interferer_names(self.tx_interferer) - - # Check if there are radios in the design - if self.tx_radios is None or self.rx_radios is None: - return - - domain = self.emitapp.results.interaction_domain() - self.all_colors, self.power_matrix = self.rev.protection_level_classification(domain, - self.global_protection_level, - self.protection_levels['Global'], - self.protection_levels, use_filter = True, - filter_list = filter) - - self.populate_table() - - ############################################################################### - # Populate the scenario matrix - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Create a scenario matrix view with the transmitters defined across the top - # and receivers down the left-most column. - - def populate_table(self): - if self.tab_widget.currentIndex() == 0: - table = self.protection_matrix - button = self.protection_export_btn - img_btn = self.protection_save_img_btn - else: - table = self.interference_matrix - button = self.interference_export_btn - img_btn = self.interference_save_img_btn - - num_cols = len(self.all_colors) - num_rows = len(self.all_colors[0]) - table.setColumnCount(num_cols) - table.setRowCount(num_rows) - table.setVerticalHeaderLabels(self.rx_radios) - table.setHorizontalHeaderLabels(self.tx_radios) - - for col in range(num_cols): - for row in range(num_rows): - item = QtWidgets.QTableWidgetItem(str(self.power_matrix[col][row])) - table.setItem(row, col, item) - cell = table.item(row, col) - cell.setBackground(self.color_dict[self.all_colors[col][row]][0]) - - button.setEnabled(True) - img_btn.setEnabled(True) - - def clear_table(self): - # get the table/buttons based on current tab - if self.tab_widget.currentIndex() == 0: - table = self.protection_matrix - button = self.protection_export_btn - img_btn = self.protection_save_img_btn - else: - table = self.interference_matrix - button = self.interference_export_btn - img_btn = self.interference_save_img_btn - - # disable export options - button.setEnabled(False) - img_btn.setEnabled(False) - - # clear the table - table.setColumnCount(0) - table.setRowCount(0) - - ############################################################################### - # GUI closing event - # ~~~~~~~~~~~~~~~~~ - # Close AEDT if the GUI is closed. - def closeEvent(self, event): - msg = QtWidgets.QMessageBox() - msg.setWindowTitle("Closing GUI") - msg.setText("Closing AEDT. Wait for the GUI to close on its own.") - x = msg.exec() - if self.emitapp: - self.emitapp.close_project() - self.emitapp.close_desktop() - else: - desktop.release_desktop(True, True) - -############################################################################### -# Run GUI -# ~~~~~~~ -# Launch the GUI. If you want to run the GUI, uncomment the ``window.show()`` and -# ``app.exec_()`` method calls. - -if __name__ == '__main__' and os.getenv("PYAEDT_DOC_GENERATION", "False") != "1": - app = QtWidgets.QApplication([]) - window = MainWindow() - window.show() - app.exec() -else: - desktop.release_desktop(True, True) diff --git a/examples/07-TwinBuilder/01-RC_Circuit_Example.py b/examples/07-TwinBuilder/01-RC_Circuit_Example.py deleted file mode 100644 index 9e6dcec59fc..00000000000 --- a/examples/07-TwinBuilder/01-RC_Circuit_Example.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -Twin Builder: RC circuit design anaysis ---------------------------------------- -This example shows how you can use PyAEDT to create a Twin Builder design -and run a Twin Builder time-domain simulation. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Select version and set launch options -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Select the Twin Builder version and set the launch options. The following code -# launches Twin Builder 2023 R2 in graphical mode. -# -# You can change the Boolean parameter ``non_graphical`` to ``True`` to launch -# Twin Builder in non-graphical mode. You can also change the Boolean parameter -# ``new_thread`` to ``False`` to launch Twin Builder in an existing AEDT session -# if one is running. - -non_graphical = False -new_thread = True - -############################################################################### -# Launch Twin Builder -# ~~~~~~~~~~~~~~~~~~~ -# Launch Twin Builder using an implicit declaration and add a new design with -# a default setup. - -tb = ansys.aedt.core.TwinBuilder(project=ansys.aedt.core.generate_unique_project_name(), - version=aedt_version, - non_graphical=non_graphical, - new_desktop=new_thread - ) -tb.modeler.schematic_units = "mil" - -############################################################################### -# Create components for RC circuit -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create components for an RC circuit driven by a pulse voltage source. -# Create components, such as a voltage source, resistor, and capacitor. - -source = tb.modeler.schematic.create_voltage_source("E1", "EPULSE", 10, 10, [0, 0]) -resistor = tb.modeler.schematic.create_resistor("R1", 10000, [1000, 1000], 90) -capacitor = tb.modeler.schematic.create_capacitor("C1", 1e-6, [2000, 0]) - -############################################################################### -# Create ground -# ~~~~~~~~~~~~~ -# Create a ground, which is needed for an analog analysis. - -gnd = tb.modeler.components.create_gnd([0, -1000]) - -############################################################################### -# Connect components -# ~~~~~~~~~~~~~~~~~~ -# Connects components with pins. - -source.pins[1].connect_to_component(resistor.pins[0]) -resistor.pins[1].connect_to_component(capacitor.pins[0]) -capacitor.pins[1].connect_to_component(source.pins[0]) -source.pins[0].connect_to_component(gnd.pins[0]) - -############################################################################### -# Parametrize transient setup -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Parametrize the default transient setup by setting the end time. - -tb.set_end_time("300ms") - -############################################################################### -# Solve transient setup -# ~~~~~~~~~~~~~~~~~~~~~ -# Solve the transient setup. - -tb.analyze_setup("TR") - - -############################################################################### -# Get report data and plot using Matplotlib -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Get report data and plot it using Matplotlib. The following code gets and plots -# the values for the voltage on the pulse voltage source and the values for the -# voltage on the capacitor in the RC circuit. - -E_Value = "E1.V" -C_Value = "C1.V" - -x = tb.post.get_solution_data([E_Value, C_Value], "TR", "Time") -x.plot([E_Value, C_Value], x_label="Time", y_label="Capacitor Voltage vs Input Pulse") - -tb.save_project() - -############################################################################### -# Close Twin Builder -# ~~~~~~~~~~~~~~~~~~ -# After the simulation completes, you can close Twin Builder or release it. -# All methods provide for saving the project before closing. - -tb.release_desktop() diff --git a/examples/07-TwinBuilder/02-Wiring_A_Rectifier.py b/examples/07-TwinBuilder/02-Wiring_A_Rectifier.py deleted file mode 100644 index 08e5a5e8608..00000000000 --- a/examples/07-TwinBuilder/02-Wiring_A_Rectifier.py +++ /dev/null @@ -1,153 +0,0 @@ -""" -Twin Builder: wiring a rectifier with a capacitor filter ---------------------------------------------------------- -This example shows how you can use PyAEDT to create a Twin Builder design -and run a Twin Builder time-domain simulation. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import math -import matplotlib.pyplot as plt -import ansys.aedt.core - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Select version and set launch options -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Select the Twin Builder version and set the launch options. The following code -# launches Twin Builder in graphical mode. -# -# You can change the Boolean parameter ``non_graphical`` to ``True`` to launch -# Twin Builder in non-graphical mode. You can also change the Boolean parameter -# ``new_thread`` to ``False`` to launch Twin Builder in an existing AEDT session -# if one is running. - -non_graphical = False -new_thread = True - -############################################################################### -# Launch Twin Builder -# ~~~~~~~~~~~~~~~~~~~ -# Launch Twin Builder using an implicit declaration and add a new design with -# a default setup. - -tb = ansys.aedt.core.TwinBuilder(project=ansys.aedt.core.generate_unique_project_name(), - version=aedt_version, - non_graphical=non_graphical, - new_desktop=new_thread - ) - -############################################################################### -# Create components for bridge rectifier -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Create components for a bridge rectifier with a capacitor filter. - -# Define the grid distance for ease in calculations. - -G = 0.00254 - -# Create an AC sinosoidal source component. - -source = tb.modeler.schematic.create_voltage_source("V_AC", "ESINE", 100, 50, [-1 * G, 0]) - -# Create the four diodes of the bridge rectifier. - -diode1 = tb.modeler.schematic.create_diode(location=[10 * G, 6 * G], angle=270) -diode2 = tb.modeler.schematic.create_diode(location=[20 * G, 6 * G], angle=270) -diode3 = tb.modeler.schematic.create_diode(location=[10 * G, -4 * G], angle=270) -diode4 = tb.modeler.schematic.create_diode(location=[20 * G, -4 * G], angle=270) - -# Create a capacitor filter. - -capacitor = tb.modeler.schematic.create_capacitor(value=1e-6, location=[29 * G, -10 * G]) - -# Create a load resistor. - -resistor = tb.modeler.schematic.create_resistor(value=100000, location=[39 * G, -10 * G]) - -# Create a ground. - -gnd = tb.modeler.components.create_gnd(location=[5 * G, -16 * G]) - -############################################################################### -# Connect components -# ~~~~~~~~~~~~~~~~~~ -# Connect components with wires. - -# Wire the diode bridge. - -tb.modeler.schematic.create_wire(points=[diode1.pins[0].location, diode3.pins[0].location]) -tb.modeler.schematic.create_wire(points=[diode2.pins[1].location, diode4.pins[1].location]) -tb.modeler.schematic.create_wire(points=[diode1.pins[1].location, diode2.pins[0].location]) -tb.modeler.schematic.create_wire(points=[diode3.pins[1].location, diode4.pins[0].location]) - -# Wire the AC source. - -tb.modeler.schematic.create_wire(points=[source.pins[1].location, [0, 10 * G], [15 * G, 10 * G], [15 * G, 5 * G]]) -tb.modeler.schematic.create_wire(points=[source.pins[0].location, [0, -10 * G], [15 * G, -10 * G], [15 * G, -5 * G]]) - -# Wire the filter capacitor and load resistor. - -tb.modeler.schematic.create_wire(points=[resistor.pins[0].location, [40 * G, 0], [22 * G, 0]]) -tb.modeler.schematic.create_wire(points=[capacitor.pins[0].location, [30 * G, 0]]) - -# Wire the ground. - -tb.modeler.schematic.create_wire(points=[resistor.pins[1].location, [40 * G, -15 * G], gnd.pins[0].location]) -tb.modeler.schematic.create_wire(points=[capacitor.pins[1].location, [30 * G, -15 * G]]) -tb.modeler.schematic.create_wire(points=[gnd.pins[0].location, [5 * G, 0], [8 * G, 0]]) - -# Zoom to fit the schematic -tb.modeler.zoom_to_fit() - -############################################################################### -# Parametrize transient setup -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Parametrize the default transient setup by setting the end time. - -tb.set_end_time("100ms") - -############################################################################### -# Solve transient setup -# ~~~~~~~~~~~~~~~~~~~~~ -# Solve the transient setup. - -tb.analyze_setup("TR") - -############################################################################### -# Get report data and plot using Matplotlib -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Get report data and plot it using Matplotlib. The following code gets and plots -# the values for the voltage on the pulse voltage source and the values for the -# voltage on the capacitor in the RC circuit. - -E_Value = "V_AC.V" -x = tb.post.get_solution_data(E_Value, "TR", "Time") -plt.plot(x.intrinsics["Time"], x.data_real(E_Value)) - -R_Value = "R1.V" -x = tb.post.get_solution_data(R_Value, "TR", "Time") -plt.plot(x.intrinsics["Time"], x.data_real(R_Value)) - -plt.grid() -plt.xlabel("Time") -plt.ylabel("AC to DC Conversion using Rectifier") -plt.show() - -############################################################################### -# Close Twin Builder -# ~~~~~~~~~~~~~~~~~~ -# After the simulation is completed, you can close Twin Builder or release it. -# All methods provide for saving the project before closing. - -tb.release_desktop() diff --git a/examples/07-TwinBuilder/03-Dynamic_ROM_Creation_And_Visualization.py b/examples/07-TwinBuilder/03-Dynamic_ROM_Creation_And_Visualization.py deleted file mode 100644 index 13d1dd2b527..00000000000 --- a/examples/07-TwinBuilder/03-Dynamic_ROM_Creation_And_Visualization.py +++ /dev/null @@ -1,184 +0,0 @@ -""" -Twin Builder: dynamic ROM creation and simulation (2023 R2 beta) ----------------------------------------------------------------- -This example shows how you can use PyAEDT to create a dynamic ROM in Twin Builder -and run a Twin Builder time-domain simulation. - -.. note:: - This example uses functionality only available in Twin Builder 2023 R2 and later. - For 2023 R2, the build date must be 8/7/2022 or later. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import shutil -import matplotlib.pyplot as plt -from ansys.aedt.core import TwinBuilder -from ansys.aedt.core import generate_unique_project_name -from ansys.aedt.core.generic.general_methods import generate_unique_folder_name -from ansys.aedt.core import downloads -from ansys.aedt.core.generic.settings import settings - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Select version and set launch options -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Select the Twin Builder version and set launch options. The following code -# launches Twin Builder in graphical mode. -# -# You can change the Boolean parameter ``non_graphical`` to ``True`` to launch -# Twin Builder in non-graphical mode. You can also change the Boolean parameter -# ``new_thread`` to ``False`` to launch Twin Builder in an existing AEDT session -# if one is running. - -non_graphical = False -new_thread = True - -############################################################################### -# Set up input data -# ~~~~~~~~~~~~~~~~~ -# Define needed file name - -source_snapshot_data_zipfilename = "Ex1_Mechanical_DynamicRom.zip" -source_build_conf_file = "dynarom_build.conf" - -# Download data from example_data repository -temp_folder = generate_unique_folder_name() -source_data_folder = downloads.download_twin_builder_data(source_snapshot_data_zipfilename, True, temp_folder) -source_data_folder = downloads.download_twin_builder_data(source_build_conf_file, True, temp_folder) - -# Toggle these for local testing -# source_data_folder = "D:\\Scratch\\TempDyn" - -data_folder = os.path.join(source_data_folder, "Ex03") - -# Unzip training data and config file -downloads.unzip(os.path.join(source_data_folder, source_snapshot_data_zipfilename), data_folder) -shutil.copyfile(os.path.join(source_data_folder, source_build_conf_file), - os.path.join(data_folder, source_build_conf_file)) - -############################################################################### -# Launch Twin Builder and build ROM component -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch Twin Builder using an implicit declaration and add a new design with -# a default setup for building the dynamic ROM component. - -tb = TwinBuilder(project=generate_unique_project_name(), - version=aedt_version, - non_graphical=non_graphical, - new_desktop=new_thread) - -# Switch the current desktop configuration and the schematic environment to "Twin Builder". -# The Dynamic ROM feature is only available with a twin builder license. -# This and the restoring section at the end are not needed if the desktop is already configured as "Twin Builder". -current_desktop_config = tb._odesktop.GetDesktopConfiguration() -current_schematic_environment = tb._odesktop.GetSchematicEnvironment() -tb._odesktop.SetDesktopConfiguration("Twin Builder") -tb._odesktop.SetSchematicEnvironment(1) - -# Get the dynamic ROM builder object -rom_manager = tb._odesign.GetROMManager() -dynamic_rom_builder = rom_manager.GetDynamicROMBuilder() - -# Build the dynamic ROM with specified configuration file -conf_file_path = os.path.join(data_folder, source_build_conf_file) -dynamic_rom_builder.Build(conf_file_path.replace('\\', '/')) - -# Test if ROM was created successfully -dynamic_rom_path = os.path.join(data_folder, 'DynamicRom.dyn') -if os.path.exists(dynamic_rom_path): - tb._odesign.AddMessage("Info", "path exists: {}".format(dynamic_rom_path.replace('\\', '/')), "") -else: - tb._odesign.AddMessage("Info", "path does not exist: {}".format(dynamic_rom_path), "") - -# Create the ROM component definition in Twin Builder -rom_manager.CreateROMComponent(dynamic_rom_path.replace('\\', '/'), 'dynarom') - -############################################################################### -# Create schematic -# ~~~~~~~~~~~~~~~~ -# Place components to create a schematic. - -# Define the grid distance for ease in calculations - -G = 0.00254 - -# Place a dynamic ROM component - -rom1 = tb.modeler.schematic.create_component("ROM1", "", "dynarom", [36 * G, 28 * G]) - -# Place two excitation sources - -source1 = tb.modeler.schematic.create_periodic_waveform_source(None, "PULSE", 190, 0.002, "300deg", 210, 0, - [20 * G, 29 * G]) -source2 = tb.modeler.schematic.create_periodic_waveform_source(None, "PULSE", 190, 0.002, "300deg", 210, 0, - [20 * G, 25 * G]) - -# Connect components with wires - -tb.modeler.schematic.create_wire([[22 * G, 29 * G], [33 * G, 29 * G]]) -tb.modeler.schematic.create_wire([[22 * G, 25 * G], [30 * G, 25 * G], [30 * G, 28 * G], [33 * G, 28 * G]]) - -# Zoom to fit the schematic -tb.modeler.zoom_to_fit() - -############################################################################### -# Parametrize transient setup -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Parametrize the default transient setup by setting the end time. - -tb.set_end_time("1000s") -tb.set_hmin("1s") -tb.set_hmax("1s") - -############################################################################### -# Solve transient setup -# ~~~~~~~~~~~~~~~~~~~~~ -# Solve the transient setup. - -tb.analyze_setup("TR") - -############################################################################### -# Get report data and plot using Matplotlib -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Get report data and plot it using Matplotlib. The following code gets and plots -# the values for the voltage on the pulse voltage source and the values for the -# output of the dynamic ROM. - -input_excitation = "PULSE1.VAL" -x = tb.post.get_solution_data(input_excitation, "TR", "Time") -plt.plot(x.intrinsics["Time"], x.data_real(input_excitation)) - -output_temperature = "ROM1.Temperature_history" -x = tb.post.get_solution_data(output_temperature, "TR", "Time") -plt.plot(x.intrinsics["Time"], x.data_real(output_temperature)) - -plt.grid() -plt.xlabel("Time") -plt.ylabel("Temperature History Variation with Input Temperature Pulse") -plt.show() - -############################################################################### -# Close Twin Builder -# ~~~~~~~~~~~~~~~~~~ -# After the simulation is completed, you can close Twin Builder or release it. -# All methods provide for saving the project before closing. - -# Clean up the downloaded data -shutil.rmtree(source_data_folder) - -# Restore earlier desktop configuration and schematic environment -tb._odesktop.SetDesktopConfiguration(current_desktop_config) -tb._odesktop.SetSchematicEnvironment(current_schematic_environment) - -tb.release_desktop() diff --git a/examples/07-TwinBuilder/04-Static_ROM_Creation_And_Visualization.py b/examples/07-TwinBuilder/04-Static_ROM_Creation_And_Visualization.py deleted file mode 100644 index 1d65667f41c..00000000000 --- a/examples/07-TwinBuilder/04-Static_ROM_Creation_And_Visualization.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -Twin Builder: static ROM creation and simulation (2023 R2 beta) ---------------------------------------------------------------- -This example shows how you can use PyAEDT to create a static ROM in Twin Builder -and run a Twin Builder time-domain simulation. - -.. note:: - This example uses functionality only available in Twin Builder 2023 R2 and later. - For 2023 R2, the build date must be 8/7/2022 or later. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import os -import math -import shutil -import matplotlib.pyplot as plt -from ansys.aedt.core import TwinBuilder -from ansys.aedt.core import generate_unique_project_name -from ansys.aedt.core.generic.general_methods import generate_unique_folder_name -from ansys.aedt.core import downloads -from ansys.aedt.core.generic.settings import settings - -########################################################## -# Set AEDT version -# ~~~~~~~~~~~~~~~~ -# Set AEDT version. - -aedt_version = "2024.2" - -############################################################################### -# Select version and set launch options -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Select the Twin Builder version and set launch options. The following code -# launches Twin Builder 2023 R2 in graphical mode. -# -# You can change the Boolean parameter ``non_graphical`` to ``True`` to launch -# Twin Builder in non-graphical mode. You can also change the Boolean parameter -# ``new_thread`` to ``False`` to launch Twin Builder in an existing AEDT session -# if one is running. - -non_graphical = False -new_thread = True - -############################################################################### -# Set up input data -# ~~~~~~~~~~~~~~~~~ -# Define needed file name - -source_snapshot_data_zipfilename = "Ex1_Fluent_StaticRom.zip" -source_build_conf_file = "SROMbuild.conf" -source_props_conf_file = "SROM_props.conf" - -# Download data from example_data repository -source_data_folder = downloads.download_twin_builder_data(source_snapshot_data_zipfilename, True) -source_data_folder = downloads.download_twin_builder_data(source_build_conf_file, True) -source_data_folder = downloads.download_twin_builder_data(source_props_conf_file, True) - -# Uncomment the following line for local testing -# source_data_folder = "D:\\Scratch\\TempStatic" - -data_folder = os.path.join(source_data_folder, "Ex04") - -# Unzip training data and config file -downloads.unzip(os.path.join(source_data_folder, source_snapshot_data_zipfilename), data_folder) -shutil.copyfile(os.path.join(source_data_folder, source_build_conf_file), - os.path.join(data_folder, source_build_conf_file)) -shutil.copyfile(os.path.join(source_data_folder, source_props_conf_file), - os.path.join(data_folder, source_props_conf_file)) - -############################################################################### -# Launch Twin Builder and build ROM component -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Launch Twin Builder using an implicit declaration and add a new design with -# a default setup for building the static ROM component. - -tb = TwinBuilder(project=generate_unique_project_name(), version=aedt_version, - non_graphical=non_graphical, new_desktop=new_thread) - -# Switch the current desktop configuration and the schematic environment to "Twin Builder". -# The Static ROM feature is only available with a twin builder license. -# This and the restoring section at the end are not needed if the desktop is already configured as "Twin Builder". -current_desktop_config = tb._odesktop.GetDesktopConfiguration() -current_schematic_environment = tb._odesktop.GetSchematicEnvironment() -tb._odesktop.SetDesktopConfiguration("Twin Builder") -tb._odesktop.SetSchematicEnvironment(1) - -# Get the static ROM builder object -rom_manager = tb._odesign.GetROMManager() -static_rom_builder = rom_manager.GetStaticROMBuilder() - -# Build the static ROM with specified configuration file -confpath = os.path.join(data_folder, source_build_conf_file) -static_rom_builder.Build(confpath.replace('\\', '/')) - -# Test if ROM was created successfully -static_rom_path = os.path.join(data_folder, 'StaticRom.rom') -if os.path.exists(static_rom_path): - tb.logger.info("Built intermediate rom file successfully at: %s", static_rom_path) -else: - tb.logger.error("Intermediate rom file not found at: %s", static_rom_path) - -# Create the ROM component definition in Twin Builder -rom_manager.CreateROMComponent(static_rom_path.replace('\\', '/'), 'staticrom') - -############################################################################### -# Create schematic -# ~~~~~~~~~~~~~~~~ -# Place components to create a schematic. - -# Define the grid distance for ease in calculations -G = 0.00254 - -# Place a dynamic ROM component -rom1 = tb.modeler.schematic.create_component("ROM1", "", "staticrom", [40 * G, 25 * G]) - -# Place two excitation sources -source1 = tb.modeler.schematic.create_periodic_waveform_source(None, "SINE", 2.5, 0.01, 0, 7.5, 0, [20 * G, 29 * G]) -source2 = tb.modeler.schematic.create_periodic_waveform_source(None, "SINE", 50, 0.02, 0, 450, 0, [20 * G, 25 * G]) - -# Connect components with wires - -tb.modeler.schematic.create_wire([[22 * G, 29 * G], [33 * G, 29 * G]]) -tb.modeler.schematic.create_wire([[22 * G, 25 * G], [30 * G, 25 * G], [30 * G, 28 * G], [33 * G, 28 * G]]) - -# Enable storage of views - -rom1.set_property("store_snapshots", 1) -rom1.set_property("view1_storage_period", "10s") -rom1.set_property("view2_storage_period", "10s") - -# Zoom to fit the schematic -tb.modeler.zoom_to_fit() - -############################################################################### -# Parametrize transient setup -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Parametrize the default transient setup by setting the end time. - -tb.set_end_time("300s") -tb.set_hmin("1s") -tb.set_hmax("1s") - -############################################################################### -# Solve transient setup -# ~~~~~~~~~~~~~~~~~~~~~ -# Solve the transient setup. Skipping in case of documentation build. - -if os.getenv("PYAEDT_DOC_GENERATION", "False") != "1": - tb.analyze_setup("TR") - -############################################################################### -# Get report data and plot using Matplotlib -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Get report data and plot it using Matplotlib. The following code gets and plots -# the values for the voltage on the pulse voltage source and the values for the -# output of the dynamic ROM. -if os.getenv("PYAEDT_DOC_GENERATION", "False") != "1": - e_value = "ROM1.outfield_mode_1" - x = tb.post.get_solution_data(e_value, "TR", "Time") - x.plot() - e_value = "ROM1.outfield_mode_2" - x = tb.post.get_solution_data(e_value, "TR", "Time") - x.plot() - e_value = "SINE1.VAL" - x = tb.post.get_solution_data(e_value, "TR", "Time") - x.plot() - e_value = "SINE2.VAL" - x = tb.post.get_solution_data(e_value, "TR", "Time") - x.plot() - - -############################################################################### -# Close Twin Builder -# ~~~~~~~~~~~~~~~~~~ -# After the simulation is completed, you can close Twin Builder or release it. -# All methods provide for saving the project before closing. - -# Clean up the downloaded data -shutil.rmtree(source_data_folder) - -# Restore earlier desktop configuration and schematic environment -tb._odesktop.SetDesktopConfiguration(current_desktop_config) -tb._odesktop.SetSchematicEnvironment(current_schematic_environment) - -tb.release_desktop() diff --git a/examples/07-TwinBuilder/Readme.txt b/examples/07-TwinBuilder/Readme.txt deleted file mode 100644 index df697376902..00000000000 --- a/examples/07-TwinBuilder/Readme.txt +++ /dev/null @@ -1,4 +0,0 @@ -Twin Builder examples -~~~~~~~~~~~~~~~~~~~~~ -These examples use PyAEDT to show some end-to-end workflows for Twin Builder. -This includes schematic generation, setup, and postprocessing. diff --git a/examples/08-FilterSolutions/Lumped_Element_Response.py b/examples/08-FilterSolutions/Lumped_Element_Response.py deleted file mode 100644 index 971463f3195..00000000000 --- a/examples/08-FilterSolutions/Lumped_Element_Response.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Design a lumped element filter ------------------------------- -This example shows how to use PyAEDT to use the ``FilterSolutions`` module to design and -visualize the frequency response of a band-pass Butterworth filter. -""" - -############################################################################### -# Perform required imports -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Perform required imports. - -import ansys.aedt.core -import ansys.aedt.core.filtersolutions_core.attributes -from ansys.aedt.core.filtersolutions_core.attributes import FilterType, FilterClass, FilterImplementation -from ansys.aedt.core.filtersolutions_core.ideal_response import FrequencyResponseColumn -from ansys.aedt.core.filtersolutions_core.export_to_aedt import PartLibraries, ExportFormat -import matplotlib.pyplot as plt - -############################################################################### -# Create the lumped design -# ~~~~~~~~~~~~~~~~~~~~~~~~ -# Create a lumped element filter design and assign the class, type, frequency, and order. -design = ansys.aedt.core.FilterSolutions(version="2025.1", implementation_type= FilterImplementation.LUMPED) -design.attributes.filter_class = FilterClass.BAND_PASS -design.attributes.filter_type = FilterType.BUTTERWORTH -design.attributes.pass_band_center_frequency = "1G" -design.attributes.pass_band_width_frequency = "500M" -design.attributes.filter_order = 5 -############################################################################## -# Plot the frequency response of the filter -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Plot the frequency response of the filter without any transmission zeros. - -freq, mag_db = design.ideal_response.frequency_response(FrequencyResponseColumn.MAGNITUDE_DB) -plt.plot(freq, mag_db, linewidth=2.0, label="Without Tx Zero") -def format_plot(): - plt.xlabel("Frequency (Hz)") - plt.ylabel("Magnitude S21 (dB)") - plt.title("Ideal Frequency Response") - plt.xscale("log") - plt.legend() - plt.grid() -format_plot() -plt.show() - -############################################################################## -# Add a transmission zero to the filter design -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Add a transmission zeros that yields nulls separated by 2 times the pass band width (1 GHz). -# Plot the frequency response of the filter with the transmission zero. - -design.transmission_zeros_ratio.append_row("2.0") -freq_with_zero, mag_db_with_zero = design.ideal_response.frequency_response(FrequencyResponseColumn.MAGNITUDE_DB) -plt.plot(freq, mag_db, linewidth=2.0, label="Without Tx Zero") -plt.plot(freq_with_zero, mag_db_with_zero, linewidth=2.0, label="With Tx Zero") -format_plot() -plt.show() - -############################################################################## -# Generate the netlist for the designed filter -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Generate and print the netlist for the designed filter with the added transmission zero to filter. -netlist = design.topology.circuit_response() -print("Netlist: \n", netlist) diff --git a/examples/Readme.txt b/examples/Readme.txt deleted file mode 100644 index 088894ba498..00000000000 --- a/examples/Readme.txt +++ /dev/null @@ -1,10 +0,0 @@ -.. _ref_example_gallery: - -Examples -======== -End-to-end examples show how you can use PyAEDT. If PyAEDT is installed -on your machine, you can download these examples as Python files or Jupyter -notebooks and run them locally. - -.. note:: - Some examples require additional Python packages. diff --git a/pyproject.toml b/pyproject.toml index 9019d657219..5f1fde6412d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,52 +71,21 @@ dotnet = [ "dotnetcore2==3.1.23; platform_system=='Linux'", "pywin32>=303; platform_system=='Windows'", ] + doc = [ "ansys-sphinx-theme>=0.10.0,<1.1", - "ipython>=8.13.0,<8.28", - "joblib>=1.3.0,<1.5", - "jupyterlab>=4.0.0,<4.3", - "matplotlib>=3.5.0,<3.10", - "nbsphinx>=0.9.0,<0.10", "numpydoc>=1.5.0,<1.9", - "openpyxl>=3.0.0,<3.2", - "osmnx>=1.1.0,<1.10", - "pypandoc>=1.10.0,<1.14", - #"pytest-sphinx", - "pyvista[io]>=0.38.0,<0.45", "recommonmark", - "scikit-rf>=0.30.0,<1.4", "Sphinx>=7.1.0,<8.1", # NOTE: latest compatible version for Python 3.8 is 2021.3.14 "sphinx-autobuild==2021.3.14; python_version == '3.8'", "sphinx-autobuild==2024.9.19; python_version > '3.8'", - #"sphinx-autodoc-typehints", "sphinx-copybutton>=0.5.0,<0.6", "sphinx-gallery>=0.14.0,<0.18", - #"sphinx-notfound-page", "sphinx_design>=0.4.0,<0.7", - #"sphinxcontrib-websupport", - "SRTM.py", - "utm", -] -doc-no-examples = [ - "ansys-sphinx-theme>=0.10.0,<1.1", - "numpydoc>=1.5.0,<1.9", - "recommonmark", - "Sphinx>=7.1.0,<8.1", - # NOTE: latest compatible version for Python 3.8 is 2021.3.14 - "sphinx-autobuild==2021.3.14; python_version == '3.8'", - "sphinx-autobuild==2024.9.19; python_version > '3.8'", - #"sphinx-autodoc-typehints", - "sphinx-copybutton>=0.5.0,<0.6", - "sphinx-gallery>=0.14.0,<0.18", - #"sphinx-notfound-page", - #"sphinxcontrib-websupport", - "sphinx_design>=0.4.0,<0.7", - "matplotlib>=3.5.0,<3.10", - "scikit-rf>=0.30.0,<1.4", "pyvista[io]>=0.38.0,<0.45", ] + all = [ "matplotlib>=3.5.0,<3.10", "numpy>=1.20.0,<2", From 1e664a3036b52e57bc47b79eb8b9eb2879e6e861 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 6 Oct 2024 08:42:50 +0200 Subject: [PATCH 18/20] BUILD: update ipython requirement from <8.28,>=7.30.0 to >=7.30.0,<8.29 (#5253) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5f1fde6412d..4bc31e33e42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ [project.optional-dependencies] tests = [ - "ipython>=7.30.0,<8.28", + "ipython>=7.30.0,<8.29", "joblib>=1.0.0,<1.5", "matplotlib>=3.5.0,<3.10", "mock>=5.1.0,<5.2", @@ -113,7 +113,7 @@ installer = [ "SRTM.py", "utm", "jupyterlab>=3.6.0,<4.3", - "ipython>=7.30.0,<8.28", + "ipython>=7.30.0,<8.29", "ipyvtklink>=0.2.0,<0.2.4", ] From 0ea13875666ba782b79e7a6db465624cd701f5b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 6 Oct 2024 08:43:16 +0200 Subject: [PATCH 19/20] BUILD: bump sphinx-autobuild from 2021.3.14 to 2024.10.3 (#5252) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4bc31e33e42..a5b94451f5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ doc = [ "Sphinx>=7.1.0,<8.1", # NOTE: latest compatible version for Python 3.8 is 2021.3.14 "sphinx-autobuild==2021.3.14; python_version == '3.8'", - "sphinx-autobuild==2024.9.19; python_version > '3.8'", + "sphinx-autobuild==2024.10.3; python_version > '3.8'", "sphinx-copybutton>=0.5.0,<0.6", "sphinx-gallery>=0.14.0,<0.18", "sphinx_design>=0.4.0,<0.7", From 3af0e496cb9399dc778c164104ddb21fc574503e Mon Sep 17 00:00:00 2001 From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:21:21 +0200 Subject: [PATCH 20/20] FEAT: Add SML component in Twin Builder (#5245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: chrpetre <108675940+chrpetre@users.noreply.github.com> Co-authored-by: Sébastien Morais <146729917+SMoraisAnsys@users.noreply.github.com> --- .../example_models/T34/Thermal_ROM_SML.sml | 196 ++++++++++++++++++ _unittest/test_34_TwinBuilder.py | 15 +- .../modeler/circuits/object_3d_circuit.py | 2 +- .../circuits/primitives_twin_builder.py | 113 ++++++++++ 4 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 _unittest/example_models/T34/Thermal_ROM_SML.sml diff --git a/_unittest/example_models/T34/Thermal_ROM_SML.sml b/_unittest/example_models/T34/Thermal_ROM_SML.sml new file mode 100644 index 00000000000..590b51575df --- /dev/null +++ b/_unittest/example_models/T34/Thermal_ROM_SML.sml @@ -0,0 +1,196 @@ +SMLDEF Thermal_ROM_SML +{ + + PORT real in : Input1_InternalHeatGeneration; + PORT real in : Input2_HeatFlow; + + PORT real in : Ref1_Temp1 = 2.951500e+02; + PORT real in : Ref2_Temp2 = 2.951500e+02; + + PORT real out : Output1_Temp1 = sssm.OutVec[0]; + PORT real out : Output2_Temp2 = sssm.OutVec[1]; + + INTERN NCStateSpaceModel sssm + ( + NumOutputs := 2, NumInputs := 4, + InVec[0] := Input1_InternalHeatGeneration , + InVec[1] := Input2_HeatFlow , + InVec[2] := Ref1_Temp1 , + InVec[3] := Ref2_Temp2 , + NumStates := 16 , + Mode := 3 , + A_row := + 0 + 1 + 3 + 5 + 6 + 7 + 9 + 11 + 12 + 13 + 14 + 15 + 16 + 18 + 20 + 21 + , + A_col := + 0 + 1 + 2 + 1 + 2 + 3 + 4 + 5 + 6 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 12 + 13 + 14 + 15 + , + A_NonZeros := 22 + , + MatrixA := + -1.088280e-01 + 0.000000e+00 + 1.000000e+00 + -6.578639e-03 + -6.795586e-03 + -4.334062e-03 + -1.719193e+00 + 0.000000e+00 + 1.000000e+00 + -8.089552e-04 + -3.902651e-03 + -4.524139e-03 + -1.185993e+00 + -9.632427e-02 + -4.795756e-03 + -4.318334e-03 + 0.000000e+00 + 1.000000e+00 + -3.008413e-03 + -1.904252e-03 + -4.033259e-03 + -2.004892e-02 + , + B_row := + 0 + 1 + 1 + 2 + 3 + 4 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 10 + 11 + 12 + , + B_col := + 0 + 0 + 0 + 1 + 1 + 1 + 0 + 0 + 0 + 0 + 1 + 1 + 1 + , + B_NonZeros := 13 + , + MatrixB := + 1.569859e-08 + 1.569859e-08 + 1.569859e-08 + 5.000000e-01 + 5.000000e-01 + 5.000000e-01 + 1.569859e-08 + 1.569859e-08 + 1.569859e-08 + 1.569859e-08 + 5.000000e-01 + 5.000000e-01 + 5.000000e-01 + , + C_row := + 0 + 8 + , + C_col := + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + , + C_NonZeros := 16 + , + MatrixC := + 1.577107e-02 + -6.230525e-06 + -1.732583e-05 + 5.174927e-03 + 1.763584e+00 + 2.605549e-06 + 5.971550e-05 + 1.493263e-03 + 9.628570e-01 + 1.471336e-02 + 2.037277e-04 + 5.018707e-03 + 2.093970e-06 + 1.080787e-04 + 1.238091e-03 + 9.353277e-04 + , + D_row := + 0 + 1 + , + D_col := + 2 + 3 + , + D_NonZeros := 2 + , + MatrixD := + 1.000000e+00 + 1.000000e+00 + ); +} diff --git a/_unittest/test_34_TwinBuilder.py b/_unittest/test_34_TwinBuilder.py index aedf482dd33..64d92f4009a 100644 --- a/_unittest/test_34_TwinBuilder.py +++ b/_unittest/test_34_TwinBuilder.py @@ -145,7 +145,7 @@ def test_14_set_variable(self): assert "var_test" in self.aedtapp.variable_manager.design_variable_names assert self.aedtapp.variable_manager.design_variables["var_test"].expression == "234" - def test_19_add_dynamic_link(self, add_app): + def test_15_add_dynamic_link(self, add_app): tb = add_app(application=TwinBuilder, project_name=self.dynamic_link, design_name="CableSystem", just_open=True) assert tb.add_q3d_dynamic_component( "Q2D_ArmouredCableExample", "2D_Extractor_Cable", "MySetupAuto", "sweep1", "Original", model_depth="100mm" @@ -203,3 +203,16 @@ def test_19_add_dynamic_link(self, add_app): tb.add_q3d_dynamic_component( "invalid", "2D_Extractor_Cable", "MySetupAuto", "sweep1", "Original", model_depth="100mm" ) + + def test_16_add_sml_component(self, local_scratch): + self.aedtapp.insert_design("SML") + input_file = local_scratch.copyfile( + os.path.join(local_path, "../_unittest/example_models", test_subfolder, "Thermal_ROM_SML.sml") + ) + pins_names = ["Input1_InternalHeatGeneration", "Input2_HeatFlow", "Output1_Temp1,Output2_Temp2"] + assert self.aedtapp.modeler.schematic.create_component_from_sml( + input_file=input_file, model="Thermal_ROM_SML", pins_names=pins_names + ) + rom1 = self.aedtapp.modeler.schematic.create_component("ROM1", "", "Thermal_ROM_SML") + + assert self.aedtapp.modeler.schematic.update_quantity_value(rom1.composed_name, "Input2_HeatFlow", "1") diff --git a/src/ansys/aedt/core/modeler/circuits/object_3d_circuit.py b/src/ansys/aedt/core/modeler/circuits/object_3d_circuit.py index 1042b54d5d9..268d6eff8eb 100644 --- a/src/ansys/aedt/core/modeler/circuits/object_3d_circuit.py +++ b/src/ansys/aedt/core/modeler/circuits/object_3d_circuit.py @@ -624,7 +624,7 @@ def pins(self): elif not pins: return [] for pin in pins: - if self._circuit_components._app.design_type != "Twin Builder": + if self._circuit_components._app.design_type: self._pins.append(CircuitPins(self, pin, idx)) elif pin not in list(self.parameters.keys()): self._pins.append(CircuitPins(self, pin, idx)) diff --git a/src/ansys/aedt/core/modeler/circuits/primitives_twin_builder.py b/src/ansys/aedt/core/modeler/circuits/primitives_twin_builder.py index 7b4f2e5ad14..6cbea886090 100644 --- a/src/ansys/aedt/core/modeler/circuits/primitives_twin_builder.py +++ b/src/ansys/aedt/core/modeler/circuits/primitives_twin_builder.py @@ -475,3 +475,116 @@ def create_periodic_waveform_source( id.set_property("PERIO", 1) return id + + @pyaedt_function_handler() + def create_component_from_sml( + self, + input_file, + model, + pins_names, + ): + """Create and place a new component based on a .sml file. + + Parameters + ---------- + input_file : str + Path to .sml file. + model : str + Model name to import. + pins_names : list + List of model pins names. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + >>> from ansys.aedt.core import TwinBuilder + >>> tb = TwinBuilder(version="2025.1") + >>> input_file = os.path.join("Your path", "test.sml") + >>> model = "Thermal_ROM_SML" + >>> pins_names = ["Input1_InternalHeatGeneration", "Input2_HeatFlow", "Output1_Temp1,Output2_Temp2"] + >>> tb.modeler.schematic.create_component_from_sml(input_file=model, model=model, pins_names=pins_names) + >>> tb.release_desktop(False, False) + """ + pins_names_str = ",".join(pins_names) + arg = ["NAME:Options", "Mode:=", 1] + arg2 = ["NAME:Models", model + ":=", [True]] + + arg3 = [ + "NAME:Components", + model + ":=", + [True, True, model, True, pins_names_str.lower(), pins_names_str.lower()], + ] + + arg.append(arg2) + arg.append(arg3) + self.o_component_manager.ImportModelsFromFile(input_file, arg) + return True + + @pyaedt_function_handler() + def update_quantity_value(self, component_name, name, value, netlist_units=""): + """Change the property value of a component. + + Parameters + ---------- + component_name : str + Component name. + name : str + Property name. + value : str + Value of the quantity. + netlist_units : str, optional + Value of the netlist unit. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + >>> from ansys.aedt.core import TwinBuilder + >>> tb = TwinBuilder(version="2025.1") + >>> G = 0.00254 + >>> modelpath = "Simplorer Elements\\Basic Elements\\Tools\\Time Functions:DATAPAIRS" + >>> source1 = tb.modeler.schematic.create_component("source1", "", modelpath, [20 * G, 29 * G]) + >>> tb.modeler.schematic.update_quantity_value(source1.composed_name, "PERIO", "0") + >>> tb.release_desktop(False, False) + """ + try: + self.oeditor.ChangeProperty( + [ + "NAME:AllTabs", + [ + "NAME:Quantities", + ["NAME:PropServers", component_name], + [ + "NAME:ChangedProps", + [ + "NAME:" + name, + "OverridingDef:=", + True, + "Value:=", + value, + "NetlistUnits:=", + netlist_units, + "ShowPin:=", + False, + "Display:=", + False, + "Sweep:=", + False, + "SDB:=", + False, + ], + ], + ], + ] + ) + return True + except Exception: # pragma: no cover + self.logger.warning(f"Property {name} has not been edited. Check if readonly.") + return False