diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8ce930ba9fa..859c1413b0e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,7 +12,7 @@ updates: - "maintenance" - "dependencies" commit-message: - prefix: "MAINT" + prefix: "CHORE" - package-ecosystem: "github-actions" directory: "/" @@ -26,4 +26,4 @@ updates: - "maintenance" - "dependencies" commit-message: - prefix: "MAINT" + prefix: "CHORE" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d36d7ebaad3..d368033e733 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - --max-line-length=120 - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.3.0 hooks: - id: codespell additional_dependencies: @@ -48,7 +48,7 @@ repos: # validate GitHub workflow files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.2 + rev: 0.28.4 hooks: - id: check-github-workflows diff --git a/README.md b/README.md index a4b5fbc0fed..8cee89d3b48 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,15 @@ PyAEDT is a Python library that interacts directly with the API for Ansys Electronics Desktop (AEDT) to make scripting simpler. The architecture for PyAEDT can be reused for all AEDT 3D products (HFSS, Icepak, Maxwell 3D, -and Q3D Extractor), 2D tools, and Ansys Mechanical. PyAEDT also provides +and Q3D Extractor), 2D tools, and Ansys Mechanical inside AEDT. PyAEDT also provides support for circuit tools like Nexxim and system simulation tools like Twin Builder. Finally, PyAEDT provides scripting capabilities in Ansys layout -tools like HFSS 3D Layout and EDB. The PyAEDT class and method structures +tools like HFSS 3D Layout. The Ansys Electronics Database +([EDB](https://edb.docs.pyansys.com/version/stable/)) is included +with PyAEDT as a dependency and is recommended for any automated manipulation and +setup of layout data for PCBs, electronic packages, and integrated circuits. + +The PyAEDT class and method structures simplify operation while reusing information as much as possible across the API. @@ -130,6 +135,31 @@ To reach the project support team, email [pyansys.core@ansys.com](mailto:pyansys To run PyAEDT, you must have a local licenced copy of AEDT. PyAEDT supports AEDT versions 2022 R1 or newer. +## Contributing + +For comprehensive information on contributing to the PyAnsys project, see the [PyAnsys developer's guide](https://dev.docs.pyansys.com). + + +Note that PyAEDT uses semantic naming for pull requests (PR). This convention +greatly simplifies the review process by providing meaningful +information in the PR title. The +following +[prefixes](https://github.com/ansys/actions/blob/main/commit-style/action.yml) +should be used for pull request name: + +- "BUILD" +- "CHORE" +- "CI" +- "DOCS" +- "FEAT" +- "FIX" +- "PERF" +- "REFACTOR" +- "REVERT" +- "STYLE" +- "TEST" + + ## Student version PyAEDT supports AEDT Student versions 2022 R1 and later. For more information, see the diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..e12991d5955 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,30 @@ + + +# Security Policy + +## Reporting a Vulnerability + +If you detect a vulnerability, contact the [PyAnsys Core team](mailto:pyansys.core@ansys.com) +mentioning the repository and the details of your finding. The team will address it as soon as possible. \ No newline at end of file diff --git a/_unittest/example_models/T20/sphere.stl b/_unittest/example_models/T20/sphere.stl new file mode 100644 index 00000000000..d0474c785f2 Binary files /dev/null and b/_unittest/example_models/T20/sphere.stl differ diff --git a/_unittest/example_models/T20/test_cad.nas b/_unittest/example_models/T20/test_cad.nas index 640b41ee14f..9bb0c0c95e7 100644 --- a/_unittest/example_models/T20/test_cad.nas +++ b/_unittest/example_models/T20/test_cad.nas @@ -67,6 +67,7 @@ CPENTA 25328 9 26 28 25 34 35 30 CHEXA 1 105 40 41 42 43 44 45 * 46 47 CTETRA 1 115 50 51 52 53 +CQUAD 1 115 50 51 52 53 40 ENDDATA $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ diff --git a/_unittest/example_models/T20/test_cad_2.nas b/_unittest/example_models/T20/test_cad_2.nas index f41ccef4bea..593099be1f2 100644 --- a/_unittest/example_models/T20/test_cad_2.nas +++ b/_unittest/example_models/T20/test_cad_2.nas @@ -15,8 +15,12 @@ GRID* 5 812.95973 486.74968* * 1495.9564 GRID* 6 812.99916 484.24676* * 1470.1764 +GRID* 7 812.99916 484.24676* +* 1070.1764 CTRIA3* 75986 19 4 5 * 6 +CQUAD4* 75987 19 4 5 +* 6 7 ENDDATA diff --git a/_unittest/test_01_Design.py b/_unittest/test_01_Design.py index eddd0ace585..bcf6d53cf0f 100644 --- a/_unittest/test_01_Design.py +++ b/_unittest/test_01_Design.py @@ -305,6 +305,7 @@ def test_27_odesktop(self): "", "", "", + "", ] def test_28_get_pyaedt_app(self): diff --git a/_unittest/test_01_toolkit_icons.py b/_unittest/test_01_toolkit_icons.py index 55ec3a74cbd..99922aa102d 100644 --- a/_unittest/test_01_toolkit_icons.py +++ b/_unittest/test_01_toolkit_icons.py @@ -110,5 +110,5 @@ def validate_file_exists_and_pyaedt_tabs_added(file_path): root = tree.getroot() panels = root.findall("./panel") panel_names = [panel.attrib["label"] for panel in panels] - assert "Panel_PyAEDT_Toolkits" in panel_names + assert "Panel_PyAEDT_Extensions" in panel_names return root diff --git a/_unittest/test_12_PostProcessing.py b/_unittest/test_12_PostProcessing.py index 2375a7e7acb..03f2ba5bd0b 100644 --- a/_unittest/test_12_PostProcessing.py +++ b/_unittest/test_12_PostProcessing.py @@ -2,7 +2,9 @@ import sys from _unittest.conftest import config +from matplotlib.figure import Figure import pytest +from pyvista.plotting.plotter import Plotter from pyaedt import Circuit from pyaedt import Icepak @@ -597,18 +599,18 @@ def test_70_far_field_data(self): assert ffdata.origin == [0, 0, 1] img1 = os.path.join(self.local_scratch.path, "ff_2d1.jpg") - ffdata.plot_2d_cut(primary_sweep="Theta", secondary_sweep_value="all", image_path=img1) + ffdata.plot_2d_cut(primary_sweep="Theta", secondary_sweep_value="all", image_path=img1, show=False) assert os.path.exists(img1) img2 = os.path.join(self.local_scratch.path, "ff_2d2.jpg") - ffdata.plot_2d_cut(secondary_sweep_value=[0, 1], image_path=img2) + ffdata.plot_2d_cut(secondary_sweep_value=[0, 1], image_path=img2, show=False) assert os.path.exists(img2) img3 = os.path.join(self.local_scratch.path, "ff_2d2.jpg") - ffdata.plot_2d_cut(image_path=img3) + ffdata.plot_2d_cut(image_path=img3, show=False) assert os.path.exists(img3) curve_2d = ffdata.plot_2d_cut(show=False) - assert len(curve_2d[0]) == 3 + assert isinstance(curve_2d, Figure) data = ffdata.polar_plot_3d(show=False) - assert len(data) == 3 + assert isinstance(data, Figure) img4 = os.path.join(self.local_scratch.path, "ff_3d1.jpg") ffdata.polar_plot_3d_pyvista( @@ -623,7 +625,7 @@ def test_70_far_field_data(self): data_pyvista = ffdata.polar_plot_3d_pyvista( quantity="RealizedGain", show=False, background=[255, 0, 0], show_geometry=False, convert_to_db=True ) - assert data_pyvista + assert isinstance(data_pyvista, Plotter) @pytest.mark.skipif(is_linux or sys.version_info < (3, 8), reason="FarFieldSolution not supported by IronPython") def test_71_antenna_plot(self, field_test): diff --git a/_unittest/test_20_HFSS.py b/_unittest/test_20_HFSS.py index cd13bf50322..ae8d68895f2 100644 --- a/_unittest/test_20_HFSS.py +++ b/_unittest/test_20_HFSS.py @@ -1403,7 +1403,12 @@ def test_59_test_nastran(self): cads = self.aedtapp.modeler.import_nastran(example_project) assert len(cads) > 0 - assert self.aedtapp.modeler.import_nastran(example_project2) + 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 pyaedt.modules.solutions import simplify_stl + + out = simplify_stl(example_project, decimation=0.8, aggressiveness=5) + assert os.path.exists(out) def test_60_set_variable(self): self.aedtapp.variable_manager.set_variable("var_test", expression="123") diff --git a/_unittest/test_21_Circuit.py b/_unittest/test_21_Circuit.py index e4a846ae2d9..bf50e185af2 100644 --- a/_unittest/test_21_Circuit.py +++ b/_unittest/test_21_Circuit.py @@ -756,9 +756,11 @@ def test_42_create_wire(self): def test_43_display_wire_properties(self): self.aedtapp.set_active_design("CreateWireTest") - assert self.aedtapp.modeler.wire.display_wire_properties( + wire = self.aedtapp.modeler.components.get_wire_by_name("wire_name_test") + assert wire.display_wire_properties( name="wire_name_test", property_to_display="NetName", visibility="Value", location="Top" ) + assert not self.aedtapp.modeler.wire.display_wire_properties( name="invalid", property_to_display="NetName", visibility="Value", location="Top" ) diff --git a/_unittest/test_41_3dlayout_modeler.py b/_unittest/test_41_3dlayout_modeler.py index d2022e9578d..1407ffd3b27 100644 --- a/_unittest/test_41_3dlayout_modeler.py +++ b/_unittest/test_41_3dlayout_modeler.py @@ -313,6 +313,8 @@ def test_13a_create_edge_port(self): assert self.aedtapp.edit_source_from_file( port_wave.name, time_domain, is_time_domain=True, x_scale=1e-6, y_scale=1e-3, data_format="Voltage" ) + self.aedtapp.boundaries[0].object_properties.props["Boundary Type"] = "PEC" + assert list(self.aedtapp.oboundary.GetAllBoundariesList())[0] == self.aedtapp.boundaries[0].name def test_14a_create_coaxial_port(self): port = self.aedtapp.create_coax_port("port_via", 0.5, "Top", "Lower") diff --git a/_unittest/test_98_Icepak.py b/_unittest/test_98_Icepak.py index dcc16aa5c9b..e884359f0be 100644 --- a/_unittest/test_98_Icepak.py +++ b/_unittest/test_98_Icepak.py @@ -1037,7 +1037,7 @@ def test_55_native_components_history(self): def test_56_mesh_priority(self): self.aedtapp.insert_design("mesh_priority") b = self.aedtapp.modeler.create_box([0, 0, 0], [20, 50, 80]) - self.aedtapp.create_ipk_3dcomponent_pcb( + board = self.aedtapp.create_ipk_3dcomponent_pcb( "Board", link_data, solution_freq, @@ -1050,6 +1050,9 @@ def test_56_mesh_priority(self): assert self.aedtapp.mesh.add_priority( entity_type=2, component=self.aedtapp.modeler.user_defined_component_names[0], priority=1 ) + fan = self.aedtapp.create_fan(name="TestFan", is_2d=True) + rect = self.aedtapp.modeler.create_rectangle(0, [0, 0, 0], [1, 2], name="TestRectangle") + assert self.aedtapp.mesh.assign_priorities([[fan.name, board.name], [b.name, rect.name]]) def test_57_update_source(self): self.aedtapp.modeler.create_box([0, 0, 0], [20, 20, 20], name="boxSource") diff --git a/_unittest/test_utils.py b/_unittest/test_utils.py new file mode 100644 index 00000000000..a87c0010036 --- /dev/null +++ b/_unittest/test_utils.py @@ -0,0 +1,72 @@ +"""Test utility functions of PyAEDT. +""" + +import logging +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest + +from pyaedt import settings +from pyaedt.generic.general_methods import pyaedt_function_handler + +SETTINGS_RELEASE_ON_EXCEPTION = settings.release_on_exception +SETTINGS_ENABLE_ERROR_HANDLER = settings.enable_error_handler +ERROR_MESSAGE = "Dummy message." + + +@pyaedt_function_handler(deprecated_arg="trigger_exception") +def foo(trigger_exception=True): + """Some dummy function used for testing.""" + if trigger_exception: + raise Exception(ERROR_MESSAGE) + + +@patch("pyaedt.generic.desktop_sessions._desktop_sessions") +def test_handler_release_on_exception(mock_sessions): + """Test handler while activating or deactivating error handler.""" + mock_session = MagicMock() + mock_sessions.values.return_value = [mock_session] + settings.enable_error_handler = True + settings.release_on_exception = True + + # Check that release desktop is called once + foo() + assert mock_session.release_desktop.assert_called_once + + # Check that release desktop is not called + settings.release_on_exception = False + foo() + assert mock_session.release_desktop.assert_called + + # Teardown + settings.enable_error_handler = SETTINGS_ENABLE_ERROR_HANDLER + settings.release_on_exception = SETTINGS_RELEASE_ON_EXCEPTION + + +def test_handler_enable_error_handler(): + """Test handler while activating/deactivating error handler.""" + settings.enable_error_handler = True + assert foo() == False + + settings.enable_error_handler = False + with pytest.raises(Exception) as exec_info: + foo() + assert str(exec_info.value) == ERROR_MESSAGE + + # Teardown + settings.enable_error_handler = SETTINGS_ENABLE_ERROR_HANDLER + + +def test_handler_deprecation_log_warning(caplog): + """Test handler deprecation argument mechanism.""" + EXPECTED_ARGUMENT = "Argument `deprecated_arg` is deprecated for method `foo`; use `trigger_exception` instead." + + with caplog.at_level(logging.WARNING, logger="Global"): + foo(deprecated_arg=False) + assert len(caplog.records) == 1 + assert "WARNING" == caplog.records[0].levelname + assert EXPECTED_ARGUMENT == caplog.records[0].message + + foo(trigger_exception=False) + assert len(caplog.records) == 1 diff --git a/doc/source/API/Application.rst b/doc/source/API/Application.rst index 32ea2e6da58..87ccb2a90b4 100644 --- a/doc/source/API/Application.rst +++ b/doc/source/API/Application.rst @@ -3,7 +3,7 @@ Application and solvers The PyAEDT API includes classes for different applications available in Ansys Electronics Desktop (AEDT). You must initialize AEDT to get access to all PyAEDT modules and methods. -.. image:: ../Resources/aedt_2.webp +.. image:: ../Resources/aedt_2.png :width: 800 :alt: Ansys Electronics Desktop (AEDT) is a platform that enables true electronics system design. diff --git a/doc/source/API/index.rst b/doc/source/API/index.rst index 143c1f692d9..923dfa7880e 100644 --- a/doc/source/API/index.rst +++ b/doc/source/API/index.rst @@ -15,7 +15,7 @@ and Mechanical solvers for comprehensive multiphysics analysis. Tight integration among these solutions provides unprecedented ease of use for setup and faster resolution of complex simulations for design and optimization. -.. image:: ../Resources/aedt_2.webp +.. image:: ../Resources/aedt_2.png :width: 800 :alt: AEDT Applications :target: https://www.ansys.com/products/electronics diff --git a/doc/source/Getting_started/About.rst b/doc/source/Getting_started/About.rst index be708bbd8ae..5f42b245b34 100644 --- a/doc/source/Getting_started/About.rst +++ b/doc/source/Getting_started/About.rst @@ -26,7 +26,7 @@ and mechanical solvers for comprehensive multiphysics analysis. Tight integration among these solutions provides unprecedented ease of use for setup and faster resolution of complex simulations for design and optimization. -.. image:: ../Resources/aedt_collage.webp +.. image:: ../Resources/aedt_collage.jpg :width: 800 :alt: AEDT Applications :target: https://www.ansys.com/products/electronics diff --git a/doc/source/Getting_started/Installation.rst b/doc/source/Getting_started/Installation.rst index 119f64c4f9d..586eb658f75 100644 --- a/doc/source/Getting_started/Installation.rst +++ b/doc/source/Getting_started/Installation.rst @@ -19,7 +19,7 @@ using the CPython interpreter included in the AEDT installation. In order to do that you can: -- Download the following file: :download:`PyAEDT Installer Python file <../Resources/PyAEDTInstallerFromDesktop.py>` +- Download the following file: :download:`PyAEDT Installer Python file <../Resources/pyaedt_installer_from_aedt.py>` - Open an Electronics Desktop Session and click on Tools->Run Script and execute the file. @@ -33,35 +33,35 @@ In order to do that you can: Please note that AEDT 2023 R1 and lower requires Python 3.7 wheelhouse while AEDT 2023 R2 and higher requires the Python 3.10 wheelhouse. -.. image:: ../Resources/wheelhouse_installation.webp +.. image:: ../Resources/wheelhouse_installation.png :width: 800 :alt: PyAEDT run script Starting from 2023R2, buttons are available in the Automation Tab as in the example below. -.. image:: ../Resources/toolkits_ribbon.webp +.. image:: ../Resources/toolkits_ribbon.png :width: 800 :alt: PyAEDT toolkit buttons available in AEDT -Toolkit Manager -~~~~~~~~~~~~~~~ -The user can install or uninstall automated workflows using the toolkit manager. +Extension manager +~~~~~~~~~~~~~~~~~ +The user can install or uninstall automated workflows using the extension manager. There are three options: - Custom PyAEDT scripts. -- Internal PyAEDT scripts. Existing workflows in the PyAEDT library. +- Existing workflows in the PyAEDT library. -- Open source PyAEDT toolkits. Open source PyAEDT toolkits following the `common library `_ standard. +- Open source PyAEDT toolkits described in the `PyAEDT Common Toolkit documentation `_. -.. image:: ../Resources/toolkit_manager_1.webp +.. image:: ../Resources/toolkit_manager_1.png :width: 800 :alt: PyAEDT toolkit manager 1 The user can select the AEDT application to install the specific workflow. -.. image:: ../Resources/toolkit_manager_2.webp +.. image:: ../Resources/toolkit_manager_2.png :width: 400 :alt: PyAEDT toolkit manager 2 diff --git a/doc/source/Getting_started/Troubleshooting.rst b/doc/source/Getting_started/Troubleshooting.rst index d5fc1592811..fc95d36e04f 100644 --- a/doc/source/Getting_started/Troubleshooting.rst +++ b/doc/source/Getting_started/Troubleshooting.rst @@ -50,7 +50,7 @@ run the Python command: Another option to install PyAEDT from the wheelhouse is to download the following file -:download:`PyAEDT Installer Python file <../Resources/PyAEDTInstallerFromDesktop.py>`. +:download:`PyAEDT Installer Python file <../Resources/pyaedt_installer_from_aedt.py>`. Run this script directly from AEDT and pass the wheelhouse file name as an argument. diff --git a/doc/source/Getting_started/index.rst b/doc/source/Getting_started/index.rst index c3d9701fb71..a981d1a5f83 100644 --- a/doc/source/Getting_started/index.rst +++ b/doc/source/Getting_started/index.rst @@ -72,7 +72,7 @@ and Mechanical solvers for comprehensive multiphysics analysis. Tight integration among these solutions provides unprecedented ease of use for setup and faster resolution of complex simulations for design and optimization. -.. image:: ../Resources/aedt_3.webp +.. image:: ../Resources/aedt_3.png :width: 800 :alt: AEDT Applications :target: https://www.ansys.com/products/electronics diff --git a/doc/source/Resources/Mesh_Operations.png b/doc/source/Resources/Mesh_Operations.png new file mode 100644 index 00000000000..d9cdc8c5f32 Binary files /dev/null and b/doc/source/Resources/Mesh_Operations.png differ diff --git a/doc/source/Resources/Mesh_Operations.webp b/doc/source/Resources/Mesh_Operations.webp deleted file mode 100644 index 7665fa48dfc..00000000000 Binary files a/doc/source/Resources/Mesh_Operations.webp and /dev/null differ diff --git a/doc/source/Resources/Optimetrics_Parametric.png b/doc/source/Resources/Optimetrics_Parametric.png new file mode 100644 index 00000000000..649423afdd5 Binary files /dev/null and b/doc/source/Resources/Optimetrics_Parametric.png differ diff --git a/doc/source/Resources/Optimetrics_Parametric.webp b/doc/source/Resources/Optimetrics_Parametric.webp deleted file mode 100644 index 14db9aa4a29..00000000000 Binary files a/doc/source/Resources/Optimetrics_Parametric.webp and /dev/null differ diff --git a/doc/source/Resources/Setups.png b/doc/source/Resources/Setups.png new file mode 100644 index 00000000000..7e1e0bd91b5 Binary files /dev/null and b/doc/source/Resources/Setups.png differ diff --git a/doc/source/Resources/Setups.webp b/doc/source/Resources/Setups.webp deleted file mode 100644 index 2ca0d13e73a..00000000000 Binary files a/doc/source/Resources/Setups.webp and /dev/null differ diff --git a/doc/source/Resources/aedt_2.png b/doc/source/Resources/aedt_2.png new file mode 100644 index 00000000000..e7213e3e4fe Binary files /dev/null and b/doc/source/Resources/aedt_2.png differ diff --git a/doc/source/Resources/aedt_2.webp b/doc/source/Resources/aedt_2.webp deleted file mode 100644 index 38519ab1544..00000000000 Binary files a/doc/source/Resources/aedt_2.webp and /dev/null differ diff --git a/doc/source/Resources/aedt_3.png b/doc/source/Resources/aedt_3.png new file mode 100644 index 00000000000..f4009cbc3c4 Binary files /dev/null and b/doc/source/Resources/aedt_3.png differ diff --git a/doc/source/Resources/aedt_3.webp b/doc/source/Resources/aedt_3.webp deleted file mode 100644 index c58b0e00590..00000000000 Binary files a/doc/source/Resources/aedt_3.webp and /dev/null differ diff --git a/doc/source/Resources/aedt_box.png b/doc/source/Resources/aedt_box.png new file mode 100644 index 00000000000..26a179e9379 Binary files /dev/null and b/doc/source/Resources/aedt_box.png differ diff --git a/doc/source/Resources/aedt_box.webp b/doc/source/Resources/aedt_box.webp deleted file mode 100644 index ee84e7fc1aa..00000000000 Binary files a/doc/source/Resources/aedt_box.webp and /dev/null differ diff --git a/doc/source/Resources/aedt_collage.jpg b/doc/source/Resources/aedt_collage.jpg new file mode 100644 index 00000000000..9028deeb9fb Binary files /dev/null and b/doc/source/Resources/aedt_collage.jpg differ diff --git a/doc/source/Resources/aedt_collage.webp b/doc/source/Resources/aedt_collage.webp deleted file mode 100644 index 1ead603ae8b..00000000000 Binary files a/doc/source/Resources/aedt_collage.webp and /dev/null differ diff --git a/doc/source/Resources/aedt_first_page.png b/doc/source/Resources/aedt_first_page.png new file mode 100644 index 00000000000..00bbdade57b Binary files /dev/null and b/doc/source/Resources/aedt_first_page.png differ diff --git a/doc/source/Resources/aedt_first_page.webp b/doc/source/Resources/aedt_first_page.webp deleted file mode 100644 index 5642a98c655..00000000000 Binary files a/doc/source/Resources/aedt_first_page.webp and /dev/null differ diff --git a/doc/source/Resources/aedt_variables.png b/doc/source/Resources/aedt_variables.png new file mode 100644 index 00000000000..caa87868d88 Binary files /dev/null and b/doc/source/Resources/aedt_variables.png differ diff --git a/doc/source/Resources/aedt_variables.webp b/doc/source/Resources/aedt_variables.webp deleted file mode 100644 index 5955a5b1175..00000000000 Binary files a/doc/source/Resources/aedt_variables.webp and /dev/null differ diff --git a/doc/source/Resources/objects_operations.gif b/doc/source/Resources/objects_operations.gif index 536420616e7..ab760dfdcfe 100644 Binary files a/doc/source/Resources/objects_operations.gif and b/doc/source/Resources/objects_operations.gif differ diff --git a/doc/source/Resources/PyAEDTInstallerFromDesktop.py b/doc/source/Resources/pyaedt_installer_from_aedt.py similarity index 75% rename from doc/source/Resources/PyAEDTInstallerFromDesktop.py rename to doc/source/Resources/pyaedt_installer_from_aedt.py index edcafa16307..b9ea6c5f4d2 100644 --- a/doc/source/Resources/PyAEDTInstallerFromDesktop.py +++ b/doc/source/Resources/pyaedt_installer_from_aedt.py @@ -8,20 +8,28 @@ is_linux = os.name == "posix" is_windows = not is_linux +VENV_DIR_PREFIX = ".pyaedt_env" + def run_pyinstaller_from_c_python(oDesktop): - # This is called when run from IronPython + # Iron Python script to create the virtual environment and install PyAEDT + # Get AEDT information version = oDesktop.GetVersion()[2:6].replace(".", "") - python_version = "3.10" - if version <= "231": - python_version = "3.7" + # From AEDT 2023.2 the installed CPython version is 3.10 + python_version = "3.10" if version > "231" else "3.7" + python_version_new = python_version.replace(".", "_") + # AEDT installation root edt_root = os.path.normpath(oDesktop.GetExeDir()) + # CPython interpreter executable if is_windows: - python_exe = os.path.normpath(os.path.join(edt_root, "commonfiles", "CPython", python_version.replace(".", "_"), + python_exe = os.path.normpath(os.path.join(edt_root, "commonfiles", "CPython", python_version_new, "winx64", "Release", "python", "python.exe")) else: - python_exe = os.path.normpath(os.path.join(edt_root, "commonfiles", "CPython", python_version.replace(".", "_"), + python_exe = os.path.normpath(os.path.join(edt_root, "commonfiles", "CPython", python_version_new, "linx64", "Release", "python", "runpython")) + + # Launch this script again from the CPython interpreter. This calls the ``install_pyaedt()`` method, + # which creates a virtual environment and installs PyAEDT and its dependencies command = ['"{}"'.format(python_exe), '"{}"'.format(os.path.normpath(__file__)), "--version=" + version] if is_student_version(oDesktop): command.append("--student") @@ -42,17 +50,21 @@ def run_pyinstaller_from_c_python(oDesktop): return_code = run_command(" ".join(command)) err_msg = "There was an error while installing PyAEDT. Refer to the Terminal window where AEDT was launched " \ "from." - if is_windows: - venv_dir = os.path.join(os.environ["APPDATA"], "pyaedt_env_ide", "v" + version) - python_exe = os.path.join(venv_dir, "Scripts", "python.exe") - else: - venv_dir = os.path.join(os.environ["HOME"], "pyaedt_env_ide", "v" + version) - python_exe = os.path.join(venv_dir, "bin", "python") + if str(return_code) != "0": oDesktop.AddMessage("", "", 2, err_msg) return else: oDesktop.AddMessage("", "", 0, "PyAEDT virtual environment created.") + + # Add PyAEDT tabs in AEDT + # Virtual environment path and Python executable + if is_windows: + venv_dir = os.path.join(os.environ["APPDATA"], VENV_DIR_PREFIX, python_version_new) + python_exe = os.path.join(venv_dir, "Scripts", "python.exe") + else: + venv_dir = os.path.join(os.environ["HOME"], VENV_DIR_PREFIX, python_version_new) + python_exe = os.path.join(venv_dir, "bin", "python") pyaedt_path = os.path.join(venv_dir, "Lib", "site-packages", "pyaedt") if is_linux: for dirpath, dirnames, _ in os.walk(venv_dir): @@ -62,35 +74,51 @@ def run_pyinstaller_from_c_python(oDesktop): ) if os.path.isdir(pyaedt_path): break + + # Create PyAEDT symbolic link in PersonalLib personal_lib_dir = oDesktop.GetPersonalLibDirectory() pers1 = os.path.join(personal_lib_dir, "pyaedt") + if os.path.exists(pers1): - oDesktop.AddMessage("", "", 2, "PersonalLib already mapped.") - else: if is_windows: - command = 'mklink /D "{}" "{}"'.format(pers1, pyaedt_path) + command = 'rmdir "{}"'.format(pers1) else: - command = 'ln -s "{}" "{}"'.format(pyaedt_path, pers1) + command = 'rm "{}"'.format(pers1) ret_code = os.system(command) if ret_code != 0: - oDesktop.AddMessage("", "", 2, "Error configuring symbolic link to pyaedt in PersonalLib.") + oDesktop.AddMessage("", "", 2, + "Error occurred while removing the symbolic link to PyAEDT in 'PersonalLib'.") + + if is_windows: + command = 'mklink /D "{}" "{}"'.format(pers1, pyaedt_path) + else: + command = 'ln -s "{}" "{}"'.format(pyaedt_path, pers1) + ret_code = os.system(command) + if ret_code != 0: + oDesktop.AddMessage("", "", 2, "Error occurred while configuring the symbolic link to PyAEDT in 'PersonalLib'.") + + # Create Toolkits in PersonalLib import tempfile python_script = os.path.join(tempfile.gettempdir(), "configure_pyaedt.py") + if os.path.isfile(python_script): + os.remove(python_script) with open(python_script, "w") as f: # enable in debu mode # f.write("import sys\n") # f.write('sys.path.insert(0, r"c:\\ansysdev\\git\\repos\\pyaedt")\n') f.write("from pyaedt.workflows.installer.pyaedt_installer import add_pyaedt_to_aedt\n") f.write( - 'add_pyaedt_to_aedt(aedt_version="{}", personallib=r"{}")\n'.format( + 'add_pyaedt_to_aedt(aedt_version="{}", personal_lib=r"{}")\n'.format( oDesktop.GetVersion()[:6], oDesktop.GetPersonalLibDirectory())) command = r'"{}" "{}"'.format(python_exe, python_script) - oDesktop.AddMessage("", "", 0, "Configuring PyAEDT Tabs.") + oDesktop.AddMessage("", "", 0, "Configuring PyAEDT panels in automation tab.") ret_code = os.system(command) if ret_code != 0: - oDesktop.AddMessage("", "", 2, "Error configuring the configuration Tab.") + oDesktop.AddMessage("", "", 2, "Error occurred configuring the PyAEDT panels.") return + # Refresh UI + oDesktop.CloseAllWindows() if version >= "232": oDesktop.RefreshToolkitUI() msg = "PyAEDT configuration complete." @@ -102,6 +130,7 @@ def run_pyinstaller_from_c_python(oDesktop): from System.Windows.Forms import MessageBox, MessageBoxButtons, MessageBoxIcon oDesktop.AddMessage("", "", 0, msg) MessageBox.Show(msg, 'Info', MessageBoxButtons.OK, MessageBoxIcon.Information) + oDesktop.AddMessage("", "", 0, "Create a project if the PyAEDT panel is not visible.") def parse_arguments_for_pyaedt_installer(args=None): @@ -123,12 +152,17 @@ def parse_arguments_for_pyaedt_installer(args=None): def install_pyaedt(): # This is called when run from CPython args = parse_arguments_for_pyaedt_installer() + + python_version = "3_10" + if args.version <= "231": + python_version = "3_7" + if is_windows: - venv_dir = os.path.join(os.environ["APPDATA"], "pyaedt_env_ide", "v" + args.version) + venv_dir = os.path.join(os.environ["APPDATA"], VENV_DIR_PREFIX, python_version) python_exe = os.path.join(venv_dir, "Scripts", "python.exe") pip_exe = os.path.join(venv_dir, "Scripts", "pip.exe") else: - venv_dir = os.path.join(os.environ["HOME"], "pyaedt_env_ide", "v" + args.version) + venv_dir = os.path.join(os.environ["HOME"], VENV_DIR_PREFIX, python_version) python_exe = os.path.join(venv_dir, "bin", "python") pip_exe = os.path.join(venv_dir, "bin", "pip") os.environ["ANSYSEM_ROOT{}".format(args.version)] = args.edt_root @@ -158,14 +192,14 @@ def install_pyaedt(): run_command('"{}" -m venv "{}"'.format(sys.executable, venv_dir)) if args.wheel and os.path.exists(args.wheel): - wheelpyaedt = args.wheel + wheel_pyaedt = args.wheel import zipfile - unzipped_path = os.path.join(os.path.dirname(wheelpyaedt), - os.path.splitext(os.path.basename(wheelpyaedt))[0]) + unzipped_path = os.path.join(os.path.dirname(wheel_pyaedt), + os.path.splitext(os.path.basename(wheel_pyaedt))[0]) if os.path.exists(unzipped_path): shutil.rmtree(unzipped_path, ignore_errors=True) - with zipfile.ZipFile(wheelpyaedt, 'r') as zip_ref: - # Extract all contents to a directory (you can specify a different extraction path if needed) + with zipfile.ZipFile(wheel_pyaedt, 'r') as zip_ref: + # Extract all contents to a directory. (You can specify a different extraction path if needed.) zip_ref.extractall(unzipped_path) run_command( @@ -178,7 +212,7 @@ def install_pyaedt(): run_command('"{}" -m pip install --upgrade pip'.format(python_exe)) run_command('"{}" --default-timeout=1000 install wheel'.format(pip_exe)) # run_command( - # '"{}" --default-timeout=1000 install git+https://github.com/ansys/pyaedt.git@fix/main'.format(pip_exe)) + # '"{}" --default-timeout=1000 install git+https://github.com/ansys/pyaedt.git@main'.format(pip_exe)) run_command('"{}" --default-timeout=1000 install pyaedt[all]'.format(pip_exe)) run_command('"{}" --default-timeout=1000 install jupyterlab'.format(pip_exe)) run_command('"{}" --default-timeout=1000 install ipython -U'.format(pip_exe)) @@ -191,14 +225,14 @@ def install_pyaedt(): run_command('"{}" uninstall --yes pyaedt'.format(pip_exe)) if args.wheel and os.path.exists(args.wheel): - wheelpyaedt = args.wheel + wheel_pyaedt = args.wheel import zipfile - unzipped_path = os.path.join(os.path.dirname(wheelpyaedt), - os.path.splitext(os.path.basename(wheelpyaedt))[0]) + unzipped_path = os.path.join(os.path.dirname(wheel_pyaedt), + os.path.splitext(os.path.basename(wheel_pyaedt))[0]) if os.path.exists(unzipped_path): shutil.rmtree(unzipped_path, ignore_errors=True) - with zipfile.ZipFile(wheelpyaedt, 'r') as zip_ref: - # Extract all contents to a directory (you can specify a different extraction path if needed) + with zipfile.ZipFile(wheel_pyaedt, 'r') as zip_ref: + # Extract all contents to a directory. (You can specify a different extraction path if needed.) zip_ref.extractall(unzipped_path) run_command('"{}" install --no-cache-dir --no-index --find-links={} pyaedt'.format(pip_exe, unzipped_path)) @@ -224,9 +258,8 @@ def run_command(command): if __name__ == "__main__": - # When launched from Toolkit (i.e. IronPython) call this same file using CPython if is_iron_python: - # Check if wheelhouse defined, wheelhouse is created for Windows only + # Check if wheelhouse defined. Wheelhouse is created for Windows only. wheelpyaedt = [] # Retrieve the script arguments script_args = ScriptArgument.split() diff --git a/doc/source/Resources/pyvista_plot.jpg b/doc/source/Resources/pyvista_plot.jpg new file mode 100644 index 00000000000..dae3cf6c31f Binary files /dev/null and b/doc/source/Resources/pyvista_plot.jpg differ diff --git a/doc/source/Resources/pyvista_plot.webp b/doc/source/Resources/pyvista_plot.webp deleted file mode 100644 index f04a918aa4c..00000000000 Binary files a/doc/source/Resources/pyvista_plot.webp and /dev/null differ diff --git a/doc/source/Resources/sparams.jpg b/doc/source/Resources/sparams.jpg new file mode 100644 index 00000000000..9572acf8151 Binary files /dev/null and b/doc/source/Resources/sparams.jpg differ diff --git a/doc/source/Resources/sparams.webp b/doc/source/Resources/sparams.webp deleted file mode 100644 index 1454ba9055c..00000000000 Binary files a/doc/source/Resources/sparams.webp and /dev/null differ diff --git a/doc/source/Resources/sparams_w_matplotlib.jpg b/doc/source/Resources/sparams_w_matplotlib.jpg new file mode 100644 index 00000000000..4fa2029cbac Binary files /dev/null and b/doc/source/Resources/sparams_w_matplotlib.jpg differ diff --git a/doc/source/Resources/sparams_w_matplotlib.webp b/doc/source/Resources/sparams_w_matplotlib.webp deleted file mode 100644 index e361a436b52..00000000000 Binary files a/doc/source/Resources/sparams_w_matplotlib.webp and /dev/null differ diff --git a/doc/source/Resources/toolkit_manager_1.png b/doc/source/Resources/toolkit_manager_1.png new file mode 100644 index 00000000000..5e093f2f703 Binary files /dev/null and b/doc/source/Resources/toolkit_manager_1.png differ diff --git a/doc/source/Resources/toolkit_manager_1.webp b/doc/source/Resources/toolkit_manager_1.webp deleted file mode 100644 index 14603fabc74..00000000000 Binary files a/doc/source/Resources/toolkit_manager_1.webp and /dev/null differ diff --git a/doc/source/Resources/toolkit_manager_2.png b/doc/source/Resources/toolkit_manager_2.png new file mode 100644 index 00000000000..51993ddc2ab Binary files /dev/null and b/doc/source/Resources/toolkit_manager_2.png differ diff --git a/doc/source/Resources/toolkit_manager_2.webp b/doc/source/Resources/toolkit_manager_2.webp deleted file mode 100644 index d7722304a8c..00000000000 Binary files a/doc/source/Resources/toolkit_manager_2.webp and /dev/null differ diff --git a/doc/source/Resources/toolkits_ribbon.png b/doc/source/Resources/toolkits_ribbon.png new file mode 100644 index 00000000000..8f33f7825f2 Binary files /dev/null and b/doc/source/Resources/toolkits_ribbon.png differ diff --git a/doc/source/Resources/toolkits_ribbon.webp b/doc/source/Resources/toolkits_ribbon.webp deleted file mode 100644 index 2a7fe214a02..00000000000 Binary files a/doc/source/Resources/toolkits_ribbon.webp and /dev/null differ diff --git a/doc/source/Resources/variables_advanced.png b/doc/source/Resources/variables_advanced.png new file mode 100644 index 00000000000..4812b016a4d Binary files /dev/null and b/doc/source/Resources/variables_advanced.png differ diff --git a/doc/source/Resources/variables_advanced.webp b/doc/source/Resources/variables_advanced.webp deleted file mode 100644 index 5bc66a5ea07..00000000000 Binary files a/doc/source/Resources/variables_advanced.webp and /dev/null differ diff --git a/doc/source/Resources/wheelhouse_installation.png b/doc/source/Resources/wheelhouse_installation.png new file mode 100644 index 00000000000..4d5e67076a6 Binary files /dev/null and b/doc/source/Resources/wheelhouse_installation.png differ diff --git a/doc/source/Resources/wheelhouse_installation.webp b/doc/source/Resources/wheelhouse_installation.webp deleted file mode 100644 index 74e9de7ccda..00000000000 Binary files a/doc/source/Resources/wheelhouse_installation.webp and /dev/null differ diff --git a/doc/source/User_guide/intro.rst b/doc/source/User_guide/intro.rst index 90313db93d7..0541b24809f 100644 --- a/doc/source/User_guide/intro.rst +++ b/doc/source/User_guide/intro.rst @@ -17,7 +17,7 @@ You can initiate AEDT in non-graphical mode from Python using this code: The preceding code launches AEDT and initializes a new Circuit design. -.. image:: ../Resources/aedt_first_page.webp +.. image:: ../Resources/aedt_first_page.png :width: 800 :alt: AEDT launched diff --git a/doc/source/User_guide/mesh.rst b/doc/source/User_guide/mesh.rst index 4ab26ab3ccd..73a48403343 100644 --- a/doc/source/User_guide/mesh.rst +++ b/doc/source/User_guide/mesh.rst @@ -16,7 +16,7 @@ All mesh operations are conveniently listed within the mesh object: my_mesh_op.update() -.. image:: ../Resources/Mesh_Operations.webp +.. image:: ../Resources/Mesh_Operations.png :width: 800 :alt: Mesh object List diff --git a/doc/source/User_guide/modeler.rst b/doc/source/User_guide/modeler.rst index 264bf77d66b..e6d58daee23 100644 --- a/doc/source/User_guide/modeler.rst +++ b/doc/source/User_guide/modeler.rst @@ -18,7 +18,7 @@ The following code creates a box and changes the color to red: -.. image:: ../Resources/aedt_box.webp +.. image:: ../Resources/aedt_box.png :width: 800 :alt: Modeler Object diff --git a/doc/source/User_guide/postprocessing.rst b/doc/source/User_guide/postprocessing.rst index d1afd8be9aa..3903a48a8b0 100644 --- a/doc/source/User_guide/postprocessing.rst +++ b/doc/source/User_guide/postprocessing.rst @@ -15,7 +15,7 @@ To use PyAEDT to create a report in AEDT, you can follow this general structure: hfss.post.create_report(["db(S11)", "db(S12)"]) -.. image:: ../Resources/sparams.webp +.. image:: ../Resources/sparams.jpg :width: 800 :alt: AEDT report @@ -34,7 +34,7 @@ You can also generate reports in Matplotlib: plt = solution.plot(solution.expressions) # Matplotlib axes object. -.. image:: ../Resources/sparams_w_matplotlib.webp +.. image:: ../Resources/sparams_w_matplotlib.jpg :width: 800 :alt: S-Parameters report created with Matplotlib @@ -97,7 +97,7 @@ PyAEDT leverages PyVista to export and plot fields outside AEDT, generating imag ) -.. image:: ../Resources/pyvista_plot.webp +.. image:: ../Resources/pyvista_plot.jpg :width: 800 :alt: Postprocessing features diff --git a/doc/source/User_guide/setup.rst b/doc/source/User_guide/setup.rst index affd2b6805f..0b9c0f32c70 100644 --- a/doc/source/User_guide/setup.rst +++ b/doc/source/User_guide/setup.rst @@ -19,7 +19,7 @@ essential components of the simulation process: new_setup = m3d.create_setup("New_Setup") -.. image:: ../Resources/Setups.webp +.. image:: ../Resources/Setups.png :width: 800 :alt: Setup Editing and Creation diff --git a/doc/source/User_guide/variables.rst b/doc/source/User_guide/variables.rst index 3064ef7853f..c789af17422 100644 --- a/doc/source/User_guide/variables.rst +++ b/doc/source/User_guide/variables.rst @@ -13,7 +13,7 @@ for the variable name, a project-wide variable is created: hfss["$dim"] = "1mm" # project variable -.. image:: ../Resources/aedt_variables.webp +.. image:: ../Resources/aedt_variables.png :width: 800 :alt: Variable Management @@ -36,7 +36,7 @@ access the ``variable_manager`` object for a more comprehensive set of functions '34 * $PrjVar1/$PrjVar2' -.. image:: ../Resources/variables_advanced.webp +.. image:: ../Resources/variables_advanced.png :width: 600 :alt: Variable Management @@ -54,7 +54,7 @@ optimetrics setups by allowing you to read existing configurations, make edits, m3d.parametrics.add("Rload", 0.1, 1, 0.1) -.. image:: ../Resources/Optimetrics_Parametric.webp +.. image:: ../Resources/Optimetrics_Parametric.png :width: 800 :alt: Optimetrics creation diff --git a/doc/source/conf.py b/doc/source/conf.py index 3acb1e171cd..4e3a6246035 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -28,7 +28,7 @@ # Additionally, when documenting images in formats other than the supported ones, # make sure to specify their types. from sphinx.builders.latex import LaTeXBuilder -LaTeXBuilder.supported_image_types = ["image/png", "image/pdf", "image/svg+xml", "image/webp" ] +LaTeXBuilder.supported_image_types = ["image/png", "image/pdf", "image/svg+xml"] from sphinx.writers.latex import CR from sphinx.writers.latex import LaTeXTranslator @@ -139,20 +139,20 @@ def setup(app): # extensions coming with Sphinx_PyAEDT (named 'sphinx.ext.*') or your custom # ones. extensions = [ - "sphinx.ext.intersphinx", + "ansys_sphinx_theme.extension.linkcode", + "numpydoc", + "recommonmark", "sphinx.ext.autodoc", - "sphinx.ext.todo", "sphinx.ext.autosummary", - "sphinx.ext.intersphinx", "sphinx.ext.coverage", - "sphinx_copybutton", - "sphinx_design", "sphinx.ext.graphviz", - "sphinx.ext.mathjax", + "sphinx.ext.imgconverter", "sphinx.ext.inheritance_diagram", - "numpydoc", - "ansys_sphinx_theme.extension.linkcode", - "recommonmark", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.todo", + "sphinx_copybutton", + "sphinx_design", ] # Intersphinx mapping diff --git a/ignore_words.txt b/ignore_words.txt index c8fa792f47c..f35fd1a4b22 100644 --- a/ignore_words.txt +++ b/ignore_words.txt @@ -32,3 +32,4 @@ aline COM gRPC Toolkits +Extensions diff --git a/pyaedt/application/Analysis.py b/pyaedt/application/Analysis.py index 6eb72b9d768..049c07517f0 100644 --- a/pyaedt/application/Analysis.py +++ b/pyaedt/application/Analysis.py @@ -34,6 +34,7 @@ from pyaedt.generic.settings import settings from pyaedt.modules.Boundary import MaxwellParameters from pyaedt.modules.Boundary import NativeComponentObject +from pyaedt.modules.Boundary import NativeComponentPCB from pyaedt.modules.DesignXPloration import OptimizationSetups from pyaedt.modules.DesignXPloration import ParametricSetups from pyaedt.modules.SolveSetup import Setup @@ -186,7 +187,7 @@ def materials(self): Materials in the project. """ - if not self._materials: + if self._materials is None: self.logger.reset_timer() from pyaedt.modules.MaterialLib import Materials @@ -203,7 +204,7 @@ def setups(self): Returns ------- - :class:`pyaedt.modules.SolveSetup.Setup` + List[:class:`pyaedt.modules.SolveSetup.Setup`] Setups in the project. """ @@ -955,7 +956,10 @@ def _get_native_data(self): if isinstance(ds, (OrderedDict, dict)): component_type = ds["NativeComponentDefinitionProvider"]["Type"] component_name = ds["BasicComponentInfo"]["ComponentName"] - native_component_object = NativeComponentObject(self, component_type, component_name, ds) + if component_type == "PCB": + native_component_object = NativeComponentPCB(self, component_type, component_name, ds) + else: + native_component_object = NativeComponentObject(self, component_type, component_name, ds) boundaries.append(native_component_object) except Exception: msg = "Failed to add native component object." diff --git a/pyaedt/application/Design.py b/pyaedt/application/Design.py index cf0beaa4818..34c3b50f2a8 100644 --- a/pyaedt/application/Design.py +++ b/pyaedt/application/Design.py @@ -323,6 +323,9 @@ def boundaries(self): bb = [] if "GetBoundaries" in self.oboundary.__dir__(): bb = list(self.oboundary.GetBoundaries()) + elif "GetAllBoundariesList" in self.oboundary.__dir__() and self.design_type == "HFSS 3D Layout Design": + bb = list(self.oboundary.GetAllBoundariesList()) + bb = [elem for sublist in zip(bb, ["Port"] * len(bb)) for elem in sublist] elif "Boundaries" in self.get_oo_name(self.odesign): bb = self.get_oo_name(self.odesign, "Boundaries") if "GetHybridRegions" in self.oboundary.__dir__(): @@ -338,6 +341,11 @@ def boundaries(self): current_excitation_types = ee[1::2] ff = [i.split(":")[0] for i in ee] bb.extend(ff) + elif "Excitations" in self.get_oo_name(self.odesign) and self.design_type == "HFSS 3D Layout Design": + ee = self.get_oo_name(self.odesign, "Excitations") + ee = [elem for sublist in zip(ee, ["Port"] * len(ee)) for elem in sublist] + current_excitations = ee[::2] + current_excitation_types = ee[1::2] # Parameters and Motion definitions if self.design_type in ["Maxwell 3D", "Maxwell 2D"]: @@ -3332,7 +3340,6 @@ def _insert_design(self, design_type, design_name=None): if not is_windows and settings.aedt_version and self.design_type == "Circuit Design": time.sleep(1) self.odesktop.CloseAllWindows() - if new_design is None: # pragma: no cover new_design = self.desktop_class.active_design(self.oproject, unique_design_name, self.design_type) if new_design is None: diff --git a/pyaedt/desktop.py b/pyaedt/desktop.py index 2e5ee69272f..e113168aa5c 100644 --- a/pyaedt/desktop.py +++ b/pyaedt/desktop.py @@ -75,9 +75,17 @@ def launch_desktop_on_port(): my_env[env] = val if is_linux: # pragma: no cover command.append("&") - subprocess.Popen(command, env=my_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.Popen( + command, env=my_env, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) else: - subprocess.Popen(" ".join(command), env=my_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.Popen( + " ".join(command), + env=my_env, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) _aedt_process_thread = threading.Thread(target=launch_desktop_on_port) _aedt_process_thread.daemon = True @@ -253,32 +261,12 @@ def _close_aedt_application(desktop_class, close_desktop, pid, is_grpc_api): return True except Exception: # pragma: no cover warnings.warn("Something went wrong closing AEDT. Exception in `_main.oDesktop.QuitApplication()`.") - elif _desktop_sessions and len(_desktop_sessions) > 1 and not desktop_class.parent_desktop_id: - pyaedt_logger.error("Release is not allowed when multiple desktop sessions are available.") - pyaedt_logger.error("Closing Desktop session.") - try: - os.kill(pid, 9) - if _desktop_sessions: - for v in _desktop_sessions.values(): - if pid in v.parent_desktop_id: - del v.parent_desktop_id[v.parent_desktop_id.index(pid)] - return True - except Exception: # pragma: no cover - warnings.warn("Something went wrong closing AEDT. Exception in `_main.oDesktop.QuitApplication()`.") - elif _desktop_sessions and len(_desktop_sessions) > 1: - pyaedt_logger.error("A child desktop session is linked to this session.") - pyaedt_logger.error("Multiple desktop sessions must be released in reverse order.") - return False else: - try: - import pyaedt.generic.grpc_plugin as python_grpc_wrapper - - python_grpc_wrapper.AedtAPI.ReleaseAll() - return True - except Exception: # pragma: no cover - warnings.warn( - "Something went wrong releasing AEDT. Exception in `StandalonePyScriptWrapper.Release()`." - ) + for k, d in _desktop_sessions.items(): + if k == pid: + d.grpc_plugin.recreate_application(True) + d.grpc_plugin.Release() + return True elif not inside_desktop: if close_desktop: try: @@ -447,16 +435,17 @@ def __new__(cls, *args, **kwargs): # machine = kwargs.get("machine") or "" if (not args or len(args)<6) else args[5] port = kwargs.get("port") or 0 if (not args or len(args) < 7) else args[6] aedt_process_id = kwargs.get("aedt_process_id") or None if (not args or len(args) < 8) else args[7] - if settings.use_multi_desktop and is_windows and not inside_desktop and new_desktop_session: + if settings.use_multi_desktop and not inside_desktop and new_desktop_session: pyaedt_logger.info("Initializing new Desktop session.") return object.__new__(cls) elif len(_desktop_sessions.keys()) > 0: - if settings.use_multi_desktop and is_windows and (port or aedt_process_id): + if settings.use_multi_desktop and (port or aedt_process_id): for el in list(_desktop_sessions.values()): if (el.port != 0 and el.port == port) or ( el.aedt_process_id and el.aedt_process_id == aedt_process_id ): return el + return object.__new__(cls) sessions = list(_desktop_sessions.keys()) try: process_id = _desktop_sessions[sessions[0]].odesktop.GetProcessID() @@ -483,18 +472,22 @@ def __init__( port=0, aedt_process_id=None, ): - if _desktop_sessions and (specified_version is None or not settings.use_grpc_api): + if _desktop_sessions and specified_version is None: specified_version = list(_desktop_sessions.values())[-1].aedt_version_id if aedt_process_id: # pragma no cover aedt_process_id = int(aedt_process_id) if getattr(self, "_initialized", None) is not None and self._initialized: + try: + self.grpc_plugin.recreate_application(True) + except Exception: + pass return else: self._initialized = True self._initialized_from_design = True if Desktop._invoked_from_design else False Desktop._invoked_from_design = False self.parent_desktop_id = [] - + self._odesktop = None self._connected_app_instances = 0 """Initialize desktop.""" @@ -990,41 +983,42 @@ def _initialize( os.environ["DesktopPluginPyAEDT"] = os.path.join(settings.aedt_install_dir, "PythonFiles", "DesktopPlugin") launch_msg = "AEDT installation Path {}".format(base_path) self.logger.info(launch_msg) - import pyaedt.generic.grpc_plugin as python_grpc_wrapper + from pyaedt.generic.grpc_plugin_dll_class import AEDT - if _desktop_sessions: - last_session = list(_desktop_sessions.values())[-1] - all_desktop = [i for i in last_session.odesktop.GetRunningInstancesMgr().GetAllRunningInstances()] - for desktop in all_desktop: - try: - if port and desktop.GetGrpcServerPort() == port: - self.isoutsideDesktop = True - self.odesktop = desktop - self.aedt_process_id = self.odesktop.GetProcessID() - self.is_grpc_api = True - last_session.parent_desktop_id.append(self.aedt_process_id) - return True - except Exception: - messages = desktop.GetMessages("", "", 0) - for message in messages: - if "gRPC server running on port: " in message and str(port) in message: - self.isoutsideDesktop = True - self.odesktop = desktop - self.aedt_process_id = self.odesktop.GetProcessID() - self.is_grpc_api = True - last_session.parent_desktop_id.append(self.aedt_process_id) - return True - if new_session: - self.launched_by_pyaedt = new_session - oapp = python_grpc_wrapper.CreateAedtApplication(machine, port, non_graphical, new_session) + if settings.use_multi_desktop: + os.environ["DesktopPluginPyAEDT"] = os.path.join( + list(installed_versions().values())[0], "PythonFiles", "DesktopPlugin" + ) + self.grpc_plugin = AEDT(os.environ["DesktopPluginPyAEDT"]) + oapp = self.grpc_plugin.CreateAedtApplication(machine, port, non_graphical, new_session) if oapp: self.isoutsideDesktop = True - self.odesktop = oapp.GetAppDesktop() self.aedt_process_id = self.odesktop.GetProcessID() self.is_grpc_api = True return True + @property + def odesktop(self): + """AEDT instance containing all projects and designs. + + Examples + -------- + Get the COM object representing the desktop. + + >>> from pyaedt import Desktop + >>> d = Desktop() + >>> d.odesktop + """ + try: + return self.grpc_plugin.odesktop + except Exception: + return self._odesktop + + @odesktop.setter + def odesktop(self, val): + self._odesktop = val + def _init_grpc(self, non_graphical, new_aedt_session, version, student_version, version_key): if settings.remote_rpc_session: # pragma: no cover settings.remote_api = True @@ -1044,12 +1038,12 @@ def _init_grpc(self, non_graphical, new_aedt_session, version, student_version, socket.getfqdn(), socket.getfqdn().split(".")[0], ]: - self.machine = "" + self.machine = "127.0.0.1" else: settings.remote_api = True if not self.port: - if self.machine: - self.logger.error("New Session of AEDT cannot be started on remote machine from Desktop Class.") + if self.machine and self.machine != "127.0.0.1": + self.logger.error("New session of AEDT cannot be started on remote machine from Desktop Class.") self.logger.error("Either use port argument or start an rpc session to start AEDT on remote machine.") self.logger.error("Use client = pyaedt.common_rpc.client(machinename) to start a remote session.") self.logger.error("Use client.aedt(port) to start aedt on remote machine before connecting.") @@ -1376,7 +1370,7 @@ def design_type(self, project_name=None, design_name=None): if not design_name: odesign = self.active_design(oproject) else: - odesign = self.active_design(oproject.design_name) + odesign = self.active_design(oproject, design_name) if odesign: return odesign.GetDesignType() return "" @@ -1563,7 +1557,6 @@ def release_desktop(self, close_projects=True, close_on_exit=True): for a in props: self.__dict__.pop(a, None) - self.odesktop = None gc.collect() return result @@ -1761,20 +1754,29 @@ def submit_job( Parameters ---------- project_file : str - Full path to the project. + Full path to the project. The path should be visible from the server where the + simulation will run. + If the client path is used then the + mapping between the client and server path must be specified in the `setting_file``. clustername : str Name of the cluster to submit the job to. aedt_full_exe_path : str, optional - Full path to the AEDT executable file. The default is ``None``, in which - case ``"/clustername/AnsysEM/AnsysEM2x.x/Win64/ansysedt.exe"`` is used. + Full path to the AEDT executable file on the server. The default is ``None``, in which + case ``"/clustername/AnsysEM/AnsysEM2x.x/Win64/ansysedt.exe"`` is used. On linux + this path should point to the Linux executable ``"ansysedt"``. numnodes : int, optional Number of nodes. The default is ``1``. numcores : int, optional Number of cores. The default is ``32``. wait_for_license : bool, optional - Whether to wait for the license to be validated. The default is ``True``. + Whether to wait for a license to become available. The default is ``True``. setting_file : str, optional - Name of the file to use as a template. The default value is ``None``. + Name of the "*.areg" file to use as a template. The default value + is ``None`` in which case a default template will be used. + If ``setting_file`` is passed it can be located either on the client or server. + If the "*.areg" file is on the client information from ``numcores`` and ``numnodes`` + will be added. If the "*.areg" file is on the server it + will be applied without modifications. Returns ------- @@ -1808,8 +1810,7 @@ def submit_job( return False else: if not os.path.exists(aedt_full_exe_path): - self.logger.error("AEDT shared path does not exist. Provide a full path.") - return False + self.logger.warning("The AEDT executable path not visible from the client.") aedt_full_exe_path.replace("\\", "\\\\") if project_name in self.project_list(): self.odesktop.CloseProject(project_name) @@ -1817,34 +1818,39 @@ def submit_job( destination_reg = os.path.join(project_path, "Job_settings.areg") if not setting_file: setting_file = os.path.join(path_file, "misc", "Job_Settings.areg") - shutil.copy(setting_file, destination_reg) + if os.path.exists(setting_file): + f1 = open_file(destination_reg, "w") + with open_file(setting_file) as f: + lines = f.readlines() + for line in lines: + if "\\ $begin" == line[:8]: + lin = "\\ $begin \\'{}\\'\\\n".format(clustername) + f1.write(lin) + elif "\\ $end" == line[:6]: + lin = "\\ $end \\'{}\\'\\\n".format(clustername) + f1.write(lin) + elif "NumCores=" in line: + lin = "\\ \\ \\ \\ NumCores={}\\\n".format(numcores) + f1.write(lin) + elif "NumNodes=1" in line: + lin = "\\ \\ \\ \\ NumNodes={}\\\n".format(numnodes) + f1.write(lin) + elif "ProductPath" in line: + lin = "\\ \\ ProductPath =\\'{}\\'\\\n".format(aedt_full_exe_path) + f1.write(lin) + elif "WaitForLicense" in line: + lin = "\\ \\ WaitForLicense={}\\\n".format(str(wait_for_license).lower()) + f1.write(lin) + else: + f1.write(line) + f1.close() + else: + self.logger.warning("Setting file not found on client machine. Considering it as server path.") + destination_reg = setting_file - f1 = open_file(destination_reg, "w") - with open_file(setting_file) as f: - lines = f.readlines() - for line in lines: - if "\\ $begin" == line[:8]: - lin = "\\ $begin \\'{}\\'\\\n".format(clustername) - f1.write(lin) - elif "\\ $end" == line[:6]: - lin = "\\ $end \\'{}\\'\\\n".format(clustername) - f1.write(lin) - elif "NumCores=" in line: - lin = "\\ \\ \\ \\ NumCores={}\\\n".format(numcores) - f1.write(lin) - elif "NumNodes=1" in line: - lin = "\\ \\ \\ \\ NumNodes={}\\\n".format(numnodes) - f1.write(lin) - elif "ProductPath" in line: - lin = "\\ \\ ProductPath =\\'{}\\'\\\n".format(aedt_full_exe_path) - f1.write(lin) - elif "WaitForLicense" in line: - lin = "\\ \\ WaitForLicense={}\\\n".format(str(wait_for_license).lower()) - f1.write(lin) - else: - f1.write(line) - f1.close() - return self.odesktop.SubmitJob(os.path.join(project_path, "Job_settings.areg"), project_file) + job = self.odesktop.SubmitJob(destination_reg, project_file) + self.logger.info("Job submitted: {}".format(str(job))) + return job @pyaedt_function_handler() def submit_ansys_cloud_job( diff --git a/pyaedt/emit_core/results/revision.py b/pyaedt/emit_core/results/revision.py index 5df33cbca7a..cbd6da8245f 100644 --- a/pyaedt/emit_core/results/revision.py +++ b/pyaedt/emit_core/results/revision.py @@ -733,10 +733,10 @@ def set_emi_category_filter_enabled(self, category: EmiCategoryFilter, enabled: def get_license_session(self): """Get a license session. - A license session can be started with checkout(), and ended with checkin(). - The `with` keyword can also be used, where checkout() is called on enter, and checkin() is called on exit. + A license session can be started with checkout(), and ended with check in(). + The `with` keyword can also be used, where checkout() is called on enter, and check in() is called on exit. - Avoids having to wait for license checkin and checkout when doing many runs. + Avoids having to wait for license check in and checkout when doing many runs. Examples -------- diff --git a/pyaedt/generic/compliance.py b/pyaedt/generic/compliance.py index c9ff5a6ffdf..9f24c0012b1 100644 --- a/pyaedt/generic/compliance.py +++ b/pyaedt/generic/compliance.py @@ -245,7 +245,7 @@ def reports(self, val): @property def parameters(self): - """Parameters available in the Virtual complaince. + """Parameters available in the Virtual compliance. Returns ------- diff --git a/pyaedt/generic/configurations.py b/pyaedt/generic/configurations.py index f6ba8b0381e..55349386ca4 100644 --- a/pyaedt/generic/configurations.py +++ b/pyaedt/generic/configurations.py @@ -26,6 +26,7 @@ from pyaedt.modules.Boundary import BoundaryObject from pyaedt.modules.Boundary import BoundaryProps from pyaedt.modules.Boundary import NativeComponentObject +from pyaedt.modules.Boundary import NativeComponentPCB from pyaedt.modules.DesignXPloration import SetupOpti from pyaedt.modules.DesignXPloration import SetupParam from pyaedt.modules.MaterialLib import Material @@ -2054,7 +2055,10 @@ def apply_operations_to_native_components(obj, operation_dict, native_dict): # nc_dict = copy.deepcopy(native_dict["Properties"]) nc_dict["TargetCS"] = instance_dict["CS"] component3d_names = list(self._app.modeler.oeditor.Get3DComponentInstanceNames(native_name)) - native = NativeComponentObject(self._app, native_dict["Type"], native_name, nc_dict) + if native_dict["Type"] == "PCB": + native = NativeComponentPCB(self._app, native_dict["Type"], native_name, nc_dict) + else: + native = NativeComponentObject(self._app, native_dict["Type"], native_name, nc_dict) prj_list = set(self._app.project_list) definition_names = set(self._app.oeditor.Get3DComponentDefinitionNames()) instance_names = { diff --git a/pyaedt/generic/general_methods.py b/pyaedt/generic/general_methods.py index ccdecf72d88..00e545e103a 100644 --- a/pyaedt/generic/general_methods.py +++ b/pyaedt/generic/general_methods.py @@ -181,7 +181,7 @@ def _check_types(arg): return "" -def raise_exception(e): +def raise_exception_or_return_false(e): if not settings.enable_error_handler: if settings.release_on_exception: from pyaedt.generic.desktop_sessions import _desktop_sessions @@ -216,13 +216,13 @@ def wrapper(*args, **kwargs): pyaedt_logger.error("") if settings.enable_file_logs: settings.error(message) - raise_exception(e) + return raise_exception_or_return_false(e) except GrpcApiError as e: _exception(sys.exc_info(), user_function, args, kwargs, "AEDT grpc API call Error") - raise_exception(e) + return raise_exception_or_return_false(e) except BaseException as e: _exception(sys.exc_info(), user_function, args, kwargs, str(sys.exc_info()[1]).capitalize()) - raise_exception(e) + return raise_exception_or_return_false(e) return wrapper @@ -1471,13 +1471,15 @@ def grpc_active_sessions(version=None, student_version=False, non_graphical=Fals @pyaedt_function_handler() -def compute_fft(time_vals, value): # pragma: no cover +def compute_fft(time_vals, value, window=None): # pragma: no cover """Compute FFT of input transient data. Parameters ---------- time_vals : `pandas.Series` value : `pandas.Series` + window : str, optional + Fft window. Options are "hamming", "hanning", "blackman", "bartlett". Returns ------- @@ -1492,10 +1494,24 @@ def compute_fft(time_vals, value): # pragma: no cover deltaT = time_vals[-1] - time_vals[0] num_points = len(time_vals) - valueFFT = np.fft.fft(value, num_points) + win = None + if window: + + if window == "hamming": + win = np.hamming(num_points) + elif window == "hanning": + win = np.hanning(num_points) + elif window == "bartlett": + win = np.bartlett(num_points) + elif window == "blackman": + win = np.blackman(num_points) + if win is not None: + valueFFT = np.fft.fft(value * win, num_points) + else: + valueFFT = np.fft.fft(value, num_points) Npoints = int(len(valueFFT) / 2) valueFFT = valueFFT[1 : Npoints + 1] - valueFFT = valueFFT / len(valueFFT) + valueFFT = 2 * valueFFT / len(valueFFT) n = np.arange(num_points) freq = n / deltaT return freq, valueFFT @@ -1577,6 +1593,7 @@ def parse_excitation_file( data_format="Power", encoding="utf-8", out_mag="Voltage", + window="hamming", ): """Parse a csv file and convert data in list that can be applied to Hfss and Hfss3dLayout sources. @@ -1598,6 +1615,8 @@ def parse_excitation_file( Csv file encoding. out_mag : str, optional Output magnitude format. It can be `"Voltage"` or `"Power"` depending on Hfss solution. + window : str, optional + Fft window. Options are ``"hamming"``, ``"hanning"``, ``"blackman"``, ``"bartlett"`` or ``None``. Returns ------- @@ -1613,7 +1632,7 @@ def parse_excitation_file( if is_time_domain: time = df[df.keys()[0]].values * x_scale val = df[df.keys()[1]].values * y_scale - freq, fval = compute_fft(time, val) + freq, fval = compute_fft(time, val, window) if data_format.lower() == "current": if out_mag == "Voltage": diff --git a/pyaedt/generic/grpc_plugin.py b/pyaedt/generic/grpc_plugin.py deleted file mode 100644 index 025e259651f..00000000000 --- a/pyaedt/generic/grpc_plugin.py +++ /dev/null @@ -1,261 +0,0 @@ -import re -import types - -from pyaedt.generic.general_methods import GrpcApiError -from pyaedt.generic.general_methods import _retry_ntimes -from pyaedt.generic.general_methods import settings -import pyaedt.generic.grpc_plugin_dll as AedtAPI - -logger = settings.logger -__all__ = ["CreateAedtApplication", "Release"] - - -def CreateAedtObj(objectID, bIsPropSvr, listFuncs): - # print("Create " + str(objectID)) - if bIsPropSvr: - return AedtPropServer(objectID, listFuncs) - return AedtObjWrapper(objectID, listFuncs) - - -def CreateAedtBlockObj(list): - count = len(list) - if count > 1: - if isinstance(list[0], str): - toks = list[0].split(":") - if len(toks) == 2: - start = -1 - if count % 2 == 0: - if toks[1] == "=": - start = 2 - elif count > 2: - if toks[0] == "NAME": - start = 1 - if start > 0: - isBlock = True - for i in range(start, count - 1, 2): - if isinstance(list[i], str): - toks = list[i].split(":") - if len(toks) != 2 or toks[1] != "=": - isBlock = False - break - else: - isBlock = False - break - if isBlock: - return AedtBlockObj(list) - return list - - -def GetAedtObjId(obj): - if isinstance(obj, AedtObjWrapper): - return obj.objectID - return None - - -class AedtBlockObj(list): - def GetName(self): - if len(self) > 0: - f = self[0] - if isinstance(f, str): - d = f.split(":") - if len(d) == 2: - if d[0] == "NAME": - return d[1] - - def __GetValueIdxByKey__(self, keyName): - for i in range(0, len(self) - 1): - if isinstance(self[i], str): - toks = self[i].split(":") - if len(toks) == 2: - if toks[1] == "=" and toks[0] == keyName: - return i + 1 - raise GrpcApiError(keyName + " is not a key!") - - def __getitem__(self, idxOrKey): - if isinstance(idxOrKey, str): - idx = self.__GetValueIdxByKey__(idxOrKey) - if idx != None: - return super().__getitem__(idx) - return super().__getitem__(idxOrKey) - - def __setitem__(self, idxOrKey, newVal): - if isinstance(idxOrKey, int): - if idxOrKey >= 0 or idxOrKey < len(self): - oldItem = self.__getitem__(idxOrKey) - if isinstance(oldItem, str): - toks = oldItem.split(":") - if len(toks) == 2 and (toks[1] == "=" or toks[0] == "NAME"): - raise GrpcApiError("The element is a key should not be overwritten!") - return super().__setitem__(idxOrKey, newVal) - if isinstance(idxOrKey, str): - idx = self.__GetValueIdxByKey__(idxOrKey) - if idx != None: - return super().__setitem__(idx, newVal) - raise GrpcApiError("Key not found") - raise GrpcApiError("Must be key name or index") - - def keys(self): - arr = [] - for i in range(0, len(self) - 1): - if isinstance(self[i], str): - toks = self[i].split(":") - if len(toks) == 2 and toks[1] == "=": - arr.append(toks[0]) - return arr - - -class AedtObjWrapper: - def __init__(self, objID, listFuncs): - self.__dict__["objectID"] = objID # avoid derive class overwrite __setattr__ - self.__dict__["__methodNames__"] = listFuncs - # print(self.objectID) - - def __str__(self): - return "Instance of an Aedt object:" + str(self.objectID) - - def __Invoke__(self, funcName, argv): - if settings.enable_debug_grpc_api_logger: - settings.logger.debug("{} {}".format(funcName, argv)) - try: - return _retry_ntimes( - settings.number_of_grpc_api_retries, AedtAPI.InvokeAedtObjMethod, self.objectID, funcName, argv - ) # Call C function - except Exception: # pragma: no cover - raise GrpcApiError("Failed to execute grpc AEDT command: {}".format(funcName)) - - def __dir__(self): - return self.__methodNames__ - - def __GetObjMethod__(self, funcName): - try: - - def DynamicFunc(self, *args): - return self.__Invoke__(funcName, args) - - return types.MethodType(DynamicFunc, self) - except (AttributeError, GrpcApiError): - raise GrpcApiError("This Aedt object has no attribute '" + funcName + "'") - - def __getattr__(self, funcName): - try: - if funcName == "ScopeID": # backward compatible for IronPython wrapper. - return self.objectID - return self.__GetObjMethod__(funcName) - except Exception: - raise GrpcApiError("Failed to get grpc API AEDT attribute {}".format(funcName)) - - def __setattr__(self, attrName, val): - if attrName == "objectID" or attrName == "__methodNames__": - raise GrpcApiError("Modify this attribute is not allowed") - elif attrName in self.__methodNames__: - raise GrpcApiError(attrName + " is a function name") - else: - super().__setattr__(attrName, val) - - def __del__(self): - AedtAPI.ReleaseAedtObject(self.objectID) - - def match(self, patternStr): # IronPython wrapper implemented this function return IEnumerable. - class IEnumerable(list): - def __getattr__(self, key): - if key == "Count": - return len(self) - - pattern = re.compile(patternStr) - found = IEnumerable() - allMethods = self.__methodNames__ - for method in allMethods: - if pattern.match(method): - found.append(method) - return found - - def GetHashCode(self): # IronPython build in function - return self.__hash__() - - -class AedtPropServer(AedtObjWrapper): - def __init__(self, objID, listFuncs): - AedtObjWrapper.__init__(self, objID, listFuncs) - self.__dict__["__propMap__"] = None - self.__dict__["__propNames__"] = None - - def __GetPropAttributes(self): - if self.__propMap__ == None: - propMap = {} - propNames = self.GetPropNames() - for prop in propNames: - attrName = "" - if prop[0].isdigit(): - attrName += "_" - for c in prop: - if c.isalnum() == True: - attrName += c - else: - attrName += "_" - propMap[attrName] = prop - self.__propMap__ = propMap - return self.__propMap__ - - def __dir__(self): - ret = super().__dir__().copy() - for attrName in self.__GetPropAttributes().keys(): - ret.append(attrName) - return ret - - def __getattr__(self, attrName): - try: - return super().__getattr__(attrName) - except AttributeError: - # if AedtAPI.IsAedtObjPropName(self.objectID, attrName, False): - # return self.GetPropValue(attrName) - propMap = self.__GetPropAttributes() - if attrName in propMap: - return self.GetPropValue(propMap[attrName]) - raise GrpcApiError("Failed to retrieve attribute {} from GRPC API".format(attrName)) - - def __setattr__(self, attrName, val): - if attrName in self.__dict__: - self.__dict__[attrName] = val - return - - # if AedtAPI.IsAedtObjPropName(self.objectID, attrName, True): - # self.SetPropValue(attrName, val) - # return - propMap = self.__GetPropAttributes() - if attrName in propMap: - self.SetPropValue(propMap[attrName], val) - return - super().__setattr__(attrName, val) - - def GetName(self): - return self.__Invoke__("GetName", ()) - - def GetObjPath(self): - return self.__Invoke__("GetObjPath", ()) - - def GetChildNames(self, childType=""): - return self.__Invoke__("GetChildNames", (childType)) - - def GetPropNames(self, includeReadOnly=True): - if includeReadOnly: - if self.__propNames__ == None: - self.__propNames__ = self.__Invoke__("GetPropNames", (includeReadOnly,)) - return self.__propNames__ - return self.__Invoke__("GetPropNames", (includeReadOnly,)) - - def GetPropValue(self, propName=""): - return self.__Invoke__("GetPropValue", (propName,)) - - def SetPropValue(self, propName, val): - return self.__Invoke__("SetPropValue", (propName, val)) - - -def CreateAedtApplication(machine="", port=0, NGmode=False, alwaysNew=True): - return AedtAPI.CreateAedtApplication(machine, port, NGmode, alwaysNew) - - -def Release(): - AedtAPI.ReleaseAll() - - -AedtAPI.SetPyObjCalbacks(CreateAedtObj, CreateAedtBlockObj, GetAedtObjId) diff --git a/pyaedt/generic/grpc_plugin_dll.py b/pyaedt/generic/grpc_plugin_dll.py deleted file mode 100644 index 0c2a4f19821..00000000000 --- a/pyaedt/generic/grpc_plugin_dll.py +++ /dev/null @@ -1,104 +0,0 @@ -from ctypes import CFUNCTYPE -from ctypes import PyDLL -from ctypes import c_bool -from ctypes import c_int -from ctypes import c_wchar_p -from ctypes import py_object -import os - -is_linux = os.name == "posix" -is_windows = not is_linux - -pathDir = os.environ["DesktopPluginPyAEDT"] # DesktopPlugin -pathDir = os.path.dirname(pathDir) # PythonFiles -pathDir = os.path.dirname(pathDir) # DesktopPlugin or Win64 -# dirName = os.path.basename(pathDir) - - -# Plugin filename depends on OS -if is_linux: - pluginFileName = r"libPyDesktopPlugin.so" -else: - pluginFileName = r"PyDesktopPlugin.dll" - -AedtAPIDll_file = os.path.join(pathDir, pluginFileName) # install dir - -if not os.path.isfile(AedtAPIDll_file): - pathDir = os.path.dirname(pathDir) # lib - pathDir = os.path.dirname(pathDir) # core - pathDir = os.path.dirname(pathDir) # view - AedtAPIDll_file = os.path.join(pathDir, r"build_output\64Release\PyDesktopPlugin.dll") # develop dir - # AedtAPIDll_file = os.path.join(pathDir, r"PyAedtStub/x64/Debug/PyAedtStub.dll") #develop dir - -# load dll -if is_windows: - # on windows, modify path - aedtDir = os.path.dirname(AedtAPIDll_file) - originalPath = os.environ["PATH"] - os.environ["PATH"] = originalPath + os.pathsep + aedtDir - AedtAPI = PyDLL(AedtAPIDll_file) - os.environ["PATH"] = originalPath -else: - AedtAPI = PyDLL(AedtAPIDll_file) - -# AedtAPI.SetPyObjCalbacks.argtypes = py_object, py_object, py_object -AedtAPI.SetPyObjCalbacks.restype = None - -# Must use global variable to hold those functions reference -callbackToCreateObj = None -callbackCreateBlock = None -callbackGetObjID = None - - -def SetPyObjCalbacks(CreateAedtObj, CreateAedtBlockObj, GetAedtObjId): - callback_type = CFUNCTYPE(py_object, c_int, c_bool, py_object) - global callbackToCreateObj - global callbackCreateBlock - global callbackGetObjID - callbackToCreateObj = callback_type(CreateAedtObj) # must use global variable to hold this function reference - RetObj_InObj_Func_type = CFUNCTYPE(py_object, py_object) - callbackCreateBlock = RetObj_InObj_Func_type(CreateAedtBlockObj) - callbackGetObjID = RetObj_InObj_Func_type(GetAedtObjId) - AedtAPI.SetPyObjCalbacks(callbackToCreateObj, callbackCreateBlock, callbackGetObjID) - - -# Find the version of AEDT from product info file -version = None -with open(os.path.join(pathDir, "product.info"), "r") as f: - for line in f: - if "AnsProductVersion" in line: - version = line.split("=")[1].strip('\n"') - break - -if version >= "24.1": - AedtAPI.CreateAedtApplication.argtypes = c_wchar_p, py_object, c_bool, c_bool -else: - AedtAPI.CreateAedtApplication.argtypes = c_wchar_p, c_int, c_bool, c_bool - -AedtAPI.CreateAedtApplication.restype = py_object - -AedtAPI.InvokeAedtObjMethod.argtypes = c_int, c_wchar_p, py_object -AedtAPI.InvokeAedtObjMethod.restype = py_object - -AedtAPI.ReleaseAedtObject.argtypes = (c_int,) -AedtAPI.ReleaseAedtObject.restype = None - - -def CreateAedtApplication(machine="", port=0, NGmode=False, alwaysNew=True): - return AedtAPI.CreateAedtApplication(machine, port, NGmode, alwaysNew) - - -def InvokeAedtObjMethod(objectID, funcName, argv): - return AedtAPI.InvokeAedtObjMethod(objectID, funcName, argv) - - -def ReleaseAedtObject(objectID): - AedtAPI.ReleaseAedtObject(objectID) - - -def ReleaseAll(): - AedtAPI.ReleaseAll() - - -def IsEmbedded(): - return False diff --git a/pyaedt/generic/grpc_plugin_dll_class.py b/pyaedt/generic/grpc_plugin_dll_class.py new file mode 100644 index 00000000000..20d8b3c8cc8 --- /dev/null +++ b/pyaedt/generic/grpc_plugin_dll_class.py @@ -0,0 +1,403 @@ +from ctypes import CFUNCTYPE +from ctypes import PyDLL +from ctypes import c_bool +from ctypes import c_int +from ctypes import c_wchar_p +from ctypes import py_object +import os +import re +import types + +from pyaedt.generic.general_methods import GrpcApiError +from pyaedt.generic.general_methods import _retry_ntimes +from pyaedt.generic.general_methods import settings + +logger = settings.logger + + +class AedtBlockObj(list): + def GetName(self): + if len(self) > 0: + f = self[0] + if isinstance(f, str): + d = f.split(":") + if len(d) == 2: + if d[0] == "NAME": + return d[1] + + def __GetValueIdxByKey__(self, keyName): + for i in range(0, len(self) - 1): + if isinstance(self[i], str): + toks = self[i].split(":") + if len(toks) == 2: + if toks[1] == "=" and toks[0] == keyName: + return i + 1 + raise GrpcApiError(keyName + " is not a key!") + + def __getitem__(self, idxOrKey): + if isinstance(idxOrKey, str): + idx = self.__GetValueIdxByKey__(idxOrKey) + if idx != None: + return super().__getitem__(idx) + return super().__getitem__(idxOrKey) + + def __setitem__(self, idxOrKey, newVal): + if isinstance(idxOrKey, int): + if idxOrKey >= 0 or idxOrKey < len(self): + oldItem = self.__getitem__(idxOrKey) + if isinstance(oldItem, str): + toks = oldItem.split(":") + if len(toks) == 2 and (toks[1] == "=" or toks[0] == "NAME"): + raise GrpcApiError("The element is a key. It should not be overwritten.") + return super().__setitem__(idxOrKey, newVal) + if isinstance(idxOrKey, str): + idx = self.__GetValueIdxByKey__(idxOrKey) + if idx != None: + return super().__setitem__(idx, newVal) + raise GrpcApiError("Key is not found.") + raise GrpcApiError("Must be a key name or index.") + + def keys(self): + arr = [] + for i in range(0, len(self) - 1): + if isinstance(self[i], str): + toks = self[i].split(":") + if len(toks) == 2 and toks[1] == "=": + arr.append(toks[0]) + return arr + + +exclude_list = ["GetAppDesktop", "GetProcessID", "GetGrpcServerPort"] + + +class AedtObjWrapper: + def __init__(self, objID, listFuncs, AedtAPI=None): + self.__dict__["objectID"] = objID # avoid derive class overwrite __setattr__ + self.__dict__["__methodNames__"] = listFuncs + self.dllapi = AedtAPI + self.is_linux = True if os.name == "posix" else False + + # print(self.objectID) + + def __str__(self): + return "Instance of an Aedt object:" + str(self.objectID) + + def __Invoke__(self, funcName, argv): + if settings.enable_debug_grpc_api_logger: + settings.logger.debug("{} {}".format(funcName, argv)) + try: + if settings.use_multi_desktop and funcName not in exclude_list: + self.dllapi.recreate_application(True if self.is_linux else False) + ret = _retry_ntimes( + settings.number_of_grpc_api_retries, + self.dllapi.AedtAPI.InvokeAedtObjMethod, + self.objectID, + funcName, + argv, + ) # Call C function + if ret and isinstance(ret, (AedtObjWrapper, AedtPropServer)): + ret.AedtAPI = self.AedtAPI + return ret + except Exception: # pragma: no cover + raise GrpcApiError("Failed to execute gRPC AEDT command: {}".format(funcName)) + + def __dir__(self): + return self.__methodNames__ + + def __GetObjMethod__(self, funcName): + try: + + def DynamicFunc(self, *args): + return self.__Invoke__(funcName, args) + + return types.MethodType(DynamicFunc, self) + except (AttributeError, GrpcApiError): + raise GrpcApiError("This AEDT object has no attribute '" + funcName + "'") + + def __getattr__(self, funcName): + try: + if funcName == "ScopeID": # backward compatible for IronPython wrapper. + return self.objectID + return self.__GetObjMethod__(funcName) + except Exception: + raise GrpcApiError("Failed to get gRPC API AEDT attribute {}".format(funcName)) + + def __setattr__(self, attrName, val): + if attrName == "objectID" or attrName == "__methodNames__": + raise GrpcApiError("This attribute cannot be modified.") + elif attrName in self.__methodNames__: + raise GrpcApiError(attrName + " is a function name.") + else: + super().__setattr__(attrName, val) + + def __del__(self): + if "ReleaseAedtObject" in dir(self.dllapi): + self.dllapi.ReleaseAedtObject(self.objectID) + + def match(self, patternStr): # IronPython wrapper implemented this function return IEnumerable. + class IEnumerable(list): + def __getattr__(self, key): + if key == "Count": + return len(self) + + pattern = re.compile(patternStr) + found = IEnumerable() + allMethods = self.__methodNames__ + for method in allMethods: + if pattern.match(method): + found.append(method) + return found + + def GetHashCode(self): # IronPython build in function + return self.__hash__() + + +class AedtPropServer(AedtObjWrapper): + def __init__(self, objID, listFuncs, aedtapi): + AedtObjWrapper.__init__(self, objID, listFuncs, aedtapi) + self.__dict__["__propMap__"] = None + self.__dict__["__propNames__"] = None + + def __GetPropAttributes(self): + if self.__propMap__ == None: + propMap = {} + propNames = self.GetPropNames() + for prop in propNames: + attrName = "" + if prop[0].isdigit(): + attrName += "_" + for c in prop: + if c.isalnum() == True: + attrName += c + else: + attrName += "_" + propMap[attrName] = prop + self.__propMap__ = propMap + return self.__propMap__ + + def __dir__(self): + ret = super().__dir__().copy() + for attrName, _ in self.__GetPropAttributes(): + ret.append(attrName) + return ret + + def __getattr__(self, attrName): + try: + return super().__getattr__(attrName) + except AttributeError: + # if AedtAPI.IsAedtObjPropName(self.objectID, attrName, False): + # return self.GetPropValue(attrName) + propMap = self.__GetPropAttributes() + if attrName in propMap: + return self.GetPropValue(propMap[attrName]) + raise GrpcApiError("Failed to retrieve attribute {} from gRPC API".format(attrName)) + + def __setattr__(self, attrName, val): + if attrName in self.__dict__: + self.__dict__[attrName] = val + return + propMap = self.__GetPropAttributes() + try: + if attrName in propMap: + self.SetPropValue(propMap[attrName], val) + return + except Exception: + pass + super().__setattr__(attrName, val) + + def GetName(self): + return self.__Invoke__("GetName", ()) + + def GetObjPath(self): + return self.__Invoke__("GetObjPath", ()) + + def GetChildNames(self, childType=""): + return self.__Invoke__("GetChildNames", (childType)) + + def GetPropNames(self, includeReadOnly=True): + if includeReadOnly: + if self.__propNames__ == None: + self.__propNames__ = self.__Invoke__("GetPropNames", (includeReadOnly,)) + return self.__propNames__ + return self.__Invoke__("GetPropNames", (includeReadOnly,)) + + def GetPropValue(self, propName=""): + return self.__Invoke__("GetPropValue", (propName,)) + + def SetPropValue(self, propName, val): + return self.__Invoke__("SetPropValue", (propName, val)) + + +class AEDT: + def __init__(self, pathDir): + is_linux = os.name == "posix" + is_windows = not is_linux + self.original_path = pathDir + self.pathDir = pathDir + self.pathDir = os.path.dirname(self.pathDir) # PythonFiles + self.pathDir = os.path.dirname(self.pathDir) # DesktopPlugin or Win64 + # dirName = os.path.basename(pathDir) + + # Plugin filename depends on OS + if is_linux: + pluginFileName = r"libPyDesktopPlugin.so" + else: + pluginFileName = r"PyDesktopPlugin.dll" + + AedtAPIDll_file = os.path.join(self.pathDir, pluginFileName) # install dir + + if not os.path.isfile(AedtAPIDll_file): + self.pathDir = os.path.dirname(self.pathDir) # lib + self.pathDir = os.path.dirname(self.pathDir) # core + self.pathDir = os.path.dirname(self.pathDir) # view + AedtAPIDll_file = os.path.join(self.pathDir, r"build_output\64Release\PyDesktopPlugin.dll") # develop dir + # AedtAPIDll_file = os.path.join(pathDir, r"PyAedtStub/x64/Debug/PyAedtStub.dll") #develop dir + + # load dll + if is_windows: + # on windows, modify path + aedtDir = os.path.dirname(AedtAPIDll_file) + originalPath = os.environ["PATH"] + os.environ["PATH"] = originalPath + os.pathsep + aedtDir + AedtAPI = PyDLL(AedtAPIDll_file) + os.environ["PATH"] = originalPath + else: + AedtAPI = PyDLL(AedtAPIDll_file) + # AedtAPI.SetPyObjCalbacks.argtypes = py_object, py_object, py_object + AedtAPI.SetPyObjCalbacks.restype = None + + # Must use global variable to hold those functions reference + self.callbackToCreateObj = None + self.callbackCreateBlock = None + self.callbackGetObjID = None + version = None + with open(os.path.join(self.pathDir, "product.info"), "r") as f: + for line in f: + if "AnsProductVersion" in line: + version = line.split("=")[1].strip('\n"') + break + + if version >= "24.1": + AedtAPI.CreateAedtApplication.argtypes = c_wchar_p, py_object, c_bool, c_bool + else: + AedtAPI.CreateAedtApplication.argtypes = c_wchar_p, c_int, c_bool, c_bool + + AedtAPI.CreateAedtApplication.restype = py_object + + AedtAPI.InvokeAedtObjMethod.argtypes = c_int, c_wchar_p, py_object + AedtAPI.InvokeAedtObjMethod.restype = py_object + + AedtAPI.ReleaseAedtObject.argtypes = (c_int,) + AedtAPI.ReleaseAedtObject.restype = None + self.AedtAPI = AedtAPI + self.SetPyObjCalbacks() + self.aedt = None + + def SetPyObjCalbacks(self): + self.callback_type = CFUNCTYPE(py_object, c_int, c_bool, py_object) + self.callbackToCreateObj = self.callback_type( + self.CreateAedtObj + ) # must use global variable to hold this function reference + RetObj_InObj_Func_type = CFUNCTYPE(py_object, py_object) + self.callbackCreateBlock = RetObj_InObj_Func_type(self.CreateAedtBlockObj) + self.callbackGetObjID = RetObj_InObj_Func_type(self.GetAedtObjId) + self.AedtAPI.SetPyObjCalbacks(self.callbackToCreateObj, self.callbackCreateBlock, self.callbackGetObjID) + + def CreateAedtApplication(self, machine="", port=0, NGmode=False, alwaysNew=True): + self.aedt = self.AedtAPI.CreateAedtApplication(machine, port, NGmode, alwaysNew) + self.machine = machine + if port == 0: + self.port = self.aedt.GetAppDesktop().GetGrpcServerPort() + else: + self.port = port + + return self.aedt + + @property + def odesktop(self): + if settings.use_multi_desktop: + self.aedt = self.recreate_application() + return self.aedt.GetAppDesktop() + + def recreate_application(self, force=False): + def run(): + self.ReleaseAedtObject(self.aedt.objectID) + port = self.port + machine = self.machine + self.__init__(self.original_path) + self.port = port + self.machine = machine + self.aedt = self.AedtAPI.CreateAedtApplication(self.machine, self.port, False, False) + return self.aedt + + if force: + return run() + else: + try: + self.aedt.GetAppDesktop() + return self.aedt + except Exception: + return run() + + def InvokeAedtObjMethod(self, objectID, funcName, argv): + return self.AedtAPI.InvokeAedtObjMethod(objectID, funcName, argv) + + def ReleaseAedtObject(self, objectID): + self.AedtAPI.ReleaseAedtObject(objectID) + + def ReleaseAll(self): + self.AedtAPI.ReleaseAll() + + def IsEmbedded(self): + return False + + def CreateAedtObj(self, objectID, bIsPropSvr, listFuncs): + # print("Create " + str(objectID)) + if bIsPropSvr: + return AedtPropServer( + objectID, + listFuncs, + self, + ) + + return AedtObjWrapper( + objectID, + listFuncs, + self, + ) + + def CreateAedtBlockObj(self, list_in): + count = len(list_in) + if count > 1: + if isinstance(list_in[0], str): + toks = list_in[0].split(":") + if len(toks) == 2: + start = -1 + if count % 2 == 0: + if toks[1] == "=": + start = 2 + elif count > 2: + if toks[0] == "NAME": + start = 1 + if start > 0: + isBlock = True + for i in range(start, count - 1, 2): + if isinstance(list_in[i], str): + toks = list_in[i].split(":") + if len(toks) != 2 or toks[1] != "=": + isBlock = False + break + else: + isBlock = False + break + if isBlock: + return AedtBlockObj(list_in) + return list_in + + def GetAedtObjId(self, obj): + if isinstance(obj, AedtObjWrapper): + return obj.objectID + return None + + def Release(self): + self.AedtAPI.ReleaseAll() diff --git a/pyaedt/generic/plot.py b/pyaedt/generic/plot.py index 839ac8aea27..a2ed87de43e 100644 --- a/pyaedt/generic/plot.py +++ b/pyaedt/generic/plot.py @@ -385,7 +385,7 @@ def plot_polar_chart( @pyaedt_function_handler() @update_plot_settings -def plot_3d_chart(plot_data, size=(2000, 1000), xlabel="", ylabel="", title="", snapshot_path=None): +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 @@ -439,8 +439,11 @@ def plot_3d_chart(plot_data, size=(2000, 1000), xlabel="", ylabel="", title="", @pyaedt_function_handler() @update_plot_settings -def plot_2d_chart(plot_data, size=(2000, 1000), show_legend=True, xlabel="", ylabel="", title="", snapshot_path=None): - """Create a Matplotlib plot based on a list of data. +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 @@ -470,10 +473,6 @@ def plot_2d_chart(plot_data, size=(2000, 1000), show_legend=True, xlabel="", yla fig, ax = plt.subplots(figsize=figsize) label_id = 1 for plo_obj in plot_data: - if len(plo_obj) == 3: - label = plo_obj[2] - else: - label = "Trace " + str(label_id) if isinstance(plo_obj[0], np.ndarray): x = plo_obj[0] y = plo_obj[1] @@ -544,11 +543,10 @@ def plot_matplotlib( show : bool, optional Whether to show the plot or return the matplotlib object. Default is `True`. - Returns ------- :class:`matplotlib.pyplot.Figure` - Matplotlib Figure object. + Matplotlib figure object. """ dpi = 100.0 figsize = (size[0] / dpi, size[1] / dpi) diff --git a/pyaedt/hfss.py b/pyaedt/hfss.py index bea573d1011..ecd98a0ca37 100644 --- a/pyaedt/hfss.py +++ b/pyaedt/hfss.py @@ -3596,6 +3596,7 @@ def edit_source_from_file( impedance=50, data_format="Power", encoding="utf-8", + window="hamming", ): """Edit a source from file data. File data is a csv containing either frequency data or time domain data that will be converted through FFT. @@ -3608,23 +3609,78 @@ def edit_source_from_file( file_name : str Full name of the input file. is_time_domain : bool, optional - Either if the input data is Time based or Frequency Based. Frequency based data are Mag/Phase (deg). + Whether the input data is time-based or frequency-based. Frequency based data are Mag/Phase (deg). x_scale : float, optional - Scaling factor for x axis. + Scaling factor for the x axis. This argument is ignored if the algorithm + identifies the format from the file header. y_scale : float, optional - Scaling factor for y axis. + Scaling factor for the y axis. This argument is ignored if the algorithm + identifies the format from the file header. impedance : float, optional Excitation impedance. Default is `50`. data_format : str, optional - Either `"Power"`, `"Current"` or `"Voltage"`. + Data format. Options are ``"Current"``, ``"Power"``, and ``"Voltage"``. This + argument is ignored if the algoritmm identifies the format from the + file header. encoding : str, optional - Csv file encoding. - + CSV file encoding. + window : str, optional + Fft window. Options are ``"hamming"``, ``"hanning"``, ``"blackman"``, ``"bartlett"`` or ``None``. Returns ------- bool """ + + def find_scale(data, header_line): + for td in data.keys(): + if td in header_line: + return data[td] + return None + + with open(file_name, "r") as f: + header = f.readlines()[0] + time_data = {"[ps]": 1e-12, "[ns]": 1e-9, "[us]": 1e-6, "[ms]": 1e-3, "[s]": 1} + curva_data_V = { + "[nV]": 1e-9, + "[pV]": 1e-12, + "[uV]": 1e-6, + "[mV]": 1e-3, + "[V]": 1, + "[kV]": 1e3, + } + curva_data_W = { + "[nW]": 1e-9, + "[pW]": 1e-12, + "[uW]": 1e-6, + "[mW]": 1e-3, + "[W]": 1, + "[kW]": 1e3, + } + curva_data_A = { + "[nA]": 1e-9, + "[pA]": 1e-12, + "[uA]": 1e-6, + "[mA]": 1e-3, + "[A]": 1, + "[kA]": 1e3, + } + scale = find_scale(time_data, header) + x_scale = scale if scale else x_scale + scale = find_scale(curva_data_V, header) + if scale: + y_scale = scale + data_format = "Voltage" + else: + scale = find_scale(curva_data_W, header) + if scale: + y_scale = scale + data_format = "Power" + else: + scale = find_scale(curva_data_A, header) + if scale: + y_scale = scale + data_format = "Current" if self.solution_type == "Modal": out = "Power" else: @@ -3638,6 +3694,7 @@ def edit_source_from_file( data_format=data_format, encoding=encoding, out_mag=out, + window=window, ) ds_name_mag = "ds_" + assignment.replace(":", "_mode_") + "_Mag" ds_name_phase = "ds_" + assignment.replace(":", "_mode_") + "_Angle" diff --git a/pyaedt/hfss3dlayout.py b/pyaedt/hfss3dlayout.py index b852dfb0531..47ca8b72569 100644 --- a/pyaedt/hfss3dlayout.py +++ b/pyaedt/hfss3dlayout.py @@ -1998,6 +1998,7 @@ def edit_source_from_file( encoding="utf-8", include_post_effects=True, incident_voltage=True, + window="hamming", ): """Edit a source from file data. File data is a csv containing either frequency data or time domain data that will be converted through FFT. @@ -2026,7 +2027,8 @@ def edit_source_from_file( Either if include or not post-processing effects. Default is `True`, incident_voltage : bool, optional Either if include or incident or total voltage. Default is `True`, for incident voltage. - + window : str, optional + Fft window. Options are ``"hamming"``, ``"hanning"``, ``"blackman"``, ``"bartlett"`` or ``None``. Returns ------- @@ -2042,6 +2044,7 @@ def edit_source_from_file( data_format=data_format, encoding=encoding, out_mag=out, + window=window, ) ds_name_mag = "ds_" + source.replace(":", "_mode_") + "_Mag" ds_name_phase = "ds_" + source.replace(":", "_mode_") + "_Angle" diff --git a/pyaedt/icepak.py b/pyaedt/icepak.py index 14f220ebec4..9d519fe44b4 100644 --- a/pyaedt/icepak.py +++ b/pyaedt/icepak.py @@ -38,6 +38,7 @@ from pyaedt.modules.Boundary import ExponentialDictionary from pyaedt.modules.Boundary import LinearDictionary from pyaedt.modules.Boundary import NativeComponentObject +from pyaedt.modules.Boundary import NativeComponentPCB from pyaedt.modules.Boundary import NetworkObject from pyaedt.modules.Boundary import PieceWiseLinearDictionary from pyaedt.modules.Boundary import PowerLawDictionary @@ -2386,7 +2387,7 @@ def create_ipk_3dcomponent_pcb( Returns ------- - :class:`pyaedt.modules.Boundary.NativeComponentObject` + :class:`pyaedt.modules.Boundary.NativeComponentPCB` NativeComponentObject object. References @@ -2481,7 +2482,7 @@ def create_ipk_3dcomponent_pcb( # compDefinition += ["Power:=", powerin, hfssLinkInfo] native_props["TargetCS"] = PCB_CS - native = NativeComponentObject(self, "PCB", compName, native_props) + native = NativeComponentPCB(self, "PCB", compName, native_props) if native.create(): user_defined_component = UserDefinedComponent( self.modeler, native.name, native_props["NativeComponentDefinitionProvider"], "PCB" @@ -5974,17 +5975,17 @@ def assign_conducting_plate( if thermal_specification == "Thickness": props["Solid Material"] = solid_material if low_side_rad_material is not None: - props["LowSide"] = {"Radiate": False} - else: props["LowSide"] = {"Radiate": True, "RadiateTo": "AllObjects", "Surface Material": low_side_rad_material} - if high_side_rad_material is not None: - props["LowSide"] = {"Radiate": False} else: + props["LowSide"] = {"Radiate": False} + if high_side_rad_material is not None: props["HighSide"] = { "Radiate": True, "RadiateTo - High": "AllObjects - High", "Surface Material - High": high_side_rad_material, } + else: + props["LowSide"] = {"Radiate": False} props["Shell Conduction"] = shell_conduction if not boundary_name: boundary_name = generate_unique_name("Plate") diff --git a/pyaedt/maxwell.py b/pyaedt/maxwell.py index 57864cc07a1..62358c05cce 100644 --- a/pyaedt/maxwell.py +++ b/pyaedt/maxwell.py @@ -1920,17 +1920,24 @@ def export_element_based_harmonic_force( self.odesign.ExportElementBasedHarmonicForce(output_directory, setup, freq_option, f1, f2) return output_directory - @pyaedt_function_handler - def edit_external_circuit(self, netlist_file_path, schematic_design_name): + @pyaedt_function_handler() + def edit_external_circuit(self, netlist_file_path, schematic_design_name, parameters=None): """ - Edit the external circuit for the winding. + Edit the external circuit for the winding and allow editing of the circuit parameters. Parameters ---------- netlist_file_path : str - Circuit netlist file path. + Path to the circuit netlist file. schematic_design_name : str Name of the schematic design. + parameters : dict, optional + Name and value of the circuit parameters. + Parameters must be provided as a dictionary, where the key is the parameter name + and the value is the parameter value. + If the dictionary is provided, the ``netlist_file_path`` parameter is automatically + set to an empty string. + The default is ``None``. Returns ------- @@ -1967,7 +1974,13 @@ def edit_external_circuit(self, netlist_file_path, schematic_design_name): sources_type_array.append(2) elif source_type == "SPEED": sources_type_array.append(3) - self.oboundary.EditExternalCircuit(netlist_file_path, sources_array, sources_type_array, [], []) + names = [] + values = [] + if parameters: + names = list(parameters.keys()) + values = list(parameters.values()) + netlist_file_path = "" + self.oboundary.EditExternalCircuit(netlist_file_path, sources_array, sources_type_array, names, values) return True @pyaedt_function_handler(setupname="name", setuptype="setup_type") diff --git a/pyaedt/misc/tb_nexxim_mapping.toml b/pyaedt/misc/tb_nexxim_mapping.toml new file mode 100644 index 00000000000..99fea623dc3 --- /dev/null +++ b/pyaedt/misc/tb_nexxim_mapping.toml @@ -0,0 +1,39 @@ +[General] +scale=2.0 + +[R] +component_library="Resistors" +component_name="RES_" +property_mapping= {"R"="R"} +rotate_deg = -90 +x_offset = 0 +y_offset = 100 +pin_1 = 1 + + +[L] +component_library="Inductors" +component_name="IND_" +property_mapping= {"L"="L"} +rotate_deg = 90 +x_offset = 0 +y_offset = 100 +pin_1 = 1 + +[C] +component_library="Capacitors" +component_name="CAP_" +property_mapping= {"C"="C"} +rotate_deg = 90 +x_offset = 0 +y_offset = 100 +pin_1 = 1 + +[D] +component_library="Diodes" +component_name="DIODE_Level1" +property_mapping= {} +rotate_deg = 90 +x_offset = 0 +y_offset = 100 +pin_1 = 1 diff --git a/pyaedt/modeler/cad/Primitives.py b/pyaedt/modeler/cad/Primitives.py index a7913b071ae..e08329a677b 100644 --- a/pyaedt/modeler/cad/Primitives.py +++ b/pyaedt/modeler/cad/Primitives.py @@ -4612,6 +4612,11 @@ def import_3d_cad( separate_disjoints_lumped_object=False, import_free_surfaces=False, point_coicidence_tolerance=1e-6, + heal_stl=True, + reduce_stl=False, + reduce_percentage=0, + reduce_error=0, + merge_planar_faces=True, ): """Import a CAD model. @@ -4643,6 +4648,16 @@ def import_3d_cad( Either to import free surfaces parts. The default is ``False``. point_coicidence_tolerance : float, optional Tolerance on point. Default is ``1e-6``. + heal_stl : bool, optional + Whether to heal the stl file on import or not. Default is ``True``. + reduce_stl : bool, optional + Whether to reduce the stl file on import or not. Default is ``True``. + reduce_percentage : int, optional + Stl reduce percentage. Default is ``0``. + reduce_error : int, optional + Stl error percentage during reduce operation. Default is ``0``. + merge_planar_faces : bool, optional + Stl automatic planar face merge during import. Default is ``True``. Returns ------- @@ -4668,7 +4683,14 @@ def import_3d_cad( vArg1.append("GroupByAssembly:="), vArg1.append(group_by_assembly) vArg1.append("CreateGroup:="), vArg1.append(create_group) vArg1.append("STLFileUnit:="), vArg1.append("Auto") - vArg1.append("MergeFacesAngle:="), vArg1.append(-1) + vArg1.append("MergeFacesAngle:="), vArg1.append( + 0.02 if input_file.endswith(".stl") and merge_planar_faces else -1 + ) + if input_file.endswith(".stl"): + vArg1.append("HealSTL:="), vArg1.append(heal_stl) + vArg1.append("ReduceSTL:="), vArg1.append(reduce_stl) + vArg1.append("ReduceMaxError:="), vArg1.append(reduce_error) + vArg1.append("ReducePercentage:="), vArg1.append(reduce_percentage) vArg1.append("PointCoincidenceTol:="), vArg1.append(point_coicidence_tolerance) vArg1.append("CreateLightweightPart:="), vArg1.append(create_lightweigth_part) vArg1.append("ImportMaterialNames:="), vArg1.append(import_materials) diff --git a/pyaedt/modeler/cad/Primitives2D.py b/pyaedt/modeler/cad/Primitives2D.py index dbf284685a5..bec2cae27d6 100644 --- a/pyaedt/modeler/cad/Primitives2D.py +++ b/pyaedt/modeler/cad/Primitives2D.py @@ -95,10 +95,20 @@ def create_circle( new_object_name = self.oeditor.CreateCircle(vArg1, vArg2) return self._create_object(new_object_name, **kwargs) + # fmt: off @pyaedt_function_handler(position="origin", matname="material") def create_ellipse( - self, origin, major_radius, ratio, is_covered=True, name=None, material=None, non_model=False, **kwargs - ): + self, + origin, + major_radius, + ratio, + is_covered=True, + name=None, + material=None, + non_model=False, + segments=0, + **kwargs + ): # fmt: on """Create an ellipse. Parameters @@ -118,10 +128,14 @@ def create_ellipse( Name of the material. The default is ``None``. If ``None``, the default material is assigned. non_model : bool, optional - Either if create the new object as model or non-model. The default is ``False``. + Whether to create the object as a non-model. The default is ``False``, in which + case the object is created as a model. + segments : int, optional + Number of segments to apply to create the segmented geometry. + The default is ``0``. **kwargs : optional - Additional keyword arguments may be passed when creating the primitive to set properties. See - ``pyaedt.modeler.cad.object3d.Object3d`` for more details. + Additional keyword arguments to pass to set properties when creating the primitive. + For more information, see ``pyaedt.modeler.cad.object3d.Object3d``. Returns @@ -151,6 +165,7 @@ def create_ellipse( vArg1.append("MajRadius:="), vArg1.append(self._arg_with_dim(major_radius)) vArg1.append("Ratio:="), vArg1.append(ratio) vArg1.append("WhichAxis:="), vArg1.append(szAxis) + vArg1.append("NumSegments:="), vArg1.append(segments) vArg2 = self._default_object_attributes(name=name, matname=material, flags="NonModel#" if non_model else "") new_object_name = self.oeditor.CreateEllipse(vArg1, vArg2) diff --git a/pyaedt/modeler/cad/Primitives3D.py b/pyaedt/modeler/cad/Primitives3D.py index f4374148283..a5206d1540a 100644 --- a/pyaedt/modeler/cad/Primitives3D.py +++ b/pyaedt/modeler/cad/Primitives3D.py @@ -834,7 +834,16 @@ def create_circle(self, orientation, origin, radius, num_sides=0, is_covered=Tru @pyaedt_function_handler(cs_plane="orientation", position="origin", matname="material") def create_ellipse( - self, orientation, origin, major_radius, ratio, is_covered=True, name=None, material=None, **kwargs + self, + orientation, + origin, + major_radius, + ratio, + is_covered=True, + name=None, + material=None, + segments=0, + **kwargs ): """Create an ellipse. @@ -859,6 +868,9 @@ def create_ellipse( material : str, optional Name of the material. The default is ``None``, in which case the default material is assigned. + segments : int, optional + Number of segments to apply to create the segmented geometry. + The default is ``0``. **kwargs : optional Additional keyword arguments may be passed when creating the primitive to set properties. See ``pyaedt.modeler.cad.object3d.Object3d`` for more details. @@ -896,8 +908,6 @@ def create_ellipse( >>> ellipse = aedtapp.modeler.create_ellipse(orientation='Z', origin=[0,0,0], ... major_radius=2, ratio=2, is_covered=True, name="myell", ... material="vacuum") - - """ szAxis = GeometryOperators.cs_plane_to_axis_str(orientation) XStart, YStart, ZStart = self._pos_with_arg(origin) @@ -912,6 +922,8 @@ def create_ellipse( vArg1.append("MajRadius:="), vArg1.append(MajorRadius) vArg1.append("Ratio:="), vArg1.append(ratio) vArg1.append("WhichAxis:="), vArg1.append(szAxis) + vArg1.append("NumSegments:="), vArg1.append(segments) + vArg2 = self._default_object_attributes(name=name, matname=material) new_object_name = self.oeditor.CreateEllipse(vArg1, vArg2) return self._create_object(new_object_name, **kwargs) @@ -1275,20 +1287,20 @@ def create_spiral(self, internal_radius=10, spacing=1, faces=8, turns=10, width= dtheta = 2 * pi / faces theta = pi / 2 pts = [(internal_radius, 0, elevation), (internal_radius, internal_radius * tan(dtheta / 2), elevation)] - rin = internal_radius * tan(dtheta / 2) * 2 - x = rin - r = rin + r_in = internal_radius * tan(dtheta / 2) * 2 + x = r_in + r = r_in for i in range(faces): r += 1 theta += dtheta x = x + r * cos(theta) - dr = (width + spacing) / (x - rin) + dr = (width + spacing) / (x - r_in) for i in range(turns * faces - int(faces / 2) - 1): - rin += dr + r_in += dr theta += dtheta x0, y0 = pts[-1][:2] - x1, y1 = x0 + rin * cos(theta), y0 + rin * sin(theta) + x1, y1 = x0 + r_in * cos(theta), y0 + r_in * sin(theta) pts.append((x1, y1, elevation)) pts.append((x1, 0, elevation)) diff --git a/pyaedt/modeler/cad/components_3d.py b/pyaedt/modeler/cad/components_3d.py index 00bca1d4bf9..b56945fa3f7 100644 --- a/pyaedt/modeler/cad/components_3d.py +++ b/pyaedt/modeler/cad/components_3d.py @@ -510,13 +510,13 @@ def duplicate_and_mirror(self, origin, vector): """ return self._primitives.duplicate_and_mirror(self.name, origin=origin, vector=vector, is_3d_comp=True) - @pyaedt_function_handler() - def mirror(self, position, vector): + @pyaedt_function_handler(position="origin") + def mirror(self, origin, vector): """Mirror a selection. Parameters ---------- - position : list, Position + origin : list, Position List of the ``[x, y, z]`` coordinates or the Application.Position object for the selection. vector : float @@ -534,11 +534,11 @@ def mirror(self, position, vector): >>> oEditor.Mirror """ if self.is3dcomponent: - if self._primitives.mirror(self.name, position=position, vector=vector): + if self._primitives.mirror(self.name, origin=origin, vector=vector): return self else: for part in self.parts: - self._primitives.mirror(part, position=position, vector=vector) + self._primitives.mirror(part, origin=origin, vector=vector) return self return False @@ -820,7 +820,7 @@ def get_component_filepath(self): ) @pyaedt_function_handler(new_filepath="output_file") - def update_definition(self, password="", output_file=""): + def update_definition(self, password="", output_file="", local_update=False): """Update 3d component definition. Parameters @@ -830,6 +830,8 @@ def update_definition(self, password="", output_file=""): output_file : str, optional New path containing the 3d component file. The default value is ``""``, which means that the 3d component file has not changed. + local_update : bool, optional + Whether to update the file only locally. Default is ``False``. Returns ------- @@ -841,7 +843,7 @@ def update_definition(self, password="", output_file=""): [ "NAME:UpdateDefinitionData", "ForLocalEdit:=", - False, + local_update, "DefinitionNames:=", self.definition_name, "Passwords:=", diff --git a/pyaedt/modeler/cad/object3d.py b/pyaedt/modeler/cad/object3d.py index 0b0aa21c65c..e1804478c70 100644 --- a/pyaedt/modeler/cad/object3d.py +++ b/pyaedt/modeler/cad/object3d.py @@ -1450,13 +1450,13 @@ def split(self, plane, sides="Both"): """ return self._primitives.split(self.name, plane, sides) - @pyaedt_function_handler() - def mirror(self, position, vector, duplicate=False): + @pyaedt_function_handler(position="origin") + def mirror(self, origin, vector, duplicate=False): """Mirror a selection. Parameters ---------- - position : list of int or float + origin : list of int or float Cartesian ``[x, y, z]`` coordinates or the ``Application.Position`` object of a point in the plane used for the mirror operation. vector : list of float @@ -1477,7 +1477,7 @@ def mirror(self, position, vector, duplicate=False): >>> oEditor.Mirror """ - if self._primitives.mirror(self.id, position=position, vector=vector, duplicate=duplicate): + if self._primitives.mirror(self.id, origin=origin, vector=vector, duplicate=duplicate): return self return False diff --git a/pyaedt/modeler/circuits/PrimitivesCircuit.py b/pyaedt/modeler/circuits/PrimitivesCircuit.py index 6fb11a2e581..87dc0caf6c1 100644 --- a/pyaedt/modeler/circuits/PrimitivesCircuit.py +++ b/pyaedt/modeler/circuits/PrimitivesCircuit.py @@ -58,12 +58,54 @@ def __init__(self, modeler): self.oeditor = self._modeler.oeditor self._currentId = 0 self.components = {} - self.wires = {} self.refresh_all_ids() self.current_position = [0, 0] self.increment_mils = [1000, 1000] self.limits_mils = 20000 + @pyaedt_function_handler() + def get_wire_by_name(self, name): + """Wire class by name. + + Parameters + ---------- + name : str + Wire name. + + Returns + ------- + :class:`pyaedt.modeler.circuits.object3dcircuit.Wire` + ` + """ + for _, w in self.wires.items(): + if w.name == name: + return w + wname = w.name.split(";")[0].split("@")[0] + if name == wname: + return w + + @property + def wires(self): + """All schematic wires in the design. + + Returns + dict + Wires. + """ + wire_names = {} + for wire in self.oeditor.GetAllElements(): + if "Wire" in wire: + w = Wire(self, composed_name=wire) + if ":" in wire.split(";")[1]: + wire_id = int(wire.split(";")[1].split(":")[0]) + else: + wire_id = int(wire.split(";")[1]) + name = wire.split(";")[0].split("@")[1] + w.id = wire_id + w.name = name + wire_names[wire_id] = w + return wire_names + @property def o_definition_manager(self): """Aedt oDefinitionManager. @@ -335,7 +377,7 @@ def create_gnd(self, location=None, angle=0): xpos, ypos = self._get_location(location) id = self.create_unique_id() - + angle = math.pi * angle / 180 name = self.oeditor.CreateGround( ["NAME:GroundProps", "Id:=", id], ["NAME:Attributes", "Page:=", 1, "X:=", xpos, "Y:=", ypos, "Angle:=", angle, "Flip:=", False], @@ -1180,12 +1222,7 @@ def create_wire(self, points, name=""): arg2 = ["NAME:Attributes", "Page:=", 1] try: wire_id = self.oeditor.CreateWire(arg1, arg2) - w = Wire(self._modeler) - for segment in self._app.oeditor.GetWireSegments(wire_id): - key = "SegmentID_{}".format(segment.split(" ")[3]) - point1 = [float(x) for x in segment.split(" ")[1].split(",")] - point2 = [float(x) for x in segment.split(" ")[2].split(",")] - w.points_in_segment[key] = [point1, point2] + w = Wire(self._modeler, composed_name=wire_id) if ":" in wire_id.split(";")[1]: wire_id = int(wire_id.split(";")[1].split(":")[0]) else: @@ -1194,7 +1231,6 @@ def create_wire(self, points, name=""): name = generate_unique_name("Wire") w.name = name w.id = int(wire_id) - self.wires[w.id] = w return w except Exception: return False diff --git a/pyaedt/modeler/circuits/object3dcircuit.py b/pyaedt/modeler/circuits/object3dcircuit.py index 5c47882bd7c..a6d8dcd3d56 100644 --- a/pyaedt/modeler/circuits/object3dcircuit.py +++ b/pyaedt/modeler/circuits/object3dcircuit.py @@ -499,21 +499,27 @@ def parameters(self): if self._parameters: return self._parameters _parameters = {} - if self._circuit_components._app.design_type == "Circuit Design": - tab = "PassedParameterTab" + if self._circuit_components._app.design_type == "Circuit Design" or self.name in [ + "CompInst@FML_INIT", + "CompInst@Measurement", + ]: + tabs = ["PassedParameterTab"] elif self._circuit_components._app.design_type == "Maxwell Circuit": - tab = "PassedParameterTab" + tabs = ["PassedParameterTab"] else: - tab = "Quantities" - try: - proparray = self._oeditor.GetProperties(tab, self.composed_name) - except Exception: - proparray = [] + tabs = ["Quantities", "PassedParameterTab"] + proparray = {} + for tab in tabs: + try: + proparray[tab] = self._oeditor.GetProperties(tab, self.composed_name) + except Exception: + proparray[tab] = [] - for j in proparray: - propval = self._oeditor.GetPropertyValue(tab, self.composed_name, j) - _parameters[j] = propval - self._parameters = ComponentParameters(self, tab, _parameters) + for tab, props in proparray.items(): + for j in props: + propval = self._oeditor.GetPropertyValue(tab, self.composed_name, j) + _parameters[j] = propval + self._parameters = ComponentParameters(self, tab, _parameters) return self._parameters @property @@ -652,6 +658,12 @@ def angle(self): if "Angle=" in info: self._angle = float(info[6:]) break + else: + self._angle = float( + self._oeditor.GetPropertyValue("BaseElementTab", self.composed_name, "Component Angle").replace( + "deg", "" + ) + ) return self._angle @angle.setter @@ -908,12 +920,28 @@ def enforce_touchstone_model_passive(self): class Wire(object): """Creates and manipulates a wire.""" - def __init__(self, modeler): + def __init__(self, modeler, composed_name=None): + self.composed_name = composed_name self._app = modeler._app self._modeler = modeler self.name = "" self.id = 0 - self.points_in_segment = {} + self._points_in_segment = {} + + @property + def points_in_segment(self): + """Points in segment.""" + if not self.composed_name: + return {} + for segment in self._app.oeditor.GetWireSegments(self.composed_name): + key = segment.split(" ")[3] + point1 = [float(x) for x in segment.split(" ")[1].split(",")] + point2 = [float(x) for x in segment.split(" ")[2].split(",")] + if key in self._points_in_segment: + self._points_in_segment[key].extend([point1, point2]) + else: + self._points_in_segment[key] = [point1, point2] + return self._points_in_segment @property def _oeditor(self): @@ -961,23 +989,26 @@ def display_wire_properties(self, name="", property_to_display="NetName", visibi ``True`` when successful, ``False`` when failed. """ try: - wire_exists = False - for wire in self.wires: - if name == wire.split("@")[1].split(";")[0]: - wire_id = wire.split("@")[1].split(";")[1].split(":")[0] - wire_exists = True - break - else: - continue - if not wire_exists: - raise ValueError("Invalid wire name provided.") - + if name: + wire_exists = False + for wire in self.wires: + if name == wire.split("@")[1].split(";")[0]: + wire_id = wire.split("@")[1].split(";")[1].split(":")[0] + wire_exists = True + break + else: + continue + if not wire_exists: + raise ValueError("Invalid wire name provided.") + composed_name = "Wire@{};{};{}".format(name, wire_id, 1) + else: + composed_name = self.composed_name self._oeditor.ChangeProperty( [ "NAME:AllTabs", [ "NAME:PropDisplayPropTab", - ["NAME:PropServers", "Wire@{};{};{}".format(name, wire_id, 1)], + ["NAME:PropServers", composed_name], [ "NAME:NewProps", ["NAME:" + property_to_display, "Format:=", visibility, "Location:=", location], diff --git a/pyaedt/modeler/modeler3d.py b/pyaedt/modeler/modeler3d.py index 7f8be80cd21..2d94ff78274 100644 --- a/pyaedt/modeler/modeler3d.py +++ b/pyaedt/modeler/modeler3d.py @@ -877,7 +877,16 @@ def objects_in_bounding_box(self, bounding_box, check_solids=True, check_lines=T return objects @pyaedt_function_handler() - def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import_as_light_weight=False, **kwargs): + def import_nastran( + self, + file_path, + import_lines=True, + lines_thickness=0, + import_as_light_weight=False, + decimation=0, + group_parts=True, + enable_planar_merge="True", + ): """Import Nastran file into 3D Modeler by converting the faces to stl and reading it. The solids are translated directly to AEDT format. @@ -892,15 +901,29 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import Every line will be parametrized with a design variable called ``xsection_linename``. import_as_light_weight : bool, optional Import the stl generatated as light weight. It works only on SBR+ and HFSS Regions. Default is ``False``. + decimation : float, optional + Fraction of the original mesh to remove before creating the stl file. If set to ``0.9``, + this function tries to reduce the data set to 10% of its + original size and removes 90% of the input triangles. + group_parts : bool, optional + Whether to group imported parts by object ID. The default is ``True``. + enable_planar_merge : str, optional + Whether to enable or not planar merge. It can be ``"True"``, ``"False"`` or ``"Auto"``. + ``"Auto"`` will disable the planar merge if stl contains more than 50000 triangles. Returns ------- List of :class:`pyaedt.modeler.Object3d.Object3d` """ + autosave = ( + True if self._app.odesktop.GetRegistryInt("Desktop/Settings/ProjectOptions/DoAutoSave") == 1 else False + ) + self._app.odesktop.EnableAutoSave(False) - def _write_solid_stl(triangle, nas_to_dict): + def _write_solid_stl(triangle, pp): try: - points = [nas_to_dict["Points"][id] for id in triangle] + # points = [nas_to_dict["Points"][id] for id in triangle] + points = [pp[i] for i in triangle] except KeyError: return fc = GeometryOperators.get_polygon_centroid(points) @@ -927,22 +950,21 @@ def _write_solid_stl(triangle, nas_to_dict): f.write(" endloop\n") f.write(" endfacet\n") - nas_to_dict = {"Points": {}, "PointsId": {}, "Triangles": {}, "Lines": {}, "Solids": {}} + nas_to_dict = {"Points": [], "PointsId": {}, "Triangles": {}, "Lines": {}, "Solids": {}} self.logger.reset_timer() self.logger.info("Loading file") - el_ids = [] + pid = 0 with open_file(file_path, "r") as f: lines = f.read().splitlines() - id = 0 for lk in range(len(lines)): line = lines[lk] line_type = line[:8].strip() if line.startswith("$") or line.startswith("*"): continue - elif line_type in ["GRID", "CTRIA3"]: + elif line_type in ["GRID", "CTRIA3", "CQUAD4"]: grid_id = int(line[8:16]) - if line_type == "CTRIA3": + if line_type in ["CTRIA3", "CQUAD4"]: tria_id = int(line[16:24]) if tria_id not in nas_to_dict["Triangles"]: nas_to_dict["Triangles"][tria_id] = [] @@ -956,16 +978,35 @@ def _write_solid_stl(triangle, nas_to_dict): if "-" in n3[1:] and "e" not in n3[1:].lower(): n3 = n3[0] + n3[1:].replace("-", "e-") if line_type == "GRID": - nas_to_dict["Points"][grid_id] = [float(n1), float(n2), float(n3)] - nas_to_dict["PointsId"][grid_id] = grid_id - id += 1 - else: - tri = [int(n1), int(n2), int(n3)] + nas_to_dict["PointsId"][grid_id] = pid + nas_to_dict["Points"].append([float(n1), float(n2), float(n3)]) + pid += 1 + elif line_type == "CTRIA3": + tri = [ + nas_to_dict["PointsId"][int(n1)], + nas_to_dict["PointsId"][int(n2)], + nas_to_dict["PointsId"][int(n3)], + ] nas_to_dict["Triangles"][tria_id].append(tri) - - elif line_type in ["GRID*", "CTRIA3*"]: + elif line_type == "CQUAD4": + n4 = line[48:56].strip() + if "-" in n4[1:] and "e" not in n4[1:].lower(): + n4 = n4[0] + n4[1:].replace("-", "e-") + tri = [ + nas_to_dict["PointsId"][int(n1)], + nas_to_dict["PointsId"][int(n2)], + nas_to_dict["PointsId"][int(n3)], + ] + nas_to_dict["Triangles"][tria_id].append(tri) + tri = [ + nas_to_dict["PointsId"][int(n1)], + nas_to_dict["PointsId"][int(n3)], + nas_to_dict["PointsId"][int(n4)], + ] + nas_to_dict["Triangles"][tria_id].append(tri) + elif line_type in ["GRID*", "CTRIA3*", "CQUAD4*"]: grid_id = int(line[8:24]) - if line_type == "CTRIA3*": + if line_type in ["CTRIA3*", "CQUAD4*"]: tria_id = int(line[24:40]) if tria_id not in nas_to_dict["Triangles"]: nas_to_dict["Triangles"][tria_id] = [] @@ -977,51 +1018,82 @@ def _write_solid_stl(triangle, nas_to_dict): n2 = n2[0] + n2[1:].replace("-", "e-") n3 = line[72:88].strip() + idx = 88 if not n3 or n3.startswith("*"): lk += 1 n3 = lines[lk][8:24].strip() + idx = 24 if "-" in n3[1:] and "e" not in n3[1:].lower(): n3 = n3[0] + n3[1:].replace("-", "e-") if line_type == "GRID*": try: - nas_to_dict["Points"][grid_id] = [float(n1), float(n2), float(n3)] + nas_to_dict["Points"].append([float(n1), float(n2), float(n3)]) except Exception: # nosec continue - nas_to_dict["PointsId"][grid_id] = id - id += 1 - else: - tri = (int(n1), int(n2), int(n3)) + nas_to_dict["PointsId"][grid_id] = pid + pid += 1 + elif line_type == "CTRIA3*": + tri = [ + nas_to_dict["PointsId"][int(n1)], + nas_to_dict["PointsId"][int(n2)], + nas_to_dict["PointsId"][int(n3)], + ] nas_to_dict["Triangles"][tria_id].append(tri) - - elif line_type in ["CPENTA", "CHEXA", "CTETRA"]: - # obj_id = line[16:24].strip() + elif line_type == "CQUAD4*": + n4 = lines[lk][idx : idx + 16].strip() + if not n4 or n4.startswith("*"): + lk += 1 + n4 = lines[lk][8:24].strip() + if "-" in n4[1:] and "e" not in n4[1:].lower(): + n4 = n4[0] + n4[1:].replace("-", "e-") + tri = [ + nas_to_dict["PointsId"][int(n1)], + nas_to_dict["PointsId"][int(n2)], + nas_to_dict["PointsId"][int(n3)], + ] + nas_to_dict["Triangles"][tria_id].append(tri) + tri = [ + nas_to_dict["PointsId"][int(n1)], + nas_to_dict["PointsId"][int(n3)], + nas_to_dict["PointsId"][int(n4)], + ] + nas_to_dict["Triangles"][tria_id].append(tri) + elif line_type in [ + "CTETRA", + "CPYRAM", + "CPYRA", + ]: + # obj_id = line[8:16].strip() n = [] - el_id = line[24:32].strip() + el_id = line[16:24].strip() if el_id not in nas_to_dict["Solids"]: nas_to_dict["Solids"][el_id] = [] + n.append(int(line[24:32])) n.append(int(line[32:40])) n.append(int(line[40:48])) n.append(int(line[48:56])) - if line_type == "CPENTA": + if line_type in ["CPYRA", "CPYRAM"]: n.append(int(line[56:64])) - n.append(int(line[64:72])) - if line_type == "CHEXA": - n.append(int(line[56:64])) - n.append(int(line[64:72])) - lk += 1 - n.append(int(lines[lk][8:16].strip())) - n.append(int(lines[lk][16:24].strip())) from itertools import combinations for k in list(combinations(n, 3)): - tri = [int(k[0]), int(k[1]), int(k[2])] + # tri = [int(k[0]), int(k[1]), int(k[2])] + 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["Solids"][el_id].append(tri) - elif line_type in ["CTETRA*"]: + elif line_type in [ + "CTETRA*", + "CPYRAM*", + "CPYRA*", + ]: # obj_id = line[8:24].strip() n = [] el_id = line[24:40].strip() @@ -1036,59 +1108,152 @@ def _write_solid_stl(triangle, nas_to_dict): from itertools import combinations - for k in list(combinations(n, 3)): - tri = [int(k[0]), int(k[1]), int(k[2])] - tri.sort() - tri = tuple(tri) - nas_to_dict["Solids"][el_id].append(tri) + if line_type == "CTETRA*": + for k in list(combinations(n, 3)): + # tri = [int(k[0]), int(k[1]), int(k[2])] + 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["Solids"][el_id].append(tri) + else: + spli1 = [n[0], n[1], n[2], n[4]] + for k in list(combinations(spli1, 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["Solids"][el_id].append(tri) + spli1 = [n[0], n[2], n[3], n[4]] + for k in list(combinations(spli1, 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["Solids"][el_id].append(tri) elif line_type in ["CROD", "CBEAM"]: obj_id = int(line[16:24]) n1 = int(line[24:32]) n2 = int(line[32:40]) if obj_id in nas_to_dict["Lines"]: - nas_to_dict["Lines"][obj_id].append([n1, n2]) + nas_to_dict["Lines"][obj_id].append( + [nas_to_dict["PointsId"][int(n1)], nas_to_dict["PointsId"][int(n2)]] + ) else: - nas_to_dict["Lines"][obj_id] = [[n1, n2]] + nas_to_dict["Lines"][obj_id] = [ + [nas_to_dict["PointsId"][int(n1)], nas_to_dict["PointsId"][int(n2)]] + ] - self.logger.info_timer("File loaded") + self.logger.info("File loaded") objs_before = [i for i in self.object_names] + if nas_to_dict["Triangles"] or nas_to_dict["Solids"] or nas_to_dict["Lines"]: - self.logger.reset_timer() self.logger.info("Creating STL file with detected faces") - f = open(os.path.join(self._app.working_directory, self._app.design_name + "_test.stl"), "w") + output_stl = "" + enable_stl_merge = False if enable_planar_merge == "False" or enable_planar_merge is False else True + if nas_to_dict["Triangles"]: + output_stl = os.path.join(self._app.working_directory, self._app.design_name + "_tria.stl") + f = open(output_stl, "w") + + def decimate(points_in, faces_in, points_out, faces_out): + if 0 < decimation < 1: + aggressivity = 3 + if 0.7 > decimation > 0.3: + aggressivity = 5 + elif decimation >= 0.7: + aggressivity = 7 + points_out, faces_out = fast_simplification.simplify( + points_in, faces_in, decimation, agg=aggressivity + ) + + return points_out, faces_out + for tri_id, triangles in nas_to_dict["Triangles"].items(): + tri_out = triangles + p_out = nas_to_dict["Points"][::] + if decimation > 0 and len(triangles) > 20: + try: + import fast_simplification + + p_out, tri_out = decimate(nas_to_dict["Points"], tri_out, p_out, tri_out) + except Exception: + self.logger.error("Package fast-decimation is needed to perform model simplification.") + self.logger.error("Please install it using pip.") f.write("solid Sheet_{}\n".format(tri_id)) - for triangle in triangles: - _write_solid_stl(triangle, nas_to_dict) + if enable_planar_merge == "Auto" and len(tri_out) > 50000: + enable_stl_merge = False + for triangle in tri_out: + _write_solid_stl(triangle, p_out) f.write("endsolid\n") + if nas_to_dict["Triangles"]: + f.close() + output_solid = "" + enable_solid_merge = False if enable_planar_merge == "False" or enable_planar_merge is False else True + if nas_to_dict["Solids"]: + output_solid = os.path.join(self._app.working_directory, self._app.design_name + "_solids.stl") + f = open(output_solid, "w") for solidid, solid_triangles in nas_to_dict["Solids"].items(): f.write("solid Solid_{}\n".format(solidid)) import pandas as pd df = pd.Series(solid_triangles) - undulicated_values = df.drop_duplicates(keep=False).to_list() - for triangle in undulicated_values: - _write_solid_stl(triangle, nas_to_dict) + tri_out = df.drop_duplicates(keep=False).to_list() + p_out = nas_to_dict["Points"][::] + if decimation > 0 and len(solid_triangles) > 20: + try: + import fast_simplification + + p_out, tri_out = decimate(nas_to_dict["Points"], tri_out, p_out, tri_out) + except Exception: + self.logger.error("Package fast-decimation is needed to perform model simplification.") + self.logger.error("Please install it using pip.") + if enable_planar_merge == "Auto" and len(tri_out) > 50000: + enable_solid_merge = False + for triangle in tri_out: + _write_solid_stl(triangle, p_out) f.write("endsolid\n") - f.close() - self.logger.info_timer("STL file created") + if output_solid: + f.close() + self.logger.info("STL file created") self._app.odesktop.CloseAllWindows() - self.logger.reset_timer() self.logger.info("Importing STL in 3D Modeler") - self.import_3d_cad( - os.path.join(self._app.working_directory, self._app.design_name + "_test.stl"), - create_lightweigth_part=import_as_light_weight, - ) - for el in nas_to_dict["Solids"].keys(): - obj_names = [i for i in self.object_names if i.startswith("Solid_{}".format(el))] - self.create_group(obj_names, group_name=str(el)) - for el in nas_to_dict["Triangles"].keys(): - obj_names = [i for i in self.object_names if i.startswith("Sheet_{}".format(el))] - self.create_group(obj_names, group_name=str(el)) - self.logger.info_timer("Model imported") - - if import_lines: + if output_stl: + self.import_3d_cad( + output_stl, + create_lightweigth_part=import_as_light_weight, + healing=False, + merge_planar_faces=enable_stl_merge, + ) + if output_solid: + self.import_3d_cad( + output_solid, + create_lightweigth_part=import_as_light_weight, + healing=False, + merge_planar_faces=enable_solid_merge, + ) + self.logger.info("Model imported") + + if group_parts: + for el in nas_to_dict["Solids"].keys(): + obj_names = [i for i in self.object_names if i.startswith("Solid_{}".format(el))] + self.create_group(obj_names, group_name=str(el)) + objs = self.object_names[::] + for el in nas_to_dict["Triangles"].keys(): + obj_names = [i for i in objs if i == "Sheet_{}".format(el) or i.startswith("Sheet_{}_".format(el))] + self.create_group(obj_names, group_name=str(el)) + self.logger.info("Parts grouped") + + if import_lines and nas_to_dict["Lines"]: for line_name, lines in nas_to_dict["Lines"].items(): if lines_thickness: self._app["x_section_{}".format(line_name)] = lines_thickness @@ -1117,10 +1282,13 @@ def _write_solid_stl(triangle, nas_to_dict): out_poly = self.unite(polys, purge=not lines_thickness) if not lines_thickness and out_poly: self.generate_object_history(out_poly) + self.logger.info("Lines imported") objs_after = [i for i in self.object_names] new_objects = [self[i] for i in objs_after if i not in objs_before] self._app.oproject.SetActiveDesign(self._app.design_name) + self._app.odesktop.EnableAutoSave(autosave) + self.logger.info_timer("Nastran model correctly imported.") return new_objects @pyaedt_function_handler() diff --git a/pyaedt/modules/AdvancedPostProcessing.py b/pyaedt/modules/AdvancedPostProcessing.py index 6b5650d6074..d3d3eb507af 100644 --- a/pyaedt/modules/AdvancedPostProcessing.py +++ b/pyaedt/modules/AdvancedPostProcessing.py @@ -340,20 +340,17 @@ def plot_field_from_fieldplot( 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``. + 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"``. + 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. + 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 @@ -497,9 +494,8 @@ def plot_field( 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"``. + 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 diff --git a/pyaedt/modules/Boundary.py b/pyaedt/modules/Boundary.py index 6df48b98432..f78dfd6d192 100644 --- a/pyaedt/modules/Boundary.py +++ b/pyaedt/modules/Boundary.py @@ -113,7 +113,10 @@ def _get_boundary_data(self, ds): except Exception: pass try: - if "MotionSetupList" in self._app.design_properties["ModelSetup"]: + if ( + "ModelSetup" in self._app.design_properties + and "MotionSetupList" in self._app.design_properties["ModelSetup"] + ): motion_list = "MotionSetupList" setup = "ModelSetup" # check moving part @@ -327,6 +330,32 @@ def delete(self): return True +class NativeComponentPCB(NativeComponentObject, object): + """Manages Native Component PCB data and execution. + + Parameters + ---------- + app : object + AEDT application from the ``pyaedt.application`` class. + component_type : str + Type of the component. + component_name : str + Name of the component. + props : dict + Properties of the boundary. + + Examples + -------- + In this example, the returned object, ``par_beam`` is a ``pyaedt.modules.Boundary.NativeComponentObject`` instance. + >>> from pyaedt import Icepak + >>> ipk = Icepak(solution_type="SBR+") + >>> par_beam = ipk.create_ipk_3dcomponent_pcb() + """ + + def __init__(self, app, component_type, component_name, props): + NativeComponentObject.__init__(self, app, component_type, component_name, props) + + class BoundaryObject(BoundaryCommon, object): """Manages boundary data and execution. @@ -1819,6 +1848,15 @@ def object_properties(self): child_object = self._app.odesign.GetChildObject("Excitations").GetChildObject(self.name) if child_object: return BinaryTreeNode(self.name, child_object, False) + + if "Boundaries" in self._app.odesign.GetChildNames(): + cc = self._app.odesign.GetChildObject("Boundaries") + if self.name in cc.GetChildNames(): + child_object = self._app.odesign.GetChildObject("Boundaries").GetChildObject(self.name) + elif self.name in self._app.odesign.GetChildObject("Boundaries").GetChildNames(): + child_object = self._app.odesign.GetChildObject("Boundaries").GetChildObject(self.name) + if child_object: + return BinaryTreeNode(self.name, child_object, False) return False @property diff --git a/pyaedt/modules/Mesh.py b/pyaedt/modules/Mesh.py index 02973d31cd1..588f7c015a2 100644 --- a/pyaedt/modules/Mesh.py +++ b/pyaedt/modules/Mesh.py @@ -1024,7 +1024,6 @@ def assign_length_mesh(self, assignment, inside_selection=True, maximum_length=1 names="assignment", skindepth="skin_depth", maxelements="maximum_elements", - triangulation_max_length="0.1mm", numlayers="layers_number", meshop_name="name", ) diff --git a/pyaedt/modules/MeshIcepak.py b/pyaedt/modules/MeshIcepak.py index 4091f62429b..6eab77ed091 100644 --- a/pyaedt/modules/MeshIcepak.py +++ b/pyaedt/modules/MeshIcepak.py @@ -2,11 +2,14 @@ from collections import OrderedDict import warnings +from pyaedt.generic.DataHandlers import _dict2arg from pyaedt.generic.general_methods import GrpcApiError from pyaedt.generic.general_methods import _dim_arg from pyaedt.generic.general_methods import generate_unique_name from pyaedt.generic.general_methods import pyaedt_function_handler from pyaedt.generic.settings import settings +from pyaedt.modeler.cad.components_3d import UserDefinedComponent +from pyaedt.modeler.cad.object3d import Object3d from pyaedt.modules.Mesh import MeshOperation from pyaedt.modules.Mesh import meshers @@ -1280,10 +1283,94 @@ def automatic_mesh_3D(self, accuracy, enable_stair_step=True): self.global_mesh_region.update() return True + @pyaedt_function_handler() + def assign_priorities(self, assignment): + """Set objects priorities. + + Parameters + ---------- + assignment : List[List[Union[Object3d, UserDefinedComponent, str] + List of lists of objects. Each list corresponds to one priority level from low to high. + This means that the first list has the lowest priority while the last list + has the highest priority. Objects not explicitly passed in the lists are assigned + to a priority level lower than the objects in the first list. + + Returns + ------- + bool + ``True`` when successful, "False" when failed. + + References + ---------- + + >>> oEditor.UpdatePriorityList + + Examples + -------- + + >>> ipk.mesh.assign_priorities([["Box1", "Rectangle1"], ["Box2", "Fan1_1"], ["Heatsink1_1"]]) + """ + if not assignment or not isinstance(assignment, list) or not isinstance(assignment[0], list): + raise AttributeError("``assignment`` input must be a list of lists.") + props = {"PriorityListParameters": []} + for level, objects in enumerate(assignment): + level += 1 + if isinstance(objects[0], str): + objects = [ + self.modeler.objects_by_name.get(o, self.modeler.user_defined_components.get(o, None)) + for o in objects + ] + obj_3d = [ + o + for o in objects + if (isinstance(o, Object3d) and o.is3d) + or (isinstance(o, UserDefinedComponent) and any(p.is3d for p in o.parts.values())) + ] + obj_2d = [ + o + for o in objects + if (isinstance(o, Object3d) and not o.is3d) + or (isinstance(o, UserDefinedComponent) and any(not p.is3d for p in o.parts.values())) + ] + if obj_3d: + level_3d = { + "EntityList": ", ".join([o.name for o in obj_3d]), + "PriorityNumber": level, + "PriorityListType": "3D", + } + if all(isinstance(o, Object3d) for o in obj_3d): + level_3d["EntityType"] = "Object" + elif all(isinstance(o, UserDefinedComponent) for o in obj_3d): + level_3d["EntityType"] = "Component" + else: + raise AttributeError("Cannot assign components and parts on the same level.") + props["PriorityListParameters"].append(level_3d) + if obj_2d: + level_2d = { + "EntityList": ", ".join([o.name for o in obj_2d]), + "PriorityNumber": level, + "PriorityListType": "2D", + } + if all(isinstance(o, Object3d) for o in obj_2d): + level_2d["EntityType"] = "Object" + elif all(isinstance(o, UserDefinedComponent) for o in obj_2d): + level_2d["EntityType"] = "Component" + else: + raise AttributeError("Cannot assign components and parts on the same level.") + props["PriorityListParameters"].append(level_2d) + props = {"UpdatePriorityListData": props} + args = [] + _dict2arg(props, args) + self.modeler.oeditor.UpdatePriorityList(args[0]) + return True + @pyaedt_function_handler(obj_list="assignment", comp_name="component") def add_priority(self, entity_type, assignment=None, component=None, priority=3): """Add priority to objects. + .. deprecated:: 0.9.1 + Use :func:`assign_priorities` function instead. + Parameters ---------- entity_type : int @@ -1315,6 +1402,7 @@ def add_priority(self, entity_type, assignment=None, component=None, priority=3) >>> app.mesh.add_priority(entity_type=1,assignment=app.modeler.object_names,priority=3) >>> app.mesh.add_priority(entity_type=2,component=app.modeler.user_defined_component_names[0],priority=2) """ + warnings.warn("Use :func:`assign_priorities` function instead.", DeprecationWarning) i = priority args = ["NAME:UpdatePriorityListData"] diff --git a/pyaedt/modules/solutions.py b/pyaedt/modules/solutions.py index a4176ef30f0..9d7f190012e 100644 --- a/pyaedt/modules/solutions.py +++ b/pyaedt/modules/solutions.py @@ -7,6 +7,7 @@ import shutil import sys import time +import warnings from pyaedt import is_ironpython from pyaedt import pyaedt_function_handler @@ -50,6 +51,46 @@ plt = None +def simplify_stl(input_file, output_file=None, decimation=0.5, aggressiveness=7): + """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. + aggressiveness : int, optional + Controls how aggressively to decimate the mesh. A value of 10 + will result in a fast decimation at the expense of mesh + quality and shape. A value of 0 will attempt to preserve the + original mesh geometry at the expense of time. Setting a low + value may result in being unable to reach the + ``decimation``. + + Returns + ------- + str + Full path to output stl. + """ + try: + import fast_simplification + except Exception: + warnings.warn("Package fast-decimation is needed to perform model simplification.") + warnings.warn("Please install it using pip.") + return False + mesh = pv.read(input_file) + if not output_file: + output_file = os.path.splitext(input_file)[0] + "_output.stl" + simple = fast_simplification.simplify_mesh(mesh, target_reduction=decimation, agg=aggressiveness, verbose=True) + simple.save(output_file) + return output_file + + class SolutionData(object): """Contains information from the :func:`GetSolutionDataPerVariation` method.""" @@ -725,13 +766,30 @@ def export_data_to_csv(self, output, delimiter=";"): ------- bool """ - header = [el for el in self._sweeps_names] + 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)") - header.append(el + " (Imag)") + + header.append(el + " (Real){}".format(data_unit)) + header.append(el + " (Imag){}".format(data_unit)) else: - header.append(el) + header.append(el + "{}".format(data_unit)) list_full = [header] for e, v in self._solutions_real[self.active_expression].items(): @@ -761,8 +819,9 @@ def plot( title="", snapshot_path=None, is_polar=False, + show=True, ): - """Create a matplotlib plot based on a list of data. + """Create a matplotlib figure based on a list of data. Parameters ---------- @@ -794,8 +853,8 @@ def plot( Returns ------- - :class:`matplotlib.plt` - Matplotlib fig object. + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. """ if is_ironpython: # pragma: no cover return False @@ -835,9 +894,9 @@ def 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) + 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) + 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( @@ -851,8 +910,9 @@ def plot_3d( formula=None, size=(2000, 1000), snapshot_path=None, + show=True, ): - """Create a matplotlib 3d plot based on a list of data. + """Create a matplotlib 3D figure based on a list of data. Parameters ---------- @@ -880,8 +940,8 @@ def plot_3d( Returns ------- - :class:`matplotlib.plt` - Matplotlib fig object. + :class:`matplotlib.figure.Figure` + Matplotlib figure object. """ if is_ironpython: return False # pragma: no cover @@ -927,7 +987,7 @@ def plot_3d( 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) + 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): @@ -1582,6 +1642,7 @@ def plot_farfield_contour( Returns ------- :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. Examples -------- @@ -1678,6 +1739,7 @@ def plot_2d_cut( image_path=None, show=True, is_polar=False, + show_legend=True, **kwargs ): # fmt: on @@ -1711,14 +1773,13 @@ def plot_2d_cut( If ``False``, the Matplotlib instance of the plot is shown. is_polar : bool, optional Whether this plot is a polar plot. The default is ``True``. + show_legend : bool, optional + Whether to display the legend or not. The default is ``True``. Returns ------- - :class:`matplotlib.plt` - Whether to show the plotted curve. - If ``show=True``, a Matplotlib figure instance of the plot is returned. - If ``show=False``, the plotted curve is returned. - + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. Examples -------- @@ -1791,30 +1852,29 @@ def plot_2d_cut( return False curves.append([x, y, "{}={}".format(y_key, data[y_key][theta_idx])]) - if show: - show_legend = True - if len(curves) > 15: - show_legend = False - if is_polar: - return plot_polar_chart( - curves, - xlabel=x_key, - ylabel=quantity, - title=title, - snapshot_path=image_path, - show_legend=show_legend, - ) - else: - return plot_2d_chart( - curves, - xlabel=x_key, - ylabel=quantity, - title=title, - snapshot_path=image_path, - show_legend=show_legend, - ) + # FIXME: See if we need to keep this check on the curves length + # if len(curves) > 15: + # show_legend = False + if is_polar: + return plot_polar_chart( + curves, + xlabel=x_key, + ylabel=quantity, + title=title, + snapshot_path=image_path, + show_legend=show_legend, + show=show, + ) else: - return curves + return plot_2d_chart( + curves, + xlabel=x_key, + ylabel=quantity, + title=title, + snapshot_path=image_path, + show_legend=show_legend, + show=show, + ) # fmt: off @pyaedt_function_handler(farfield_quantity="quantity", @@ -1855,14 +1915,12 @@ def polar_plot_3d( Full path for the image file. The default is ``None``, in which case a file is not exported. show : bool, optional Whether to show the plot. The default is ``True``. - If ``False``, the Matplotlib instance of the plot is shown. + If ``False``, the Matplotlib instance of the plot is not shown. Returns ------- - :class:`matplotlib.plt` - Whether to show the plotted curve. - If ``show=True``, a Matplotlib figure instance of the plot is returned. - If ``show=False``, the plotted curve is returned. + :class:`matplotlib.pyplot.Figure` + Matplotlib figure object. Examples -------- @@ -1910,10 +1968,7 @@ def polar_plot_3d( 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) - if show: # pragma: no cover - plot_3d_chart([x, y, z], xlabel="Theta", ylabel="Phi", title=title, snapshot_path=image_path) - else: - return x, y, z + return plot_3d_chart([x, y, z], xlabel="Theta", ylabel="Phi", title=title, snapshot_path=image_path, show=show) # fmt: off @pyaedt_function_handler(farfield_quantity="quantity", export_image_path="image_path") @@ -1970,8 +2025,7 @@ def polar_plot_3d_pyvista( Returns ------- bool or :class:`Pyvista.Plotter` - ``True`` when successful. The :class:`Pyvista.Plotter` is returned when ``show`` and - ``export_image_path`` are ``False``. + ``False`` when the method fails, Pyvista plotter object otherwise. Examples -------- @@ -2150,10 +2204,8 @@ def scale(value=1): if image_path: p.show(screenshot=image_path) - return True - elif show: # pragma: no cover + if show: # pragma: no cover p.show() - return True return p @pyaedt_function_handler() diff --git a/pyaedt/workflows/customize_automation_tab.py b/pyaedt/workflows/customize_automation_tab.py index fa8972d3844..d6419246c34 100644 --- a/pyaedt/workflows/customize_automation_tab.py +++ b/pyaedt/workflows/customize_automation_tab.py @@ -19,6 +19,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 logging import os import shutil @@ -48,7 +49,7 @@ def add_automation_tab( product="Project", template="Run PyAEDT Toolkit Script", overwrite=False, - panel="Panel_PyAEDT_Toolkits", + panel="Panel_PyAEDT_Extensions", ): """Add an automation tab in AEDT. @@ -68,7 +69,7 @@ def add_automation_tab( Whether to overwrite the existing automation tab. The default is ``False``, in which case is adding new tabs to the existing ones. panel : str, optional - Panel name. The default is ``"Panel_PyAEDT_Toolkits"``. + Panel name. The default is ``"Panel_PyAEDT_Extensions"``. Returns ------- @@ -79,6 +80,10 @@ def add_automation_tab( product = __tab_map(product) + toolkit_name = name + if "/" in name: + toolkit_name = name.replace("/", "_") + tab_config_file_path = os.path.join(lib_dir, product, "TabConfig.xml") if not os.path.isfile(tab_config_file_path) or overwrite: root = ET.Element("TabConfig") @@ -113,7 +118,8 @@ def add_automation_tab( icon_file = os.path.join(os.path.dirname(pyaedt.workflows.__file__), "images", "large", "pyansys.png") file_name = os.path.basename(icon_file) - dest_dir = os.path.normpath(os.path.join(lib_dir, product, name, "images", "large")) + + dest_dir = os.path.normpath(os.path.join(lib_dir, product, toolkit_name, "images", "large")) dest_file = os.path.normpath(os.path.join(dest_dir, file_name)) os.makedirs(os.path.dirname(dest_dir), exist_ok=True) if not os.path.exists(dest_dir): @@ -128,7 +134,7 @@ def add_automation_tab( label=name, isLarge="1", image=relative_image_path, - script="{}/{}".format(name, template), + script="{}/{}".format(toolkit_name, template), ) # Backup any existing file if present @@ -139,7 +145,7 @@ def add_automation_tab( return tab_config_file_path -def remove_automation_tab(name, lib_dir, panel="Panel_PyAEDT_Toolkits"): +def remove_automation_tab(name, lib_dir, panel="Panel_PyAEDT_Extensions"): """Remove automation tab in AEDT. Parameters @@ -149,7 +155,7 @@ def remove_automation_tab(name, lib_dir, panel="Panel_PyAEDT_Toolkits"): lib_dir : str Path to the library directory. panel : str, optional - Panel name. The default is ``"Panel_PyAEDT_Toolkits"``. + Panel name. The default is ``"Panel_PyAEDT_Extensions"``. Returns ------- @@ -207,7 +213,7 @@ def create_xml_tab(root, output_file): f.write(xml_str) -def remove_xml_tab(toolkit_dir, name, panel="Panel_PyAEDT_Toolkits"): +def remove_xml_tab(toolkit_dir, name, panel="Panel_PyAEDT_Extensions"): """Remove a toolkit configuration file.""" tab_config_file_path = os.path.join(toolkit_dir, "TabConfig.xml") if not os.path.isfile(tab_config_file_path): @@ -254,7 +260,7 @@ def available_toolkits(): "Project", "Q2D", "Q3D", - "Simplorer", + "TwinBuilder", ] product_toolkits = {} @@ -269,12 +275,12 @@ def available_toolkits(): def add_script_to_menu( name, script_file, - template_file="Run_PyAEDT_Toolkit_Script", + template_file="run_pyaedt_toolkit_script", icon_file=None, product="Project", copy_to_personal_lib=True, executable_interpreter=None, - panel="Panel_PyAEDT_Toolkits", + panel="Panel_PyAEDT_Extensions", personal_lib=None, aedt_version="", ): @@ -292,7 +298,7 @@ def add_script_to_menu( script_file : str Full path to the script file. The script will be moved to Personal Lib. template_file : str - Script template name to use. The default is ``"Run_PyAEDT_Toolkit_Script"``. + Script template name to use. The default is ``"run_pyaedt_toolkit_script"``. icon_file : str, optional Full path to the icon (a 30x30 pixel PNG file) to add to the UI. The default is ``None``. @@ -304,7 +310,7 @@ def add_script_to_menu( executable_interpreter : str, optional Executable python path. The default is the one current interpreter. panel : str, optional - Panel name. The default is ``"Panel_PyAEDT_Toolkits"``. + Panel name. The default is ``"Panel_PyAEDT_Extensions"``. personal_lib : str, optional aedt_version : str, optional @@ -328,11 +334,14 @@ def add_script_to_menu( return False toolkit_dir = os.path.join(personal_lib, "Toolkits") tool_map = __tab_map(product) - tool_dir = os.path.join(toolkit_dir, tool_map, name) + file_name = name + if "/" in file_name: + file_name = file_name.replace("/", "_") + tool_dir = os.path.join(toolkit_dir, tool_map, file_name) lib_dir = os.path.join(tool_dir, "Lib") toolkit_rel_lib_dir = os.path.relpath(lib_dir, tool_dir) if is_linux and aedt_version <= "2023.1": - toolkit_rel_lib_dir = os.path.join("Lib", name) + toolkit_rel_lib_dir = os.path.join("Lib", file_name) lib_dir = os.path.join(toolkit_dir, toolkit_rel_lib_dir) toolkit_rel_lib_dir = "../../" + toolkit_rel_lib_dir os.makedirs(lib_dir, exist_ok=True) @@ -359,14 +368,13 @@ def add_script_to_menu( jupyter_executable = executable_version_agnostic.replace("python" + __exe(), "jupyter" + __exe()) with open(os.path.join(templates_dir, template_file + ".py_build"), "r") as build_file: - file_name_dest = template_file.replace("_", " ") - with open(os.path.join(tool_dir, file_name_dest + ".py"), "w") as out_file: + with open(os.path.join(tool_dir, template_file + ".py"), "w") as out_file: build_file_data = build_file.read() build_file_data = build_file_data.replace("##TOOLKIT_REL_LIB_DIR##", toolkit_rel_lib_dir) build_file_data = build_file_data.replace("##IPYTHON_EXE##", ipython_executable) build_file_data = build_file_data.replace("##PYTHON_EXE##", executable_version_agnostic) build_file_data = build_file_data.replace("##JUPYTER_EXE##", jupyter_executable) - build_file_data = build_file_data.replace("##TOOLKIT_NAME##", name) + build_file_data = build_file_data.replace("##TOOLKIT_NAME##", file_name) if dest_script_path: build_file_data = build_file_data.replace("##PYTHON_SCRIPT##", dest_script_path) @@ -374,10 +382,7 @@ def add_script_to_menu( build_file_data = build_file_data.replace(" % version", "") out_file.write(build_file_data) - if aedt_version >= "2023.2": - add_automation_tab( - name, toolkit_dir, icon_file=icon_file, product=product, template=file_name_dest, panel=panel - ) + add_automation_tab(name, toolkit_dir, icon_file=icon_file, product=product, template=template_file, panel=panel) logger.info("{} installed".format(name)) return True @@ -430,14 +435,14 @@ def add_custom_toolkit(desktop_object, toolkit_name, wheel_toolkit=None, install # Set Python version based on AEDT version python_version = "3.10" if desktop_object.aedt_version_id > "2023.1" else "3.7" - + python_version_new = python_version.replace(".", "_") if not is_linux: base_venv = os.path.normpath( os.path.join( desktop_object.install_path, "commonfiles", "CPython", - python_version.replace(".", "_"), + python_version_new, "winx64", "Release", "python", @@ -450,7 +455,7 @@ def add_custom_toolkit(desktop_object, toolkit_name, wheel_toolkit=None, install desktop_object.install_path, "commonfiles", "CPython", - python_version.replace(".", "_"), + python_version_new, "linx64", "Release", "python", @@ -476,19 +481,19 @@ def run_command(command): version = desktop_object.odesktop.GetVersion()[2:6].replace(".", "") if not is_linux: - venv_dir = os.path.join(os.environ["APPDATA"], "pyaedt_env_ide", "toolkits_v{}".format(version)) + venv_dir = os.path.join(os.environ["APPDATA"], ".pyaedt_env", "toolkits_{}".format(python_version_new)) python_exe = os.path.join(venv_dir, "Scripts", "python.exe") pip_exe = os.path.join(venv_dir, "Scripts", "pip.exe") package_dir = os.path.join(venv_dir, "Lib") else: - venv_dir = os.path.join(os.environ["HOME"], "pyaedt_env_ide", "toolkits_v{}".format(version)) + venv_dir = os.path.join(os.environ["HOME"], ".pyaedt_env", "toolkits_{}".format(python_version_new)) python_exe = os.path.join(venv_dir, "bin", "python") pip_exe = os.path.join(venv_dir, "bin", "pip") package_dir = os.path.join(venv_dir, "lib") edt_root = os.path.normpath(desktop_object.odesktop.GetExeDir()) os.environ["ANSYSEM_ROOT{}".format(version)] = edt_root ld_library_path_dirs_to_add = [ - "{}/commonfiles/CPython/{}/linx64/Release/python/lib".format(edt_root, python_version.replace(".", "_")), + "{}/commonfiles/CPython/{}/linx64/Release/python/lib".format(edt_root, python_version_new), "{}/common/mono/Linux64/lib64".format(edt_root), "{}".format(edt_root), ] @@ -562,7 +567,7 @@ def run_command(command): script_file=script_file, icon_file=script_image, product=product_name, - template_file=toolkit_info.get("template", "Run_PyAEDT_Toolkit_Script"), + template_file=toolkit_info.get("template", "run_pyaedt_toolkit_script"), copy_to_personal_lib=True, executable_interpreter=python_exe, personal_lib=desktop_object.personallib, diff --git a/pyaedt/workflows/hfss/images/large/push.png b/pyaedt/workflows/hfss/images/large/push.png new file mode 100644 index 00000000000..2eb7c639efb Binary files /dev/null and b/pyaedt/workflows/hfss/images/large/push.png differ diff --git a/pyaedt/workflows/hfss/push_excitation_from_file.py b/pyaedt/workflows/hfss/push_excitation_from_file.py new file mode 100644 index 00000000000..63d8c54d3b6 --- /dev/null +++ b/pyaedt/workflows/hfss/push_excitation_from_file.py @@ -0,0 +1,125 @@ +import os.path +from tkinter import Button + +# import filedialog module +from tkinter import END +from tkinter import Label +from tkinter import StringVar +from tkinter import Text +from tkinter import Tk +from tkinter import filedialog +from tkinter import mainloop +from tkinter import ttk +from tkinter.ttk import Combobox + +import PIL.Image +import PIL.ImageTk + +import pyaedt +from pyaedt import Desktop +from pyaedt import Hfss +import pyaedt.workflows +from pyaedt.workflows.misc import get_aedt_version +from pyaedt.workflows.misc import get_port +from pyaedt.workflows.misc import get_process_id + +choice = "" +file_path = "" + + +def browse_port(port_selection): + # Function for opening the + # file explorer window + + master = Tk() + + master.geometry("700x150") + + master.title("Assign push excitation to port from transient data") + + # Load the logo for the main window + icon_path = os.path.join(pyaedt.workflows.__path__[0], "images", "large", "logo.png") + im = PIL.Image.open(icon_path) + photo = PIL.ImageTk.PhotoImage(im) + + # Set the icon for the main window + master.iconphoto(True, photo) + + # Configure style for ttk buttons + style = ttk.Style() + style.configure("Toolbutton.TButton", padding=6, font=("Helvetica", 8)) + + var = StringVar() + label = Label(master, textvariable=var) + var.set("Choose a port:") + label.grid(row=0, column=0, pady=10) + # label.pack(pady=10) + combo = Combobox(master, width=30) # Set the width of the combobox + combo["values"] = port_selection + combo.current(0) + # combo.pack(pady=10) + combo.grid(row=0, column=1, pady=10, padx=5) + combo.focus_set() + var2 = StringVar() + label2 = Label(master, textvariable=var2) + var2.set("Browse file:") + label2.grid(row=1, column=0, pady=10) + text = Text(master, width=50, height=1) + # text.pack(pady=10) + text.grid(row=1, column=1, pady=10, padx=5) + + def browseFiles(): + filename = filedialog.askopenfilename( + initialdir="/", + title="Select a Transient File", + filetypes=(("Transient curve", "*.csv*"), ("all files", "*.*")), + ) + text.insert(END, filename) + # # Change label contents + # return filename + + b1 = Button(master, text="...", width=10, command=browseFiles) + b1.grid(row=3, column=0) + # b1.pack(pady=10) + b1.grid(row=1, column=2, pady=10) + + def callback(): + global choice, file_path + choice = combo.get() + file_path = text.get("1.0", END).strip() + master.destroy() + return True + + b = Button(master, text="Ok", width=40, command=callback) + # b.pack(pady=10) + b.grid(row=2, column=1, pady=10) + + mainloop() + + +port = get_port() +version = get_aedt_version() +aedt_process_id = get_process_id() + +with Desktop( + new_desktop_session=False, + close_on_exit=False, + specified_version=version, + port=port, + aedt_process_id=aedt_process_id, +) as d: + proj = d.active_project() + des = d.active_design() + projname = proj.GetName() + desname = des.GetName() + app = Hfss(projname, desname) + browse_port(port_selection=app.excitations) + if choice: + app.edit_source_from_file( + choice, + file_path, + is_time_domain=True, + ) + d.logger.info("Excitation assigned correctly.") + else: + d.logger.error("Failed to select a port.") diff --git a/pyaedt/workflows/hfss/toolkits_catalog.toml b/pyaedt/workflows/hfss/toolkits_catalog.toml index 8626340b3fa..c947ec3df43 100644 --- a/pyaedt/workflows/hfss/toolkits_catalog.toml +++ b/pyaedt/workflows/hfss/toolkits_catalog.toml @@ -2,6 +2,13 @@ name = "Antenna Wizard" script = "ansys/aedt/toolkits/antenna/run_toolkit.py" icon = "images/large/antenna.png" -template = "Run_PyAEDT_Toolkit_Script" +template = "run_pyaedt_toolkit_script" pip = "git+https://github.com/ansys/pyaedt-antenna-toolkit.git" package = "ansys.aedt.toolkits.antenna" + +[PushfromTransient] +name = "Push Excitation from transient data" +script = "push_excitation_from_file.py" +icon = "images/large/push.png" +template = "run_pyaedt_toolkit_script" +pip = "" diff --git a/pyaedt/workflows/hfss3dlayout/export_to_3D.py b/pyaedt/workflows/hfss3dlayout/export_to_3D.py index 3749cd1ec3a..3ed6a24ae4a 100644 --- a/pyaedt/workflows/hfss3dlayout/export_to_3D.py +++ b/pyaedt/workflows/hfss3dlayout/export_to_3D.py @@ -18,6 +18,9 @@ from pyaedt import Maxwell3d from pyaedt import Q3d import pyaedt.workflows.hfss3dlayout +from pyaedt.workflows.misc import get_aedt_version +from pyaedt.workflows.misc import get_port +from pyaedt.workflows.misc import get_process_id master = Tk() @@ -64,14 +67,17 @@ def callback(): suffixes = {"Export to HFSS": "HFSS", "Export to Q3D": "Q3D", "Export to Maxwell 3D": "M3D", "Export to Icepak": "IPK"} -if "PYAEDT_SCRIPT_PORT" in os.environ and "PYAEDT_SCRIPT_VERSION" in os.environ: - port = int(os.environ["PYAEDT_SCRIPT_PORT"]) - version = os.environ["PYAEDT_SCRIPT_VERSION"] -else: - port = 0 - version = "2024.1" - -with Desktop(new_desktop_session=False, close_on_exit=False, specified_version=version, port=port) as d: +port = get_port() +version = get_aedt_version() +aedt_process_id = get_process_id() + +with Desktop( + new_desktop_session=False, + close_on_exit=False, + specified_version=version, + port=port, + aedt_process_id=aedt_process_id, +) as d: proj = d.active_project() des = d.active_design() projname = proj.GetName() diff --git a/pyaedt/workflows/hfss3dlayout/toolkits_catalog.toml b/pyaedt/workflows/hfss3dlayout/toolkits_catalog.toml index 071b7ee4269..733215cff34 100644 --- a/pyaedt/workflows/hfss3dlayout/toolkits_catalog.toml +++ b/pyaedt/workflows/hfss3dlayout/toolkits_catalog.toml @@ -2,5 +2,5 @@ name = "Export to 3D" script = "export_to_3D.py" icon = "images/large/cad3d.png" -template = "Run_PyAEDT_Toolkit_Script" +template = "run_pyaedt_toolkit_script" pip = "" diff --git a/pyaedt/workflows/installer/toolkit_manager.py b/pyaedt/workflows/installer/extension_manager.py similarity index 90% rename from pyaedt/workflows/installer/toolkit_manager.py rename to pyaedt/workflows/installer/extension_manager.py index 47a08bc0a5f..59ad4f49ac9 100644 --- a/pyaedt/workflows/installer/toolkit_manager.py +++ b/pyaedt/workflows/installer/extension_manager.py @@ -34,27 +34,37 @@ from pyaedt.workflows.customize_automation_tab import add_script_to_menu from pyaedt.workflows.customize_automation_tab import available_toolkits from pyaedt.workflows.customize_automation_tab import remove_script_from_menu +from pyaedt.workflows.misc import get_aedt_version +from pyaedt.workflows.misc import get_port +from pyaedt.workflows.misc import get_process_id +from pyaedt.workflows.misc import is_student import pyaedt.workflows.templates -env_vars = ["PYAEDT_SCRIPT_VERSION", "PYAEDT_SCRIPT_PORT", "PYAEDT_STUDENT_VERSION"] -if all(var in os.environ for var in env_vars): - version = os.environ["PYAEDT_SCRIPT_VERSION"] - port = int(os.environ["PYAEDT_SCRIPT_PORT"]) - student_version = False if os.environ["PYAEDT_STUDENT_VERSION"] == "False" else True -else: - version = "241" - port = 0 - student_version = False +port = get_port() +version = get_aedt_version() +aedt_process_id = get_process_id() +student_version = is_student() + +# Set Python version based on AEDT version +python_version = "3.10" if version > "2023.1" else "3.7" + +VENV_DIR_PREFIX = ".pyaedt_env" if is_windows: - venv_dir = os.path.join(os.environ["APPDATA"], "pyaedt_env_ide", "toolkits_v{}".format(version)) + venv_dir = os.path.join( + os.environ["APPDATA"], VENV_DIR_PREFIX, "toolkits_{}".format(python_version.replace(".", "_")) + ) python_exe = os.path.join(venv_dir, "Scripts", "python.exe") package_dir = os.path.join(venv_dir, "Lib", "site-packages") + pyaedt_venv_dir = os.path.join( + os.environ["APPDATA"], VENV_DIR_PREFIX, "{}".format(python_version.replace(".", "_")) + ) else: - venv_dir = os.path.join(os.environ["HOME"], "pyaedt_env_ide", "toolkits_v{}".format(version)) + venv_dir = os.path.join(os.environ["HOME"], VENV_DIR_PREFIX, "toolkits_{}".format(python_version.replace(".", "_"))) python_exe = os.path.join(venv_dir, "bin", "python") package_dir = os.path.join(venv_dir, "lib", "site-packages") + pyaedt_venv_dir = os.path.join(os.environ["HOME"], VENV_DIR_PREFIX, "{}".format(python_version.replace(".", "_"))) def create_toolkit_page(frame, window_name, internal_toolkits): @@ -74,7 +84,7 @@ def create_toolkit_page(frame, window_name, internal_toolkits): offline_installation_radio.grid(row=1, column=1, padx=5, pady=5) # Combobox with available toolkit options - toolkits_combo_label = tk.Label(frame, text="Toolkit:", width=max_length) + toolkits_combo_label = tk.Label(frame, text="Extension:", width=max_length) toolkits_combo_label.grid(row=2, column=0, padx=5, pady=5) toolkits_combo = ttk.Combobox( @@ -111,7 +121,9 @@ def update_page(event=None): if selected_toolkit == "Custom" or not selected_toolkit_info.get("pip"): install_button.config(text="Install") uninstall_button.config(state="normal") + toolkits_combo_label.config(text="Extension") else: + toolkits_combo_label.config(text="Toolkit") if is_toolkit_installed(selected_toolkit, window_name): install_button.config(text="Update") uninstall_button.config(state="normal") @@ -234,6 +246,7 @@ def button_is_clicked( non_graphical=False, close_on_exit=False, student_version=student_version, + aedt_process_id=aedt_process_id, ) desktop.odesktop.CloseAllWindows() @@ -274,10 +287,8 @@ def button_is_clicked( if install_action: desktop.logger.info("Install {}".format(name)) if is_windows: - pyaedt_venv_dir = os.path.join(os.environ["APPDATA"], "pyaedt_env_ide", "v{}".format(version)) executable_interpreter = os.path.join(pyaedt_venv_dir, "Scripts", "python.exe") else: - pyaedt_venv_dir = os.path.join(os.environ["HOME"], "pyaedt_env_ide", "v{}".format(version)) executable_interpreter = os.path.join(pyaedt_venv_dir, "bin", "python") if not file: file = os.path.join(os.path.dirname(pyaedt.workflows.templates.__file__), "toolkit_template.py") @@ -304,7 +315,7 @@ def button_is_clicked( root = tk.Tk() -root.title("AEDT Toolkit Manager") +root.title("Extension Manager") # Load the logo for the main window icon_path = os.path.join(os.path.dirname(pyaedt.workflows.__file__), "images", "large", "logo.png") @@ -333,7 +344,7 @@ def button_is_clicked( "Mechanical", "Circuit", "EMIT", - "Simplorer", + "TwinBuilder", "", ] @@ -351,7 +362,10 @@ def button_is_clicked( col_num = i % 4 if level: toolkit_button = ttk.Button( - root, text=level, command=lambda l=level: toolkit_window(l), style="Toolbutton.TButton" + root, + text=level, + command=lambda toolkit_level=level: toolkit_window(toolkit_level), + style="Toolbutton.TButton", ) toolkit_button.grid(row=row_num, column=col_num, padx=10, pady=10) diff --git a/pyaedt/workflows/installer/toolkits_catalog.toml b/pyaedt/workflows/installer/extensions_catalog.toml similarity index 53% rename from pyaedt/workflows/installer/toolkits_catalog.toml rename to pyaedt/workflows/installer/extensions_catalog.toml index a5baf22c440..bb054192289 100644 --- a/pyaedt/workflows/installer/toolkits_catalog.toml +++ b/pyaedt/workflows/installer/extensions_catalog.toml @@ -2,22 +2,22 @@ name = "PyAEDT Console" script = "console_setup.py" icon = "console.png" -template = "PyAEDT_Console" +template = "pyaedt_console" [Jupyter] name = "Jupyter Notebook" script = "jupyter_template.ipynb" icon = "jupyter.png" -template = "Jupyter" +template = "jupyter" [Run_Script] name = "Run PyAEDT Script" script = "" icon = "run_script.png" -template = "Run_PyAEDT_Script" +template = "run_pyaedt_script" -[ToolkitManager] -name = "Toolkit Manager" -script = "toolkit_manager.py" -icon = "toolkit_manager.png" -template = "Run_Toolkit_Manager" +[ExtensionManager] +name = "Extension Manager" +script = "extension_manager.py" +icon = "extension_manager.png" +template = "run_extension_manager" diff --git a/pyaedt/workflows/installer/images/large/toolkit_manager.png b/pyaedt/workflows/installer/images/large/extension_manager.png similarity index 100% rename from pyaedt/workflows/installer/images/large/toolkit_manager.png rename to pyaedt/workflows/installer/images/large/extension_manager.png diff --git a/pyaedt/workflows/installer/pyaedt_installer.py b/pyaedt/workflows/installer/pyaedt_installer.py index 64ca94211e5..8d7a5ef0d63 100644 --- a/pyaedt/workflows/installer/pyaedt_installer.py +++ b/pyaedt/workflows/installer/pyaedt_installer.py @@ -22,15 +22,18 @@ """Methods to add PyAEDT in AEDT.""" +import logging import os +import shutil from pyaedt.generic.general_methods import read_toml from pyaedt.workflows import customize_automation_tab +import pyaedt.workflows.templates def add_pyaedt_to_aedt( aedt_version, - personallib, + personal_lib, ): """Add PyAEDT tabs in AEDT. @@ -38,32 +41,51 @@ def add_pyaedt_to_aedt( ---------- aedt_version : str AEDT release. - personallib : str - AEDT Personal Lib folder. + personal_lib : str + AEDT personal library folder. """ - __add_pyaedt_tabs(personallib, aedt_version) + logger = logging.getLogger("Global") + if not personal_lib or not aedt_version: + from pyaedt.generic.desktop_sessions import _desktop_sessions + if not _desktop_sessions: + logger.error("'personal_lib' or AEDT version is not provided. There is no available desktop session.") + return False + d = list(_desktop_sessions.values())[0] + personal_lib = d.personallib + aedt_version = d.aedt_version_id -def __add_pyaedt_tabs(personallib, aedt_version): + extensions_dir = os.path.join(personal_lib, "Toolkits") + os.makedirs(extensions_dir, exist_ok=True) + + templates_dir = os.path.dirname(pyaedt.workflows.templates.__file__) + script_file = os.path.join(templates_dir, "pyaedt_utils.py") + dest_script_path = os.path.join(extensions_dir, "pyaedt_utils.py") + shutil.copy2(script_file, dest_script_path) + + __add_pyaedt_tabs(personal_lib, aedt_version) + + +def __add_pyaedt_tabs(personal_lib, aedt_version): """Add PyAEDT tabs in AEDT.""" - pyaedt_tabs = ["Console", "Jupyter", "Run_Script", "ToolkitManager"] + pyaedt_tabs = ["Console", "Jupyter", "Run_Script", "ExtensionManager"] - toolkits_catalog = read_toml(os.path.join(os.path.dirname(__file__), "toolkits_catalog.toml")) + extensions_catalog = read_toml(os.path.join(os.path.dirname(__file__), "extensions_catalog.toml")) project_workflows_dir = os.path.dirname(__file__) - for toolkit in pyaedt_tabs: - if toolkit in toolkits_catalog.keys(): - toolkit_info = toolkits_catalog[toolkit] + for extension in pyaedt_tabs: + if extension in extensions_catalog.keys(): + extension_info = extensions_catalog[extension] script_path = None - if toolkit_info["script"]: - script_path = os.path.join(project_workflows_dir, toolkit_info["script"]) - icon_file = os.path.join(project_workflows_dir, "images", "large", toolkit_info["icon"]) - template_name = toolkit_info["template"] + if extension_info["script"]: + script_path = os.path.join(project_workflows_dir, extension_info["script"]) + icon_file = os.path.join(project_workflows_dir, "images", "large", extension_info["icon"]) + template_name = extension_info["template"] customize_automation_tab.add_script_to_menu( - toolkit_info["name"], + extension_info["name"], script_path, template_name, icon_file=icon_file, @@ -71,6 +93,6 @@ def __add_pyaedt_tabs(personallib, aedt_version): copy_to_personal_lib=True, executable_interpreter=None, panel="Panel_PyAEDT_Installer", - personal_lib=personallib, + personal_lib=personal_lib, aedt_version=aedt_version, ) diff --git a/pyaedt/workflows/maxwell3d/toolkits_catalog.toml b/pyaedt/workflows/maxwell3d/toolkits_catalog.toml index 1b403d6e8df..ac0264e3b95 100644 --- a/pyaedt/workflows/maxwell3d/toolkits_catalog.toml +++ b/pyaedt/workflows/maxwell3d/toolkits_catalog.toml @@ -2,6 +2,6 @@ name = "Magnet Segmentation Wizard" script = "ansys/aedt/toolkits/magnet_segmentation/run_toolkit.py" icon = "images/large/magnet_segmentation.png" -template = "Run_PyAEDT_Toolkit_Script" +template = "run_pyaedt_toolkit_script" pip = "ansys-magnet-segmentation-toolkit" package = "ansys-magnet-segmentation-toolkit" diff --git a/pyaedt/workflows/misc.py b/pyaedt/workflows/misc.py new file mode 100644 index 00000000000..f57f1360042 --- /dev/null +++ b/pyaedt/workflows/misc.py @@ -0,0 +1,59 @@ +# Copyright (C) 2023 - 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. + +"""Miscellaneous Methods for PyAEDT workflows.""" + +import os + +from pyaedt.misc import current_version + + +def get_process_id(): + """Get process ID from environment variable.""" + aedt_process_id = None + if os.getenv("PYAEDT_SCRIPT_PROCESS_ID", None): + aedt_process_id = int(os.getenv("PYAEDT_SCRIPT_PROCESS_ID")) + return aedt_process_id + + +def get_port(): + """Get GRPC port from environment variable.""" + port = 0 + if "PYAEDT_SCRIPT_PORT" in os.environ: + port = int(os.environ["PYAEDT_SCRIPT_PORT"]) + return port + + +def get_aedt_version(): + """Get AEDT release from environment variable.""" + version = current_version() + if "PYAEDT_SCRIPT_VERSION" in os.environ: + version = os.environ["PYAEDT_SCRIPT_VERSION"] + return version + + +def is_student(): + """Get if AEDT student is opened from environment variable.""" + student_version = False + if "PYAEDT_STUDENT_VERSION" in os.environ: + student_version = False if os.environ["PYAEDT_STUDENT_VERSION"] == "False" else True + return student_version diff --git a/pyaedt/workflows/project/create_report.py b/pyaedt/workflows/project/create_report.py index f6d49ded21b..940d6b3787a 100644 --- a/pyaedt/workflows/project/create_report.py +++ b/pyaedt/workflows/project/create_report.py @@ -6,15 +6,21 @@ from pyaedt import Desktop from pyaedt import get_pyaedt_app from pyaedt.generic.pdf import AnsysReport +from pyaedt.workflows.misc import get_aedt_version +from pyaedt.workflows.misc import get_port +from pyaedt.workflows.misc import get_process_id -if "PYAEDT_SCRIPT_PORT" in os.environ and "PYAEDT_SCRIPT_VERSION" in os.environ: - port = int(os.environ["PYAEDT_SCRIPT_PORT"]) - version = os.environ["PYAEDT_SCRIPT_VERSION"] -else: - port = 0 - version = "2024.1" +port = get_port() +version = get_aedt_version() +aedt_process_id = get_process_id() -with Desktop(new_desktop_session=False, close_on_exit=False, specified_version=version, port=port) as d: +with Desktop( + new_desktop_session=False, + close_on_exit=False, + specified_version=version, + port=port, + aedt_process_id=aedt_process_id, +) as d: proj = d.active_project() des = d.active_design() diff --git a/pyaedt/workflows/project/import_nastran.py b/pyaedt/workflows/project/import_nastran.py index b9e9f4abebf..a9165fd8236 100644 --- a/pyaedt/workflows/project/import_nastran.py +++ b/pyaedt/workflows/project/import_nastran.py @@ -1,39 +1,140 @@ import os.path +from tkinter import Button +from tkinter import Checkbutton # import filedialog module +from tkinter import END +from tkinter import IntVar +from tkinter import Label +from tkinter import StringVar +from tkinter import Text +from tkinter import Tk from tkinter import filedialog +from tkinter import mainloop +from tkinter import ttk + +import PIL.Image +import PIL.ImageTk from pyaedt import Desktop from pyaedt import get_pyaedt_app +import pyaedt.workflows +from pyaedt.workflows.misc import get_aedt_version +from pyaedt.workflows.misc import get_port +from pyaedt.workflows.misc import get_process_id +decimate = 0.0 -# Function for opening the -# file explorer window -def browseFiles(): - filename = filedialog.askopenfilename( - initialdir="/", title="Select a File", filetypes=(("Nastran files", "*.nas*"), ("all files", "*.*")) - ) +lightweight = False +file_path = "" - # Change label contents - return filename +def browse_nastran(): + # Function for opening the + # file explorer window -nas_input = browseFiles() -if "PYAEDT_SCRIPT_PORT" in os.environ and "PYAEDT_SCRIPT_VERSION" in os.environ: - port = int(os.environ["PYAEDT_SCRIPT_PORT"]) - version = os.environ["PYAEDT_SCRIPT_VERSION"] -else: - port = 0 - version = "2024.1" -if os.path.exists(nas_input): - with Desktop(new_desktop_session=False, close_on_exit=False, specified_version=version, port=port) as d: + master = Tk() + + master.geometry("700x200") + + master.title("Import Nastran or STL file") + + # Load the logo for the main window + icon_path = os.path.join(pyaedt.workflows.__path__[0], "images", "large", "logo.png") + im = PIL.Image.open(icon_path) + photo = PIL.ImageTk.PhotoImage(im) + + # Set the icon for the main window + master.iconphoto(True, photo) + + # Configure style for ttk buttons + style = ttk.Style() + style.configure("Toolbutton.TButton", padding=6, font=("Helvetica", 8)) + + var = StringVar() + label = Label(master, textvariable=var) + var.set("Decimation factor (0-0.9). It may affect results:") + label.grid(row=0, column=0, pady=10) + check = Text(master, width=20, height=1) # Set the width of the combobox + check.insert(END, "0.0") + check.grid(row=0, column=1, pady=10, padx=5) + var = StringVar() + label = Label(master, textvariable=var) + var.set("Import as lightweight (only HFSS):") + label.grid(row=1, column=0, pady=10) + light = IntVar() + check2 = Checkbutton(master, width=30, variable=light) # Set the width of the combobox + check2.grid(row=1, column=1, pady=10, padx=5) + var2 = StringVar() + label2 = Label(master, textvariable=var2) + var2.set("Browse file:") + label2.grid(row=2, column=0, pady=10) + text = Text(master, width=40, height=1) + text.grid(row=2, column=1, pady=10, padx=5) + + def browseFiles(): + filename = filedialog.askopenfilename( + initialdir="/", + title="Select a Nastran or stl File", + filetypes=(("Nastran", "*.nas"), ("STL", "*.stl"), ("all files", "*.*")), + ) + text.insert(END, filename) + # # Change label contents + # return filename + + b1 = Button(master, text="...", width=10, command=browseFiles) + b1.grid(row=3, column=0) + # b1.pack(pady=10) + b1.grid(row=2, column=2, pady=10) + + def callback(): + global lightweight, decimate, file_path + decimate = float(check.get("1.0", END).strip()) + lightweight = True if light.get() == 1 else False + file_path = text.get("1.0", END).strip() + master.destroy() + return True + + b = Button(master, text="Ok", width=40, command=callback) + # b.pack(pady=10) + b.grid(row=3, column=1, pady=10) + + mainloop() + + +browse_nastran() + +port = get_port() +version = get_aedt_version() +aedt_process_id = get_process_id() + +if os.path.exists(file_path): + with Desktop( + new_desktop_session=False, + close_on_exit=False, + specified_version=version, + port=port, + aedt_process_id=aedt_process_id, + ) as d: proj = d.active_project() des = d.active_design() projname = proj.GetName() desname = des.GetName() app = get_pyaedt_app(projname, desname) - app.modeler.import_nastran(nas_input) + if file_path.endswith(".nas"): + app.modeler.import_nastran(file_path, import_as_light_weight=lightweight, decimation=decimate) + else: + from pyaedt.modules.solutions import simplify_stl + + outfile = simplify_stl(file_path, decimation=decimate, aggressiveness=5) + app.modeler.import_3d_cad(outfile, healing=False, create_lightweigth_part=lightweight) d.logger.info("Nastran imported correctly.") else: - with Desktop(new_desktop_session=False, close_on_exit=False, specified_version=version, port=port) as d: + with Desktop( + new_desktop_session=False, + close_on_exit=False, + specified_version=version, + port=port, + aedt_process_id=aedt_process_id, + ) as d: d.odesktop.AddMessage("", "", 3, "Wrong file selected. Select a .nas file") diff --git a/pyaedt/workflows/project/toolkits_catalog.toml b/pyaedt/workflows/project/toolkits_catalog.toml index 0706b1685e5..60ece1bdd81 100644 --- a/pyaedt/workflows/project/toolkits_catalog.toml +++ b/pyaedt/workflows/project/toolkits_catalog.toml @@ -2,12 +2,12 @@ name = "Generate report" script = "create_report.py" icon = "images/large/pdf.png" -template = "Run_PyAEDT_Toolkit_Script" +template = "run_pyaedt_toolkit_script" pip = "" [ImportNastran] -name = "Import Nastran" +name = "Import Nastran/STL" script = "import_nastran.py" icon = "images/large/cad3d.png" -template = "Run_PyAEDT_Toolkit_Script" +template = "run_pyaedt_toolkit_script" pip = "" diff --git a/pyaedt/workflows/templates/Jupyter.py_build b/pyaedt/workflows/templates/Jupyter.py_build deleted file mode 100644 index 4aa2674ff55..00000000000 --- a/pyaedt/workflows/templates/Jupyter.py_build +++ /dev/null @@ -1,93 +0,0 @@ -# -*- coding: utf-8 -*- -""" -* * * This script launches a Jupyter Notebook. * * * - -This script makes a copy of the referenced Jupyter Notebook script, ``Lib/jupyter_template.ipynb``, to the project -directory and launches it. - -""" -import os -import random -import string -import sys - -from System.Windows.Forms import MessageBox -from System.Windows.Forms import MessageBoxButtons -from System.Windows.Forms import MessageBoxIcon - -is_linux = os.name == "posix" - -if is_linux: - import subprocessdotnet as subprocess -else: - import subprocess - - -def main(): - # Launch file - proj_dir = oDesktop.GetProjectDirectory() - os.chdir(proj_dir) - - version = oDesktop.GetVersion()[2:6].replace(".", "") - current_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) - pyaedt_toolkit_dir = os.path.normpath(os.path.join(current_dir, r"##TOOLKIT_REL_LIB_DIR##")) - jupyter_exe = r"##JUPYTER_EXE##" % version - template = os.path.join(pyaedt_toolkit_dir, "jupyter_template.ipynb") - target = os.path.join(proj_dir, generate_unique_name("pyaedt", ".ipynb", n=3)) - check_file(jupyter_exe) - check_file(template) - notebook_dir = os.environ.get("AEDT_NOTEBOOK_DIR",None) - - with open(template, "r") as source: - with open(target, "w") as t: - for line in source: - line = line.replace("PROCESSID", str(oDesktop.GetProcessID())).replace( - "AEDTVERSION", oDesktop.GetVersion()[:6] - ) - t.write(line) - if is_linux: - edt_root = os.path.normpath(oDesktop.GetExeDir()) - os.environ["ANSYSEM_ROOT{}".format(version)] = edt_root - ld_library_path_dirs_to_add = [ - "{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), - "{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), - "{}/common/mono/Linux64/lib64".format(edt_root), - "{}/Delcross".format(edt_root), - "{}".format(edt_root), - ] - os.environ["LD_LIBRARY_PATH"] = ":".join(ld_library_path_dirs_to_add) + ":" + os.getenv("LD_LIBRARY_PATH", "") - if notebook_dir: - command = [jupyter_exe, "lab", target,"--notebook-dir",notebook_dir] - else: - command = [jupyter_exe, "lab", target] - subprocess.Popen(command) - else: - if notebook_dir: - command = ['"{}"'.format(jupyter_exe), "lab", '"{}"'.format(target),"--notebook-dir",'"{0}"'.format(notebook_dir)] - else: - command = ['"{}"'.format(jupyter_exe), "lab", '"{}"'.format(target)] - subprocess.Popen(" ".join(command)) - - -def generate_unique_name(rootname, suffix="", n=6): - char_set = string.ascii_uppercase + string.digits - unique_name = rootname + "_" + "".join(random.choice(char_set) for _ in range(n)) - if suffix: - unique_name += suffix - return unique_name - - -def check_file(file_path): - if not os.path.isfile(file_path): - show_error('"{}" does not exist. Please click on the "Install PyAEDT" button in the Automation ribbon.'.format( - file_path)) - - -def show_error(msg): - oDesktop.AddMessage("", "", 2, str(msg)) - MessageBox.Show(str(msg), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) - sys.exit() - - -if __name__ == "__main__": - main() diff --git a/pyaedt/workflows/templates/PyAEDT_Console.py_build b/pyaedt/workflows/templates/PyAEDT_Console.py_build deleted file mode 100644 index fb80f53afff..00000000000 --- a/pyaedt/workflows/templates/PyAEDT_Console.py_build +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- -""" -* * * This script is meant to run in IronPython within AEDT. * * * - -It looks for a reference to a Python interpreter in the ``python_interpreter.bat`` file. - -This script executes the CPython script ``Lib/console_setup.py``. -The ``console_setup.py`` script sets up the HFSS variable based on the command line arguments and kicks off an -interactive Python session. - -""" -import os -import sys - -from System.Windows.Forms import MessageBox -from System.Windows.Forms import MessageBoxButtons -from System.Windows.Forms import MessageBoxIcon - -is_linux = os.name == "posix" - -if is_linux: - import subprocessdotnet as subprocess -else: - import subprocess - - -def main(): - # Launch file - version = oDesktop.GetVersion()[2:6].replace(".", "") - current_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) - pyaedt_toolkit_dir = os.path.normpath(os.path.join(current_dir, r"##TOOLKIT_REL_LIB_DIR##")) - python_exe = r"##IPYTHON_EXE##" % version - pyaedt_script = os.path.join(pyaedt_toolkit_dir, "console_setup.py") - check_file(python_exe) - check_file(pyaedt_script) - if is_linux: - term = get_linux_terminal() - if not term: - show_error("No Terminals found.") - edt_root = os.path.normpath(oDesktop.GetExeDir()) - os.environ["ANSYSEM_ROOT{}".format(version)] = edt_root - ld_library_path_dirs_to_add = [ - "{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), - "{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), - "{}/common/mono/Linux64/lib64".format(edt_root), - "{}/Delcross".format(edt_root), - "{}".format(edt_root), - ] - os.environ["LD_LIBRARY_PATH"] = ":".join(ld_library_path_dirs_to_add) + ":" + os.getenv("LD_LIBRARY_PATH", "") - - command = [ - term, - "-e", - python_exe, - "-i", - pyaedt_script, - str(oDesktop.GetProcessID()), - str(oDesktop.GetVersion()[:6]), - ] - subprocess.Popen(command) - else: - command = [ - '"{}"'.format(python_exe), - "-i", - '"{}"'.format(pyaedt_script), - str(oDesktop.GetProcessID()), - str(oDesktop.GetVersion()[:6]), - ] - subprocess.Popen(" ".join(command)) - - -def get_linux_terminal(): - for terminal in ["x-terminal-emulator", "konsole", "xterm", "gnome-terminal", "lxterminal", "mlterm"]: - term = which(terminal) - if term: - return term - return None - - -def which(program): - # http://stackoverflow.com/a/377028 - def is_exe(fpath): - return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - - fpath, fname = os.path.split(program) - if fpath: - if is_exe(program): - return program - else: - for path in os.environ["PATH"].split(os.pathsep): - path = path.strip('"') - exe_file = os.path.join(path, program) - if is_exe(exe_file): - return exe_file - - return None - - -def check_file(file_path): - if not os.path.isfile(file_path): - show_error('"{}" does not exist. Please click on the "Install PyAEDT" button in the Automation ribbon.'.format( - file_path)) - - -def show_error(msg): - oDesktop.AddMessage("", "", 2, str(msg)) - MessageBox.Show(str(msg), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) - sys.exit() - - -if __name__ == "__main__": - main() diff --git a/pyaedt/workflows/templates/Run_PyAEDT_Script.py_build b/pyaedt/workflows/templates/Run_PyAEDT_Script.py_build deleted file mode 100644 index 9913717274e..00000000000 --- a/pyaedt/workflows/templates/Run_PyAEDT_Script.py_build +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- -""" -* * * This script is meant to run in IronPython within AEDT. * * * -The script provides for choosing the Python script to execute. - -It looks for a reference to a Python interpreter in the ``python_interpreter.bat`` file. - -It then uses this Python interpreter to execute the script. -See the declaration of the command variable to see the order in which arguments are passed to the script. - -The commands allow the launched script to still reference the project and design that was active when the script -was launched as well as the AEDT instance that has them open. - -""" -import os -import sys - -from System.Windows.Forms import MessageBox -from System.Windows.Forms import MessageBoxButtons -from System.Windows.Forms import MessageBoxIcon -from System.Windows.Forms import OpenFileDialog - -is_linux = os.name == "posix" -script_name = os.path.splitext(os.path.basename(__file__))[0] - -if is_linux: - import subprocessdotnet as subprocess -else: - import subprocess - - -def main(): - oProject = oDesktop.GetActiveProject() - - # Choose file to launch - file_dialog = OpenFileDialog() - file_dialog.InitialDirectory = oProject.GetPath() - file_dialog.Filter = "python files (*.py)|*.py" - ret = file_dialog.ShowDialog() - debug("ret: " + repr(ret)) - - try: - if ret == ret.OK: - # launch file - version = oDesktop.GetVersion()[2:6].replace(".", "") - python_exe = r"##PYTHON_EXE##" % version - pyaedt_script = file_dialog.FileName - check_file(python_exe) - check_file(pyaedt_script) - os.environ["PYAEDT_SCRIPT_PROCESS_ID"] = str(oDesktop.GetProcessID()) - version = str(oDesktop.GetVersion()[:6]) - os.environ["PYAEDT_SCRIPT_VERSION"] = version - if version > "2022.2": - os.environ["PYAEDT_SCRIPT_PORT"] = str(oDesktop.GetGrpcServerPort()) - if is_linux: - edt_root = os.path.normpath(oDesktop.GetExeDir()) - os.environ["ANSYSEM_ROOT{}".format(version)] = edt_root - ld_library_path_dirs_to_add = [ - "{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), - "{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), - "{}/common/mono/Linux64/lib64".format(edt_root), - "{}/Delcross".format(edt_root), - "{}".format(edt_root), - ] - os.environ["LD_LIBRARY_PATH"] = ":".join(ld_library_path_dirs_to_add) + ":" + os.getenv( - "LD_LIBRARY_PATH", "") - command = [ - python_exe, - pyaedt_script, - str(oDesktop.GetProcessID()), - str(oDesktop.GetVersion()[:6]), - ] - my_env = os.environ.copy() - subprocess.Popen(command, env=my_env) - else: - command = [ - '"{}"'.format(python_exe), - '"{}"'.format(pyaedt_script), - str(oDesktop.GetProcessID()), - str(oDesktop.GetVersion()[:6]), - ] - my_env = os.environ.copy() - subprocess.Popen(" ".join(command), env=my_env) - else: - debug("ret didn't pass the equivalence. ret:" + repr(ret)) - except Exception as e: - show_error(str(e)) - - -def check_file(file_path): - if not os.path.isfile(file_path): - show_error('"{}" does not exist. Please click on the "Install PyAEDT" button in the Automation ribbon.'.format( - file_path)) - - -def show_error(msg): - oDesktop.AddMessage("", "", 2, str(msg)) - MessageBox.Show(str(msg), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) - sys.exit() - - -def debug(msg): - print("[debug] {}: {}".format(script_name, str(msg))) - LogDebug("{}: {}\n".format(script_name, str(msg))) - - -if __name__ == "__main__": - main() diff --git a/pyaedt/workflows/templates/Run_PyAEDT_Toolkit_Script.py_build b/pyaedt/workflows/templates/Run_PyAEDT_Toolkit_Script.py_build deleted file mode 100644 index ce406fd37e1..00000000000 --- a/pyaedt/workflows/templates/Run_PyAEDT_Toolkit_Script.py_build +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- -""" -* * * This script is meant to run in IronPython within AEDT. * * * -The script provides for choosing the Python script to execute. - -It looks for a reference to a Python interpreter in the ``python_interpreter.bat`` file. - -It then uses this Python interpreter to execute the script. -See the declaration of the command variable to see the order in which arguments are passed to the script. - -The commands allow the launched script to still reference the project and design that was active when the script -was launched as well as the AEDT instance that has them open. - -""" -import os -import sys - -from System.Windows.Forms import MessageBox -from System.Windows.Forms import MessageBoxButtons -from System.Windows.Forms import MessageBoxIcon - -is_linux = os.name == "posix" -script_name = os.path.splitext(os.path.basename(__file__))[0] - -if is_linux: - import subprocessdotnet as subprocess - -def main(): - try: - oDesktop.AddMessage("", "", 0, "Toolkit launched. Please wait.") - # launch file - python_exe = r"##PYTHON_EXE##" - pyaedt_script = r"##PYTHON_SCRIPT##" - check_file(python_exe) - check_file(pyaedt_script) - os.environ["PYAEDT_SCRIPT_PROCESS_ID"] = str(oDesktop.GetProcessID()) - version = str(oDesktop.GetVersion()[:6]) - os.environ["PYAEDT_SCRIPT_VERSION"] = version - if version > "2022.2": - os.environ["PYAEDT_SCRIPT_PORT"] = str(oDesktop.GetGrpcServerPort()) - - if is_linux: - edt_root = os.path.normpath(oDesktop.GetExeDir()) - os.environ["ANSYSEM_ROOT{}".format(version)] = edt_root - ld_library_path_dirs_to_add = [ - "{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), - "{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), - "{}/common/mono/Linux64/lib64".format(edt_root), - "{}/Delcross".format(edt_root), - "{}".format(edt_root), - ] - os.environ["LD_LIBRARY_PATH"] = ":".join(ld_library_path_dirs_to_add) + ":" + os.getenv( - "LD_LIBRARY_PATH", "") - - if version > "2023.1": - os.environ["TCL_LIBRARY"] = os.path.join("{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), "tcl8.5") - os.environ["TK_LIBRARY"] = os.path.join("{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), "tk8.5") - os.environ["TKPATH"] = os.path.join("{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), "tk8.5") - else: - os.environ["TCL_LIBRARY"] = os.path.join("{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), "tcl8.5") - os.environ["TK_LIBRARY"] = os.path.join("{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), "tk8.5") - os.environ["TKPATH"] = os.path.join("{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), "tk8.5") - - my_env = dict(os.environ.copy()) - command = [python_exe, pyaedt_script] - subprocess.Popen(command, env=my_env) - else: - toolkit_name = r"##TOOLKIT_NAME##" - runme = "run_pyaedt_toolkit_{0}.cmd".format(toolkit_name) - edt_temp = oDesktop.GetTempDirectory() - cmd = '"{0}"'.format(pyaedt_script) - target = os.path.join(edt_temp, runme) - my_env = dict(os.environ.copy()) - with open(target, 'w') as cmdfile: - for env in my_env: - cmdfile.write('SET {0}={1}\n'.format(env, my_env[env])) - cmdfile.write('"{0}" {1}\n'.format(python_exe, cmd)) - oDesktop.RunProgram(runme, edt_temp, edt_temp, "") - except Exception as e: - show_error(str(e)) - - -def check_file(file_path): - if not os.path.isfile(file_path): - show_error('"{}" does not exist.'.format( - file_path)) - - -def show_error(msg): - oDesktop.AddMessage("", "", 2, str(msg)) - MessageBox.Show(str(msg), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) - sys.exit() - - -if __name__ == "__main__": - main() diff --git a/pyaedt/workflows/templates/Run_Toolkit_Manager.py_build b/pyaedt/workflows/templates/Run_Toolkit_Manager.py_build deleted file mode 100644 index 4bebbdba872..00000000000 --- a/pyaedt/workflows/templates/Run_Toolkit_Manager.py_build +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -""" -* * * This script is meant to run in IronPython within AEDT. * * * -The script provides for choosing the Python script to execute. - -It looks for a reference to a Python interpreter in the ``python_interpreter.bat`` file. - -It then uses this Python interpreter to execute the script. -See the declaration of the command variable to see the order in which arguments are passed to the script. - -The commands allow the launched script to still reference the project and design that was active when the script -was launched as well as the AEDT instance that has them open. - -""" -import os -import sys - -from System.Windows.Forms import MessageBox -from System.Windows.Forms import MessageBoxButtons -from System.Windows.Forms import MessageBoxIcon -from System.Windows.Forms import OpenFileDialog - -is_linux = os.name == "posix" -script_name = os.path.splitext(os.path.basename(__file__))[0] - -if is_linux: - import subprocessdotnet as subprocess -else: - import subprocess - - -def main(): - try: - version = oDesktop.GetVersion()[2:6].replace(".", "") - # launch toolkit manager - python_exe = r"##PYTHON_EXE##" % version - current_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) - pyaedt_toolkit_dir = os.path.normpath(os.path.join(current_dir, r"##TOOLKIT_REL_LIB_DIR##")) - pyaedt_script = os.path.join(pyaedt_toolkit_dir, "toolkit_manager.py") - check_file(python_exe) - check_file(pyaedt_script) - os.environ["PYAEDT_SCRIPT_PROCESS_ID"] = str(oDesktop.GetProcessID()) - os.environ["PYAEDT_SCRIPT_VERSION"] = version - if "Ansys Student" in str(oDesktop.GetExeDir()): - os.environ["PYAEDT_STUDENT_VERSION"] = "True" - else: - os.environ["PYAEDT_STUDENT_VERSION"] = "False" - if version > "2022.2": - os.environ["PYAEDT_SCRIPT_PORT"] = str(oDesktop.GetGrpcServerPort()) - if is_linux: - - edt_root = os.path.normpath(oDesktop.GetExeDir()) - os.environ["ANSYSEM_ROOT{}".format(version)] = edt_root - ld_library_path_dirs_to_add = [ - "{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), - "{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), - "{}/common/mono/Linux64/lib64".format(edt_root), - "{}/Delcross".format(edt_root), - "{}".format(edt_root), - ] - os.environ["LD_LIBRARY_PATH"] = ":".join(ld_library_path_dirs_to_add) + ":" + os.getenv( - "LD_LIBRARY_PATH", "") - command = [ - python_exe, - pyaedt_script, - ] - my_env = os.environ.copy() - subprocess.Popen(command, env=my_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) - else: - command = [ - '"{}"'.format(python_exe), - '"{}"'.format(pyaedt_script), - ] - my_env = os.environ.copy() - subprocess.Popen(" ".join(command), env=my_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, shell=True) - except Exception as e: - show_error(str(e)) - - -def check_file(file_path): - if not os.path.isfile(file_path): - show_error('"{}" does not exist. Click the "Install PyAEDT" button in the Automation ribbon.'.format( - file_path)) - - -def show_error(msg): - oDesktop.AddMessage("", "", 2, str(msg)) - MessageBox.Show(str(msg), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) - sys.exit() - - -def debug(msg): - print("[debug] {}: {}".format(script_name, str(msg))) - LogDebug("{}: {}\n".format(script_name, str(msg))) - - -if __name__ == "__main__": - main() diff --git a/pyaedt/workflows/templates/jupyter.py_build b/pyaedt/workflows/templates/jupyter.py_build new file mode 100644 index 00000000000..02539325d4e --- /dev/null +++ b/pyaedt/workflows/templates/jupyter.py_build @@ -0,0 +1,97 @@ +# Copyright (C) 2023 - 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 script launches a Jupyter Notebook. * * * + +This script makes a copy of the referenced Jupyter Notebook script, ``Lib/jupyter_template.ipynb``, to the project +directory and launches it. + +""" +import os +import sys + +is_linux = os.name == "posix" + +if is_linux: + import subprocessdotnet as subprocess +else: + import subprocess + +toolkits_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) + +sys.path.append(toolkits_dir) + +import pyaedt_utils + + +def main(): + # Get AEDT version + version_short = oDesktop.GetVersion()[2:6].replace(".", "") + proj_dir = oDesktop.GetProjectDirectory() + os.chdir(proj_dir) + # Extension directory + current_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) + pyaedt_toolkit_dir = os.path.normpath(os.path.join(current_dir, r"##TOOLKIT_REL_LIB_DIR##")) + # Jupyter interpreter + jupyter_exe = r"##JUPYTER_EXE##" % version + # Check if Jupyter version and AEDT release match + python_exe = pyaedt_utils.sanitize_interpreter_path(jupyter_exe, version_short) + template = os.path.join(pyaedt_toolkit_dir, "jupyter_template.ipynb") + target = os.path.join(proj_dir, pyaedt_utils.generate_unique_name("pyaedt", ".ipynb", n=3)) + # Check executable + jupyter_exe_flag = pyaedt_utils.check_file(jupyter_exe, oDesktop) + if not jupyter_exe_flag: + return + pyaedt_script_flag = pyaedt_utils.check_file(template, oDesktop) + if not pyaedt_script_flag: + return + + notebook_dir = os.environ.get("AEDT_NOTEBOOK_DIR",None) + + with open(template, "r") as source: + with open(target, "w") as t: + for line in source: + line = line.replace("PROCESSID", str(oDesktop.GetProcessID())).replace( + "AEDTVERSION", oDesktop.GetVersion()[:6] + ) + t.write(line) + + # Add environment variables + pyaedt_utils.environment_variables(oDesktop) + + if is_linux: + if notebook_dir: + command = [jupyter_exe, "lab", target,"--notebook-dir",notebook_dir] + else: + command = [jupyter_exe, "lab", target] + subprocess.Popen(command) + else: + if notebook_dir: + command = ['"{}"'.format(jupyter_exe), "lab", '"{}"'.format(target),"--notebook-dir",'"{0}"'.format(notebook_dir)] + else: + command = ['"{}"'.format(jupyter_exe), "lab", '"{}"'.format(target)] + subprocess.Popen(" ".join(command)) + + +if __name__ == "__main__": + main() diff --git a/pyaedt/workflows/templates/pyaedt_console.py_build b/pyaedt/workflows/templates/pyaedt_console.py_build new file mode 100644 index 00000000000..7c3053e57f8 --- /dev/null +++ b/pyaedt/workflows/templates/pyaedt_console.py_build @@ -0,0 +1,97 @@ +# Copyright (C) 2023 - 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 script is meant to run in IronPython within AEDT. * * * + +This script executes the CPython script ``Lib/console_setup.py``. +The ``console_setup.py`` launches an interactive Python session. + +""" +import os +import sys + +is_linux = os.name == "posix" + +if is_linux: + import subprocessdotnet as subprocess +else: + import subprocess + +toolkits_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) + +sys.path.append(toolkits_dir) + +import pyaedt_utils + + +def main(): + # Get AEDT version + version_short = oDesktop.GetVersion()[2:6].replace(".", "") + # Extension directory + current_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) + pyaedt_toolkit_dir = os.path.normpath(os.path.join(current_dir, r"##TOOLKIT_REL_LIB_DIR##")) + # CPython interpreter + python_exe = r"##IPYTHON_EXE##" % version + # Check if CPython interpreter and AEDT release match + python_exe = pyaedt_utils.sanitize_interpreter_path(python_exe, version_short) + # Console launcher + pyaedt_script = os.path.join(pyaedt_toolkit_dir, "console_setup.py") + # Check python executable + python_exe_flag = pyaedt_utils.check_file(python_exe, oDesktop) + if not python_exe_flag: + return + # Check console launcher file + pyaedt_script_flag = pyaedt_utils.check_file(pyaedt_script, oDesktop) + if not pyaedt_script_flag: + return + # Add environment variables + pyaedt_utils.environment_variables(oDesktop) + # Call console script + if is_linux: + term = pyaedt_utils.get_linux_terminal() + if not term: + pyaedt_utils.show_error("No Terminals found.", oDesktop) + + command = [ + term, + "-e", + python_exe, + "-i", + pyaedt_script, + str(oDesktop.GetProcessID()), + str(oDesktop.GetVersion()[:6]), + ] + subprocess.Popen(command) + else: + command = [ + '"{}"'.format(python_exe), + "-i", + '"{}"'.format(pyaedt_script), + str(oDesktop.GetProcessID()), + str(oDesktop.GetVersion()[:6]), + ] + subprocess.Popen(" ".join(command)) + + +if __name__ == "__main__": + main() diff --git a/pyaedt/workflows/templates/pyaedt_utils.py b/pyaedt/workflows/templates/pyaedt_utils.py new file mode 100644 index 00000000000..7e26a366cf0 --- /dev/null +++ b/pyaedt/workflows/templates/pyaedt_utils.py @@ -0,0 +1,142 @@ +# Copyright (C) 2023 - 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 script is meant to run in IronPython within AEDT. * * * + +It contains common methods for the PyAEDT panels. +""" +import os +import random +import string +import sys + +from System.Windows.Forms import MessageBox +from System.Windows.Forms import MessageBoxButtons +from System.Windows.Forms import MessageBoxIcon + +is_linux = os.name == "posix" + + +def sanitize_interpreter_path(interpreter_path, version): + python_version = "3_10" if version > "231" else "3_7" + if version > "231" and python_version not in interpreter_path: + interpreter_path = interpreter_path.replace("3_7", "3_10") + elif version <= "231" and python_version not in interpreter_path: + interpreter_path = interpreter_path.replace("3_10", "3_7") + return interpreter_path + + +def check_file(file_path, oDesktop): + if not os.path.isfile(file_path): + show_error( + '"{}" does not exist. Install PyAEDT using the Python script installer from the PyAEDT ' + "documentation.".format(file_path), + oDesktop, + ) + return False + return True + + +def get_linux_terminal(): + for terminal in ["x-terminal-emulator", "konsole", "xterm", "gnome-terminal", "lxterminal", "mlterm"]: + term = which(terminal) + if term: + return term + return None + + +def which(program): + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + + fpath, _ = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + path = path.strip('"') + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + + return None + + +def show_error(msg, oDesktop): + oDesktop.AddMessage("", "", 2, str(msg)) + MessageBox.Show(str(msg), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) + sys.exit() + + +def environment_variables(oDesktop): + os.environ["PYAEDT_SCRIPT_PROCESS_ID"] = str(oDesktop.GetProcessID()) + version = str(oDesktop.GetVersion()[:6]) + os.environ["PYAEDT_SCRIPT_VERSION"] = version + if version > "2023.1": + os.environ["PYAEDT_SCRIPT_PORT"] = str(oDesktop.GetGrpcServerPort()) + else: + os.environ["PYAEDT_SCRIPT_PORT"] = str(0) + if "Ansys Student" in str(oDesktop.GetExeDir()): + os.environ["PYAEDT_STUDENT_VERSION"] = "True" + else: + os.environ["PYAEDT_STUDENT_VERSION"] = "False" + if is_linux: + edt_root = os.path.normpath(oDesktop.GetExeDir()) + os.environ["ANSYSEM_ROOT{}".format(version)] = edt_root + ld_library_path_dirs_to_add = [ + "{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), + "{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), + "{}/common/mono/Linux64/lib64".format(edt_root), + "{}/Delcross".format(edt_root), + "{}".format(edt_root), + ] + os.environ["LD_LIBRARY_PATH"] = ":".join(ld_library_path_dirs_to_add) + ":" + os.getenv("LD_LIBRARY_PATH", "") + if version > "2023.1": + os.environ["TCL_LIBRARY"] = os.path.join( + "{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), "tcl8.5" + ) + os.environ["TK_LIBRARY"] = os.path.join( + "{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), "tk8.5" + ) + os.environ["TKPATH"] = os.path.join( + "{}/commonfiles/CPython/3_10/linx64/Release/python/lib".format(edt_root), "tk8.5" + ) + else: + os.environ["TCL_LIBRARY"] = os.path.join( + "{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), "tcl8.5" + ) + os.environ["TK_LIBRARY"] = os.path.join( + "{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), "tk8.5" + ) + os.environ["TKPATH"] = os.path.join( + "{}/commonfiles/CPython/3_7/linx64/Release/python/lib".format(edt_root), "tk8.5" + ) + + +def generate_unique_name(root_name, suffix="", n=6): + char_set = string.ascii_uppercase + string.digits + unique_name = root_name + "_" + "".join(random.choice(char_set) for _ in range(n)) # nosec + if suffix: + unique_name += suffix + return unique_name diff --git a/pyaedt/workflows/templates/run_extension_manager.py_build b/pyaedt/workflows/templates/run_extension_manager.py_build new file mode 100644 index 00000000000..8e92d3741c0 --- /dev/null +++ b/pyaedt/workflows/templates/run_extension_manager.py_build @@ -0,0 +1,88 @@ +# Copyright (C) 2023 - 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 script is meant to run in IronPython within AEDT. * * * + +The script executes the extension manager. + +""" +import os +import sys + +is_linux = os.name == "posix" + +if is_linux: + import subprocessdotnet as subprocess +else: + import subprocess + +extensions_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) + +sys.path.append(extensions_dir) + +import pyaedt_utils + + +def main(): + try: + # Get AEDT version + version_short = oDesktop.GetVersion()[2:6].replace(".", "") + # Launch extension manager + python_exe = r"##PYTHON_EXE##" % version + # Extensions directory + current_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) + pyaedt_extensions_dir = os.path.normpath(os.path.join(current_dir, r"##TOOLKIT_REL_LIB_DIR##")) + pyaedt_script = os.path.join(pyaedt_extensions_dir, "extension_manager.py") + # Check if CPython interpreter and AEDT release match + python_exe = pyaedt_utils.sanitize_interpreter_path(python_exe, version_short) + # Check python executable + python_exe_flag = pyaedt_utils.check_file(python_exe, oDesktop) + if not python_exe_flag: + return + # Check script file + pyaedt_script_flag = pyaedt_utils.check_file(pyaedt_script, oDesktop) + if not pyaedt_script_flag: + return + # Add environment variables + pyaedt_utils.environment_variables(oDesktop) + # Open extension manager + if is_linux: + command = [ + python_exe, + pyaedt_script, + ] + my_env = os.environ.copy() + subprocess.Popen(command, env=my_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) + else: + command = [ + '"{}"'.format(python_exe), + '"{}"'.format(pyaedt_script), + ] + my_env = os.environ.copy() + subprocess.Popen(" ".join(command), env=my_env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) + except Exception as e: + pyaedt_utils.show_error(str(e), oDesktop) + + +if __name__ == "__main__": + main() diff --git a/pyaedt/workflows/templates/run_pyaedt_script.py_build b/pyaedt/workflows/templates/run_pyaedt_script.py_build new file mode 100644 index 00000000000..f7425c0bbaf --- /dev/null +++ b/pyaedt/workflows/templates/run_pyaedt_script.py_build @@ -0,0 +1,101 @@ +# Copyright (C) 2023 - 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 script is meant to run in IronPython within AEDT. * * * + +The script provides for choosing the PyAEDT script to execute. + +""" +import os +import sys +from System.Windows.Forms import OpenFileDialog + +is_linux = os.name == "posix" + +if is_linux: + import subprocessdotnet as subprocess +else: + import subprocess + +toolkits_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) + +sys.path.append(toolkits_dir) + +import pyaedt_utils + + +def main(): + oProject = oDesktop.GetActiveProject() + + # Choose file to launch + file_dialog = OpenFileDialog() + file_dialog.InitialDirectory = oProject.GetPath() + file_dialog.Filter = "python files (*.py)|*.py" + ret = file_dialog.ShowDialog() + + try: + if ret == ret.OK: + # Get AEDT version + version_short = oDesktop.GetVersion()[2:6].replace(".", "") + # CPython interpreter + python_exe = r"##PYTHON_EXE##" % version + # Check if CPython interpreter and AEDT release match + python_exe = pyaedt_utils.sanitize_interpreter_path(python_exe, version_short) + # Python script + pyaedt_script = file_dialog.FileName + # Check Python executable + python_exe_flag = pyaedt_utils.check_file(python_exe, oDesktop) + if not python_exe_flag: + return + # Check script file + pyaedt_script_flag = pyaedt_utils.check_file(pyaedt_script, oDesktop) + if not pyaedt_script_flag: + return + # Add environment variables + pyaedt_utils.environment_variables(oDesktop) + # Run script + if is_linux: + command = [ + python_exe, + pyaedt_script, + str(oDesktop.GetProcessID()), + str(oDesktop.GetVersion()[:6]), + ] + my_env = os.environ.copy() + subprocess.Popen(command, env=my_env) + else: + command = [ + '"{}"'.format(python_exe), + '"{}"'.format(pyaedt_script), + str(oDesktop.GetProcessID()), + str(oDesktop.GetVersion()[:6]), + ] + my_env = os.environ.copy() + subprocess.Popen(" ".join(command), env=my_env) + + except Exception as e: + pyaedt_utils.show_error(str(e), oDesktop) + + +if __name__ == "__main__": + main() diff --git a/pyaedt/workflows/templates/run_pyaedt_toolkit_script.py_build b/pyaedt/workflows/templates/run_pyaedt_toolkit_script.py_build new file mode 100644 index 00000000000..355709a26cc --- /dev/null +++ b/pyaedt/workflows/templates/run_pyaedt_toolkit_script.py_build @@ -0,0 +1,87 @@ +# Copyright (C) 2023 - 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 script is meant to run in IronPython within AEDT. * * * + +The script executes the PyAEDT workflow. + +""" +import os +import sys + +is_linux = os.name == "posix" + +if is_linux: + import subprocessdotnet as subprocess + +toolkits_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) + +sys.path.append(toolkits_dir) + +import pyaedt_utils + + +def main(): + try: + # Get AEDT version + short_version = oDesktop.GetVersion()[2:6].replace(".", "") + oDesktop.AddMessage("", "", 0, "Toolkit launched. Wait.") + # CPython interpreter + python_exe = r"##PYTHON_EXE##" + # Python script + pyaedt_script = r"##PYTHON_SCRIPT##" + # Check if CPython interpreter and AEDT release match + python_exe = pyaedt_utils.sanitize_interpreter_path(python_exe, short_version) + # Check python executable + python_exe_flag = pyaedt_utils.check_file(python_exe, oDesktop) + if not python_exe_flag: + return + # Check script file + pyaedt_script_flag = pyaedt_utils.check_file(pyaedt_script, oDesktop) + if not pyaedt_script_flag: + return + # Add environment variables + pyaedt_utils.environment_variables(oDesktop) + # Run workflow + if pyaedt_utils.is_linux: + my_env = dict(os.environ.copy()) + command = [python_exe, pyaedt_script] + subprocess.Popen(command, env=my_env) + else: + toolkit_name = r"##TOOLKIT_NAME##" + runme = "run_pyaedt_toolkit_{0}.cmd".format(toolkit_name) + edt_temp = oDesktop.GetTempDirectory() + cmd = '"{0}"'.format(pyaedt_script) + target = os.path.join(edt_temp, runme) + my_env = dict(os.environ.copy()) + with open(target, 'w') as cmdfile: + for env in my_env: + cmdfile.write('SET {0}={1}\n'.format(env, my_env[env])) + cmdfile.write('"{0}" {1}\n'.format(python_exe, cmd)) + oDesktop.RunProgram(runme, edt_temp, edt_temp, "") + except Exception as e: + pyaedt_utils.show_error(str(e), oDesktop) + + +if __name__ == "__main__": + main() diff --git a/pyaedt/workflows/templates/toolkit_template.py b/pyaedt/workflows/templates/toolkit_template.py index 07fec8f0169..73b5a880838 100644 --- a/pyaedt/workflows/templates/toolkit_template.py +++ b/pyaedt/workflows/templates/toolkit_template.py @@ -22,19 +22,17 @@ # Toolkit template if the user does not pass any valid script in the toolkit manager -import os - import pyaedt from pyaedt import get_pyaedt_app +from pyaedt.workflows.misc import get_aedt_version +from pyaedt.workflows.misc import get_port +from pyaedt.workflows.misc import get_process_id -if "PYAEDT_SCRIPT_PORT" in os.environ and "PYAEDT_SCRIPT_VERSION" in os.environ: - port = int(os.environ["PYAEDT_SCRIPT_PORT"]) - version = os.environ["PYAEDT_SCRIPT_VERSION"] -else: - port = 0 - version = "2024.1" +port = get_port() +version = get_aedt_version() +aedt_process_id = get_process_id() -app = pyaedt.Desktop(new_desktop_session=False, specified_version=version, port=port) +app = pyaedt.Desktop(new_desktop_session=False, specified_version=version, port=port, aedt_process_id=aedt_process_id) active_project = app.odesktop.GetActiveProject() active_design = active_project.GetActiveDesign() diff --git a/pyaedt/workflows/simplorer/__init__.py b/pyaedt/workflows/twinbuilder/__init__.py similarity index 100% rename from pyaedt/workflows/simplorer/__init__.py rename to pyaedt/workflows/twinbuilder/__init__.py diff --git a/pyaedt/workflows/twinbuilder/convert_to_circuit.py b/pyaedt/workflows/twinbuilder/convert_to_circuit.py new file mode 100644 index 00000000000..0f393fa91a0 --- /dev/null +++ b/pyaedt/workflows/twinbuilder/convert_to_circuit.py @@ -0,0 +1,104 @@ +import math +import os +import sys + +import pyaedt +from pyaedt import Circuit +from pyaedt import Desktop +from pyaedt import TwinBuilder +from pyaedt.generic.general_methods import read_toml + +if "PYAEDT_SCRIPT_PORT" in os.environ and "PYAEDT_SCRIPT_VERSION" in os.environ: + port = int(os.environ["PYAEDT_SCRIPT_PORT"]) + version = os.environ["PYAEDT_SCRIPT_VERSION"] +else: + port = 0 + version = "2024.1" + +with Desktop(new_desktop_session=False, close_on_exit=False, specified_version=version, port=port) as d: + proj = d.active_project() + des = d.active_design() + projname = proj.GetName() + if des.GetDesignType() in ["Twin Builder"]: + desname = des.GetName().split(";")[1] + tb = TwinBuilder(designname=desname, projectname=projname) + else: + d.logger.error("An active TwinBuilder Design is needed.") + sys.exit() + catalog = read_toml(os.path.join(pyaedt.__path__[0], "misc", "tb_nexxim_mapping.toml")) + scale = catalog["General"]["scale"] + cir = Circuit(designname=tb.design_name + "_Translated") + from pyaedt.generic.constants import unit_converter + + pins_unconnected = [] + added_components = {} + for wire in tb.modeler.components.wires.values(): + seg_vals = list(wire.points_in_segment.values()) + for points in seg_vals: + scaled_point = [] + for p in points: + scaled_point.append([i * scale for i in p]) + cir.modeler.components.create_wire(scaled_point, wire.name) + for vname, var in tb.variable_manager.independent_variables.items(): + cir[vname] = var.expression + for vname, var in tb.variable_manager.dependent_variables.items(): + cir[vname] = var.expression + for cmp, el in tb.modeler.components.components.items(): + if el.name in ["CompInst@FML_INIT"]: + for k, p in el.parameters.items(): + if k.startswith("EQU"): + var = p.split(":=") + cir[var[0]] = var[1] + for cmp, el in tb.modeler.components.components.items(): + cmp_name = el.name.split("@")[1] + if cmp_name in catalog: + x1 = unit_converter(catalog[cmp_name]["x_offset"] * scale, input_units="mil", output_units="meter") + y1 = unit_converter(catalog[cmp_name]["y_offset"] * scale, input_units="mil", output_units="meter") + offsetx = x1 * math.sin(math.pi * el.angle / 180) + y1 * math.cos(math.pi * el.angle / 180) + offsety = +y1 * math.sin(math.pi * el.angle / 180) + x1 * math.cos(math.pi * el.angle / 180) + refdes = "" + if "InstanceName" in el.parameters: + refdes = el.parameters["InstanceName"] + cmpid = cir.modeler.components.create_component( + refdes, + component_library=catalog[cmp_name]["component_library"], + component_name=catalog[cmp_name]["component_name"], + location=[el.location[0] * scale + offsetx, el.location[1] * scale + offsety], + angle=el.angle + catalog[cmp_name]["rotate_deg"], + ) + if abs(offsetx) > 1e-9: + for pin in cmpid.pins: + if pin.net == "": + origin = pin.location[:] + if pin.location[1] < cmpid.location[1]: + origin[1] = origin[1] - abs(offsetx) + cir.modeler.components.create_wire([pin.location[:], origin]) + else: + origin[1] = origin[1] + abs(offsetx) + cir.modeler.components.create_wire([pin.location[:], origin]) + else: + for pin in cmpid.pins: + if pin.net == "": + origin = pin.location[:] + if pin.location[0] < cmpid.location[0]: + origin[0] = origin[0] - abs(offsety) + cir.modeler.components.create_wire([pin.location[:], origin]) + else: + origin[0] = origin[0] + abs(offsety) + cir.modeler.components.create_wire([origin, pin.location[:]]) + + for p, value in catalog[cmp_name]["property_mapping"].items(): + cmpid.set_property(value, el.parameters[p]) + elif "GPort" in el.name: + cmpid = cir.modeler.components.create_gnd([i * scale for i in el.location], el.angle) + x1 = unit_converter(100, input_units="mil", output_units="meter") + offsetx = x1 * math.sin(el.angle * math.pi / 180) + offsety = x1 * math.cos(el.angle * math.pi / 180) + cir.modeler.move(cmpid, offset=[-offsetx, offsety]) + cir.modeler.move(cmpid, offset=[offsetx, -offsety]) + for cpms in pins_unconnected: + cir.modeler.move(cpms[0], cpms[1]) + for pin in cpms[0].pins: + if pin.net == "": + offsety = [-i for i in cpms[1]] + cir.modeler.move(cpms[0], offsety) diff --git a/pyaedt/workflows/twinbuilder/images/large/export_to_circuit.png b/pyaedt/workflows/twinbuilder/images/large/export_to_circuit.png new file mode 100644 index 00000000000..33069a477d8 Binary files /dev/null and b/pyaedt/workflows/twinbuilder/images/large/export_to_circuit.png differ diff --git a/pyaedt/workflows/twinbuilder/toolkits_catalog.toml b/pyaedt/workflows/twinbuilder/toolkits_catalog.toml new file mode 100644 index 00000000000..961c44494fd --- /dev/null +++ b/pyaedt/workflows/twinbuilder/toolkits_catalog.toml @@ -0,0 +1,6 @@ +[ConverttoCircuit] +name = "Export to Circuit" +script = "convert_to_circuit.py" +icon = "images/large/export_to_circuit.png" +template = "run_pyaedt_toolkit_script" +pip = "" diff --git a/pyproject.toml b/pyproject.toml index 85f1a7bedbb..fbc659194ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,9 @@ tests = [ "pytest-cov>=4.0.0,<5.1", "pytest-xdist>=3.5.0,<3.7", "pyvista>=0.38.0,<0.44", - "scikit-learn>=1.0.0,<1.5", + "fast-simplification>=0.1.7", + # Never directly imported but required when loading ML related file see #4713 + "scikit-learn>=1.0.0,<1.6", "scikit-rf>=0.30.0,<1.1", "SRTM.py", "utm", @@ -63,7 +65,7 @@ dotnet = [ "pywin32>=303; platform_system=='Windows'", ] doc = [ - "ansys-sphinx-theme>=0.10.0,<0.16", + "ansys-sphinx-theme>=0.10.0,<0.17", "imageio>=2.30.0,<2.35", #"imageio-ffmpeg>=0.4.0,<0.5", "ipython>=7.34.0; python_version == '3.7'", @@ -79,8 +81,8 @@ doc = [ "pypandoc>=1.10.0,<1.14", #"pytest-sphinx", "pyvista>=0.38.0,<0.44", + "fast-simplification>=0.1.7", "recommonmark", - #"scikit-learn", "scikit-rf>=0.30.0,<1.1", "Sphinx==5.3.0; python_version == '3.7'", "Sphinx>=7.1.0,<7.4; python_version > '3.7'", @@ -91,14 +93,14 @@ doc = [ "sphinx-copybutton>=0.5.0,<0.6", "sphinx-gallery>=0.14.0,<0.17", #"sphinx-notfound-page", - "sphinx_design>=0.4.0,<0.6", + "sphinx_design>=0.4.0,<0.7", #"sphinxcontrib-websupport", "SRTM.py", "utm", "vtk==9.2.6", ] doc-no-examples = [ - "ansys-sphinx-theme>=0.10.0,<0.16", + "ansys-sphinx-theme>=0.10.0,<0.17", "imageio>=2.30.0,<2.35", #"imageio-ffmpeg", "numpydoc>=1.5.0,<1.8", @@ -113,7 +115,7 @@ doc-no-examples = [ "sphinx-gallery>=0.14.0,<0.17", #"sphinx-notfound-page", #"sphinxcontrib-websupport", - "sphinx_design>=0.4.0,<0.6", + "sphinx_design>=0.4.0,<0.7", ] all = [ "imageio>=2.30.0,<2.35", @@ -123,6 +125,9 @@ all = [ "osmnx>=1.1.0,<1.10", "pandas>=1.1.0,<2.3", "pyvista>=0.38.0,<0.44", + "fast-simplification>=0.1.7", + # Never directly imported but required when loading ML related file see #4713 + "scikit-learn>=1.0.0,<1.6", "scikit-rf>=0.30.0,<1.1", "SRTM.py", "utm",