From 22a1fb507dc8b743226a8dae0b9ea2f5a61974a3 Mon Sep 17 00:00:00 2001 From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> Date: Fri, 3 May 2024 18:31:41 +0200 Subject: [PATCH] FEAT: Toolkit installer (#4593) Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> Co-authored-by: maxcapodi78 --- _unittest/test_01_Design.py | 12 +- _unittest/test_01_toolkit_icons.py | 34 +- .../Resources/PyAEDTInstallerFromDesktop.py | 38 +- pyaedt/desktop.py | 169 +---- pyaedt/generic/general_methods.py | 10 +- pyaedt/misc/aedtlib_personalib_install.py | 250 -------- pyaedt/misc/images/gallery/PyAEDT.png | Bin 15250 -> 0 bytes pyaedt/misc/install_extra_toolkits.py | 132 ---- pyaedt/modeler/modeler3d.py | 223 +++---- pyaedt/workflows/__init__.py | 21 + pyaedt/workflows/circuit/__init__.py | 21 + pyaedt/workflows/customize_automation_tab.py | 601 ++++++++++++++++++ pyaedt/workflows/emit/__init__.py | 21 + pyaedt/workflows/hfss/__init__.py | 21 + .../workflows/hfss/images/large/antenna.png | Bin 0 -> 1145 bytes pyaedt/workflows/hfss/toolkits_catalog.toml | 7 + pyaedt/workflows/hfss3dlayout/__init__.py | 21 + pyaedt/workflows/hfss3dlayout/export_to_3D.py | 111 ++++ .../hfss3dlayout/images/large/cad3d.png | Bin 0 -> 2447 bytes .../hfss3dlayout/toolkits_catalog.toml | 6 + pyaedt/workflows/icepak/__init__.py | 21 + pyaedt/workflows/images/large/logo.png | Bin 0 -> 35360 bytes .../images/large/pyansys.png | Bin pyaedt/workflows/installer/__init__.py | 21 + .../installer}/console_setup.py | 2 +- pyaedt/workflows/installer/create_report.py | 39 ++ .../installer/images/large/console.png | Bin 0 -> 1247 bytes .../installer/images/large/jupyter.png | Bin 0 -> 1920 bytes .../installer/images/large/run_script.png | Bin 0 -> 1845 bytes .../images/large/toolkit_manager.png | Bin 0 -> 719 bytes .../installer}/jupyter_template.ipynb | 0 .../workflows/installer/pyaedt_installer.py | 124 ++++ pyaedt/workflows/installer/toolkit_manager.py | 356 +++++++++++ .../workflows/installer/toolkits_catalog.toml | 23 + pyaedt/workflows/maxwell2d/__init__.py | 21 + pyaedt/workflows/maxwell3d/__init__.py | 21 + .../images/large/magnet_segmentation.png | Bin 0 -> 1206 bytes .../workflows/maxwell3d/toolkits_catalog.toml | 7 + pyaedt/workflows/mechanical/__init__.py | 21 + pyaedt/workflows/project/__init__.py | 21 + pyaedt/workflows/project/create_report.py | 39 ++ .../workflows/project/images/large/cad3d.png | Bin 0 -> 2447 bytes pyaedt/workflows/project/images/large/pdf.png | Bin 0 -> 573 bytes pyaedt/workflows/project/import_nastran.py | 39 ++ .../workflows/project/toolkits_catalog.toml | 13 + pyaedt/workflows/q2d/__init__.py | 21 + pyaedt/workflows/q3d/__init__.py | 21 + pyaedt/workflows/simplorer/__init__.py | 21 + .../templates}/Jupyter.py_build | 0 .../templates/PyAEDT_Console.py_build} | 2 +- .../templates}/Run_PyAEDT_Script.py_build | 2 +- .../Run_PyAEDT_Toolkit_Script.py_build | 5 +- .../templates/Run_Toolkit_Manager.py_build | 98 +++ pyaedt/workflows/templates/__init__.py | 21 + 54 files changed, 1924 insertions(+), 733 deletions(-) delete mode 100644 pyaedt/misc/aedtlib_personalib_install.py delete mode 100644 pyaedt/misc/images/gallery/PyAEDT.png delete mode 100644 pyaedt/misc/install_extra_toolkits.py create mode 100644 pyaedt/workflows/__init__.py create mode 100644 pyaedt/workflows/circuit/__init__.py create mode 100644 pyaedt/workflows/customize_automation_tab.py create mode 100644 pyaedt/workflows/emit/__init__.py create mode 100644 pyaedt/workflows/hfss/__init__.py create mode 100644 pyaedt/workflows/hfss/images/large/antenna.png create mode 100644 pyaedt/workflows/hfss/toolkits_catalog.toml create mode 100644 pyaedt/workflows/hfss3dlayout/__init__.py create mode 100644 pyaedt/workflows/hfss3dlayout/export_to_3D.py create mode 100644 pyaedt/workflows/hfss3dlayout/images/large/cad3d.png create mode 100644 pyaedt/workflows/hfss3dlayout/toolkits_catalog.toml create mode 100644 pyaedt/workflows/icepak/__init__.py create mode 100644 pyaedt/workflows/images/large/logo.png rename pyaedt/{misc => workflows}/images/large/pyansys.png (100%) create mode 100644 pyaedt/workflows/installer/__init__.py rename pyaedt/{misc => workflows/installer}/console_setup.py (97%) create mode 100644 pyaedt/workflows/installer/create_report.py create mode 100644 pyaedt/workflows/installer/images/large/console.png create mode 100644 pyaedt/workflows/installer/images/large/jupyter.png create mode 100644 pyaedt/workflows/installer/images/large/run_script.png create mode 100644 pyaedt/workflows/installer/images/large/toolkit_manager.png rename pyaedt/{misc => workflows/installer}/jupyter_template.ipynb (100%) create mode 100644 pyaedt/workflows/installer/pyaedt_installer.py create mode 100644 pyaedt/workflows/installer/toolkit_manager.py create mode 100644 pyaedt/workflows/installer/toolkits_catalog.toml create mode 100644 pyaedt/workflows/maxwell2d/__init__.py create mode 100644 pyaedt/workflows/maxwell3d/__init__.py create mode 100644 pyaedt/workflows/maxwell3d/images/large/magnet_segmentation.png create mode 100644 pyaedt/workflows/maxwell3d/toolkits_catalog.toml create mode 100644 pyaedt/workflows/mechanical/__init__.py create mode 100644 pyaedt/workflows/project/__init__.py create mode 100644 pyaedt/workflows/project/create_report.py create mode 100644 pyaedt/workflows/project/images/large/cad3d.png create mode 100644 pyaedt/workflows/project/images/large/pdf.png create mode 100644 pyaedt/workflows/project/import_nastran.py create mode 100644 pyaedt/workflows/project/toolkits_catalog.toml create mode 100644 pyaedt/workflows/q2d/__init__.py create mode 100644 pyaedt/workflows/q3d/__init__.py create mode 100644 pyaedt/workflows/simplorer/__init__.py rename pyaedt/{misc => workflows/templates}/Jupyter.py_build (100%) rename pyaedt/{misc/Console.py_build => workflows/templates/PyAEDT_Console.py_build} (97%) rename pyaedt/{misc => workflows/templates}/Run_PyAEDT_Script.py_build (97%) rename pyaedt/{misc => workflows/templates}/Run_PyAEDT_Toolkit_Script.py_build (92%) create mode 100644 pyaedt/workflows/templates/Run_Toolkit_Manager.py_build create mode 100644 pyaedt/workflows/templates/__init__.py diff --git a/_unittest/test_01_Design.py b/_unittest/test_01_Design.py index c1d1a1c7acd..ae2150c5391 100644 --- a/_unittest/test_01_Design.py +++ b/_unittest/test_01_Design.py @@ -15,6 +15,7 @@ from pyaedt.application.design_solutions import model_names from pyaedt.generic.general_methods import is_linux from pyaedt.generic.general_methods import settings +from pyaedt.workflows import customize_automation_tab test_subfolder = "T01" if config["desktopVersion"] > "2022.2": @@ -398,17 +399,18 @@ def test_36_test_load(self, add_app): assert True def test_37_add_custom_toolkit(self, desktop): - assert desktop.get_available_toolkits() + assert customize_automation_tab.available_toolkits def test_38_toolkit(self, desktop): file = os.path.join(self.local_scratch.path, "test.py") with open(file, "w") as f: f.write("import pyaedt\n") - assert desktop.add_script_to_menu( - "test_toolkit", - file, + assert customize_automation_tab.add_script_to_menu( + desktop_object=self.aedtapp.desktop_class, name="test_toolkit", script_file=file + ) + assert customize_automation_tab.remove_script_from_menu( + desktop_object=self.aedtapp.desktop_class, name="test_toolkit" ) - assert desktop.remove_script_from_menu("test_toolkit") def test_39_load_project(self, desktop): new_project = os.path.join(self.local_scratch.path, "new.aedt") diff --git a/_unittest/test_01_toolkit_icons.py b/_unittest/test_01_toolkit_icons.py index 1cad078d49e..55ec3a74cbd 100644 --- a/_unittest/test_01_toolkit_icons.py +++ b/_unittest/test_01_toolkit_icons.py @@ -1,9 +1,14 @@ import os -import xml.etree.ElementTree as ET + +import defusedxml.ElementTree as ET +import defusedxml.minidom + +defusedxml.defuse_stdlib() + import pytest -from pyaedt.misc.aedtlib_personalib_install import write_tab_config +from pyaedt.workflows.customize_automation_tab import add_automation_tab @pytest.fixture(scope="module", autouse=True) @@ -17,8 +22,7 @@ def init(self, local_scratch): self.local_scratch = local_scratch def test_00_write_new_xml(self): - file_path = os.path.join(self.local_scratch.path, "TabConfig.xml") - write_tab_config(os.path.dirname(file_path), self.local_scratch.path) + file_path = add_automation_tab(name="Test", lib_dir=self.local_scratch.path) root = self.validate_file_exists_and_pyaedt_tabs_added(file_path) panels = root.findall("./panel") panel_names = [panel.attrib["label"] for panel in panels] @@ -29,7 +33,7 @@ def test_01_add_pyaedt_config_to_existing_existing_xml(self): First write a dummy XML with a different Panel and then add PyAEDT's tabs :return: """ - file_path = os.path.join(self.local_scratch.path, "TabConfig.xml") + file_path = os.path.join(self.local_scratch.path, "Project", "TabConfig.xml") with open(file_path, "w") as fid: fid.write( """ @@ -47,7 +51,7 @@ def test_01_add_pyaedt_config_to_existing_existing_xml(self): """ ) - write_tab_config(os.path.dirname(file_path), self.local_scratch.path) + file_path = add_automation_tab(name="Test", lib_dir=self.local_scratch.path) root = self.validate_file_exists_and_pyaedt_tabs_added(file_path) panels = root.findall("./panel") panel_names = [panel.attrib["label"] for panel in panels] @@ -55,7 +59,7 @@ def test_01_add_pyaedt_config_to_existing_existing_xml(self): assert "Panel_1" in panel_names def test_03_overwrite_existing_pyaedt_config(self): - file_path = os.path.join(self.local_scratch.path, "TabConfig.xml") + file_path = os.path.join(self.local_scratch.path, "Project", "TabConfig.xml") with open(file_path, "w") as fid: fid.write( """ @@ -72,14 +76,14 @@ def test_03_overwrite_existing_pyaedt_config(self): """ ) - write_tab_config(os.path.dirname(file_path), self.local_scratch.path) + file_path = add_automation_tab(name="Test", lib_dir=self.local_scratch.path) root = self.validate_file_exists_and_pyaedt_tabs_added(file_path) panels = root.findall("./panel") panel_names = [panel.attrib["label"] for panel in panels] - assert len(panel_names) == 1 + assert len(panel_names) == 2 def test_04_write_to_existing_file_but_no_panels(self): - file_path = os.path.join(self.local_scratch.path, "TabConfig.xml") + file_path = os.path.join(self.local_scratch.path, "Project", "TabConfig.xml") with open(file_path, "w") as fid: fid.write( """ @@ -88,7 +92,7 @@ def test_04_write_to_existing_file_but_no_panels(self): """ ) - write_tab_config(os.path.dirname(file_path), self.local_scratch.path) + file_path = add_automation_tab(name="Test", lib_dir=self.local_scratch.path) root = self.validate_file_exists_and_pyaedt_tabs_added(file_path) junks = root.findall("./junk") junk_names = [junk.attrib["label"] for junk in junks] @@ -98,15 +102,13 @@ def test_04_write_to_existing_file_but_no_panels(self): panel_names = [panel.attrib["label"] for panel in panels] assert len(panel_names) == 1 - def validate_file_exists_and_pyaedt_tabs_added(self, file_path): + @staticmethod + def validate_file_exists_and_pyaedt_tabs_added(file_path): assert os.path.isfile(file_path) is True assert ET.parse(file_path) is not None tree = ET.parse(file_path) root = tree.getroot() panels = root.findall("./panel") panel_names = [panel.attrib["label"] for panel in panels] - assert "Panel_PyAEDT" in panel_names - files_to_verify = ["images/large/pyansys.png", "images/gallery/PyAEDT.png"] - for file_name in files_to_verify: - assert os.path.isfile(os.path.join(os.path.dirname(file_path), file_name)) + assert "Panel_PyAEDT_Toolkits" in panel_names return root diff --git a/doc/source/Resources/PyAEDTInstallerFromDesktop.py b/doc/source/Resources/PyAEDTInstallerFromDesktop.py index 8513222d0d9..7d857c83b6c 100644 --- a/doc/source/Resources/PyAEDTInstallerFromDesktop.py +++ b/doc/source/Resources/PyAEDTInstallerFromDesktop.py @@ -60,10 +60,10 @@ def run_pyinstaller_from_c_python(oDesktop): # 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.misc.aedtlib_personalib_install import add_pyaedt_to_aedt\n") + f.write("from pyaedt.workflows.installer.pyaedt_installer import add_pyaedt_to_aedt\n") f.write( - 'add_pyaedt_to_aedt(aedt_version="{}", is_student_version={}, use_sys_lib=False, new_desktop_session=False, pers_dir=r"{}")\n'.format( - oDesktop.GetVersion()[:6], is_student_version(oDesktop), oDesktop.GetPersonalLibDirectory())) + 'add_pyaedt_to_aedt(aedt_version="{}", student_version={}, new_desktop_session=False)\n'.format( + oDesktop.GetVersion()[:6], is_student_version(oDesktop))) command = r'"{}" "{}"'.format(python_exe, python_script) oDesktop.AddMessage("", "", 0, command) @@ -119,6 +119,14 @@ def install_pyaedt(): if args.version < "232": ld_library_path_dirs_to_add.append("{}/Delcross".format(args.edt_root)) os.environ["LD_LIBRARY_PATH"] = ":".join(ld_library_path_dirs_to_add) + ":" + os.getenv("LD_LIBRARY_PATH", "") + os.environ["TK_LIBRARY"] = ("{}/commonfiles/CPython/{}/linx64/Release/python/lib/tk8.5". + format(args.edt_root, + args.python_version.replace( + ".", "_"))) + os.environ["TCL_LIBRARY"] = ("{}/commonfiles/CPython/{}/linx64/Release/python/lib/tcl8.5". + format(args.edt_root, + args.python_version.replace( + ".", "_"))) if not os.path.exists(venv_dir): @@ -139,7 +147,8 @@ def install_pyaedt(): zip_ref.extractall(unzipped_path) run_command( - '"{}" install --no-cache-dir --no-index --find-links={} pyaedt[all,dotnet]'.format(pip_exe, unzipped_path)) + '"{}" install --no-cache-dir --no-index --find-links={} pyaedt[all,dotnet]'.format(pip_exe, + unzipped_path)) run_command( '"{}" install --no-cache-dir --no-index --find-links={} jupyterlab'.format(pip_exe, unzipped_path)) @@ -147,14 +156,11 @@ 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 pyaedt[all]'.format(pip_exe)) - # run_command('"{}" --default-timeout=1000 install git+https://github.com/ansys/pyaedt.git@main'.format(pip_exe)) + # run_command( + # '"{}" --default-timeout=1000 install git+https://github.com/ansys/pyaedt.git@main'.format(pip_exe)) run_command('"{}" --default-timeout=1000 install jupyterlab'.format(pip_exe)) run_command('"{}" --default-timeout=1000 install ipython -U'.format(pip_exe)) run_command('"{}" --default-timeout=1000 install ipyvtklink'.format(pip_exe)) - # User can uncomment these lines to install Pyside6 modules - # run_command('"{}" --default-timeout=1000 install pyside6==6.4.0'.format(pip_exe)) - # run_command('"{}" --default-timeout=1000 install pyqtgraph'.format(pip_exe)) - # run_command('"{}" --default-timeout=1000 install qdarkstyle'.format(pip_exe)) if args.version == "231": run_command('"{}" uninstall -y pywin32'.format(pip_exe)) @@ -176,20 +182,6 @@ def install_pyaedt(): run_command('"{}" install --no-cache-dir --no-index --find-links={} pyaedt'.format(pip_exe, unzipped_path)) else: run_command('"{}" --default-timeout=1000 install pyaedt[all]'.format(pip_exe)) - - # if is_windows: - # pyaedt_setup_script = "{}/Lib/site-packages/pyaedt/misc/aedtlib_personalib_install.py".format(venv_dir) - # else: - # pyaedt_setup_script = "{}/lib/python{}/site-packages/pyaedt/misc/aedtlib_personalib_install.py".format( - # venv_dir, args.python_version) - # - # if not os.path.isfile(pyaedt_setup_script): - # sys.exit("[ERROR] PyAEDT was not setup properly since {} file does not exist.".format(pyaedt_setup_script)) - # - # command = '"{}" "{}" --version={}'.format(python_exe, pyaedt_setup_script, args.version) - # if args.student: - # command += " --student" - # run_command(command) sys.exit(0) diff --git a/pyaedt/desktop.py b/pyaedt/desktop.py index 38feb28ff68..2e5ee69272f 100644 --- a/pyaedt/desktop.py +++ b/pyaedt/desktop.py @@ -1461,7 +1461,8 @@ def _exception(self, ex_value, tb_data): tblist = tb_trace[0].split("\n") self.logger.error(str(ex_value)) for el in tblist: - self.logger.error(el) + if el: + self.logger.error(el) return str(ex_value) @@ -1744,172 +1745,6 @@ def get_available_toolkits(self): return list(available_toolkits.keys()) - @pyaedt_function_handler() - def add_custom_toolkit(self, toolkit_name): # pragma: no cover - """Add toolkit to AEDT Automation Tab. - - Parameters - ---------- - toolkit_name : str - Name of toolkit to add. - - Returns - ------- - bool - """ - from pyaedt.misc.install_extra_toolkits import available_toolkits - - toolkit = available_toolkits[toolkit_name] - toolkit_name = toolkit_name.replace("_", "") - - def install(package_path, package_name=None): - executable = '"{}"'.format(sys.executable) if is_windows else sys.executable - - commands = [] - if package_path.startswith("git") and package_name: - commands.append([executable, "-m", "pip", "uninstall", "--yes", package_name]) - - commands.append([executable, "-m", "pip", "install", "--upgrade", package_path]) - - if self.aedt_version_id == "2023.1" and is_windows and "AnsysEM" in sys.base_prefix: - commands.append([executable, "-m", "pip", "uninstall", "--yes", "pywin32"]) - - for command in commands: - if is_linux: - p = subprocess.Popen(command) - else: - p = subprocess.Popen(" ".join(command)) - p.wait() - - install(toolkit["pip"], toolkit.get("package_name", None)) - import site - - packages = site.getsitepackages() - full_path = None - for pkg in packages: - if os.path.exists(os.path.join(pkg, toolkit["toolkit_script"])): - full_path = os.path.join(pkg, toolkit["toolkit_script"]) - break - if not full_path: - raise FileNotFoundError("Error finding the package.") - self.add_script_to_menu( - toolkit_name=toolkit_name, - script_path=full_path, - script_image=toolkit, - product=toolkit["installation_path"], - copy_to_personal_lib=False, - add_pyaedt_desktop_init=False, - ) - - @pyaedt_function_handler() - def add_script_to_menu( - self, - toolkit_name, - script_path, - script_image=None, - product="Project", - copy_to_personal_lib=True, - add_pyaedt_desktop_init=True, - ): - """Add a script to the ribbon menu. - - .. note:: - This method is available in AEDT 2023 R2 and later. PyAEDT must be installed - in AEDT to allow this method to run. For more information, see `Installation - `_. - - Parameters - ---------- - toolkit_name : str - Name of the toolkit to appear in AEDT. - script_path : str - Full path to the script file. The script will be moved to Personal Lib. - script_image : str, optional - Full path to the image logo (a 30x30 pixel PNG file) to add to the UI. - The default is ``None``. - product : str, optional - Product to which the toolkit applies. The default is ``"Project"``, in which case - it applies to all designs. You can also specify a product, such as ``"HFSS"``. - copy_to_personal_lib : bool, optional - Whether to copy the script to Personal Lib or link the original script. Default is ``True``. - - Returns - ------- - bool - - """ - if not os.path.exists(script_path): - self.logger.error("Script does not exists.") - return False - from pyaedt.misc.install_extra_toolkits import write_toolkit_config - - toolkit_dir = os.path.join(self.personallib, "Toolkits") - aedt_version = self.aedt_version_id - tool_dir = os.path.join(toolkit_dir, product, toolkit_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", toolkit_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) - os.makedirs(tool_dir, exist_ok=True) - dest_script_path = script_path - if copy_to_personal_lib: - dest_script_path = os.path.join(lib_dir, os.path.split(script_path)[-1]) - shutil.copy2(script_path, dest_script_path) - files_to_copy = ["Run_PyAEDT_Toolkit_Script"] - executable_version_agnostic = sys.executable - for file_name in files_to_copy: - src = os.path.join(pathname, "misc", file_name + ".py_build") - dst = os.path.join(tool_dir, file_name.replace("_", " ") + ".py") - if not os.path.isfile(src): - raise FileNotFoundError("File not found: {}".format(src)) - with open_file(src, "r") as build_file: - with open_file(dst, "w") as out_file: - self.logger.info("Building to " + dst) - build_file_data = build_file.read() - build_file_data = ( - build_file_data.replace("##TOOLKIT_REL_LIB_DIR##", toolkit_rel_lib_dir) - .replace("##PYTHON_EXE##", executable_version_agnostic) - .replace("##PYTHON_SCRIPT##", dest_script_path) - ) - build_file_data = build_file_data.replace(" % version", "") - out_file.write(build_file_data) - if aedt_version >= "2023.2": - if not script_image: - script_image = os.path.join(os.path.dirname(__file__), "misc", "images", "large", "pyansys.png") - write_toolkit_config(os.path.join(toolkit_dir, product), lib_dir, toolkit_name, toolkit=script_image) - self.logger.info("{} toolkit installed.".format(toolkit_name)) - return True - - @pyaedt_function_handler() - def remove_script_from_menu(self, toolkit_name, product="Project"): - """Remove a toolkit script from the menu. - - Parameters - ---------- - toolkit_name : str - Name of the toolkit to remove. - product : str, optional - Product to which the toolkit applies. The default is ``"Project"``, in which case - it applies to all designs. You can also specify a product, such as ``"HFSS"``. - - Returns - ------- - bool - """ - from pyaedt.misc.install_extra_toolkits import remove_toolkit_config - - toolkit_dir = os.path.join(self.personallib, "Toolkits") - aedt_version = self.aedt_version_id - tool_dir = os.path.join(toolkit_dir, product, toolkit_name) - shutil.rmtree(tool_dir, ignore_errors=True) - if aedt_version >= "2023.2": - remove_toolkit_config(os.path.join(toolkit_dir, product), toolkit_name) - self.logger.info("{} toolkit removed successfully.".format(toolkit_name)) - return True - @pyaedt_function_handler() def submit_job( self, diff --git a/pyaedt/generic/general_methods.py b/pyaedt/generic/general_methods.py index 432acb95a12..5dad1863a90 100644 --- a/pyaedt/generic/general_methods.py +++ b/pyaedt/generic/general_methods.py @@ -110,7 +110,6 @@ def _exception(ex_info, func, args, kwargs, message="Type Error"): ] if any(exc in trace for exc in exceptions): continue - # if func.__name__ in trace: for el in trace.split("\n"): _write_mes(el) for trace in tb_trace: @@ -118,14 +117,10 @@ def _exception(ex_info, func, args, kwargs, message="Type Error"): continue tblist = trace.split("\n") for el in tblist: - # if func.__name__ in el: - _write_mes(el) + if el: + _write_mes(el) _write_mes("{} on {}".format(message, func.__name__)) - # try: - # _write_mes(ex_info[1].args[0]) - # except (IndexError, AttributeError): - # pass message_to_print = "" messages = "" @@ -138,7 +133,6 @@ def _exception(ex_info, func, args, kwargs, message="Type Error"): pass if "error" in messages: message_to_print = messages[messages.index("[error]") :] - # _write_mes("{} - {} - {}.".format(ex_info[1], func.__name__, message.upper())) if message_to_print: _write_mes("Last Electronics Desktop Message - " + message_to_print) diff --git a/pyaedt/misc/aedtlib_personalib_install.py b/pyaedt/misc/aedtlib_personalib_install.py deleted file mode 100644 index 2eb3b9740c6..00000000000 --- a/pyaedt/misc/aedtlib_personalib_install.py +++ /dev/null @@ -1,250 +0,0 @@ -import argparse -import os -import shutil -import sys -import warnings -from xml.dom.minidom import parseString -import xml.etree.ElementTree as ET -from xml.etree.ElementTree import ParseError - -current_dir = os.path.dirname(os.path.realpath(__file__)) -pyaedt_path = os.path.normpath( - os.path.join( - current_dir, - "..", - ) -) -sys.path.append(os.path.normpath(os.path.join(pyaedt_path, ".."))) - -is_linux = os.name == "posix" -is_windows = not is_linux -pid = 0 - - -def main(): - args = parse_arguments() - add_pyaedt_to_aedt( - args.version, is_student_version=args.student, use_sys_lib=args.sys_lib, new_desktop_session=args.new_session - ) - - -def parse_arguments(): - parser = argparse.ArgumentParser(description="Install PyAEDT and setup PyAEDT toolkits in AEDT.") - parser.add_argument( - "--version", "-v", default="231", metavar="XY.Z", help="AEDT three-digit version (e.g. 231). Default=231" - ) - parser.add_argument( - "--student", "--student_version", action="store_true", help="Install toolkits for AEDT Student Version." - ) - parser.add_argument("--sys_lib", "--syslib", action="store_true", help="Install toolkits in SysLib.") - parser.add_argument( - "--new_session", action="store_true", help="Start a new session of AEDT after installing PyAEDT." - ) - - args = parser.parse_args() - args = process_arguments(args, parser) - return args - - -def process_arguments(args, parser): - if len(args.version) != 3: - parser.print_help() - parser.error("Version should be a three digit number (e.g. 231)") - - args.version = "20" + args.version[-3:-1] + "." + args.version[-1:] - return args - - -def add_pyaedt_to_aedt( - aedt_version, is_student_version=False, use_sys_lib=False, new_desktop_session=False, sys_dir="", pers_dir="" -): - if not (sys_dir or pers_dir): - from pyaedt import Desktop - from pyaedt.generic.general_methods import grpc_active_sessions - from pyaedt.generic.settings import settings - - sessions = grpc_active_sessions(aedt_version, is_student_version) - close_on_exit = True - if not sessions: - if not new_desktop_session: - print("Launching a new AEDT desktop session.") - new_desktop_session = True - else: - close_on_exit = False - settings.use_grpc_api = True - with Desktop( - specified_version=aedt_version, - non_graphical=new_desktop_session, - new_desktop_session=new_desktop_session, - student_version=is_student_version, - close_on_exit=close_on_exit, - ) as d: - desktop = sys.modules["__main__"].oDesktop - pers1 = os.path.join(desktop.GetPersonalLibDirectory(), "pyaedt") - pid = desktop.GetProcessID() - # Linking pyaedt in PersonalLib for IronPython compatibility. - if os.path.exists(pers1): - d.logger.info("PersonalLib already mapped.") - else: - if is_windows: - os.system('mklink /D "{}" "{}"'.format(pers1, pyaedt_path)) - else: - os.system('ln -s "{}" "{}"'.format(pyaedt_path, pers1)) - sys_dir = d.syslib - pers_dir = d.personallib - if pid and new_desktop_session: - try: - os.kill(pid, 9) - except Exception: - pass - - toolkits = ["Project"] - # Bug on Linux 23.1 and before where Project level toolkits don't show up. Thus copying to individual design - # toolkits. - if is_linux and aedt_version <= "2023.1": - toolkits = [ - "2DExtractor", - "CircuitDesign", - "HFSS", - "HFSS-IE", - "HFSS3DLayoutDesign", - "Icepak", - "Maxwell2D", - "Maxwell3D", - "Q3DExtractor", - "Mechanical", - ] - - for product in toolkits: - if use_sys_lib: - try: - sys_dir = os.path.join(sys_dir, "Toolkits") - install_toolkit(sys_dir, product, aedt_version) - print("Installed toolkit for {} in sys lib.".format(product)) - # d.logger.info("Installed toolkit for {} in sys lib.".format(product)) - - except IOError: - pers_dir = os.path.join(pers_dir, "Toolkits") - install_toolkit(pers_dir, product, aedt_version) - print("Installed toolkit for {} in sys lib.".format(product)) - # d.logger.info("Installed toolkit for {} in personal lib.".format(product)) - else: - pers_dir = os.path.join(pers_dir, "Toolkits") - install_toolkit(pers_dir, product, aedt_version) - print("Installed toolkit for {} in sys lib.".format(product)) - # d.logger.info("Installed toolkit for {} in personal lib.".format(product)) - - -def install_toolkit(toolkit_dir, product, aedt_version): - tool_dir = os.path.join(toolkit_dir, product, "PyAEDT") - lib_dir = os.path.join(tool_dir, "Lib") - toolkit_rel_lib_dir = os.path.relpath(lib_dir, tool_dir) - # Bug on Linux 23.1 and before where Project level toolkits don't show up. Thus copying to individual design - # toolkits. - if is_linux and aedt_version <= "2023.1": - toolkit_rel_lib_dir = os.path.join("Lib", "PyAEDT") - lib_dir = os.path.join(toolkit_dir, toolkit_rel_lib_dir) - toolkit_rel_lib_dir = "../../" + toolkit_rel_lib_dir - tool_dir = os.path.join(toolkit_dir, product, "PyAEDT") - os.makedirs(lib_dir, exist_ok=True) - os.makedirs(tool_dir, exist_ok=True) - files_to_copy = ["Console", "Run_PyAEDT_Script", "Jupyter"] - # Remove hard-coded version number from Python virtual environment path, and replace it with the corresponding AEDT - # version's Python virtual environment. - version_agnostic = False - if aedt_version[2:6].replace(".", "") in sys.executable: - executable_version_agnostic = sys.executable.replace(aedt_version[2:6].replace(".", ""), "%s") - version_agnostic = True - else: - executable_version_agnostic = sys.executable - jupyter_executable = executable_version_agnostic.replace("python" + exe(), "jupyter" + exe()) - ipython_executable = executable_version_agnostic.replace("python" + exe(), "ipython" + exe()) - for file_name in files_to_copy: - with open(os.path.join(current_dir, file_name + ".py_build"), "r") as build_file: - file_name_dest = file_name.replace("_", " ") + ".py" - with open(os.path.join(tool_dir, file_name_dest), "w") as out_file: - print("Building to " + os.path.join(tool_dir, file_name_dest)) - build_file_data = build_file.read() - build_file_data = ( - build_file_data.replace("##TOOLKIT_REL_LIB_DIR##", toolkit_rel_lib_dir) - .replace("##PYTHON_EXE##", executable_version_agnostic) - .replace("##IPYTHON_EXE##", ipython_executable) - .replace("##JUPYTER_EXE##", jupyter_executable) - ) - if not version_agnostic: - build_file_data = build_file_data.replace(" % version", "") - out_file.write(build_file_data) - shutil.copyfile(os.path.join(current_dir, "console_setup.py"), os.path.join(lib_dir, "console_setup.py")) - shutil.copyfile( - os.path.join(current_dir, "jupyter_template.ipynb"), - os.path.join(lib_dir, "jupyter_template.ipynb"), - ) - if aedt_version >= "2023.2": - write_tab_config(os.path.join(toolkit_dir, product), lib_dir) - - -def write_tab_config(product_toolkit_dir, pyaedt_lib_dir, force_write=False): - tab_config_file_path = os.path.join(product_toolkit_dir, "TabConfig.xml") - if not os.path.isfile(tab_config_file_path) or force_write: - root = ET.Element("TabConfig") - else: - try: - tree = ET.parse(tab_config_file_path) - except ParseError as e: - warnings.warn("Unable to parse %s\nError received = %s" % (tab_config_file_path, str(e))) - return - root = tree.getroot() - - panels = root.findall("./panel") - if panels: - panel_names = [panel.attrib["label"] for panel in panels] - if "Panel_PyAEDT" in panel_names: - # Remove previously existing PyAEDT panel and update with newer one. - panel = [panel for panel in panels if panel.attrib["label"] == "Panel_PyAEDT"][0] - root.remove(panel) - - # Write a new "Panel_PyAEDT" sub-element. - panel = ET.SubElement(root, "panel", label="Panel_PyAEDT") - gallery = ET.SubElement(panel, "gallery", imagewidth="120", imageheight="72") - image_rel_path = os.path.relpath(pyaedt_lib_dir, product_toolkit_dir).replace("\\", "/") + "/" - if image_rel_path == "./": - image_rel_path = "" - ET.SubElement(gallery, "button", label="PyAEDT", isLarge="1", image=image_rel_path + "images/large/pyansys.png") - group = ET.SubElement(gallery, "group", label="PyAEDT Menu", image=image_rel_path + "images/gallery/PyAEDT.png") - ET.SubElement(group, "button", label="Console", script="PyAEDT/Console") - ET.SubElement(group, "button", label="Jupyter Notebook", script="PyAEDT/Jupyter") - ET.SubElement(group, "button", label="Run PyAEDT Script", script="PyAEDT/Run PyAEDT Script") - - # Backup any existing file if present - if os.path.isfile(tab_config_file_path): - shutil.copy(tab_config_file_path, tab_config_file_path + ".orig") - - write_pretty_xml(root, tab_config_file_path) - - files_to_copy = ["images/large/pyansys.png", "images/gallery/PyAEDT.png"] - for file_name in files_to_copy: - dest_file = os.path.normpath(os.path.join(pyaedt_lib_dir, file_name)) - os.makedirs(os.path.dirname(dest_file), exist_ok=True) - shutil.copy(os.path.normpath(os.path.join(current_dir, file_name)), dest_file) - - -def write_pretty_xml(root, file_path): - """Write the XML in a pretty format.""" - # If we use the commented code below, then the previously existing lines will have double lines added. We need to - # split and ignore the double lines. - # xml_str = parseString(ET.tostring(root)).toprettyxml(indent=" " * 4) - lines = [line for line in parseString(ET.tostring(root)).toprettyxml(indent=" " * 4).split("\n") if line.strip()] - xml_str = "\n".join(lines) - - with open(file_path, "w") as f: - f.write(xml_str) - - -def exe(): - if is_windows: - return ".exe" - return "" - - -if __name__ == "__main__": - main() diff --git a/pyaedt/misc/images/gallery/PyAEDT.png b/pyaedt/misc/images/gallery/PyAEDT.png deleted file mode 100644 index a51a6cd31aa53293a6d6796d25afab0ae120fa07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15250 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 diff --git a/pyaedt/misc/install_extra_toolkits.py b/pyaedt/misc/install_extra_toolkits.py deleted file mode 100644 index fc89a420d04..00000000000 --- a/pyaedt/misc/install_extra_toolkits.py +++ /dev/null @@ -1,132 +0,0 @@ -import os -import shutil -import warnings -import xml.etree.ElementTree as ET -from xml.etree.ElementTree import ParseError - -from pyaedt.misc.aedtlib_personalib_install import current_dir -from pyaedt.misc.aedtlib_personalib_install import write_pretty_xml - -available_toolkits = { - "AntennaWizard": { - "pip": "git+https://github.com/ansys/pyaedt-antenna-toolkit.git", - "image": "pyansys.png", - "toolkit_script": "ansys/aedt/toolkits/antenna/run_toolkit.py", - "installation_path": "HFSS", - "package_name": "ansys.aedt.toolkits.antenna", - }, - "ChokeWizard": { - "pip": "git+https://github.com/ansys/pyaedt-choke-toolkit.git", - "image": "pyansys.png", - "toolkit_script": "ansys/aedt/toolkits/choke/choke_toolkit.py", - "installation_path": "Project", - "package_name": "ansys.aedt.toolkits.choke", - }, - "MagnetSegmentationWizard": { - "pip": "git+https://github.com/ansys/magnet-segmentation-toolkit.git", - "image": "pyansys.png", - "toolkit_script": "ansys/aedt/toolkits/magnet_segmentation/run_toolkit.py", - "installation_path": "Maxwell3d", - "package_name": "magnet-segmentation-toolkit", - }, -} - - -def write_toolkit_config(product_toolkit_dir, pyaedt_lib_dir, toolkitname, toolkit, force_write=False): - """Write a toolkit configuration file and, if needed a button in Automation menu.""" - tab_config_file_path = os.path.join(product_toolkit_dir, "TabConfig.xml") - if not os.path.isfile(tab_config_file_path) or force_write: - root = ET.Element("TabConfig") - else: - try: - tree = ET.parse(tab_config_file_path) - except ParseError as e: - warnings.warn("Unable to parse %s\nError received = %s" % (tab_config_file_path, str(e))) - return - root = tree.getroot() - - panels = root.findall("./panel") - if panels: - panel_names = [panel.attrib["label"] for panel in panels] - if "Panel_PyAEDT_Toolkits" in panel_names: - # Remove previously existing PyAEDT panel and update with newer one. - panel = [panel for panel in panels if panel.attrib["label"] == "Panel_PyAEDT_Toolkits"][0] - else: - panel = ET.SubElement(root, "panel", label="Panel_PyAEDT_Toolkits") - else: - panel = ET.SubElement(root, "panel", label="Panel_PyAEDT_Toolkits") - - # Write a new "Panel_PyAEDT_Toolkits" sub-element. - image_rel_path = os.path.relpath(pyaedt_lib_dir, product_toolkit_dir).replace("\\", "/") + "/" - if image_rel_path == "./": - image_rel_path = "" - - buttons = panel.findall("./button") - if buttons: - button_names = [button.attrib["label"] for button in buttons] - if toolkitname in button_names: - # Remove previously existing PyAEDT panel and update with newer one. - b = [button for button in buttons if button.attrib["label"] == toolkitname][0] - panel.remove(b) - if isinstance(toolkit, str) and os.path.exists(toolkit): - image_name = os.path.split(toolkit)[-1] - else: - image_name = toolkit["image"] - image_abs_path = image_rel_path + "images/large/{}".format(image_name) - ET.SubElement( - panel, - "button", - label=toolkitname, - isLarge="1", - image=image_abs_path, - script="{}/Run PyAEDT Toolkit Script".format(toolkitname), - ) - - # Backup any existing file if present - if os.path.isfile(tab_config_file_path): - shutil.copy(tab_config_file_path, tab_config_file_path + ".orig") - - write_pretty_xml(root, tab_config_file_path) - - files_to_copy = ["images/large/{}".format(image_name)] - for file_name in files_to_copy: - dest_file = os.path.normpath(os.path.join(pyaedt_lib_dir, file_name)) - os.makedirs(os.path.dirname(dest_file), exist_ok=True) - if isinstance(toolkit, str): - shutil.copy(toolkit, dest_file) - else: - shutil.copy(os.path.normpath(os.path.join(current_dir, file_name)), dest_file) - - -def remove_toolkit_config(product_toolkit_dir, toolkitname): - """Remove a toolkit configuration file and, if needed a button in Automation menu.""" - tab_config_file_path = os.path.join(product_toolkit_dir, "TabConfig.xml") - if not os.path.isfile(tab_config_file_path): - return True - try: - tree = ET.parse(tab_config_file_path) - except ParseError as e: - warnings.warn("Unable to parse %s\nError received = %s" % (tab_config_file_path, str(e))) - return - root = tree.getroot() - - panels = root.findall("./panel") - if panels: - panel_names = [panel.attrib["label"] for panel in panels] - if "Panel_PyAEDT_Toolkits" in panel_names: - # Remove previously existing PyAEDT panel and update with newer one. - panel = [panel for panel in panels if panel.attrib["label"] == "Panel_PyAEDT_Toolkits"][0] - else: - panel = ET.SubElement(root, "panel", label="Panel_PyAEDT_Toolkits") - else: - panel = ET.SubElement(root, "panel", label="Panel_PyAEDT_Toolkits") - - buttons = panel.findall("./button") - if buttons: - button_names = [button.attrib["label"] for button in buttons] - if toolkitname in button_names: - # Remove previously existing PyAEDT panel and update with newer one. - b = [button for button in buttons if button.attrib["label"] == toolkitname][0] - panel.remove(b) - - write_pretty_xml(root, tab_config_file_path) diff --git a/pyaedt/modeler/modeler3d.py b/pyaedt/modeler/modeler3d.py index 590a94e0420..b2f19ef0ec0 100644 --- a/pyaedt/modeler/modeler3d.py +++ b/pyaedt/modeler/modeler3d.py @@ -877,7 +877,7 @@ 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_solids=True): + def import_nastran(self, file_path, import_lines=True, lines_thickness=0, **kwargs): """Import Nastran file into 3D Modeler by converting the faces to stl and reading it. The solids are translated directly to AEDT format. @@ -890,17 +890,46 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import lines_thickness : float, optional Whether to thicken lines after creation and it's default value. Every line will be parametrized with a design variable called ``xsection_linename``. - import_solids : bool, optional - Whether to import the solids or only triangles. Default is ``True``. Returns ------- List of :class:`pyaedt.modeler.Object3d.Object3d` """ - nas_to_dict = {"Points": {}, "PointsId": {}, "Triangles": {}, "Lines": {}, "Solids": {}} + + def _write_solid_stl(triangle, nas_to_dict): + try: + points = [nas_to_dict["Points"][id] for id in triangle] + except KeyError: + return + fc = GeometryOperators.get_polygon_centroid(points) + v1 = points[0] + v2 = points[1] + cv1 = GeometryOperators.v_points(fc, v1) + cv2 = GeometryOperators.v_points(fc, v2) + if cv2[0] == cv1[0] == 0.0 and cv2[1] == cv1[1] == 0.0: + n = [0, 0, 1] + elif cv2[0] == cv1[0] == 0.0 and cv2[2] == cv1[2] == 0.0: + n = [0, 1, 0] + elif cv2[1] == cv1[1] == 0.0 and cv2[2] == cv1[2] == 0.0: + n = [1, 0, 0] + else: + n = GeometryOperators.v_cross(cv1, cv2) + + normal = GeometryOperators.normalize_vector(n) + if normal: + f.write(" facet normal {} {} {}\n".format(normal[0], normal[1], normal[2])) + f.write(" outer loop\n") + f.write(" vertex {} {} {}\n".format(points[0][0], points[0][1], points[0][2])) + f.write(" vertex {} {} {}\n".format(points[1][0], points[1][1], points[1][2])) + f.write(" vertex {} {} {}\n".format(points[2][0], points[2][1], points[2][2])) + f.write(" endloop\n") + f.write(" endfacet\n") + + nas_to_dict = {"Points": {}, "PointsId": {}, "Triangles": [], "Lines": {}, "Solids": {}} self.logger.reset_timer() self.logger.info("Loading file") + el_ids = [] with open_file(file_path, "r") as f: lines = f.read().splitlines() id = 0 @@ -927,22 +956,11 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import nas_to_dict["PointsId"][grid_id] = grid_id id += 1 else: - if tria_id in nas_to_dict["Triangles"]: - nas_to_dict["Triangles"][tria_id].append( - [ - int(n1), - int(n2), - int(n3), - ] - ) - else: - nas_to_dict["Triangles"][tria_id] = [ - [ - int(n1), - int(n2), - int(n3), - ] - ] + tri = [int(n1), int(n2), int(n3)] + tri.sort() + if tri not in nas_to_dict["Triangles"]: + nas_to_dict["Triangles"].append(tri) + elif line_type in ["GRID*", "CTRIA3*"]: grid_id = int(line[8:24]) if line_type == "CTRIA3*": @@ -955,7 +973,7 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import n2 = n2[0] + n2[1:].replace("-", "e-") n3 = line[72:88].strip() - if not n3 or n3 == "*": + if not n3 or n3.startswith("*"): lk += 1 n3 = lines[lk][8:24].strip() if "-" in n3[1:]: @@ -965,46 +983,60 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import nas_to_dict["PointsId"][grid_id] = id id += 1 else: - if tria_id in nas_to_dict["Triangles"]: - nas_to_dict["Triangles"][tria_id].append( - [ - int(n1), - int(n2), - int(n3), - ] - ) - else: - nas_to_dict["Triangles"][tria_id] = [ - [ - int(n1), - int(n2), - int(n3), - ] - ] + tri = [int(n1), int(n2), int(n3)] + tri.sort() + if tri not in nas_to_dict["Triangles"]: + nas_to_dict["Triangles"].append(tri) + elif line_type in ["CPENTA", "CHEXA", "CTETRA"]: - obj_id = int(line[16:24]) - n1 = int(line[24:32]) - n2 = int(line[32:40]) - n3 = int(line[40:48]) - n4 = int(line[48:56]) - obj_list = [line_type, n1, n2, n3, n4] + obj_id = line[16:24].strip() + n = [] + el_id = line[24:32].strip() + # n = [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": - n5 = int(line[56:64]) - n6 = int(line[64:72]) - obj_list.extend([n5, n6]) + n.append(int(line[56:64])) + n.append(int(line[64:72])) if line_type == "CHEXA": - n5 = int(line[56:64]) - n6 = int(line[64:72]) + n.append(int(line[56:64])) + n.append(int(line[64:72])) lk += 1 - n7 = int(lines[lk][8:16].strip()) - n8 = int(lines[lk][16:24].strip()) + n.append(int(lines[lk][8:16].strip())) + n.append(int(lines[lk][16:24].strip())) + from itertools import combinations + + tris = [] + for k in list(combinations(n, 3)): + tri = [int(k[0]), int(k[1]), int(k[2])] + tris.append(tri) + nas_to_dict["Solids"]["{}_{}".format(el_id, obj_id)] = tris + if el_id not in el_ids: + el_ids.append(el_id) + elif line_type in ["CTETRA*"]: + obj_id = line[8:24].strip() + n = [] + el_id = line[24:40].strip() + # n.append(line[24:40].strip()) + n.append(line[40:56].strip()) + + n.append(line[56:72].strip()) + lk += 1 + n.extend([lines[lk][i : i + 16] for i in range(16, len(lines[lk]), 16)]) + + from itertools import combinations + + tris = [] + for k in list(combinations(n, 3)): + tri = [int(k[0]), int(k[1]), int(k[2])] + tris.append(tri) + + nas_to_dict["Solids"]["{}_{}".format(el_id, obj_id)] = tris + if el_id not in el_ids: + el_ids.append(el_id) - obj_list.extend([n5, n6, n7, n8]) - if obj_id in nas_to_dict["Solids"]: - nas_to_dict["Solids"][obj_id].append(obj_list) - else: - nas_to_dict["Solids"][obj_id] = [[i for i in obj_list]] elif line_type in ["CROD", "CBEAM"]: obj_id = int(line[16:24]) n1 = int(line[24:32]) @@ -1021,40 +1053,21 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import 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") f.write("solid PyaedtStl\n") - for triangles in nas_to_dict["Triangles"].values(): - for triangle in triangles: - try: - points = [nas_to_dict["Points"][id] for id in triangle] - except KeyError: - continue - fc = GeometryOperators.get_polygon_centroid(points) - v1 = points[0] - v2 = points[1] - cv1 = GeometryOperators.v_points(fc, v1) - cv2 = GeometryOperators.v_points(fc, v2) - if cv2[0] == cv1[0] == 0.0 and cv2[1] == cv1[1] == 0.0: - n = [0, 0, 1] - elif cv2[0] == cv1[0] == 0.0 and cv2[2] == cv1[2] == 0.0: - n = [0, 1, 0] - elif cv2[1] == cv1[1] == 0.0 and cv2[2] == cv1[2] == 0.0: - n = [1, 0, 0] - else: - n = GeometryOperators.v_cross(cv1, cv2) - - normal = GeometryOperators.normalize_vector(n) - if normal: - f.write(" facet normal {} {} {}\n".format(normal[0], normal[1], normal[2])) - f.write(" outer loop\n") - f.write(" vertex {} {} {}\n".format(points[0][0], points[0][1], points[0][2])) - f.write(" vertex {} {} {}\n".format(points[1][0], points[1][1], points[1][2])) - f.write(" vertex {} {} {}\n".format(points[2][0], points[2][1], points[2][2])) - f.write(" endloop\n") - f.write(" endfacet\n") + for triangle in nas_to_dict["Triangles"]: + _write_solid_stl(triangle, nas_to_dict) f.write("endsolid\n") + for solidid, solid_triangles in nas_to_dict["Solids"].items(): + f.write("solid Solid_{}\n".format(solidid)) + for triangle in solid_triangles: + _write_solid_stl(triangle, nas_to_dict) + f.write("endsolid\n") f.close() self.logger.info("STL file created") self.import_3d_cad(os.path.join(self._app.working_directory, self._app.design_name + "_test.stl")) + for el in el_ids: + obj_names = [i for i in self.solid_names if i.startswith("Solid_{}_".format(el))] + self.create_group(obj_names, group_name=el) self.logger.info_timer("Faces imported") if import_lines: @@ -1087,48 +1100,6 @@ def import_nastran(self, file_path, import_lines=True, lines_thickness=0, import if not lines_thickness and out_poly: self.generate_object_history(out_poly) - if import_solids and nas_to_dict["Solids"]: - self.logger.reset_timer() - self.logger.info("Loading solids") - for solid_pid in nas_to_dict["Solids"]: - for solid in nas_to_dict["Solids"][solid_pid]: - points = [nas_to_dict["Points"][id] for id in solid[1:]] - if solid[0] == "CPENTA": - element1 = self._app.modeler.create_polyline( - points=[points[0], points[1], points[2]], cover_surface=True, close_surface=True - ) - element2 = self._app.modeler.create_polyline( - points=[points[3], points[4], points[5]], cover_surface=True, close_surface=True - ) - self._app.modeler.connect([element1.name, element2.name]) - element1.group_name = "PID_" + str(solid_pid) - elif solid[0] == "CHEXA": - element1 = self._app.modeler.create_polyline( - points=[points[0], points[1], points[2], points[3]], cover_surface=True, close_surface=True - ) - element2 = self._app.modeler.create_polyline( - points=[points[4], points[5], points[6], points[7]], cover_surface=True, close_surface=True - ) - self._app.modeler.connect([element1.name, element2.name]) - element1.group_name = "PID_" + str(solid_pid) - elif solid[0] == "CTETRA": - element1 = self._app.modeler.create_polyline( - points=[points[0], points[1], points[2]], cover_surface=True, close_surface=True - ) - element2 = self._app.modeler.create_polyline( - points=[points[0], points[1], points[3]], cover_surface=True, close_surface=True - ) - element3 = self._app.modeler.create_polyline( - points=[points[0], points[2], points[3]], cover_surface=True, close_surface=True - ) - element4 = self._app.modeler.create_polyline( - points=[points[1], points[2], points[3]], cover_surface=True, close_surface=True - ) - self._app.modeler.unite([element1.name, element2.name, element3.name, element4.name]) - element1.group_name = "PID_" + str(solid_pid) - - self.logger.info_timer("Solids loaded") - objs_after = [i for i in self.object_names] new_objects = [self[i] for i in objs_after if i not in objs_before] return new_objects diff --git a/pyaedt/workflows/__init__.py b/pyaedt/workflows/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/__init__.py @@ -0,0 +1,21 @@ +# 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. diff --git a/pyaedt/workflows/circuit/__init__.py b/pyaedt/workflows/circuit/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/circuit/__init__.py @@ -0,0 +1,21 @@ +# 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. diff --git a/pyaedt/workflows/customize_automation_tab.py b/pyaedt/workflows/customize_automation_tab.py new file mode 100644 index 00000000000..5f9eb6dd85e --- /dev/null +++ b/pyaedt/workflows/customize_automation_tab.py @@ -0,0 +1,601 @@ +# 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. + +import os +import shutil +import subprocess # nosec +import sys +import xml.etree.ElementTree as ET # nosec + +import defusedxml.minidom + +defusedxml.defuse_stdlib() + +import warnings + +from defusedxml.ElementTree import ParseError +from defusedxml.minidom import parseString + +from pyaedt import is_linux +from pyaedt.generic.general_methods import read_toml +import pyaedt.workflows +import pyaedt.workflows.templates + + +def add_automation_tab( + name, + lib_dir, + icon_file=None, + product="Project", + template="Run PyAEDT Toolkit Script", + overwrite=False, + panel="Panel_PyAEDT_Toolkits", +): + """Add an automation tab in AEDT. + + Parameters + ---------- + name : str + Toolkit name. + lib_dir : str + Path to the library directory. + icon_file : str + Full path to the icon file. The default is the PyAnsys icon. + product : str, optional + Product directory to install the toolkit. + template : str, optional + Script template name to use + overwrite : bool, optional + 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"``. + + Returns + ------- + str + Automation tab path. + + """ + + product = __tab_map(product) + + 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") + else: + try: + tree = ET.parse(tab_config_file_path) # nosec + except ParseError as e: + warnings.warn("Unable to parse %s\nError received = %s" % (tab_config_file_path, str(e))) + return + root = tree.getroot() + + panels = root.findall("./panel") + if panels: + panel_names = [panel_element.attrib["label"] for panel_element in panels] + if panel in panel_names: + # Remove previously existing PyAEDT panel and update with newer one. + panel_element = [panel_element for panel_element in panels if panel_element.attrib["label"] == panel][0] + else: + panel_element = ET.SubElement(root, "panel", label=panel) + else: + panel_element = ET.SubElement(root, "panel", label=panel) + + buttons = panel_element.findall("./button") + if buttons: + button_names = [button.attrib["label"] for button in buttons] + if name in button_names: + # Remove previously existing PyAEDT panel and update with newer one. + b = [button for button in buttons if button.attrib["label"] == name][0] + panel_element.remove(b) + + if not icon_file: + 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_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): + os.makedirs(dest_dir) + shutil.copy(icon_file, dest_file) + + relative_image_path = os.path.relpath(dest_file, os.path.join(lib_dir, product)) + + ET.SubElement( + panel_element, + "button", + label=name, + isLarge="1", + image=relative_image_path, + script="{}/{}".format(name, template), + ) + + # Backup any existing file if present + if os.path.isfile(tab_config_file_path): + shutil.copy(tab_config_file_path, tab_config_file_path + ".orig") + + create_xml_tab(root, tab_config_file_path) + return tab_config_file_path + + +def remove_automation_tab(name, lib_dir, panel="Panel_PyAEDT_Toolkits"): + """Remove automation tab in AEDT. + + Parameters + ---------- + name : str + Toolkit name. + lib_dir : str + Path to the library directory. + panel : str, optional + Panel name. The default is ``"Panel_PyAEDT_Toolkits"``. + + Returns + ------- + float + Result of the dot product. + + """ + + tab_config_file_path = os.path.join(lib_dir, "TabConfig.xml") + if not os.path.isfile(tab_config_file_path): + return True + try: + tree = ET.parse(tab_config_file_path) # nosec + except ParseError as e: + warnings.warn("Unable to parse %s\nError received = %s" % (tab_config_file_path, str(e))) + return + root = tree.getroot() + + panels = root.findall("./panel") + if panels: + panel_names = [panel_element.attrib["label"] for panel_element in panels] + if panel in panel_names: + # Remove previously existing PyAEDT panel and update with newer one. + panel_element = [panel_element for panel_element in panels if panel.attrib["label"] == panel][0] + else: + panel_element = ET.SubElement(root, "panel", label=panel) + else: + panel_element = ET.SubElement(root, "panel", label=panel) + + buttons = panel_element.findall("./button") + if buttons: + button_names = [button.attrib["label"] for button in buttons] + if name in button_names: + # Remove previously existing PyAEDT panel and update with newer one. + b = [button for button in buttons if button.attrib["label"] == name][0] + panel_element.remove(b) + + create_xml_tab(root, tab_config_file_path) + + +def create_xml_tab(root, output_file): + """Write the XML file to create the automation tab. + + Parameters + ---------- + root : :class:xml.etree.ElementTree + Root element of the main panel. + output_file : str + Full name of the file to save the XML tab. + """ + lines = [line for line in parseString(ET.tostring(root)).toprettyxml(indent=" " * 4).split("\n") if line.strip()] + xml_str = "\n".join(lines) + + with open(output_file, "w") as f: + f.write(xml_str) + + +def remove_xml_tab(toolkit_dir, name, panel="Panel_PyAEDT_Toolkits"): + """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): + return True + try: + tree = ET.parse(tab_config_file_path) # nosec + except ParseError as e: + warnings.warn("Unable to parse %s\nError received = %s" % (tab_config_file_path, str(e))) + return + root = tree.getroot() + + panels = root.findall("./panel") + if panels: + panel_names = [panel_element.attrib["label"] for panel_element in panels] + if panel in panel_names: + # Remove previously existing PyAEDT panel and update with newer one. + panel_element = [panel_element for panel_element in panels if panel_element.attrib["label"] == panel][0] + else: + panel_element = ET.SubElement(root, "panel", label=panel) + else: + panel_element = ET.SubElement(root, "panel", label=panel) + + buttons = panel_element.findall("./button") + if buttons: + button_names = [button.attrib["label"] for button in buttons] + if name in button_names: + # Remove previously existing PyAEDT panel and update with newer one. + b = [button for button in buttons if button.attrib["label"] == name][0] + panel_element.remove(b) + + create_xml_tab(root, tab_config_file_path) + + +def available_toolkits(): + product_list = [ + "Circuit", + "EMIT", + "HFSS", + "HFSS3DLayout", + "Icepak", + "Maxwell2D", + "Maxwell3D", + "Mechanical", + "Project", + "Q2D", + "Q3D", + "Simplorer", + ] + + product_toolkits = {} + for product in product_list: + toml_file = os.path.join(os.path.dirname(__file__), product.lower(), "toolkits_catalog.toml") + if os.path.isfile(toml_file): + toolkits_catalog = read_toml(toml_file) + product_toolkits[product] = toolkits_catalog + return product_toolkits + + +def add_script_to_menu( + desktop_object, + name, + script_file, + template_file="Run_PyAEDT_Toolkit_Script", + icon_file=None, + product="Project", + copy_to_personal_lib=True, + executable_interpreter=None, + panel="Panel_PyAEDT_Toolkits", +): + """Add a script to the ribbon menu. + + .. note:: + This method is available in AEDT 2023 R2 and later. PyAEDT must be installed + in AEDT to allow this method to run. For more information, see `Installation + `_. + + Parameters + ---------- + desktop_object : :class:pyaedt.desktop.Desktop + Desktop object. + name : str + Name of the toolkit to appear in AEDT. + 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"``. + icon_file : str, optional + Full path to the icon (a 30x30 pixel PNG file) to add to the UI. + The default is ``None``. + product : str, optional + Product to which the toolkit applies. The default is ``"Project"``, in which case + it applies to all designs. You can also specify a product, such as ``"HFSS"``. + copy_to_personal_lib : bool, optional + Whether to copy the script to Personal Lib or link the original script. Default is ``True``. + 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"``. + + Returns + ------- + bool + + """ + + if script_file and not os.path.exists(script_file): + desktop_object.logger.error("Script does not exists.") + return False + + toolkit_dir = os.path.join(desktop_object.personallib, "Toolkits") + aedt_version = desktop_object.aedt_version_id + tool_map = __tab_map(product) + tool_dir = os.path.join(toolkit_dir, tool_map, 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) + 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) + os.makedirs(tool_dir, exist_ok=True) + dest_script_path = None + if script_file and copy_to_personal_lib: + dest_script_path = os.path.join(lib_dir, os.path.split(script_file)[-1]) + shutil.copy2(script_file, dest_script_path) + + version_agnostic = False + if aedt_version[2:6].replace(".", "") in sys.executable: + executable_version_agnostic = sys.executable.replace(aedt_version[2:6].replace(".", ""), "%s") + version_agnostic = True + else: + executable_version_agnostic = sys.executable + + if executable_interpreter: + executable_version_agnostic = executable_interpreter + + templates_dir = os.path.dirname(pyaedt.workflows.templates.__file__) + + ipython_executable = executable_version_agnostic.replace("python" + __exe(), "ipython" + __exe()) + 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: + 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) + if dest_script_path: + build_file_data = build_file_data.replace("##PYTHON_SCRIPT##", dest_script_path) + + if not version_agnostic: + 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 + ) + desktop_object.logger.info("{} installed".format(name)) + return True + + +def __tab_map(product): # pragma: no cover + """Map exceptions in AEDT applications.""" + if product.lower() == "hfss3dlayout": + return "HFSS3DLayoutDesign" + elif product.lower() == "circuit": + return "CircuitDesign" + elif product.lower() == "q2d": + return "2DExtractor" + elif product.lower() == "q3d": + return "Q3DExtractor" + elif product.lower() == "simplorer": + return "TwinBuilder" + else: + return product + + +def add_custom_toolkit(desktop_object, toolkit_name, wheel_toolkit=None, install=True): # pragma: no cover + """Add toolkit to AEDT Automation Tab. + + Parameters + ---------- + desktop_object : :class:pyaedt.desktop.Desktop + Desktop object. + toolkit_name : str + Name of toolkit to add. + wheel_toolkit : str + Wheelhouse path. + install : bool, optional + Whether to install the toolkit. + + Returns + ------- + bool + """ + toolkits = available_toolkits() + toolkit_info = None + product_name = None + for product in toolkits: + if toolkit_name in toolkits[product]: + toolkit_info = toolkits[product][toolkit_name] + product_name = product + break + if not toolkit_info: + desktop_object.logger.error("Toolkit does not exist.") + return False + + # Set Python version based on AEDT version + python_version = "3.10" if desktop_object.aedt_version_id > "2023.1" else "3.7" + + if not is_linux: + base_venv = os.path.normpath( + os.path.join( + desktop_object.install_path, + "commonfiles", + "CPython", + python_version.replace(".", "_"), + "winx64", + "Release", + "python", + "python.exe", + ) + ) + else: + base_venv = os.path.normpath( + os.path.join( + desktop_object.install_path, + "commonfiles", + "CPython", + python_version.replace(".", "_"), + "linx64", + "Release", + "python", + "runpython", + ) + ) + + def run_command(command): + try: + if is_linux: # pragma: no cover + process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # nosec + else: + process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # nosec + _, stderr = process.communicate() + ret_code = process.returncode + if ret_code != 0: + print("Error occurred:", stderr.decode("utf-8")) + return ret_code + except Exception as e: + print("Exception occurred:", str(e)) + return 1 # Return non-zero exit code for indicating an error + + 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)) + 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)) + 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(".", "_")), + "{}/common/mono/Linux64/lib64".format(edt_root), + "{}".format(edt_root), + ] + if version < "232": + ld_library_path_dirs_to_add.append("{}/Delcross".format(edt_root)) + os.environ["LD_LIBRARY_PATH"] = ":".join(ld_library_path_dirs_to_add) + ":" + os.getenv("LD_LIBRARY_PATH", "") + + # Create virtual environment + + if not os.path.exists(venv_dir): + desktop_object.logger.info("Creating virtual environment") + run_command('"{}" -m venv "{}" --system-site-packages'.format(base_venv, venv_dir)) + desktop_object.logger.info("Virtual environment created.") + + is_installed = False + script_file = None + if os.path.isdir(os.path.normpath(os.path.join(package_dir, toolkit_info["script"]))): + script_file = os.path.normpath(os.path.join(package_dir, toolkit_info["script"])) + else: + for dirpath, dirnames, _ in os.walk(package_dir): + if "site-packages" in dirnames: + script_file = os.path.normpath(os.path.join(dirpath, "site-packages", toolkit_info["script"])) + break + if os.path.isfile(script_file): + is_installed = True + if wheel_toolkit: + wheel_toolkit = os.path.normpath(wheel_toolkit) + desktop_object.logger.info("Installing dependencies") + if install and wheel_toolkit and os.path.exists(wheel_toolkit): + desktop_object.logger.info("Starting offline installation") + if is_installed: + run_command('"{}" uninstall --yes {}'.format(pip_exe, toolkit_info["pip"])) + import zipfile + + unzipped_path = os.path.join( + os.path.dirname(wheel_toolkit), os.path.splitext(os.path.basename(wheel_toolkit))[0] + ) + if os.path.exists(unzipped_path): + shutil.rmtree(unzipped_path, ignore_errors=True) + with zipfile.ZipFile(wheel_toolkit, "r") as zip_ref: + zip_ref.extractall(unzipped_path) + + package_name = toolkit_info["package"] + run_command( + '"{}" install --no-cache-dir --no-index --find-links={} {}'.format(pip_exe, unzipped_path, package_name) + ) + elif install and not is_installed: + # Install the specified package + run_command('"{}" --default-timeout=1000 install {}'.format(pip_exe, toolkit_info["pip"])) + elif not install and is_installed: + # Uninstall toolkit + run_command('"{}" --default-timeout=1000 uninstall -y {}'.format(pip_exe, toolkit_info["package"])) + elif install and is_installed: + # Update toolkit + run_command('"{}" --default-timeout=1000 install {} -U'.format(pip_exe, toolkit_info["pip"])) + else: + desktop_object.logger.info("Incorrect input") + return + toolkit_dir = os.path.join(desktop_object.personallib, "Toolkits") + tool_dir = os.path.join(toolkit_dir, product_name, toolkit_info["name"]) + + script_image = os.path.abspath( + os.path.join(os.path.dirname(pyaedt.workflows.__file__), product_name.lower(), toolkit_info["icon"]) + ) + + if install: + if not os.path.exists(tool_dir): + # Install toolkit inside AEDT + add_script_to_menu( + desktop_object=desktop_object, + name=toolkit_info["name"], + script_file=script_file, + icon_file=script_image, + product=product_name, + template_file="Run_PyAEDT_Toolkit_Script", + copy_to_personal_lib=True, + executable_interpreter=python_exe, + ) + else: + if os.path.exists(tool_dir): + # Install toolkit inside AEDT + remove_script_from_menu( + desktop_object=desktop_object, + name=toolkit_info["name"], + product=product_name, + ) + + +def remove_script_from_menu(desktop_object, name, product="Project"): + """Remove a toolkit script from the menu. + + Parameters + ---------- + desktop_object : :class:pyaedt.desktop.Desktop + Desktop object. + name : str + Name of the toolkit to remove. + product : str, optional + Product to which the toolkit applies. The default is ``"Project"``, in which case + it applies to all designs. You can also specify a product, such as ``"HFSS"``. + + Returns + ------- + bool + """ + product = __tab_map(product) + toolkit_dir = os.path.join(desktop_object.personallib, "Toolkits") + aedt_version = desktop_object.aedt_version_id + tool_dir = os.path.join(toolkit_dir, product, name) + shutil.rmtree(tool_dir, ignore_errors=True) + if aedt_version >= "2023.2": + remove_xml_tab(os.path.join(toolkit_dir, product), name) + desktop_object.logger.info("{} toolkit removed successfully.".format(name)) + return True + + +def __exe(): + if not is_linux: + return ".exe" + return "" diff --git a/pyaedt/workflows/emit/__init__.py b/pyaedt/workflows/emit/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/emit/__init__.py @@ -0,0 +1,21 @@ +# 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. diff --git a/pyaedt/workflows/hfss/__init__.py b/pyaedt/workflows/hfss/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/hfss/__init__.py @@ -0,0 +1,21 @@ +# 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. diff --git a/pyaedt/workflows/hfss/images/large/antenna.png b/pyaedt/workflows/hfss/images/large/antenna.png new file mode 100644 index 0000000000000000000000000000000000000000..205f7c54aca8f2c4deeda0009155c9628f86736b GIT binary patch literal 1145 zcmV-<1cv*GP)Z2XG8hbxJyZcmkop(-oS~8~?ffiP0s5v$SSH|wZYBd_}alwM^Cdi&0435Q3nX=TVL-3<$>8YY^Wx(l`S`{DP%#4-V@szPyjrOFXH z!xj^>0F%k_JQHxD`(!vN>jif2;8A~=JR_$7ED3J-b@ciQ7jE9V4MMzYL02h^#^epG zj;#j|hZ6>aA;|HTfM8F8fx!!qy`xAYKR@_=)KctkY}HQ9&BXpf>@JAPId4JPUw#8X zATR-mDH*KOfyBFDVCcfCy%3m~1m3EsNRTUH7J!LV#yiVRxng zXQA+Z3;)6>w{{1v#9(^c*7 zNa#--M_<%!KPJ*eM5GZm75RL=brzbMZgr;4vE*zU=H!)VwzJrtSE3y}i@#-MoE^h) zah@l1kuxA7?PLwenJG3bL3QKtr_1Zxf5gZx%z(B@1Hm~HNF5s^Pe#BO@v znE;vTE0{?0h)4~Qo;P(2ZA>eyzE;)JxnxSZJX`0#+~oG;6=H#==qC61eCx0voxwyJ zC&c|!wD&NP=ijNQSuv_5Wfrlw z02ZX{e;dJL4vx?+-2yn0@ERsEyE6zQ3+8Q>8N{{j94ltWTL8{vBj00000 LNkvXXu0mjfh#nnn literal 0 HcmV?d00001 diff --git a/pyaedt/workflows/hfss/toolkits_catalog.toml b/pyaedt/workflows/hfss/toolkits_catalog.toml new file mode 100644 index 00000000000..8626340b3fa --- /dev/null +++ b/pyaedt/workflows/hfss/toolkits_catalog.toml @@ -0,0 +1,7 @@ +[AntennaWizard] +name = "Antenna Wizard" +script = "ansys/aedt/toolkits/antenna/run_toolkit.py" +icon = "images/large/antenna.png" +template = "Run_PyAEDT_Toolkit_Script" +pip = "git+https://github.com/ansys/pyaedt-antenna-toolkit.git" +package = "ansys.aedt.toolkits.antenna" diff --git a/pyaedt/workflows/hfss3dlayout/__init__.py b/pyaedt/workflows/hfss3dlayout/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/hfss3dlayout/__init__.py @@ -0,0 +1,21 @@ +# 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. diff --git a/pyaedt/workflows/hfss3dlayout/export_to_3D.py b/pyaedt/workflows/hfss3dlayout/export_to_3D.py new file mode 100644 index 00000000000..0ab251c87d7 --- /dev/null +++ b/pyaedt/workflows/hfss3dlayout/export_to_3D.py @@ -0,0 +1,111 @@ +import os +from tkinter import Button +from tkinter import Label +from tkinter import RAISED +from tkinter import StringVar +from tkinter import Tk +from tkinter import mainloop +from tkinter import ttk +from tkinter.ttk import Combobox + +import PIL.Image +import PIL.ImageTk + +from pyaedt import Desktop +from pyaedt import Hfss +from pyaedt import Hfss3dLayout +from pyaedt import Icepak +from pyaedt import Maxwell3d +from pyaedt import Q3d +import pyaedt.workflows.hfss3dlayout + +master = Tk() + +master.geometry("400x150") + +master.title("Export to 3D") + +# Load the logo for the main window +icon_path = os.path.join(os.path.dirname(pyaedt.workflows.__file__), "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", 10)) + +var = StringVar() +label = Label(master, textvariable=var, relief=RAISED) +var.set("Choose an option:") +label.pack(pady=10) +combo = Combobox(master, width=40) # Set the width of the combobox +combo["values"] = ("Export to HFSS", "Export to Q3D", "Export to Maxwell 3D", "Export to Icepak") +combo.current(0) +combo.pack(pady=10) + +combo.focus_set() +choice = "Export to HFSS" + + +def callback(): + global choice + choice = combo.get() + master.destroy() + return True + + +b = Button(master, text="Export", width=40, command=callback) +b.pack(pady=10) + +mainloop() + +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 = 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 ["HFSS 3D Layout Design"]: + desname = des.GetName().split(";")[1] + else: + d.odesktop.AddMessage("", "", 3, "Hfss 3D Layout project is needed.") + d.release_desktop(False, False) + raise Exception("Hfss 3D Layout project is needed.") + h3d = Hfss3dLayout(projectname=projname, designname=desname) + setup = h3d.create_setup() + suffix = suffixes[choice] + + if choice == "Export to Q3D": + setup.export_to_q3d(h3d.project_file[:-5] + f"_{suffix}.aedt", keep_net_name=True) + else: + setup.export_to_hfss(h3d.project_file[:-5] + f"_{suffix}.aedt", keep_net_name=True) + h3d.delete_setup(setup.name) + if choice == "Export to Q3D": + app = Q3d(projectname=h3d.project_file[:-5] + f"_{suffix}.aedt") + else: + app = Hfss(projectname=h3d.project_file[:-5] + f"_{suffix}.aedt") + app2 = None + if choice == "Export to Maxwell 3D": + app2 = Maxwell3d(projectname=app.project_name) + elif choice == "Export to Icepak": + app2 = Icepak(projectname=app.project_name) + if app2: + app2.copy_solid_bodies_from( + app, + no_vacuum=False, + no_pec=False, + include_sheets=True, + ) + app2.delete_design(app.design_name) + app2.save_project() + d.logger.info("Project generated correctly.") diff --git a/pyaedt/workflows/hfss3dlayout/images/large/cad3d.png b/pyaedt/workflows/hfss3dlayout/images/large/cad3d.png new file mode 100644 index 0000000000000000000000000000000000000000..13e423090603b085bf31b25a7a842b5628cb679c GIT binary patch literal 2447 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 literal 0 HcmV?d00001 diff --git a/pyaedt/misc/images/large/pyansys.png b/pyaedt/workflows/images/large/pyansys.png similarity index 100% rename from pyaedt/misc/images/large/pyansys.png rename to pyaedt/workflows/images/large/pyansys.png diff --git a/pyaedt/workflows/installer/__init__.py b/pyaedt/workflows/installer/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/installer/__init__.py @@ -0,0 +1,21 @@ +# 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. diff --git a/pyaedt/misc/console_setup.py b/pyaedt/workflows/installer/console_setup.py similarity index 97% rename from pyaedt/misc/console_setup.py rename to pyaedt/workflows/installer/console_setup.py index d044e63a1a5..b32f3273fa1 100644 --- a/pyaedt/misc/console_setup.py +++ b/pyaedt/workflows/installer/console_setup.py @@ -20,7 +20,7 @@ # to PyAEDT is created in the personal library. console_setup_dir = os.path.dirname(__file__) if "PersonalLib" in console_setup_dir: - sys.path.append(os.path.join(console_setup_dir, "..", "..", "..")) + sys.path.append(os.path.join(console_setup_dir, "../..", "..", "..")) import pyaedt diff --git a/pyaedt/workflows/installer/create_report.py b/pyaedt/workflows/installer/create_report.py new file mode 100644 index 00000000000..55a0ad348cd --- /dev/null +++ b/pyaedt/workflows/installer/create_report.py @@ -0,0 +1,39 @@ +# Generate pdf report +# ~~~~~~~~~~~~~~~~~~~ +# Generate a pdf report with output of simultion. +import os + +from pyaedt import Desktop +from pyaedt import get_pyaedt_app +from pyaedt.generic.pdf import AnsysReport + +if "PYAEDT_SCRIPT_PORT" in os.environ and "PYAEDT_SCRIPT_VERSION" in os.environ: + port = 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() + desname = des.GetName() + if des.GetDesignType() in ["HFSS 3D Layout Design", "Circuit Design"]: + desname = None + app = get_pyaedt_app(projname, desname) + + report = AnsysReport(version=d.aedt_version_id, design_name=app.design_name, project_name=app.project_name) + report.create() + report.add_section() + report.add_chapter(f"{app.solution_type} Results") + report.add_sub_chapter("Plots") + report.add_text("This section contains all reports results.") + for plot in app.post.plots: + app.post.export_report_to_jpg(app.working_directory, plot.plot_name) + report.add_image(os.path.join(app.working_directory, plot.plot_name + ".jpg"), plot.plot_name) + report.add_page_break() + report.add_toc() + out = report.save_pdf(app.working_directory, "AEDT_Results.pdf") + d.odesktop.AddMessage("", "", 0, f"Report Generated. {out}") diff --git a/pyaedt/workflows/installer/images/large/console.png b/pyaedt/workflows/installer/images/large/console.png new file mode 100644 index 0000000000000000000000000000000000000000..5d22ff8a4c9d43128fe2161e031ee2d6dd84ed96 GIT binary patch literal 1247 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 BO>", update_page) + + update_page() + + return install_button, uninstall_button, input_file, toolkits_combo, toolkit_name + + +def is_toolkit_installed(toolkit_name, window_name): + """Check if toolkit is installed.""" + if toolkit_name == "Custom": + return False + toolkits = available_toolkits() + script_file = os.path.normpath(os.path.join(package_dir, toolkits[window_name][toolkit_name]["script"])) + if os.path.isfile(script_file): + return True + else: + lib_dir = os.path.dirname(package_dir) + for dirpath, dirnames, _ in os.walk(lib_dir): + if "site-packages" in dirnames: + script_file = os.path.normpath( + os.path.join(dirpath, "site-packages", toolkits[window_name][toolkit_name]["script"]) + ) + if os.path.isfile(script_file): + return True + break + return False + + +def open_window(window, window_name, internal_toolkits): + """Open a window.""" + if not hasattr(window, "opened"): + window.opened = True + window.title(window_name) + install_button, uninstall_button, input_file, toolkits_combo, toolkit_name = create_toolkit_page( + window, window_name, internal_toolkits + ) + root.minsize(500, 250) + return install_button, uninstall_button, input_file, toolkits_combo, toolkit_name + else: + window.deiconify() + + +def __get_command_function( + is_install, toolkit_level, input_file, toolkits_combo, toolkit_name, install_button, uninstall_button +): + return lambda: button_is_clicked( + is_install, toolkit_level, input_file, toolkits_combo, toolkit_name, install_button, uninstall_button + ) + + +def toolkit_window(toolkit_level="Project"): + """Create interactive toolkit window.""" + toolkit_window_var = tk.Toplevel(root) + + toolkits = available_toolkits() + + if toolkit_level not in toolkits: + install_button, uninstall_button, input_file, toolkits_combo, toolkit_name = open_window( + toolkit_window_var, toolkit_level, [] + ) + else: + install_button, uninstall_button, input_file, toolkits_combo, toolkit_name = open_window( + toolkit_window_var, toolkit_level, list(toolkits[toolkit_level].keys()) + ) + toolkit_window_var.minsize(250, 150) + + install_command = __get_command_function( + True, toolkit_level, input_file, toolkits_combo, toolkit_name, install_button, uninstall_button + ) + uninstall_command = __get_command_function( + False, toolkit_level, input_file, toolkits_combo, toolkit_name, install_button, uninstall_button + ) + + install_button.configure(command=install_command) + uninstall_button.configure(command=uninstall_command) + + +def button_is_clicked( + install_action, toolkit_level, input_file, combo_toolkits, toolkit_name, install_button, uninstall_button +): + """Set up a button for installing and uninstalling the toolkit.""" + file = input_file.get() + selected_toolkit_name = combo_toolkits.get() + name = toolkit_name.get() + + desktop = Desktop( + specified_version=version, + port=port, + new_desktop_session=False, + non_graphical=False, + close_on_exit=False, + student_version=student_version, + ) + + desktop.odesktop.CloseAllWindows() + + toolkits = available_toolkits() + selected_toolkit_info = {} + icon = None + if toolkit_level in toolkits and selected_toolkit_name in toolkits[toolkit_level]: + selected_toolkit_info = toolkits[toolkit_level][selected_toolkit_name] + if not selected_toolkit_info.get("pip"): + product_path = os.path.join(os.path.dirname(pyaedt.workflows.__file__), toolkit_level.lower()) + file = os.path.abspath(os.path.join(product_path, selected_toolkit_info.get("script"))) + name = selected_toolkit_info.get("name") + icon = os.path.abspath(os.path.join(product_path, selected_toolkit_info.get("icon"))) + + if selected_toolkit_name != "Custom" and selected_toolkit_info.get("pip"): + if is_toolkit_installed(selected_toolkit_name, toolkit_level) and install_action: + desktop.logger.info("Updating {}".format(selected_toolkit_name)) + add_custom_toolkit(desktop, selected_toolkit_name, file) + install_button.config(text="Update") + uninstall_button.config(state="normal") + desktop.logger.info("{} updated".format(selected_toolkit_name)) + elif install_action: + desktop.logger.info("Installing {}".format(selected_toolkit_name)) + add_custom_toolkit(desktop, selected_toolkit_name, file) + install_button.config(text="Update") + uninstall_button.config(state="normal") + elif is_toolkit_installed(selected_toolkit_name, toolkit_level) and not install_action: + desktop.logger.info("Uninstalling {}".format(selected_toolkit_name)) + add_custom_toolkit(desktop, selected_toolkit_name, install=False) + install_button.config(text="Install") + uninstall_button.config(state="disabled") + desktop.logger.info("{} uninstalled".format(selected_toolkit_name)) + else: + desktop.logger.info("{} not installed".format(selected_toolkit_name)) + + else: + 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 os.path.isfile(executable_interpreter): + add_script_to_menu( + desktop_object=desktop, + name=name, + script_file=file, + product=toolkit_level, + icon_file=icon, + executable_interpreter=executable_interpreter, + ) + else: + desktop.logger.info("PyAEDT environment is not installed.") + else: + desktop.logger.info("Uninstall {}.".format(name)) + remove_script_from_menu(desktop_object=desktop, name=name, product=toolkit_level) + + desktop.odesktop.CloseAllWindows() + desktop.odesktop.RefreshToolkitUI() + desktop.release_desktop(False, False) + + +root = tk.Tk() +root.title("AEDT Toolkit Manager") + +# Load the logo for the main window +icon_path = os.path.join(os.path.dirname(pyaedt.workflows.__file__), "images", "large", "logo.png") +im = PIL.Image.open(icon_path) +photo = PIL.ImageTk.PhotoImage(im) + +# Set the icon for the main window +root.iconphoto(True, photo) + +# Configure style for ttk buttons +style = ttk.Style() +style.configure("Toolbutton.TButton", padding=6, font=("Helvetica", 10)) + +toolkit_levels = [ + "Project", + "", + "", + "", + "HFSS", + "Maxwell3D", + "Icepak", + "Q3D", + "Maxwell2D", + "Q2D", + "HFSS3DLayout", + "Mechanical", + "Circuit", + "EMIT", + "Simplorer", + "", +] + +window_width, window_height = 500, 250 +screen_width = root.winfo_screenwidth() +screen_height = root.winfo_screenheight() +x_position = (screen_width - window_width) // 2 +y_position = (screen_height - window_height) // 2 + +root.geometry(f"{window_width}x{window_height}+{x_position}+{y_position}") + +# Create buttons in a 4x4 grid, centered +for i, level in enumerate(toolkit_levels): + row_num = i // 4 + col_num = i % 4 + if level: + toolkit_button = ttk.Button( + root, text=level, command=lambda l=level: toolkit_window(l), style="Toolbutton.TButton" + ) + toolkit_button.grid(row=row_num, column=col_num, padx=10, pady=10) + +root.minsize(window_width, window_height) + +root.mainloop() diff --git a/pyaedt/workflows/installer/toolkits_catalog.toml b/pyaedt/workflows/installer/toolkits_catalog.toml new file mode 100644 index 00000000000..a5baf22c440 --- /dev/null +++ b/pyaedt/workflows/installer/toolkits_catalog.toml @@ -0,0 +1,23 @@ +[Console] +name = "PyAEDT Console" +script = "console_setup.py" +icon = "console.png" +template = "PyAEDT_Console" + +[Jupyter] +name = "Jupyter Notebook" +script = "jupyter_template.ipynb" +icon = "jupyter.png" +template = "Jupyter" + +[Run_Script] +name = "Run PyAEDT Script" +script = "" +icon = "run_script.png" +template = "Run_PyAEDT_Script" + +[ToolkitManager] +name = "Toolkit Manager" +script = "toolkit_manager.py" +icon = "toolkit_manager.png" +template = "Run_Toolkit_Manager" diff --git a/pyaedt/workflows/maxwell2d/__init__.py b/pyaedt/workflows/maxwell2d/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/maxwell2d/__init__.py @@ -0,0 +1,21 @@ +# 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. diff --git a/pyaedt/workflows/maxwell3d/__init__.py b/pyaedt/workflows/maxwell3d/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/maxwell3d/__init__.py @@ -0,0 +1,21 @@ +# 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. diff --git a/pyaedt/workflows/maxwell3d/images/large/magnet_segmentation.png b/pyaedt/workflows/maxwell3d/images/large/magnet_segmentation.png new file mode 100644 index 0000000000000000000000000000000000000000..2732690092d358c737617d2f6a4fe8e23915cd66 GIT binary patch literal 1206 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 "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/__init__.py b/pyaedt/workflows/templates/__init__.py new file mode 100644 index 00000000000..3bc3c8e5b19 --- /dev/null +++ b/pyaedt/workflows/templates/__init__.py @@ -0,0 +1,21 @@ +# 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.