From d3ed2f3995d942398fa7991c1573b44bc198e9f8 Mon Sep 17 00:00:00 2001
From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com>
Date: Wed, 21 Aug 2024 09:02:29 +0200
Subject: [PATCH 01/23] CHORE: Update v0.11.dev0 (#5063)
---
doc/source/release_1_0.rst | 2 +-
src/ansys/aedt/core/__init__.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/doc/source/release_1_0.rst b/doc/source/release_1_0.rst
index c46b1819b8d..0563eabe3c7 100644
--- a/doc/source/release_1_0.rst
+++ b/doc/source/release_1_0.rst
@@ -65,7 +65,7 @@ accordingly. An example of migration is shown below:
.. code-block:: python
- from ansys.aedt import Circuit
+ from ansys.aedt.core import Circuit
Python files renaming
---------------------
diff --git a/src/ansys/aedt/core/__init__.py b/src/ansys/aedt/core/__init__.py
index f73b7bb71f6..b6a1a526b37 100644
--- a/src/ansys/aedt/core/__init__.py
+++ b/src/ansys/aedt/core/__init__.py
@@ -64,7 +64,7 @@ def custom_show_warning(message, category, filename, lineno, file=None, line=Non
#
pyaedt_path = os.path.dirname(__file__)
-__version__ = "0.10.dev0"
+__version__ = "0.11.dev0"
version = __version__
#
From efec464c49b0cec294835813cf78a282bb17304b Mon Sep 17 00:00:00 2001
From: gmalinve <103059376+gmalinve@users.noreply.github.com>
Date: Wed, 21 Aug 2024 09:15:59 +0200
Subject: [PATCH 02/23] FEAT: add mesh skin depth for maxwell 2d (#5041)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Sébastien Morais <146729917+SMoraisAnsys@users.noreply.github.com>
---
_unittest/test_27_Maxwell2D.py | 18 ++++++++-
_unittest/test_28_Maxwell3D.py | 12 +++++-
src/ansys/aedt/core/modules/mesh.py | 59 +++++++++++++++--------------
3 files changed, 59 insertions(+), 30 deletions(-)
diff --git a/_unittest/test_27_Maxwell2D.py b/_unittest/test_27_Maxwell2D.py
index c154266883b..74b1741de59 100644
--- a/_unittest/test_27_Maxwell2D.py
+++ b/_unittest/test_27_Maxwell2D.py
@@ -450,7 +450,7 @@ def test_28_set_variable(self):
assert "var_test" in self.aedtapp.variable_manager.design_variable_names
assert self.aedtapp.variable_manager.design_variables["var_test"].expression == "234"
- def test_31_cylindrical_gap(self):
+ def test_31a_cylindrical_gap(self):
assert not self.aedtapp.mesh.assign_cylindrical_gap("Band")
[
x.delete()
@@ -467,6 +467,22 @@ def test_31_cylindrical_gap(self):
]
assert self.aedtapp.mesh.assign_cylindrical_gap("Band", name="cyl_gap_test", band_mapping_angle=2)
+ def test_31b_skin_depth(self):
+ edge = self.aedtapp.modeler["Rotor"].edges[0]
+ mesh = self.aedtapp.mesh.assign_skin_depth(assignment=edge, skin_depth="0.3mm", layers_number=3)
+ assert mesh
+ assert mesh.type == "SkinDepthBased"
+ assert mesh.props["Edges"][0] == edge.id
+ assert mesh.props["SkinDepth"] == "0.3mm"
+ assert mesh.props["NumLayers"] == 3
+ edge1 = self.aedtapp.modeler["Rotor"].edges[1]
+ mesh = self.aedtapp.mesh.assign_skin_depth(assignment=edge1.id, skin_depth="0.3mm", layers_number=3)
+ assert mesh
+ assert mesh.type == "SkinDepthBased"
+ assert mesh.props["Edges"][0] == edge1.id
+ assert mesh.props["SkinDepth"] == "0.3mm"
+ assert mesh.props["NumLayers"] == 3
+
def test_32_control_program(self):
user_ctl_path = "user.ctl"
ctrl_prg_path = os.path.join(local_path, "example_models", test_subfolder, ctrl_prg_file)
diff --git a/_unittest/test_28_Maxwell3D.py b/_unittest/test_28_Maxwell3D.py
index 449cb5a5543..e1667a35407 100644
--- a/_unittest/test_28_Maxwell3D.py
+++ b/_unittest/test_28_Maxwell3D.py
@@ -282,7 +282,17 @@ def test_22_create_length_mesh(self):
assert self.aedtapp.mesh.assign_length_mesh(["Plate"])
def test_23_create_skin_depth(self):
- assert self.aedtapp.mesh.assign_skin_depth(["Plate"], "1mm")
+ mesh = self.aedtapp.mesh.assign_skin_depth(["Plate"], "1mm")
+ assert mesh
+ mesh.delete()
+ mesh = self.aedtapp.mesh.assign_skin_depth(["Plate"], "1mm", 1000)
+ assert mesh
+ mesh.delete()
+ mesh = self.aedtapp.mesh.assign_skin_depth(self.aedtapp.modeler["Plate"].faces[0].id, "1mm")
+ assert mesh
+ mesh.delete()
+ mesh = self.aedtapp.mesh.assign_skin_depth(self.aedtapp.modeler["Plate"], "1mm")
+ assert mesh
def test_24_create_curvilinear(self):
assert self.aedtapp.mesh.assign_curvilinear_elements(["Coil"], "1mm")
diff --git a/src/ansys/aedt/core/modules/mesh.py b/src/ansys/aedt/core/modules/mesh.py
index fc94dd7d94f..cf1522607ae 100644
--- a/src/ansys/aedt/core/modules/mesh.py
+++ b/src/ansys/aedt/core/modules/mesh.py
@@ -1066,7 +1066,7 @@ def assign_skin_depth(
Parameters
----------
assignment : list
- List of the object names or face IDs.
+ List of the object names, face IDs or edges IDs for Maxwell 2D design.
skin_depth : str, float, optional
Skin depth value.
It can be either provided as a float or as a string.
@@ -1092,7 +1092,7 @@ def assign_skin_depth(
"""
assignment = self.modeler.convert_to_selections(assignment, True)
- if self._app.design_type != "HFSS" and self._app.design_type != "Maxwell 3D":
+ if self._app.design_type not in ["HFSS", "Maxwell 3D", "Maxwell 2D"]:
raise MethodNotSupportedError
if name:
for m in self.meshoperations:
@@ -1101,35 +1101,38 @@ def assign_skin_depth(
else:
name = generate_unique_name("SkinDepth")
- if maximum_elements is None:
- restrictlength = False
- maximum_elements = "1000"
+ if self._app.design_type == "Maxwell 2D":
+ props = OrderedDict(
+ {
+ "Edges": assignment,
+ "SkinDepth": skin_depth,
+ "NumLayers": layers_number,
+ }
+ )
else:
- restrictlength = True
- assignment = self._app.modeler.convert_to_selections(assignment, True)
+ if maximum_elements is None:
+ restrictlength = False
+ maximum_elements = "1000"
+ else:
+ restrictlength = True
- if isinstance(assignment[0], int):
- seltype = "Faces"
- elif isinstance(assignment[0], str):
- seltype = "Objects"
- else:
- seltype = None
- if seltype is None:
- self.logger.error("Error in Assignment")
- return
+ if isinstance(assignment[0], int):
+ seltype = "Faces"
+ elif isinstance(assignment[0], str):
+ seltype = "Objects"
- props = OrderedDict(
- {
- "Type": "SkinDepthBased",
- "Enabled": True,
- seltype: assignment,
- "RestrictElem": restrictlength,
- "NumMaxElem": str(maximum_elements),
- "SkinDepth": skin_depth,
- "SurfTriMaxLength": triangulation_max_length,
- "NumLayers": layers_number,
- }
- )
+ props = OrderedDict(
+ {
+ "Type": "SkinDepthBased",
+ "Enabled": True,
+ seltype: assignment,
+ "RestrictElem": restrictlength,
+ "NumMaxElem": str(maximum_elements),
+ "SkinDepth": skin_depth,
+ "SurfTriMaxLength": triangulation_max_length,
+ "NumLayers": layers_number,
+ }
+ )
mop = MeshOperation(self, name, props, "SkinDepthBased")
mop.create()
From 466a1dd2b87dd36dffc36827e018a4465fba4349 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Morais?=
<146729917+SMoraisAnsys@users.noreply.github.com>
Date: Wed, 21 Aug 2024 09:31:12 +0200
Subject: [PATCH 03/23] TEST: Improve test delegation warning (#5077)
---
_unittest/test_warnings.py | 11 +++--------
src/pyaedt/__init__.py | 4 ++++
2 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/_unittest/test_warnings.py b/_unittest/test_warnings.py
index da8285a2f88..6fe7d14590d 100644
--- a/_unittest/test_warnings.py
+++ b/_unittest/test_warnings.py
@@ -30,6 +30,7 @@
from ansys.aedt.core import PYTHON_VERSION_WARNING
from ansys.aedt.core import deprecation_warning
from pyaedt import ALIAS_WARNING
+import pytest
VALID_PYTHON_VERSION = (LATEST_DEPRECATED_PYTHON_VERSION[0], LATEST_DEPRECATED_PYTHON_VERSION[1] + 1)
@@ -60,11 +61,5 @@ def test_alias_deprecation_warning():
import pyaedt
- # Ensure that the warning will be triggered again
- del pyaedt.__warningregistry__
-
- importlib.reload(pyaedt)
-
- # Hardcoded test where 28 is the line number associated to the warning call
- # TODO: See if pytest.warns can be 'fixed' to work with module reload
- assert (ALIAS_WARNING, FutureWarning, 28) in pyaedt.__warningregistry__
+ with pytest.warns(FutureWarning, match=ALIAS_WARNING):
+ importlib.reload(pyaedt)
diff --git a/src/pyaedt/__init__.py b/src/pyaedt/__init__.py
index 13da1ede8a2..f43843f5806 100644
--- a/src/pyaedt/__init__.py
+++ b/src/pyaedt/__init__.py
@@ -4,6 +4,7 @@
__version__ = __version__
import warnings
+import os
ALIAS_WARNING = (
"Module 'pyaedt' has become an alias to the new package structure. " \
@@ -22,6 +23,9 @@ def alias_deprecation_warning(): # pragma: no cover
def custom_show_warning(message, category, filename, lineno, file=None, line=None):
"""Custom warning used to remove :loc: pattern."""
print("{}: {}".format(category.__name__, message))
+ # NOTE: This line is added to ensure that pytest handle the warning correctly.
+ if "PYTEST_CURRENT_TEST" in os.environ:
+ existing_showwarning(message, category, filename, lineno, file=file, line=line)
warnings.showwarning = custom_show_warning
From 885e505101f024575136bd46640dee440f668e17 Mon Sep 17 00:00:00 2001
From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com>
Date: Wed, 21 Aug 2024 09:47:56 +0200
Subject: [PATCH 04/23] DOCS: Update python version (#5076)
---
README.md | 2 +-
README_CN.md | 2 +-
doc/source/Getting_started/Installation.rst | 28 ++++---------------
.../Getting_started/Troubleshooting.rst | 4 +--
4 files changed, 9 insertions(+), 27 deletions(-)
diff --git a/README.md b/README.md
index 435b79794af..fbefa97cc5a 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
[![PyAnsys](https://img.shields.io/badge/Py-Ansys-ffc107.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABDklEQVQ4jWNgoDfg5mD8vE7q/3bpVyskbW0sMRUwofHD7Dh5OBkZGBgW7/3W2tZpa2tLQEOyOzeEsfumlK2tbVpaGj4N6jIs1lpsDAwMJ278sveMY2BgCA0NFRISwqkhyQ1q/Nyd3zg4OBgYGNjZ2ePi4rB5loGBhZnhxTLJ/9ulv26Q4uVk1NXV/f///////69du4Zdg78lx//t0v+3S88rFISInD59GqIH2esIJ8G9O2/XVwhjzpw5EAam1xkkBJn/bJX+v1365hxxuCAfH9+3b9/+////48cPuNehNsS7cDEzMTAwMMzb+Q2u4dOnT2vWrMHu9ZtzxP9vl/69RVpCkBlZ3N7enoDXBwEAAA+YYitOilMVAAAAAElFTkSuQmCC)](https://docs.pyansys.com/)
[![pypi](https://img.shields.io/pypi/v/pyaedt.svg?logo=python&logoColor=white)](https://pypi.org/project/pyaedt/)
[![PyPIact](https://static.pepy.tech/badge/pyaedt/month)](https://www.pepy.tech/projects/pyaedt)
-[![PythonVersion](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/)
+[![PythonVersion](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![GH-CI](https://github.com/ansys/pyaedt/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/ansys/pyaedt/actions/workflows/unit_tests.yml)
[![codecov](https://codecov.io/gh/ansys/pyaedt/branch/main/graph/badge.svg)](https://codecov.io/gh/ansys/pyaedt)
[![MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/blog/license/mit)[![black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat)](https://github.com/psf/black)[![Anaconda](https://anaconda.org/conda-forge/pyaedt/badges/version.svg)](https://anaconda.org/conda-forge/pyaedt)
diff --git a/README_CN.md b/README_CN.md
index c2934de6367..000205ec7f9 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -10,7 +10,7 @@
English | ä¸æ–‡
-[![PyAnsys](https://img.shields.io/badge/Py-Ansys-ffc107.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABDklEQVQ4jWNgoDfg5mD8vE7q/3bpVyskbW0sMRUwofHD7Dh5OBkZGBgW7/3W2tZpa2tLQEOyOzeEsfumlK2tbVpaGj4N6jIs1lpsDAwMJ278sveMY2BgCA0NFRISwqkhyQ1q/Nyd3zg4OBgYGNjZ2ePi4rB5loGBhZnhxTLJ/9ulv26Q4uVk1NXV/f///////69du4Zdg78lx//t0v+3S88rFISInD59GqIH2esIJ8G9O2/XVwhjzpw5EAam1xkkBJn/bJX+v1365hxxuCAfH9+3b9/+////48cPuNehNsS7cDEzMTAwMMzb+Q2u4dOnT2vWrMHu9ZtzxP9vl/69RVpCkBlZ3N7enoDXBwEAAA+YYitOilMVAAAAAElFTkSuQmCC)](https://docs.pyansys.com/)[![pypi](https://img.shields.io/pypi/v/pyaedt.svg?logo=python&logoColor=white)](https://pypi.org/project/pyaedt/)[![PyPIact](https://pepy.tech/badge/pyaedt/month)](https://pypi.org/project/pyaedt/)[![PythonVersion](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/)[![GH-CI](https://github.com/ansys/pyaedt/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/ansys/pyaedt/actions/workflows/unit_tests.yml)[![codecov](https://codecov.io/gh/ansys/pyaedt/branch/main/graph/badge.svg)](https://codecov.io/gh/ansys/pyaedt)[![MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/blog/license/mit)[![black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat)](https://github.com/psf/black)[![Anaconda](https://anaconda.org/conda-forge/pyaedt/badges/version.svg)](https://anaconda.org/conda-forge/pyaedt)[![pre-commit](https://results.pre-commit.ci/badge/github/ansys/pyaedt/main.svg)](https://results.pre-commit.ci/latest/github/ansys/pyaedt/main)
+[![PyAnsys](https://img.shields.io/badge/Py-Ansys-ffc107.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABDklEQVQ4jWNgoDfg5mD8vE7q/3bpVyskbW0sMRUwofHD7Dh5OBkZGBgW7/3W2tZpa2tLQEOyOzeEsfumlK2tbVpaGj4N6jIs1lpsDAwMJ278sveMY2BgCA0NFRISwqkhyQ1q/Nyd3zg4OBgYGNjZ2ePi4rB5loGBhZnhxTLJ/9ulv26Q4uVk1NXV/f///////69du4Zdg78lx//t0v+3S88rFISInD59GqIH2esIJ8G9O2/XVwhjzpw5EAam1xkkBJn/bJX+v1365hxxuCAfH9+3b9/+////48cPuNehNsS7cDEzMTAwMMzb+Q2u4dOnT2vWrMHu9ZtzxP9vl/69RVpCkBlZ3N7enoDXBwEAAA+YYitOilMVAAAAAElFTkSuQmCC)](https://docs.pyansys.com/)[![pypi](https://img.shields.io/pypi/v/pyaedt.svg?logo=python&logoColor=white)](https://pypi.org/project/pyaedt/)[![PyPIact](https://pepy.tech/badge/pyaedt/month)](https://pypi.org/project/pyaedt/)[![PythonVersion](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)[![GH-CI](https://github.com/ansys/pyaedt/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/ansys/pyaedt/actions/workflows/unit_tests.yml)[![codecov](https://codecov.io/gh/ansys/pyaedt/branch/main/graph/badge.svg)](https://codecov.io/gh/ansys/pyaedt)[![MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/blog/license/mit)[![black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat)](https://github.com/psf/black)[![Anaconda](https://anaconda.org/conda-forge/pyaedt/badges/version.svg)](https://anaconda.org/conda-forge/pyaedt)[![pre-commit](https://results.pre-commit.ci/badge/github/ansys/pyaedt/main.svg)](https://results.pre-commit.ci/latest/github/ansys/pyaedt/main)
## PyAEDT 简介
diff --git a/doc/source/Getting_started/Installation.rst b/doc/source/Getting_started/Installation.rst
index 16e491ee992..f1480f7873e 100644
--- a/doc/source/Getting_started/Installation.rst
+++ b/doc/source/Getting_started/Installation.rst
@@ -71,7 +71,7 @@ see `Extensions `_
+Wheelhouses for CPython 3.8, 3.9, 3.10, 3.11, and 3.12 are available in the releases for both Windows and Linux. From the `Releases `_
page in the PyAEDT repository, you can find the wheelhouses for a particular release in its
assets and download the wheelhouse specific to your setup.
You can then install PyAEDT and all of its dependencies from one single entry point that can be shared internally,
which eases the security review of the PyAEDT package content.
-For example, on Windows with Python 3.7, install PyAEDT and all its dependencies from a wheelhouse with code like this:
+For example, on Windows with Python 3.10, install PyAEDT and all its dependencies from a wheelhouse with code like this:
.. code::
- pip install --no-cache-dir --no-index --find-links=file:////PyAEDT-v-wheelhouse-Windows-3.7 pyaedt
-
-
-Use IronPython in AEDT
-~~~~~~~~~~~~~~~~~~~~~~
-PyAEDT is designed to work in CPython 3.7+ and supports many advanced processing packages like
-``matplotlib``, ``numpy``, and ``pyvista``. A user can still use PyAEDT in the IronPython
-environment available in AEDT with many limitations.
-
-To use IronPython in AEDT:
-
-1. Download the PyAEDT package from ``https://pypi.org/project/pyaedt/#files``.
-2. Extract the files.
-3. Install PyAEDT into AEDT, specifying the full paths to ``ipy64`` and ``setup-distutils.py`` as needed:
-
-.. code::
-
- ipy64 setup-distutils.py install --user
+ pip install --no-cache-dir --no-index --find-links=file:////PyAEDT-v-wheelhouse-Windows-3.10 pyaedt
Install PyAEDT in Conda virtual environment
diff --git a/doc/source/Getting_started/Troubleshooting.rst b/doc/source/Getting_started/Troubleshooting.rst
index 33e105cb44b..1d62085640f 100644
--- a/doc/source/Getting_started/Troubleshooting.rst
+++ b/doc/source/Getting_started/Troubleshooting.rst
@@ -14,11 +14,11 @@ In this case, you can use the Python interpreter available in the AEDT installat
Python 3.7 is available in AEDT 2023 R1 and earlier. Python 3.10 is available in AEDT 2023 R2.
-Here is the path to the Python 3.7 interpreter for the 2023 R1 installation:
+Here is the path to the Python 3.10 interpreter for the 2024 R2 installation:
.. code:: python
- path\to\AnsysEM\v231\commonfiles\CPython\3_7\winx64\Release\python"
+ path\to\AnsysEM\v242\commonfiles\CPython\3_10\winx64\Release\python"
Error installing PyAEDT using pip
From e40bc359e7bcd37dff8eeb3b9737aa33e090d1f1 Mon Sep 17 00:00:00 2001
From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com>
Date: Wed, 21 Aug 2024 11:22:29 +0200
Subject: [PATCH 05/23] FEAT: Unite option in export Q3D and HFSS (#5054)
---
_unittest/test_41_3dlayout_modeler.py | 10 +++++
src/ansys/aedt/core/modules/solve_setup.py | 47 +++++++++++++++-------
2 files changed, 43 insertions(+), 14 deletions(-)
diff --git a/_unittest/test_41_3dlayout_modeler.py b/_unittest/test_41_3dlayout_modeler.py
index 07da92d854a..df072e9f2be 100644
--- a/_unittest/test_41_3dlayout_modeler.py
+++ b/_unittest/test_41_3dlayout_modeler.py
@@ -526,20 +526,30 @@ def test_19d_export_to_hfss(self):
self.aedtapp.save_project()
filename = "export_to_hfss_test"
filename2 = "export_to_hfss_test2"
+ filename3 = "export_to_hfss_test_non_unite"
file_fullname = os.path.join(self.local_scratch.path, filename)
file_fullname2 = os.path.join(self.local_scratch.path, filename2)
+ file_fullname3 = os.path.join(self.local_scratch.path, filename3)
setup = self.aedtapp.get_setup(self.aedtapp.existing_analysis_setups[0])
assert setup.export_to_hfss(output_file=file_fullname)
if not is_linux:
# TODO: EDB failing in Linux
assert setup.export_to_hfss(output_file=file_fullname2, keep_net_name=True)
+ assert setup.export_to_hfss(output_file=file_fullname3, keep_net_name=True, unite=False)
+
def test_19e_export_to_q3d(self):
filename = "export_to_q3d_test"
file_fullname = os.path.join(self.local_scratch.path, filename)
setup = self.aedtapp.get_setup(self.aedtapp.existing_analysis_setups[0])
assert setup.export_to_q3d(file_fullname)
+ def test_19f_export_to_q3d(self):
+ filename = "export_to_q3d_non_unite_test"
+ file_fullname = os.path.join(self.local_scratch.path, filename)
+ setup = self.aedtapp.get_setup(self.aedtapp.existing_analysis_setups[0])
+ assert setup.export_to_q3d(file_fullname, keep_net_name=True, unite=False)
+
def test_21_variables(self):
assert isinstance(self.aedtapp.available_variations.nominal_w_values_dict, dict)
assert isinstance(self.aedtapp.available_variations.nominal_w_values, list)
diff --git a/src/ansys/aedt/core/modules/solve_setup.py b/src/ansys/aedt/core/modules/solve_setup.py
index bc853bea27a..c70979c3cfa 100644
--- a/src/ansys/aedt/core/modules/solve_setup.py
+++ b/src/ansys/aedt/core/modules/solve_setup.py
@@ -1902,7 +1902,7 @@ def disable(self):
return True
@pyaedt_function_handler(file_fullname="output_file")
- def export_to_hfss(self, output_file, keep_net_name=False):
+ def export_to_hfss(self, output_file, keep_net_name=False, unite=True):
"""Export the HFSS 3D Layout design to an HFSS 3D design.
This method is not supported with IronPython.
@@ -1911,9 +1911,12 @@ def export_to_hfss(self, output_file, keep_net_name=False):
----------
output_file : str
Full path and file name for exporting the project.
-
- keep_net_name : bool
- Keep net name in 3D export when ``True`` or by default when ``False``. Default value is ``False``.
+ keep_net_name : bool, optional
+ Keep net name in 3D export.
+ The default is ``False``.
+ unite : bool, optional
+ Unite bodies which belong to the same net.
+ The default is ``True``.
Returns
-------
@@ -1938,13 +1941,14 @@ def export_to_hfss(self, output_file, keep_net_name=False):
if not is_ironpython:
from ansys.aedt.core import Hfss
- self._get_net_names(Hfss, output_file)
+ self._get_net_names(Hfss, output_file, unite)
else:
self.p_app.logger.error("Exporting layout while keeping net name is not supported with IronPython")
return succeeded
@pyaedt_function_handler()
- def _get_net_names(self, app, file_fullname):
+ def _get_net_names(self, app, file_fullname, unite):
+ """Identify nets and unite bodies that belong to the same net."""
primitives_3d_pts_per_nets = self._get_primitives_points_per_net()
self.p_app.logger.info("Processing vias...")
via_per_nets = self._get_via_position_per_net()
@@ -1956,6 +1960,7 @@ def _get_net_names(self, app, file_fullname):
aedtapp = app(project=file_fullname)
units = aedtapp.modeler.model_units
aedt_units = AEDT_UNITS["Length"][units]
+ object_list = aedtapp.modeler.object_names
self._convert_edb_to_aedt_units(input_dict=primitives_3d_pts_per_nets, output_unit=aedt_units)
self._convert_edb_to_aedt_units(input_dict=via_per_nets, output_unit=aedt_units)
self._convert_edb_layer_elevation_to_aedt_units(input_dict=layers_elevation, output_units=aedt_units)
@@ -1997,16 +2002,27 @@ def _get_net_names(self, app, file_fullname):
self.p_app.logger.info("Renaming primitives for net {}...".format(net_name))
object_names = list(set(object_names))
if len(object_names) == 1:
-
object_p = aedtapp.modeler[object_names[0]]
object_p.name = net_name
object_p.color = [randrange(255), randrange(255), randrange(255)] # nosec
elif len(object_names) > 1:
- united_object = aedtapp.modeler.unite(object_names, purge=True)
- obj_ind = aedtapp.modeler.objects[united_object].id
- if obj_ind:
- aedtapp.modeler.objects[obj_ind].name = net_name
- aedtapp.modeler.objects[obj_ind].color = [randrange(255), randrange(255), randrange(255)] # nosec
+ if unite:
+ united_object = aedtapp.modeler.unite(object_names, purge=True)
+ obj_ind = aedtapp.modeler.objects[united_object].id
+ if obj_ind:
+ aedtapp.modeler.objects[obj_ind].name = net_name
+ aedtapp.modeler.objects[obj_ind].color = [
+ randrange(255),
+ randrange(255),
+ randrange(255),
+ ] # nosec
+ else:
+ name_cont = 0
+ for body in object_names:
+ body_name = net_name + f"_{name_cont}"
+ if body in object_list:
+ aedtapp.modeler.objects[body].name = body_name
+ name_cont += 1
if aedtapp.design_type == "Q3D Extractor":
aedtapp.auto_identify_nets()
@@ -2159,7 +2175,7 @@ def _check_export_log(self, info_messages, error_messages, file_fullname):
return succeeded
@pyaedt_function_handler(file_fullname="output_file")
- def export_to_q3d(self, output_file, keep_net_name=False):
+ def export_to_q3d(self, output_file, keep_net_name=False, unite=True):
"""Export the HFSS 3D Layout design to a Q3D design.
Parameters
@@ -2168,6 +2184,9 @@ def export_to_q3d(self, output_file, keep_net_name=False):
Full path and file name for exporting the project.
keep_net_name : bool
Whether to keep the net name in the 3D export, The default is ``False``.
+ unite : bool, optional
+ Unite bodies which belong to the same net.
+ The default is ``True``.
Returns
-------
@@ -2195,7 +2214,7 @@ def export_to_q3d(self, output_file, keep_net_name=False):
if not is_ironpython:
from ansys.aedt.core import Q3d
- self._get_net_names(Q3d, output_file)
+ self._get_net_names(Q3d, output_file, unite)
else:
self.p_app.logger.error("Exporting layout while keeping net name is not supported with IronPython.")
return succeeded
From da9399ebbacaa40f190306df7e7c604fd8a19247 Mon Sep 17 00:00:00 2001
From: Lorenzo Vecchietti <58366962+lorenzovecchietti@users.noreply.github.com>
Date: Wed, 21 Aug 2024 14:36:45 +0200
Subject: [PATCH 06/23] FIX: Reduce run-time of Sherlock example (#5043)
---
examples/04-Icepak/Sherlock_Example.py | 106 +++++++++----------------
1 file changed, 36 insertions(+), 70 deletions(-)
diff --git a/examples/04-Icepak/Sherlock_Example.py b/examples/04-Icepak/Sherlock_Example.py
index 70406929952..a55c2eead1e 100644
--- a/examples/04-Icepak/Sherlock_Example.py
+++ b/examples/04-Icepak/Sherlock_Example.py
@@ -44,13 +44,11 @@
component_step = "TutorialBoard.stp"
aedt_odb_project = "SherlockTutorial.aedt"
aedt_odb_design_name = "PCB"
-stackup_thickness = 2.11836
-outline_polygon_name = "poly_14188"
###############################################################################
# Launch AEDT
# ~~~~~~~~~~~
-# Launch AEDT 2023 R2 in graphical mode.
+# Launch AEDT 2024 R2 in graphical mode.
d = ansys.aedt.core.launch_desktop(version=aedt_version, non_graphical=non_graphical, new_desktop=True)
@@ -60,22 +58,21 @@
validation = os.path.join(project_folder, "validation.log")
file_path = os.path.join(input_dir, component_step)
project_name = os.path.join(project_folder, component_step[:-3] + "aedt")
+component_name = "from_ODB"
###############################################################################
# Create Icepak project
# ~~~~~~~~~~~~~~~~~~~~~
# Create an Icepak project.
-ipk = ansys.aedt.core.Icepak(project_name)
+ipk = ansys.aedt.core.Icepak(project=project_name)
###############################################################################
-# Delete region to speed up import
+# Disable autosave to speed up import
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-# Delete the region and disable autosave to speed up the import.
+# Disable autosave to speed up the import.
d.disable_autosave()
-ipk.modeler.delete("Region")
-component_name = "from_ODB"
###############################################################################
# Import PCB from AEDB file
@@ -92,49 +89,35 @@
# Create an offset coordinate system to match ODB++ with the
# Sherlock STEP file.
-ipk.modeler.create_coordinate_system(origin=[0, 0, stackup_thickness / 2], mode="view", view="XY")
+bb = ipk.modeler.user_defined_components[component_name+"1"].bounding_box
+stackup_thickness = bb[-1] - bb[2]
+ipk.modeler.create_coordinate_system(
+ origin=[0, 0, stackup_thickness / 2], mode="view", view="XY"
+)
###############################################################################
# Import CAD file
# ~~~~~~~~~~~~~~~
-# Import a CAD file.
+# Import a CAD file and delete the CAD "pcb" object as the ECAD is already in the design.
ipk.modeler.import_3d_cad(file_path, refresh_all_ids=False)
-
-###############################################################################
-# Save CAD file
-# ~~~~~~~~~~~~~
-# Save the CAD file and refresh the properties from the parsing of the AEDT file.
-
-ipk.save_project(refresh_ids=True)
-
-###############################################################################
-# Plot model
-# ~~~~~~~~~~
-# Plot the model.
-
-ipk.plot(show=False, output_file=os.path.join(project_folder, "Sherlock_Example.jpg"), plot_air_objects=False)
-
-###############################################################################
-# Delete PCB objects
-# ~~~~~~~~~~~~~~~~~~
-# Delete the PCB objects.
-
ipk.modeler.delete_objects_containing("pcb", False)
###############################################################################
-# Create region
+# Modify air region
# ~~~~~~~~~~~~~
-# Create an air region.
+# Modify air region dimensions.
-ipk.modeler.create_air_region(*[20, 20, 300, 20, 20, 300])
+ipk.mesh.global_mesh_region.global_region.padding_values = [20, 20, 20, 20, 300, 300]
###############################################################################
# Assign materials
# ~~~~~~~~~~~~~~~~
# Assign materials from Sherlock file.
-ipk.assignmaterial_from_sherlock_files(component_list, material_list)
+ipk.assignmaterial_from_sherlock_files(
+ component_file=component_list, material_file=material_list
+)
###############################################################################
# Delete objects with no material assignments
@@ -159,42 +142,42 @@
total_power = ipk.assign_block_from_sherlock_file(csv_name=component_list)
+###############################################################################
+# Assign openings
+# ~~~~~~~~~~~~~~~~~~~
+# Assign opening boundary condition to all the faces of the region.
+ipk.assign_openings(ipk.modeler.get_object_faces("Region"))
+
###############################################################################
# Plot model
# ~~~~~~~~~~
# Plot the model again now that materials are assigned.
-ipk.plot(show=False, output_file=os.path.join(project_folder, "Sherlock_Example.jpg"), plot_air_objects=False)
+ipk.plot(
+ show=False,
+ output_file=os.path.join(project_folder, "Sherlock_Example.jpg"),
+ plot_air_objects=False,
+ plot_as_separate_objects=False
+)
###############################################################################
-# Set up boundaries
+# Set up mesh settings
# ~~~~~~~~~~~~~~~~~
-# Set up boundaries.
-
# Mesh settings that is tailored for PCB
-# Max iterations is set to 20 for quick demonstration, please increase to at least 100 for better accuracy.
ipk.globalMeshSettings(3, gap_min_elements='1', noOgrids=True, MLM_en=True,
MLM_Type='2D', edge_min_elements='2', object='Region')
+###############################################################################
+# Numerical settings
+# ~~~~~~~~~~~~~~~~~
+
setup1 = ipk.create_setup()
setup1.props["Solution Initialization - Y Velocity"] = "1m_per_sec"
setup1.props["Radiation Model"] = "Discrete Ordinates Model"
setup1.props["Include Gravity"] = True
setup1.props["Secondary Gradient"] = True
-setup1.props["Convergence Criteria - Max Iterations"] = 10
-ipk.assign_openings(ipk.modeler.get_object_faces("Region"))
-
-###############################################################################
-# Create point monitor
-# ~~~~~~~~~~~~~~~~~~~~
-
-point1 = ipk.assign_point_monitor(ipk.modeler["COMP_U10"].top_face_z.center, monitor_name="Point1")
-ipk.modeler.set_working_coordinate_system("Global")
-line = ipk.modeler.create_polyline(
- [ipk.modeler["COMP_U10"].top_face_z.vertices[0].position, ipk.modeler["COMP_U10"].top_face_z.vertices[2].position],
- non_model=True)
-ipk.post.create_report(expressions="Point1.Temperature", primary_sweep_variable="X")
+setup1.props["Convergence Criteria - Max Iterations"] = 100
###############################################################################
# Check for intersections
@@ -208,24 +191,9 @@
# Compute power budget
# ~~~~~~~~~~~~~~~~~~~~
-power_budget, total = ipk.post.power_budget("W" )
+power_budget, total = ipk.post.power_budget("W")
print(total)
-###############################################################################
-# Analyze the model
-# ~~~~~~~~~~~~~~~~~
-
-# ipk.analyze(cores=4, tasks=4)
-ipk.save_project()
-
-###############################################################################
-# Get solution data and plots
-# ~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-plot1 = ipk.post.create_fieldplot_surface(ipk.modeler["COMP_U10"].faces, "SurfTemperature")
-# ipk.post.plot_field("SurfPressure", ipk.modeler["COMP_U10"].faces, show=False, export_path=ipk.working_directory)
-
-
###############################################################################
# Save project and release AEDT
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -237,5 +205,3 @@
print("Elapsed time: {}".format(datetime.timedelta(seconds=end)))
print("Project Saved in {} ".format(ipk.project_file))
ipk.release_desktop()
-
-
From 263416e3749723e3ecd5db4d053e9dc51a83efc6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Morais?=
<146729917+SMoraisAnsys@users.noreply.github.com>
Date: Wed, 21 Aug 2024 15:21:33 +0200
Subject: [PATCH 07/23] CI: Fix deploy stable docs index (#5081)
---
.github/workflows/ci_cd.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml
index b20d2f17963..49048e51208 100644
--- a/.github/workflows/ci_cd.yml
+++ b/.github/workflows/ci_cd.yml
@@ -523,7 +523,9 @@ jobs:
uses: actions/checkout@v4
- name: Install the package requirements
- run: pip install -e .
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e .
- name: Get the version to PyMeilisearch
run: |
From 0a2c4bace5d72b7938d6abccf2f779073ca6a5a1 Mon Sep 17 00:00:00 2001
From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com>
Date: Thu, 22 Aug 2024 12:30:53 +0200
Subject: [PATCH 08/23] CHORE: Import tomllib from python 3.12 (#5084)
---
pyproject.toml | 2 +-
src/ansys/aedt/core/generic/general_methods.py | 14 +++++++++++---
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 8862669cecb..d584dc2b76a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -38,7 +38,7 @@ dependencies = [
"psutil",
"pyedb>=0.4.0; python_version == '3.7'",
"pyedb>=0.24.0; python_version > '3.7'",
- "pytomlpp",
+ "pytomlpp; python_version < '3.12'",
"rpyc>=6.0.0,<6.1",
]
diff --git a/src/ansys/aedt/core/generic/general_methods.py b/src/ansys/aedt/core/generic/general_methods.py
index 0d9d96bc973..2f96eb64080 100644
--- a/src/ansys/aedt/core/generic/general_methods.py
+++ b/src/ansys/aedt/core/generic/general_methods.py
@@ -510,7 +510,7 @@ def read_json(fn):
@pyaedt_function_handler()
-def read_toml(file_path):
+def read_toml(file_path): # pragma: no cover
"""Read a TOML file and return as a dictionary.
Parameters
@@ -523,7 +523,11 @@ def read_toml(file_path):
dict
Parsed TOML file as a dictionary.
"""
- import pytomlpp as tomllib
+ current_version = sys.version_info[:2]
+ if current_version < (3, 12):
+ import pytomlpp as tomllib
+ else:
+ import tomllib
with open_file(file_path, "rb") as fb:
return tomllib.load(fb)
@@ -1319,7 +1323,11 @@ def is_digit(c):
@pyaedt_function_handler()
def _create_toml_file(input_dict, full_toml_path):
- import pytomlpp as tomllib
+ current_version = sys.version_info[:2]
+ if current_version < (3, 12):
+ import pytomlpp as tomllib
+ else:
+ import tomllib
if not os.path.exists(os.path.dirname(full_toml_path)):
os.makedirs(os.path.dirname(full_toml_path))
From 558e2e360299db229eb7f17a7fa709764935e33a Mon Sep 17 00:00:00 2001
From: Hui Zhou
Date: Fri, 23 Aug 2024 08:41:51 +0200
Subject: [PATCH 09/23] FIX: h3d plot dc field (#5086)
Co-authored-by: ring630 <@gmail.com>
---
src/ansys/aedt/core/modules/post_processor.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/ansys/aedt/core/modules/post_processor.py b/src/ansys/aedt/core/modules/post_processor.py
index d96a9251565..aee16da526a 100644
--- a/src/ansys/aedt/core/modules/post_processor.py
+++ b/src/ansys/aedt/core/modules/post_processor.py
@@ -3788,6 +3788,7 @@ def create_fieldplot_layers_nets(
lst = []
for layer in layers_nets:
for el in layer[1:]:
+ el = "" if el == "no-net" else el
get_ids = self._odesign.GetGeometryIdsForNetLayerCombination(el, layer[0], setup)
if isinstance(get_ids, (tuple, list)) and len(get_ids) > 2:
lst.extend([int(i) for i in get_ids[2:]])
From d54c1d27f2c1ea447d541c70a56ec8e996f8c8f5 Mon Sep 17 00:00:00 2001
From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com>
Date: Fri, 23 Aug 2024 12:50:53 +0200
Subject: [PATCH 10/23] FIX: Solve setup in optimetrics (#5090)
---
_unittest_solvers/test_00_analyze.py | 4 ++++
src/ansys/aedt/core/application/analysis.py | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/_unittest_solvers/test_00_analyze.py b/_unittest_solvers/test_00_analyze.py
index a3f0d2991f2..032ebd83917 100644
--- a/_unittest_solvers/test_00_analyze.py
+++ b/_unittest_solvers/test_00_analyze.py
@@ -217,6 +217,9 @@ def test_02_hfss_export_results(self, hfss_app):
intrinsics=[])
assert not os.path.exists(fld_file2)
+ hfss_app.variable_manager.set_variable(name="dummy", expression=1, is_post_processing=True)
+ sweep = hfss_app.parametrics.add(variable="dummy", start_point=0, end_point=1, step=2)
+ assert hfss_app.analyze_setup(name=sweep.name, cores=4)
def test_03a_icepak_analyze_and_export_summary(self):
self.icepak_app.solution_type = self.icepak_app.SOLUTIONS.Icepak.SteadyFlowOnly
@@ -548,6 +551,7 @@ def test_09c_compute_com(self, local_scratch):
com_param.export_spisim_cfg(str(Path(local_scratch.path) / "test.cfg"))
com_0, com_1 = spisim.compute_com(0, Path(local_scratch.path) / "test.cfg")
assert com_0 and com_1
+
def test_10_export_to_maxwell(self, add_app):
app = add_app("assm_test", application=Rmxprt, subfolder="T00")
app.analyze(cores=1)
diff --git a/src/ansys/aedt/core/application/analysis.py b/src/ansys/aedt/core/application/analysis.py
index 2a8bb4f07a5..d31c8bfa54d 100644
--- a/src/ansys/aedt/core/application/analysis.py
+++ b/src/ansys/aedt/core/application/analysis.py
@@ -1919,7 +1919,7 @@ def analyze_setup(
else:
try:
self.logger.info("Solving Optimetrics")
- self.ooptimetrics.solve_setup(name)
+ self.ooptimetrics.SolveSetup(name)
except Exception: # pragma: no cover
if set_custom_dso and active_config:
self.set_registry_key(r"Desktop/ActiveDSOConfigurations/" + self.design_type, active_config)
From 175650d7a424c6bb595bb9b33081b2705a7dd352 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Morais?=
<146729917+SMoraisAnsys@users.noreply.github.com>
Date: Fri, 23 Aug 2024 14:22:22 +0200
Subject: [PATCH 11/23] DOCS: Use new ansys-sphinx-theme (#5089)
---
doc/source/conf.py | 4 ++--
pyproject.toml | 8 ++++----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 6fc8c127c04..91697a923be 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -10,7 +10,7 @@
import numpy as np
from sphinx_gallery.sorting import FileNameSortKey
from ansys_sphinx_theme import (ansys_favicon,
- get_version_match, pyansys_logo_black,
+ get_version_match,
watermark,
ansys_logo_white,
ansys_logo_white_cropped, latex)
@@ -313,7 +313,6 @@ def setup(app):
# -- Options for HTML output -------------------------------------------------
html_short_title = html_title = "PyAEDT"
html_theme = "ansys_sphinx_theme"
-html_logo = pyansys_logo_black
html_context = {
"github_user": "ansys",
"github_repo": "pyaedt",
@@ -323,6 +322,7 @@ def setup(app):
# specify the location of your github repo
html_theme_options = {
+ "logo": "pyansys",
"github_url": "https://github.com/ansys/pyaedt",
"navigation_with_keys": False,
"show_prev_next": False,
diff --git a/pyproject.toml b/pyproject.toml
index d584dc2b76a..0328d3eb1c6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -70,7 +70,7 @@ dotnet = [
"pywin32>=303; platform_system=='Windows'",
]
doc = [
- "ansys-sphinx-theme>=0.10.0,<0.17",
+ "ansys-sphinx-theme>=0.10.0,<1.1",
"ipython>=8.13.0,<8.27",
"joblib>=1.3.0,<1.5",
"jupyterlab>=4.0.0,<4.3",
@@ -84,7 +84,7 @@ doc = [
"pyvista[io]>=0.38.0,<0.45",
"recommonmark",
"scikit-rf>=0.30.0,<1.3",
- "Sphinx>=7.1.0,<7.4",
+ "Sphinx>=7.1.0,<8.1",
"sphinx-autobuild==2021.3.14; python_version == '3.8'",
"sphinx-autobuild==2024.4.16; python_version > '3.8'",
#"sphinx-autodoc-typehints",
@@ -97,10 +97,10 @@ doc = [
"utm",
]
doc-no-examples = [
- "ansys-sphinx-theme>=0.10.0,<0.17",
+ "ansys-sphinx-theme>=0.10.0,<1.1",
"numpydoc>=1.5.0,<1.9",
"recommonmark",
- "Sphinx>=7.1.0,<7.4",
+ "Sphinx>=7.1.0,<8.1",
"sphinx-autobuild==2021.3.14; python_version == '3.8'",
"sphinx-autobuild==2024.4.16; python_version > '3.8'",
#"sphinx-autodoc-typehints",
From b726b28cb7e51428a5aa12a502ff90b788900820 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 27 Aug 2024 09:28:50 +0200
Subject: [PATCH 12/23] CHORE: pre-commit autoupdate (#5093)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 83d0e5ee859..8409e50c7a5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -48,7 +48,7 @@ repos:
# validate GitHub workflow files
- repo: https://github.com/python-jsonschema/check-jsonschema
- rev: 0.29.1
+ rev: 0.29.2
hooks:
- id: check-github-workflows
From 61dfaf92c8211bf5b56fa902648b37766c55d2ce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Morais?=
<146729917+SMoraisAnsys@users.noreply.github.com>
Date: Tue, 27 Aug 2024 13:40:59 +0200
Subject: [PATCH 13/23] REFACTOR: Settings from YAML configuration file (#5092)
Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com>
---
_unittest/test_utils.py | 46 +
doc/source/Resources/pyaedt_settings.yaml | 122 +++
doc/source/User_guide/settings.rst | 141 +++
.../config/vocabularies/ANSYS/accept.txt | 1 +
pyproject.toml | 1 +
src/ansys/aedt/core/__init__.py | 9 +-
src/ansys/aedt/core/application/design.py | 33 +-
src/ansys/aedt/core/desktop.py | 4 +
.../aedt/core/generic/general_methods.py | 1 +
src/ansys/aedt/core/generic/settings.py | 867 +++++++++++-------
10 files changed, 879 insertions(+), 346 deletions(-)
create mode 100644 doc/source/Resources/pyaedt_settings.yaml
create mode 100644 doc/source/User_guide/settings.rst
diff --git a/_unittest/test_utils.py b/_unittest/test_utils.py
index 06d4252eae6..269eb6bbf3c 100644
--- a/_unittest/test_utils.py
+++ b/_unittest/test_utils.py
@@ -30,6 +30,7 @@
from unittest.mock import patch
from ansys.aedt.core.generic.general_methods import pyaedt_function_handler
+from ansys.aedt.core.generic.settings import Settings
from ansys.aedt.core.generic.settings import settings
import pytest
@@ -93,3 +94,48 @@ def test_handler_deprecation_log_warning(caplog):
foo(trigger_exception=False)
assert len(caplog.records) == 1
+
+
+def test_settings_load_yaml(tmp_path):
+ """Test loading a configure file with correct input."""
+ default_settings = Settings()
+
+ # Create temporary YAML configuration file
+ yaml_path = tmp_path / "pyaedt_settings.yaml"
+ yaml_path.write_text(
+ """
+ log:
+ global_log_file_name: 'dummy'
+ lsf:
+ lsf_num_cores: 12
+ general:
+ desktop_launch_timeout: 12
+ """
+ )
+
+ default_settings.load_yaml_configuration(str(yaml_path))
+
+ assert default_settings.global_log_file_name == "dummy"
+ assert default_settings.lsf_num_cores == 12
+ assert default_settings.desktop_launch_timeout == 12
+
+
+def test_settings_load_yaml_with_non_allowed_key(tmp_path):
+ """Test loading a configuration file with invalid key."""
+ default_settings = Settings()
+
+ # Create temporary YAML configuration file
+ yaml_path = tmp_path / "pyaedt_settings.yaml"
+ yaml_path.write_text(
+ """
+ general:
+ dummy: 12.0
+ """
+ )
+
+ default_settings.load_yaml_configuration(str(yaml_path), raise_on_wrong_key=False)
+ assert not hasattr(default_settings, "dummy")
+
+ with pytest.raises(KeyError) as excinfo:
+ default_settings.load_yaml_configuration(str(yaml_path), raise_on_wrong_key=True)
+ assert str(excinfo) in "Key 'dummy' is not part of the allowed keys"
diff --git a/doc/source/Resources/pyaedt_settings.yaml b/doc/source/Resources/pyaedt_settings.yaml
new file mode 100644
index 00000000000..13b4b214f72
--- /dev/null
+++ b/doc/source/Resources/pyaedt_settings.yaml
@@ -0,0 +1,122 @@
+# This file contains the settings used by default to set the PyAEDT and AEDT including logging,
+# LSF, environment variables and general settings. If you want to have a different behavior
+# you can modify this file and save it. To be used in PyAEDT, the path to the configuration file
+# should be specified with the environment variable ``PYAEDT_LOCAL_SETTINGS_PATH``. If no
+# environment variable is set, PyAEDT looks for the configuration file ``pyaedt_settings.yaml``
+# in the user's ``APPDATA`` folder for Windows and ``HOME`` folder for Linux.
+
+# Settings related to logging
+log:
+ # Enable or disable the logging of EDB API methods
+ enable_debug_edb_logger: false
+ # Enable or disable the logging of the geometry operators
+ enable_debug_geometry_operator_logger: false
+ # Enable or disable the logging of the gRPC API calls
+ enable_debug_grpc_api_logger: false
+ # Enable or disable the logging of internal methods
+ enable_debug_internal_methods_logger: false
+ # Enable or disable the logging at debug level
+ enable_debug_logger: false
+ # Enable or disable the logging of methods' arguments at debug level
+ enable_debug_methods_argument_logger: false
+ # Enable or disable the logging to the AEDT message window
+ enable_desktop_logs: true
+ # Enable or disable the logging to a file
+ enable_file_logs: true
+ # Enable or disable the global PyAEDT log file located in the global temp folder
+ enable_global_log_file: true
+ # Enable or disable the local PyAEDT log file located in the ``projectname.pyaedt`` project folder
+ enable_local_log_file: false
+ # Enable or disable the logging overall
+ enable_logger: true
+ # Enable or disable the logging to STDOUT
+ enable_screen_logs: true
+ # Global PyAEDT log file path
+ global_log_file_name: null
+ # Global PyAEDT log file size in MB
+ global_log_file_size: 10
+ # Date format of the log entries
+ logger_datefmt: '%Y/%m/%d %H.%M.%S'
+ # PyAEDT log file path
+ logger_file_path: null
+ # Message format of the log entries
+ logger_formatter: '%(asctime)s:%(destination)s:%(extra)s%(levelname)-8s:%(message)s'
+ # Path to the AEDT log file
+ aedt_log_file: null
+
+# Settings related to Linux systems running LSF scheduler
+lsf:
+ # Command to launch in the LSF Scheduler
+ custom_lsf_command: null
+ # Command to launch the task in the LSF Scheduler
+ lsf_aedt_command: 'ansysedt'
+ # Number of LSF cores
+ lsf_num_cores: 2
+ # Operating system string
+ lsf_osrel: null
+ # LSF queue name
+ lsf_queue: null
+ # RAM allocated for the LSF job
+ lsf_ram: 1000
+ # Timeout in seconds for trying to start the interactive session
+ lsf_timeout: 3600
+ # Value passed in the LSF 'select' string to the ui resource
+ lsf_ui: null
+ # Enable or disable use LSF Scheduler
+ use_lsf_scheduler: false
+
+# Settings related to environment variables thare are set before launching a new AEDT session
+# This includes those that enable the beta features !
+aedt_env_var:
+ ANSYSEM_FEATURE_F335896_MECHANICAL_STRUCTURAL_SOLN_TYPE_ENABLE: '1'
+ ANSYSEM_FEATURE_F395486_RIGID_FLEX_BENDING_ENABLE: '1'
+ ANSYSEM_FEATURE_F538630_MECH_TRANSIENT_THERMAL_ENABLE: '1'
+ ANSYSEM_FEATURE_F545177_ECAD_INTEGRATION_WITH_APHI_ENABLE: '1'
+ ANSYSEM_FEATURE_F650636_MECH_LAYOUT_COMPONENT_ENABLE: '1'
+ ANSYSEM_FEATURE_S432616_LAYOUT_COMPONENT_IN_3D_ENABLE: '1'
+ ANSYSEM_FEATURE_SF159726_SCRIPTOBJECT_ENABLE: '1'
+ ANSYSEM_FEATURE_SF222134_CABLE_MODELING_ENHANCEMENTS_ENABLE: '1'
+ ANSYSEM_FEATURE_SF6694_NON_GRAPHICAL_COMMAND_EXECUTION_ENABLE: '1'
+ ANS_MESHER_PROC_DUMP_PREPOST_BEND_SM3: '1'
+
+general:
+ # Enable or disable the lazy load
+ lazy_load: true
+ # Enable or disable the lazy load dedicated to objects associated to the modeler
+ objects_lazy_load: true
+ # AEDT installation path
+ aedt_install_dir: null
+ # AEDT version in the form ``"2023.x"``
+ aedt_version: null
+ # Timeout in seconds for trying to launch AEDT
+ desktop_launch_timeout: 120
+ # Enable or disable bounding box evaluation by exporting a SAT file
+ disable_bounding_box_sat: false
+ # Optional path for the EDB DLL file
+ edb_dll_path: null
+ # Enable or disable the internal PyAEDT error handling
+ enable_error_handler: true
+ # Enable or disable the use of Pandas to export dictionaries and lists
+ enable_pandas_output: false
+ # Enable or disable the check of the project path
+ force_error_on_missing_project: false
+ # Number of gRPC API retries
+ number_of_grpc_api_retries: 6
+ # Enable or disable the release of AEDT on exception
+ release_on_exception: true
+ # Time interval between the retries by the ``_retry_n_times`` inner method
+ retry_n_times_time_interval: 0.1
+ # Enable or disable the use of the gRPC API or legacy COM object
+ use_grpc_api: null
+ # Enable or disable the use of multiple desktop sessions in the same Python script
+ use_multi_desktop: false
+ # Enable or disable the use of the flag `-waitforlicense` when launching AEDT
+ wait_for_license: false
+ # State whether the remote API is used or not
+ remote_api: false
+ # Specify the port the RPyC server is to listen to
+ remote_rpc_service_manager_port: 17878
+ # Specify the path to AEDT in the server
+ pyaedt_server_path: ''
+ # Remote temp folder
+ remote_rpc_session_temp_folder: ''
diff --git a/doc/source/User_guide/settings.rst b/doc/source/User_guide/settings.rst
new file mode 100644
index 00000000000..7bf7bf4d82f
--- /dev/null
+++ b/doc/source/User_guide/settings.rst
@@ -0,0 +1,141 @@
+Settings
+========
+
+The Settings class is designed to handle the configurations of PyAEDT and AEDT.
+This includes behavior for logging, LSF scheduler, environment variable and general
+settings. Most of the default values used can be modified using a YAML configuration file.
+The path to this YAML file should be defined through the environment variable
+``PYAEDT_LOCAL_SETTINGS_PATH``. If the environment variable is set and a file exists,
+the default configuration settings are updated according to the content of the file.
+If the environment variable is not defined, a check is performed to see if a file named
+``"pyaedt_settings.yaml"`` exist in the user's ``APPDATA`` folder for Windows and
+``HOME`` folder for Linux. If such file exists, it is then used to update the default
+configuration.
+
+Below is the content that can be updated through the YAML file.
+
+:download:`YAML configuration file <../Resources/pyaedt_settings.yaml>`
+
+.. note::
+ Not all settings from class ``Settings`` can be modified through this file
+ as some of them expect Python objects or values obtained from code execution.
+ For example, that is the case for ``formatter`` which expects an object of type
+ ``Formatter`` and ``time_tick`` which expects a time value, in seconds, since the
+ `epoch `_ as a floating-point number.
+
+
+.. code-block:: yaml
+
+ # Settings related to logging
+ log:
+ # Enable or disable the logging of EDB API methods
+ enable_debug_edb_logger: false
+ # Enable or disable the logging of the geometry operators
+ enable_debug_geometry_operator_logger: false
+ # Enable or disable the logging of the gRPC API calls
+ enable_debug_grpc_api_logger: false
+ # Enable or disable the logging of internal methods
+ enable_debug_internal_methods_logger: false
+ # Enable or disable the logging at debug level
+ enable_debug_logger: false
+ # Enable or disable the logging of methods' arguments at debug level
+ enable_debug_methods_argument_logger: false
+ # Enable or disable the logging to the AEDT message window
+ enable_desktop_logs: true
+ # Enable or disable the logging to a file
+ enable_file_logs: true
+ # Enable or disable the global PyAEDT log file located in the global temp folder
+ enable_global_log_file: true
+ # Enable or disable the local PyAEDT log file located in the ``projectname.pyaedt`` project folder
+ enable_local_log_file: false
+ # Enable or disable the logging overall
+ enable_logger: true
+ # Enable or disable the logging to STDOUT
+ enable_screen_logs: true
+ # Global PyAEDT log file path
+ global_log_file_name: null
+ # Global PyAEDT log file size in MB
+ global_log_file_size: 10
+ # Date format of the log entries
+ logger_datefmt: '%Y/%m/%d %H.%M.%S'
+ # PyAEDT log file path
+ logger_file_path: null
+ # Message format of the log entries
+ logger_formatter: '%(asctime)s:%(destination)s:%(extra)s%(levelname)-8s:%(message)s'
+
+ # Settings related to Linux systems running LSF scheduler
+ lsf:
+ # Command to launch in the LSF Scheduler
+ custom_lsf_command: null
+ # Command to launch the task in the LSF Scheduler
+ lsf_aedt_command: 'ansysedt'
+ # Number of LSF cores
+ lsf_num_cores: 2
+ # Operating system string
+ lsf_osrel: null
+ # LSF queue name
+ lsf_queue: null
+ # RAM allocated for the LSF job
+ lsf_ram: 1000
+ # Timeout in seconds for trying to start the interactive session
+ lsf_timeout: 3600
+ # Value passed in the LSF 'select' string to the ui resource
+ lsf_ui: null
+ # Enable or disable use LSF Scheduler
+ use_lsf_scheduler: false
+
+ # Settings related to environment variables thare are set before launching a new AEDT session
+ # This includes those that enable the beta features !
+ aedt_env_var:
+ ANSYSEM_FEATURE_F335896_MECHANICAL_STRUCTURAL_SOLN_TYPE_ENABLE: '1'
+ ANSYSEM_FEATURE_F395486_RIGID_FLEX_BENDING_ENABLE: '1'
+ ANSYSEM_FEATURE_F538630_MECH_TRANSIENT_THERMAL_ENABLE: '1'
+ ANSYSEM_FEATURE_F545177_ECAD_INTEGRATION_WITH_APHI_ENABLE: '1'
+ ANSYSEM_FEATURE_F650636_MECH_LAYOUT_COMPONENT_ENABLE: '1'
+ ANSYSEM_FEATURE_S432616_LAYOUT_COMPONENT_IN_3D_ENABLE: '1'
+ ANSYSEM_FEATURE_SF159726_SCRIPTOBJECT_ENABLE: '1'
+ ANSYSEM_FEATURE_SF222134_CABLE_MODELING_ENHANCEMENTS_ENABLE: '1'
+ ANSYSEM_FEATURE_SF6694_NON_GRAPHICAL_COMMAND_EXECUTION_ENABLE: '1'
+ ANS_MESHER_PROC_DUMP_PREPOST_BEND_SM3: '1'
+
+ general:
+ # Enable or disable the lazy load
+ lazy_load: true
+ # Enable or disable the lazy load dedicated to objects associated to the modeler
+ objects_lazy_load: true
+ # AEDT installation path
+ aedt_install_dir: null
+ # AEDT version in the form ``"2023.x"``
+ aedt_version: null
+ # Timeout in seconds for trying to launch AEDT
+ desktop_launch_timeout: 120
+ # Enable or disable bounding box evaluation by exporting a SAT file
+ disable_bounding_box_sat: false
+ # Optional path for the EDB DLL file
+ edb_dll_path: null
+ # Enable or disable the internal PyAEDT error handling
+ enable_error_handler: true
+ # Enable or disable the use of Pandas to export dictionaries and lists
+ enable_pandas_output: false
+ # Enable or disable the check of the project path
+ force_error_on_missing_project: false
+ # Number of gRPC API retries
+ number_of_grpc_api_retries: 6
+ # Enable or disable the release of AEDT on exception
+ release_on_exception: true
+ # Time interval between the retries by the ``_retry_n_times`` inner method
+ retry_n_times_time_interval: 0.1
+ # Enable or disable the use of the gRPC API or legacy COM object
+ use_grpc_api: null
+ # Enable or disable the use of multiple desktop sessions in the same Python script
+ use_multi_desktop: false
+ # Enable or disable the use of the flag `-waitforlicense` when launching Electronic Desktop
+ wait_for_license: false
+ # State whether the remote API is used or not
+ remote_api: false
+ # Specify the port the RPyC server is to listen to
+ remote_rpc_service_manager_port: 17878
+ # Specify the path to AEDT in the server
+ pyaedt_server_path: ''
+ # Remote temp folder
+ remote_rpc_session_temp_folder: ''
diff --git a/doc/styles/config/vocabularies/ANSYS/accept.txt b/doc/styles/config/vocabularies/ANSYS/accept.txt
index 4125d4a4f2c..f9d92aaea61 100644
--- a/doc/styles/config/vocabularies/ANSYS/accept.txt
+++ b/doc/styles/config/vocabularies/ANSYS/accept.txt
@@ -39,6 +39,7 @@ Icepak
IronPython
[Ll]ayout
limitilines
+LSF
matplotlib
Maxwell 2D
Maxwell 3D
diff --git a/pyproject.toml b/pyproject.toml
index 0328d3eb1c6..08beeffd978 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,6 +40,7 @@ dependencies = [
"pyedb>=0.24.0; python_version > '3.7'",
"pytomlpp; python_version < '3.12'",
"rpyc>=6.0.0,<6.1",
+ "pyyaml",
]
[project.optional-dependencies]
diff --git a/src/ansys/aedt/core/__init__.py b/src/ansys/aedt/core/__init__.py
index b6a1a526b37..6417813f3f8 100644
--- a/src/ansys/aedt/core/__init__.py
+++ b/src/ansys/aedt/core/__init__.py
@@ -67,7 +67,13 @@ def custom_show_warning(message, category, filename, lineno, file=None, line=Non
__version__ = "0.11.dev0"
version = __version__
-#
+# isort: off
+# Settings have to be imported before importing other PyAEDT modules
+from ansys.aedt.core.generic.general_methods import settings
+from ansys.aedt.core.generic.general_methods import inner_project_settings
+
+# isort: on
+
if not ("IronPython" in sys.version or ".NETFramework" in sys.version): # pragma: no cover
import ansys.aedt.core.downloads as downloads
from ansys.aedt.core.edb import Edb # nosec
@@ -103,7 +109,6 @@ def custom_show_warning(message, category, filename, lineno, file=None, line=Non
from ansys.aedt.core.generic.general_methods import is_windows
from ansys.aedt.core.generic.general_methods import online_help
from ansys.aedt.core.generic.general_methods import pyaedt_function_handler
-from ansys.aedt.core.generic.general_methods import settings
from ansys.aedt.core.misc import current_student_version
from ansys.aedt.core.misc import current_version
from ansys.aedt.core.misc import installed_versions
diff --git a/src/ansys/aedt/core/application/design.py b/src/ansys/aedt/core/application/design.py
index 4d579fcbe6f..1eca96b0298 100644
--- a/src/ansys/aedt/core/application/design.py
+++ b/src/ansys/aedt/core/application/design.py
@@ -66,6 +66,7 @@
from ansys.aedt.core.generic.general_methods import GrpcApiError
from ansys.aedt.core.generic.general_methods import check_and_download_file
from ansys.aedt.core.generic.general_methods import generate_unique_name
+from ansys.aedt.core.generic.general_methods import inner_project_settings
from ansys.aedt.core.generic.general_methods import is_ironpython
from ansys.aedt.core.generic.general_methods import is_project_locked
from ansys.aedt.core.generic.general_methods import is_windows
@@ -88,8 +89,8 @@
def load_aedt_thread(project_path):
pp = load_entire_aedt_file(project_path)
- settings._project_properties[os.path.normpath(project_path)] = pp
- settings._project_time_stamp = os.path.getmtime(project_path)
+ inner_project_settings.properties[os.path.normpath(project_path)] = pp
+ inner_project_settings.time_stamp = os.path.getmtime(project_path)
class Design(AedtObjects):
@@ -550,24 +551,28 @@ def project_properties(self):
start = time.time()
if self.project_timestamp_changed or (
os.path.exists(self.project_file)
- and os.path.normpath(self.project_file) not in settings._project_properties
+ and os.path.normpath(self.project_file) not in inner_project_settings.properties
):
- settings._project_properties[os.path.normpath(self.project_file)] = load_entire_aedt_file(self.project_file)
+ inner_project_settings.properties[os.path.normpath(self.project_file)] = load_entire_aedt_file(
+ self.project_file
+ )
self._logger.info("aedt file load time {}".format(time.time() - start))
elif (
- os.path.normpath(self.project_file) not in settings._project_properties
+ os.path.normpath(self.project_file) not in inner_project_settings.properties
and settings.remote_rpc_session
and settings.remote_rpc_session.filemanager.pathexists(self.project_file)
):
file_path = check_and_download_file(self.project_file)
try:
- settings._project_properties[os.path.normpath(self.project_file)] = load_entire_aedt_file(file_path)
+ inner_project_settings.properties[os.path.normpath(self.project_file)] = load_entire_aedt_file(
+ file_path
+ )
except Exception:
self._logger.info("Failed to load AEDT file.")
else:
self._logger.info("Time to load AEDT file: {}.".format(time.time() - start))
- if os.path.normpath(self.project_file) in settings._project_properties:
- return settings._project_properties[os.path.normpath(self.project_file)]
+ if os.path.normpath(self.project_file) in inner_project_settings.properties:
+ return inner_project_settings.properties[os.path.normpath(self.project_file)]
return {}
@property
@@ -769,15 +774,15 @@ def project_path(self):
def project_time_stamp(self):
"""Return Project time stamp."""
if os.path.exists(self.project_file):
- settings._project_time_stamp = os.path.getmtime(self.project_file)
+ inner_project_settings.time_stamp = os.path.getmtime(self.project_file)
else:
- settings._project_time_stamp = 0
- return settings._project_time_stamp
+ inner_project_settings.time_stamp = 0
+ return inner_project_settings.time_stamp
@property
def project_timestamp_changed(self):
"""Return a bool if time stamp changed or not."""
- old_time = settings._project_time_stamp
+ old_time = inner_project_settings.time_stamp
return old_time != self.project_time_stamp
@property
@@ -3297,8 +3302,8 @@ def close_project(self, name=None, save=True):
i += 0.2
time.sleep(0.2)
- if os.path.normpath(proj_file) in settings._project_properties:
- del settings._project_properties[os.path.normpath(proj_file)]
+ if os.path.normpath(proj_file) in inner_project_settings.properties:
+ del inner_project_settings.properties[os.path.normpath(proj_file)]
return True
@pyaedt_function_handler()
diff --git a/src/ansys/aedt/core/desktop.py b/src/ansys/aedt/core/desktop.py
index 9a0e9419947..6170baea1b5 100644
--- a/src/ansys/aedt/core/desktop.py
+++ b/src/ansys/aedt/core/desktop.py
@@ -94,6 +94,8 @@ def launch_desktop_on_port():
command.append("-ng")
if settings.wait_for_license:
command.append("-waitforlicense")
+ if settings.aedt_log_file:
+ command.extend(["-Logfile", settings.aedt_log_file])
my_env = os.environ.copy()
for env, val in settings.aedt_environment_variables.items():
my_env[env] = val
@@ -174,6 +176,8 @@ def launch_aedt_in_lsf(non_graphical, port): # pragma: no cover
command.append("-ng")
if settings.wait_for_license:
command.append("-waitforlicense")
+ if settings.aedt_log_file:
+ command.extend(["-Logfile", settings.aedt_log_file])
else: # pragma: no cover
command = settings.custom_lsf_command.split(" ")
command.append("-grpcsrv")
diff --git a/src/ansys/aedt/core/generic/general_methods.py b/src/ansys/aedt/core/generic/general_methods.py
index 2f96eb64080..eabcc54dcd8 100644
--- a/src/ansys/aedt/core/generic/general_methods.py
+++ b/src/ansys/aedt/core/generic/general_methods.py
@@ -47,6 +47,7 @@
from ansys.aedt.core.aedt_logger import pyaedt_logger
from ansys.aedt.core.generic.constants import CSS4_COLORS
+from ansys.aedt.core.generic.settings import inner_project_settings # noqa: F401
from ansys.aedt.core.generic.settings import settings
from ansys.aedt.core.misc.misc import installed_versions
diff --git a/src/ansys/aedt/core/generic/settings.py b/src/ansys/aedt/core/generic/settings.py
index fbedec918fd..2dddf658537 100644
--- a/src/ansys/aedt/core/generic/settings.py
+++ b/src/ansys/aedt/core/generic/settings.py
@@ -22,12 +22,98 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
+"""This module contains the ``Settings`` and ``_InnerProjectSettings`` classes.
+
+The first class encapsulates the settings associated with PyAEDT and AEDT including logging,
+LSF, environment variables and general settings. Most of the default values used can be modified
+using a YAML configuration file. An example of such file can be found in the documentation, see
+`Settings YAML file `_.
+The path to the configuration file should be specified with the environment variable
+``PYAEDT_LOCAL_SETTINGS_PATH``. If no environment variable is set, the class will look for the
+configuration file ``pyaedt_settings.yaml`` in the user's ``APPDATA`` folder for Windows and
+``HOME`` folder for Linux.
+
+The second class is intended for internal use only and shouldn't be modified by users.
+"""
+
+import logging
import os
import time
+from typing import Any
+from typing import List
+from typing import Optional
+from typing import Union
import uuid
is_linux = os.name == "posix"
+# Settings allowed to be updated using a YAML configuration file.
+ALLOWED_LOG_SETTINGS = [
+ "enable_debug_edb_logger",
+ "enable_debug_geometry_operator_logger",
+ "enable_debug_grpc_api_logger",
+ "enable_debug_internal_methods_logger",
+ "enable_debug_logger",
+ "enable_debug_methods_argument_logger",
+ "enable_desktop_logs",
+ "enable_file_logs",
+ "enable_global_log_file",
+ "enable_local_log_file",
+ "enable_logger",
+ "enable_screen_logs",
+ "global_log_file_name",
+ "global_log_file_size",
+ "logger_datefmt",
+ "logger_file_path",
+ "logger_formatter",
+ "aedt_log_file",
+]
+ALLOWED_LSF_SETTINGS = [
+ "custom_lsf_command",
+ "lsf_aedt_command",
+ "lsf_num_cores",
+ "lsf_osrel",
+ "lsf_queue",
+ "lsf_ram",
+ "lsf_timeout",
+ "lsf_ui",
+ "use_lsf_scheduler",
+]
+ALLOWED_GENERAL_SETTINGS = [
+ "lazy_load",
+ "objects_lazy_load",
+ "aedt_install_dir",
+ "aedt_version",
+ "desktop_launch_timeout",
+ "disable_bounding_box_sat",
+ "edb_dll_path",
+ "enable_error_handler",
+ "enable_pandas_output",
+ "force_error_on_missing_project",
+ "number_of_grpc_api_retries",
+ "release_on_exception",
+ "retry_n_times_time_interval",
+ "use_grpc_api",
+ "use_multi_desktop",
+ "wait_for_license",
+ "remote_api",
+ "remote_rpc_service_manager_port",
+ "pyaedt_server_path",
+ "remote_rpc_session_temp_folder",
+]
+ALLOWED_AEDT_ENV_VAR_SETTINGS = [
+ "ANSYSEM_FEATURE_F335896_MECHANICAL_STRUCTURAL_SOLN_TYPE_ENABLE",
+ "ANSYSEM_FEATURE_F395486_RIGID_FLEX_BENDING_ENABLE",
+ "ANSYSEM_FEATURE_F538630_MECH_TRANSIENT_THERMAL_ENABLE",
+ "ANSYSEM_FEATURE_F545177_ECAD_INTEGRATION_WITH_APHI_ENABLE",
+ "ANSYSEM_FEATURE_F650636_MECH_LAYOUT_COMPONENT_ENABLE",
+ "ANSYSEM_FEATURE_S432616_LAYOUT_COMPONENT_IN_3D_ENABLE",
+ "ANSYSEM_FEATURE_SF159726_SCRIPTOBJECT_ENABLE",
+ "ANSYSEM_FEATURE_SF222134_CABLE_MODELING_ENHANCEMENTS_ENABLE",
+ "ANSYSEM_FEATURE_SF6694_NON_GRAPHICAL_COMMAND_EXECUTION_ENABLE",
+ "ANS_MESHER_PROC_DUMP_PREPOST_BEND_SM3",
+]
+
def generate_log_filename():
"""Generate a log filename."""
@@ -37,58 +123,54 @@ def generate_log_filename():
return "{}_{}_{}.log".format(base, username, unique_id)
+class _InnerProjectSettings: # pragma: no cover
+ """Global inner project settings.
+
+ This class is intended for internal use only.
+ """
+
+ properties: dict = {}
+ time_stamp: Union[int, float] = 0
+
+
class Settings(object): # pragma: no cover
"""Manages all PyAEDT environment variables and global settings."""
def __init__(self):
- self._logger = None
- self._enable_logger = True
- self._enable_desktop_logs = True
- self._enable_screen_logs = True
- self._enable_file_logs = True
- self.pyaedt_server_path = ""
- self._logger_file_path = None
- self._logger_formatter = "%(asctime)s:%(destination)s:%(extra)s%(levelname)-8s:%(message)s"
- self._logger_datefmt = "%Y/%m/%d %H.%M.%S"
- self._enable_debug_edb_logger = False
- self._enable_debug_grpc_api_logger = False
- self._enable_debug_methods_argument_logger = False
- self._enable_debug_geometry_operator_logger = False
- self._enable_debug_internal_methods_logger = False
- self._enable_debug_logger = False
- self._enable_error_handler = True
- self._release_on_exception = True
- self._aedt_version = None
- self._aedt_install_dir = None
- self._use_multi_desktop = False
- self.remote_api = False
- self._use_grpc_api = None
- self.formatter = None
- self.remote_rpc_session = None
- self.remote_rpc_session_temp_folder = ""
- self.remote_rpc_service_manager_port = 17878
- self._project_properties = {}
- self._project_time_stamp = 0
- self._disable_bounding_box_sat = False
- self._force_error_on_missing_project = False
- self._enable_pandas_output = False
- self.time_tick = time.time()
- self._global_log_file_name = generate_log_filename()
- self._enable_global_log_file = True
- self._enable_local_log_file = False
- self._global_log_file_size = 10
- self._edb_dll_path = None
- self._lsf_num_cores = 2
- self._lsf_ram = 1000
- self._use_lsf_scheduler = False
- self._lsf_osrel = None
- self._lsf_ui = None
- self._lsf_aedt_command = "ansysedt"
- self._lsf_timeout = 3600
- self._lsf_queue = None
- self._custom_lsf_command = None
- self._aedt_environment_variables = {
- "ANS_MESHER_PROC_DUMP_PREPOST_BEND_SM3": "1",
+ # Setup default values then load values from PersoalLib' settings_config.yaml if it exists.
+ # Settings related to logging
+ self.__logger: Optional[logging.Logger] = None
+ self.__enable_logger: bool = True
+ self.__enable_desktop_logs: bool = True
+ self.__enable_screen_logs: bool = True
+ self.__enable_file_logs: bool = True
+ self.__logger_file_path: Optional[str] = None
+ self.__logger_formatter: str = "%(asctime)s:%(destination)s:%(extra)s%(levelname)-8s:%(message)s"
+ self.__logger_datefmt: str = "%Y/%m/%d %H.%M.%S"
+ self.__enable_debug_edb_logger: bool = False
+ self.__enable_debug_grpc_api_logger: bool = False
+ self.__enable_debug_methods_argument_logger: bool = False
+ self.__enable_debug_geometry_operator_logger: bool = False
+ self.__enable_debug_internal_methods_logger: bool = False
+ self.__enable_debug_logger: bool = False
+ self.__global_log_file_name: str = generate_log_filename()
+ self.__enable_global_log_file: bool = True
+ self.__enable_local_log_file: bool = False
+ self.__global_log_file_size: int = 10
+ self.__aedt_log_file: Optional[str] = None
+ # Settings related to Linux systems running LSF scheduler
+ self.__lsf_num_cores: int = 2
+ self.__lsf_ram: int = 1000
+ self.__use_lsf_scheduler: bool = False
+ self.__lsf_osrel: Optional[str] = None
+ self.__lsf_ui: Optional[int] = None
+ self.__lsf_aedt_command: str = "ansysedt"
+ self.__lsf_timeout: int = 3600
+ self.__lsf_queue: Optional[str] = None
+ self.__custom_lsf_command: Optional[str] = None
+ # Settings related to environment variables that are set before launching a new AEDT session
+ # This includes those that enable the beta features !
+ self.__aedt_environment_variables: dict[str, str] = {
"ANSYSEM_FEATURE_SF6694_NON_GRAPHICAL_COMMAND_EXECUTION_ENABLE": "1",
"ANSYSEM_FEATURE_SF159726_SCRIPTOBJECT_ENABLE": "1",
"ANSYSEM_FEATURE_SF222134_CABLE_MODELING_ENHANCEMENTS_ENABLE": "1",
@@ -98,220 +180,470 @@ def __init__(self):
"ANSYSEM_FEATURE_F650636_MECH_LAYOUT_COMPONENT_ENABLE": "1",
"ANSYSEM_FEATURE_F538630_MECH_TRANSIENT_THERMAL_ENABLE": "1",
"ANSYSEM_FEATURE_F335896_MECHANICAL_STRUCTURAL_SOLN_TYPE_ENABLE": "1",
+ "ANS_MESHER_PROC_DUMP_PREPOST_BEND_SM3": "1",
}
if is_linux:
- self._aedt_environment_variables["ANS_NODEPCHECK"] = "1"
- self._desktop_launch_timeout = 120
- self._number_of_grpc_api_retries = 6
- self._retry_n_times_time_interval = 0.1
- self._wait_for_license = False
- self.__lazy_load = True
- self.__objects_lazy_load = True
+ self.__aedt_environment_variables["ANS_NODEPCHECK"] = "1"
+ # General settings
+ self.__enable_error_handler: bool = True
+ self.__release_on_exception: bool = True
+ self.__aedt_version: Optional[str] = None
+ self.__aedt_install_dir: Optional[str] = None
+ self.__use_multi_desktop: bool = False
+ self.__use_grpc_api: Optional[bool] = None
+ self.__disable_bounding_box_sat = False
+ self.__force_error_on_missing_project = False
+ self.__enable_pandas_output = False
+ self.__edb_dll_path: Optional[str] = None
+ self.__desktop_launch_timeout: int = 120
+ self.__number_of_grpc_api_retries: int = 6
+ self.__retry_n_times_time_interval: float = 0.1
+ self.__wait_for_license: bool = False
+ self.__lazy_load: bool = True
+ self.__objects_lazy_load: bool = True
+ # Previously 'public' attributes
+ self.__formatter: Optional[logging.Formatter] = None
+ self.__remote_rpc_session: Any = None
+ self.__remote_rpc_session_temp_folder: str = ""
+ self.__remote_rpc_service_manager_port: int = 17878
+ self.__remote_api: bool = False
+ self.__time_tick = time.time()
+ self.__pyaedt_server_path = ""
+
+ # Load local settings if YAML configuration file exists.
+ pyaedt_settings_path = os.environ.get("PYAEDT_LOCAL_SETTINGS_PATH", "")
+ if not pyaedt_settings_path:
+ if os.name == "posix":
+ pyaedt_settings_path = os.path.join(os.environ["HOME"], "pyaedt_settings.yaml")
+ else:
+ pyaedt_settings_path = os.path.join(os.environ["APPDATA"], "pyaedt_settings.yaml")
+ self.load_yaml_configuration(pyaedt_settings_path)
+
+ # ########################## Logging properties ##########################
@property
- def release_on_exception(self):
- """
+ def logger(self):
+ """Active logger."""
+ return self.__logger
- Returns
- -------
+ @logger.setter
+ def logger(self, val):
+ self.__logger = val
- """
- return self._release_on_exception
+ @property
+ def enable_desktop_logs(self):
+ """Enable or disable the logging to the AEDT message window."""
+ return self.__enable_desktop_logs
- @release_on_exception.setter
- def release_on_exception(self, value):
- self._release_on_exception = value
+ @enable_desktop_logs.setter
+ def enable_desktop_logs(self, val):
+ self.__enable_desktop_logs = val
@property
- def objects_lazy_load(self):
- """Flag for enabling and disabling the lazy load.
- The default is ``True``.
+ def global_log_file_size(self):
+ """Global PyAEDT log file size in MB. The default value is ``10``."""
+ return self.__global_log_file_size
- Returns
- -------
- bool
- """
- return self.__objects_lazy_load
+ @global_log_file_size.setter
+ def global_log_file_size(self, value):
+ self.__global_log_file_size = value
- @objects_lazy_load.setter
- def objects_lazy_load(self, value):
- self.__objects_lazy_load = value
+ @property
+ def enable_global_log_file(self):
+ """Enable or disable the global PyAEDT log file located in the global temp folder.
+ The default is ``True``."""
+ return self.__enable_global_log_file
+
+ @enable_global_log_file.setter
+ def enable_global_log_file(self, value):
+ self.__enable_global_log_file = value
@property
- def lazy_load(self):
- """Flag for enabling and disabling the lazy load.
- The default is ``True``.
+ def enable_local_log_file(self):
+ """Enable or disable the local PyAEDT log file located in the ``projectname.pyaedt`` project folder.
+ The default is ``True``."""
+ return self.__enable_local_log_file
- Returns
- -------
- bool
- """
- return self.__lazy_load
+ @enable_local_log_file.setter
+ def enable_local_log_file(self, value):
+ self.__enable_local_log_file = value
- @lazy_load.setter
- def lazy_load(self, value):
- self.__lazy_load = value
+ @property
+ def global_log_file_name(self):
+ """Global PyAEDT log file path. The default is ``pyaedt_username.log``."""
+ return self.__global_log_file_name
+
+ @global_log_file_name.setter
+ def global_log_file_name(self, value):
+ self.__global_log_file_name = value
@property
- def wait_for_license(self):
- """Whether if Electronics Desktop has to be launched with ``-waitforlicense`` flag enabled or not.
- Default is ``False``.
+ def enable_debug_methods_argument_logger(self):
+ """Flag for whether to write out the method's arguments in the debug logger.
+ The default is ``False``."""
+ return self.__enable_debug_methods_argument_logger
- Returns
- -------
- bool
- """
- return self._wait_for_license
+ @enable_debug_methods_argument_logger.setter
+ def enable_debug_methods_argument_logger(self, val):
+ self.__enable_debug_methods_argument_logger = val
- @wait_for_license.setter
- def wait_for_license(self, value):
- self._wait_for_license = value
+ @property
+ def enable_screen_logs(self):
+ """Enable or disable the logging to STDOUT."""
+ return self.__enable_screen_logs
+
+ @enable_screen_logs.setter
+ def enable_screen_logs(self, val):
+ self.__enable_screen_logs = val
@property
- def retry_n_times_time_interval(self):
- """Time interval between the retries by the ``_retry_n_times`` method."""
- return self._retry_n_times_time_interval
+ def enable_file_logs(self):
+ """Enable or disable the logging to a file."""
+ return self.__enable_file_logs
- @retry_n_times_time_interval.setter
- def retry_n_times_time_interval(self, value):
- self._retry_n_times_time_interval = float(value)
+ @enable_file_logs.setter
+ def enable_file_logs(self, val):
+ self.__enable_file_logs = val
@property
- def number_of_grpc_api_retries(self):
- """Number of gRPC API retries. The default is ``3``."""
- return self._number_of_grpc_api_retries
+ def enable_logger(self):
+ """Enable or disable the logging overall."""
+ return self.__enable_logger
- @number_of_grpc_api_retries.setter
- def number_of_grpc_api_retries(self, value):
- self._number_of_grpc_api_retries = int(value)
+ @enable_logger.setter
+ def enable_logger(self, val):
+ self.__enable_logger = val
@property
- def desktop_launch_timeout(self):
- """Timeout in seconds for trying to launch AEDT. The default is ``90`` seconds."""
- return self._desktop_launch_timeout
+ def logger_file_path(self):
+ """PyAEDT log file path."""
+ return self.__logger_file_path
- @desktop_launch_timeout.setter
- def desktop_launch_timeout(self, value):
- self._desktop_launch_timeout = int(value)
+ @logger_file_path.setter
+ def logger_file_path(self, val):
+ self.__logger_file_path = val
@property
- def aedt_environment_variables(self):
- """Environment variables that are set before launching a new AEDT session,
- including those that enable the beta features."""
- return self._aedt_environment_variables
+ def logger_formatter(self):
+ """Message format of the log entries.
+ The default is ``'%(asctime)s:%(destination)s:%(extra)s%(levelname)-8s:%(message)s'``"""
+ return self.__logger_formatter
- @aedt_environment_variables.setter
- def aedt_environment_variables(self, value):
- self._aedt_environment_variables = value
+ @logger_formatter.setter
+ def logger_formatter(self, val):
+ self.__logger_formatter = val
+
+ @property
+ def logger_datefmt(self):
+ """Date format of the log entries.
+ The default is ``'%Y/%m/%d %H.%M.%S'``"""
+ return self.__logger_datefmt
+
+ @logger_datefmt.setter
+ def logger_datefmt(self, val):
+ self.__logger_datefmt = val
+
+ @property
+ def enable_debug_edb_logger(self):
+ """Enable or disable the logger for any EDB API methods."""
+ return self.__enable_debug_edb_logger
+
+ @enable_debug_edb_logger.setter
+ def enable_debug_edb_logger(self, val):
+ self.__enable_debug_edb_logger = val
+
+ @property
+ def enable_debug_grpc_api_logger(self):
+ """Enable or disable the logging for the gRPC API calls."""
+ return self.__enable_debug_grpc_api_logger
+
+ @enable_debug_grpc_api_logger.setter
+ def enable_debug_grpc_api_logger(self, val):
+ self.__enable_debug_grpc_api_logger = val
+
+ @property
+ def enable_debug_geometry_operator_logger(self):
+ """Enable or disable the logging for the geometry operators.
+ This setting is useful for debug purposes."""
+ return self.__enable_debug_geometry_operator_logger
+
+ @enable_debug_geometry_operator_logger.setter
+ def enable_debug_geometry_operator_logger(self, val):
+ self.__enable_debug_geometry_operator_logger = val
+
+ @property
+ def enable_debug_internal_methods_logger(self):
+ """Enable or disable the logging for internal methods.
+ This setting is useful for debug purposes."""
+ return self.__enable_debug_internal_methods_logger
+
+ @enable_debug_internal_methods_logger.setter
+ def enable_debug_internal_methods_logger(self, val):
+ self.__enable_debug_internal_methods_logger = val
+
+ @property
+ def enable_debug_logger(self):
+ """Enable or disable the debug level logger."""
+ return self.__enable_debug_logger
+
+ @enable_debug_logger.setter
+ def enable_debug_logger(self, val):
+ self.__enable_debug_logger = val
+
+ @property
+ def aedt_log_file(self):
+ """Path to the AEDT log file.
+
+ Used to specify that Electronics Desktop has to be launched with ``-Logfile`` option.
+ """
+ return self.__aedt_log_file
+
+ @aedt_log_file.setter
+ def aedt_log_file(self, value: str):
+ self.__aedt_log_file = value
+
+ # ############################# LSF properties ############################
@property
def lsf_queue(self):
"""LSF queue name. This attribute is valid only on Linux
systems running LSF Scheduler."""
- return self._lsf_queue
+ return self.__lsf_queue
@lsf_queue.setter
def lsf_queue(self, value):
- self._lsf_queue = value
+ self.__lsf_queue = value
@property
def use_lsf_scheduler(self):
"""Whether to use LSF Scheduler. This attribute is valid only on Linux
systems running LSF Scheduler."""
- return self._use_lsf_scheduler
+ return self.__use_lsf_scheduler
@use_lsf_scheduler.setter
def use_lsf_scheduler(self, value):
- self._use_lsf_scheduler = value
+ self.__use_lsf_scheduler = value
@property
def lsf_aedt_command(self):
"""Command to launch the task in the LSF Scheduler. The default is ``"ansysedt"``.
This attribute is valid only on Linux systems running LSF Scheduler."""
- return self._lsf_aedt_command
+ return self.__lsf_aedt_command
@lsf_aedt_command.setter
def lsf_aedt_command(self, value):
- self._lsf_aedt_command = value
+ self.__lsf_aedt_command = value
@property
def lsf_num_cores(self):
"""Number of LSF cores. This attribute is valid only
on Linux systems running LSF Scheduler."""
- return self._lsf_num_cores
+ return self.__lsf_num_cores
@lsf_num_cores.setter
def lsf_num_cores(self, value):
- self._lsf_num_cores = int(value)
+ self.__lsf_num_cores = int(value)
@property
def lsf_ram(self):
"""RAM allocated for the LSF job. This attribute is valid
only on Linux systems running LSF Scheduler."""
- return self._lsf_ram
+ return self.__lsf_ram
@lsf_ram.setter
def lsf_ram(self, value):
- self._lsf_ram = int(value)
+ self.__lsf_ram = int(value)
@property
def lsf_ui(self):
"""Value passed in the LSF 'select' string to the ui resource."""
- return self._lsf_ui
+ return self.__lsf_ui
@lsf_ui.setter
def lsf_ui(self, value):
- self._lsf_ui = int(value)
+ self.__lsf_ui = int(value)
@property
def lsf_timeout(self):
"""Timeout in seconds for trying to start the interactive session. The default is ``3600`` seconds."""
- return self._lsf_timeout
+ return self.__lsf_timeout
@lsf_timeout.setter
def lsf_timeout(self, value):
- self._lsf_timeout = int(value)
+ self.__lsf_timeout = int(value)
@property
def lsf_osrel(self):
"""Operating system string.
This attribute is valid only on Linux systems running LSF Scheduler."""
- return self._lsf_osrel
+ return self.__lsf_osrel
@lsf_osrel.setter
def lsf_osrel(self, value):
- self._lsf_osrel = value
+ self.__lsf_osrel = value
@property
def custom_lsf_command(self):
"""Command to launch in the LSF Scheduler. The default is ``None``.
This attribute is valid only on Linux systems running LSF Scheduler."""
- return self._custom_lsf_command
+ return self.__custom_lsf_command
@custom_lsf_command.setter
def custom_lsf_command(self, value):
- self._custom_lsf_command = value
+ self.__custom_lsf_command = value
+
+ # ############################## Environment variable properties ##############################
+
+ @property
+ def aedt_environment_variables(self):
+ """Environment variables that are set before launching a new AEDT session,
+ including those that enable the beta features."""
+ return self.__aedt_environment_variables
+
+ @aedt_environment_variables.setter
+ def aedt_environment_variables(self, value):
+ self._aedt_environment_variables = value
+
+ # ##################################### General properties ####################################
+
+ @property
+ def remote_api(self):
+ """State whether remote API is used or not."""
+ return self.__remote_api
+
+ @remote_api.setter
+ def remote_api(self, value: bool):
+ self.__remote_api = value
+
+ @property
+ def formatter(self):
+ """Get the formatter."""
+ return self.__formatter
+
+ @formatter.setter
+ def formatter(self, value: logging.Formatter):
+ self.__formatter = value
+
+ @property
+ def remote_rpc_session(self):
+ """Get the RPyC connection."""
+ return self.__remote_rpc_session
+
+ @remote_rpc_session.setter
+ def remote_rpc_session(self, value: Any):
+ self.__remote_rpc_session = value
+
+ @property
+ def remote_rpc_session_temp_folder(self):
+ """Get the remote RPyC session temp folder."""
+ return self.__remote_rpc_session_temp_folder
+
+ @remote_rpc_session_temp_folder.setter
+ def remote_rpc_session_temp_folder(self, value: str):
+ self.__remote_rpc_session_temp_folder = value
+
+ @property
+ def remote_rpc_service_manager_port(self):
+ """Get the remote RPyC service manager port."""
+ return self.__remote_rpc_service_manager_port
+
+ @remote_rpc_service_manager_port.setter
+ def remote_rpc_service_manager_port(self, value: int):
+ self.__remote_rpc_service_manager_port = value
+
+ @property
+ def time_tick(self):
+ """Time in seconds since the 'epoch' as a floating-point number."""
+ return self.__time_tick
+
+ @time_tick.setter
+ def time_tick(self, value: float):
+ self.__time_tick = value
+
+ @property
+ def release_on_exception(self):
+ """Enable or disable the release of AEDT on exception."""
+ return self.__release_on_exception
+
+ @release_on_exception.setter
+ def release_on_exception(self, value):
+ self.__release_on_exception = value
+
+ @property
+ def objects_lazy_load(self):
+ """Flag for enabling and disabling the lazy load. The default value is ``True``."""
+ return self.__objects_lazy_load
+
+ @objects_lazy_load.setter
+ def objects_lazy_load(self, value):
+ self.__objects_lazy_load = value
+
+ @property
+ def lazy_load(self):
+ """Flag for enabling and disabling the lazy load. The default value is ``True``."""
+ return self.__lazy_load
+
+ @lazy_load.setter
+ def lazy_load(self, value):
+ self.__lazy_load = value
+
+ @property
+ def wait_for_license(self):
+ """Enable or disable the use of the flag `-waitforlicense` when launching Electronic Desktop.
+ The default value is ``False``."""
+ return self.__wait_for_license
+
+ @wait_for_license.setter
+ def wait_for_license(self, value):
+ self.__wait_for_license = value
+
+ @property
+ def retry_n_times_time_interval(self):
+ """Time interval between the retries by the ``_retry_n_times`` method."""
+ return self.__retry_n_times_time_interval
+
+ @retry_n_times_time_interval.setter
+ def retry_n_times_time_interval(self, value):
+ self.__retry_n_times_time_interval = float(value)
+
+ @property
+ def number_of_grpc_api_retries(self):
+ """Number of gRPC API retries. The default is ``3``."""
+ return self.__number_of_grpc_api_retries
+
+ @number_of_grpc_api_retries.setter
+ def number_of_grpc_api_retries(self, value):
+ self.__number_of_grpc_api_retries = int(value)
+
+ @property
+ def desktop_launch_timeout(self):
+ """Timeout in seconds for trying to launch AEDT. The default is ``120`` seconds."""
+ return self.__desktop_launch_timeout
+
+ @desktop_launch_timeout.setter
+ def desktop_launch_timeout(self, value):
+ self.__desktop_launch_timeout = int(value)
@property
def aedt_version(self):
"""AEDT version in the form ``"2023.x"``. In AEDT 2022 R2 and later,
evaluating a bounding box by exporting a SAT file is disabled."""
- return self._aedt_version
+ return self.__aedt_version
@aedt_version.setter
def aedt_version(self, value):
- self._aedt_version = value
- if self._aedt_version >= "2023.1":
+ self.__aedt_version = value
+ if self.__aedt_version >= "2023.1":
self.disable_bounding_box_sat = True
@property
def aedt_install_dir(self):
"""AEDT installation path."""
- return self._aedt_install_dir
+ return self.__aedt_install_dir
@aedt_install_dir.setter
def aedt_install_dir(self, value):
- self._aedt_install_dir = value
+ self.__aedt_install_dir = value
@property
def use_multi_desktop(self):
@@ -323,248 +655,123 @@ def use_multi_desktop(self):
Enabling multiple desktop sessions is a beta feature."""
- return self._use_multi_desktop
+ return self.__use_multi_desktop
@use_multi_desktop.setter
def use_multi_desktop(self, value):
- self._use_multi_desktop = value
+ self.__use_multi_desktop = value
@property
def edb_dll_path(self):
"""Optional path for the EDB DLL file."""
- return self._edb_dll_path
+ return self.__edb_dll_path
@edb_dll_path.setter
def edb_dll_path(self, value):
if os.path.exists(value):
- self._edb_dll_path = value
-
- @property
- def global_log_file_size(self):
- """Global PyAEDT log file size in MB. The default value is ``10``."""
- return self._global_log_file_size
-
- @global_log_file_size.setter
- def global_log_file_size(self, value):
- self._global_log_file_size = value
-
- @property
- def enable_global_log_file(self):
- """Flag for enabling and disabling the global PyAEDT log file located in the global temp folder.
- The default is ``True``."""
- return self._enable_global_log_file
-
- @enable_global_log_file.setter
- def enable_global_log_file(self, value):
- self._enable_global_log_file = value
-
- @property
- def enable_local_log_file(self):
- """Flag for enabling and disabling the local PyAEDT log file located
- in the ``projectname.pyaedt`` project folder. The default is ``True``."""
- return self._enable_local_log_file
-
- @enable_local_log_file.setter
- def enable_local_log_file(self, value):
- self._enable_local_log_file = value
-
- @property
- def global_log_file_name(self):
- """Global PyAEDT log file path. The default is ``pyaedt_username.log``."""
- return self._global_log_file_name
-
- @global_log_file_name.setter
- def global_log_file_name(self, value):
- self._global_log_file_name = value
+ self.__edb_dll_path = value
@property
def enable_pandas_output(self):
"""Flag for whether Pandas is being used to export dictionaries and lists. This attribute
applies to Solution data output. The default is ``False``. If ``True``, the property or
method returns a Pandas object. This property is valid only in the CPython environment."""
- return self._enable_pandas_output
+ return self.__enable_pandas_output
@enable_pandas_output.setter
def enable_pandas_output(self, val):
- self._enable_pandas_output = val
-
- @property
- def enable_debug_methods_argument_logger(self):
- """Flag for whether to write out the method's arguments in the debug logger.
- The default is ``False``."""
- return self._enable_debug_methods_argument_logger
-
- @enable_debug_methods_argument_logger.setter
- def enable_debug_methods_argument_logger(self, val):
- self._enable_debug_methods_argument_logger = val
+ self.__enable_pandas_output = val
@property
def force_error_on_missing_project(self):
"""Flag for whether to check the project path. The default is ``False``. If
``True``, when passing a project path, the project has to exist. Otherwise, an
error is raised."""
- return self._force_error_on_missing_project
+ return self.__force_error_on_missing_project
@force_error_on_missing_project.setter
def force_error_on_missing_project(self, val):
- self._force_error_on_missing_project = val
+ self.__force_error_on_missing_project = val
@property
def disable_bounding_box_sat(self):
"""Flag for enabling and disabling bounding box evaluation by exporting a SAT file."""
- return self._disable_bounding_box_sat
+ return self.__disable_bounding_box_sat
@disable_bounding_box_sat.setter
def disable_bounding_box_sat(self, val):
- self._disable_bounding_box_sat = val
+ self.__disable_bounding_box_sat = val
@property
def use_grpc_api(self):
"""Flag for whether to use the gRPC API or legacy COM object."""
- return self._use_grpc_api
+ return self.__use_grpc_api
@use_grpc_api.setter
def use_grpc_api(self, val):
- self._use_grpc_api = val
-
- @property
- def logger(self):
- """Active logger."""
- return self._logger
-
- @logger.setter
- def logger(self, val):
- self._logger = val
+ self.__use_grpc_api = val
@property
def enable_error_handler(self):
"""Flag for enabling and disabling the internal PyAEDT error handling."""
- return self._enable_error_handler
+ return self.__enable_error_handler
@enable_error_handler.setter
def enable_error_handler(self, val):
- self._enable_error_handler = val
-
- @property
- def enable_desktop_logs(self):
- """Flag for enabling and disabling the logging to the AEDT message window."""
- return self._enable_desktop_logs
-
- @enable_desktop_logs.setter
- def enable_desktop_logs(self, val):
- self._enable_desktop_logs = val
-
- @property
- def enable_screen_logs(self):
- """Flag for enabling and disabling the logging to STDOUT."""
- return self._enable_screen_logs
-
- @enable_screen_logs.setter
- def enable_screen_logs(self, val):
- self._enable_screen_logs = val
+ self.__enable_error_handler = val
@property
def pyaedt_server_path(self):
- """``PYAEDT_SERVER_AEDT_PATH`` environment variable."""
- return os.getenv("PYAEDT_SERVER_AEDT_PATH", "")
+ """Get ``PYAEDT_SERVER_AEDT_PATH`` environment variable."""
+ self.__pyaedt_server_path = os.getenv("PYAEDT_SERVER_AEDT_PATH", "")
+ return self.__pyaedt_server_path
+ # NOTE: Convenient way to set the environment variable for RPyC
@pyaedt_server_path.setter
def pyaedt_server_path(self, val):
os.environ["PYAEDT_SERVER_AEDT_PATH"] = str(val)
-
- @property
- def enable_file_logs(self):
- """Flag for enabling and disabling the logging to a file."""
- return self._enable_file_logs
-
- @enable_file_logs.setter
- def enable_file_logs(self, val):
- self._enable_file_logs = val
-
- @property
- def enable_logger(self):
- """Flag for enabling and disabling the logging overall."""
- return self._enable_logger
-
- @enable_logger.setter
- def enable_logger(self, val):
- self._enable_logger = val
-
- @property
- def logger_file_path(self):
- """PyAEDT log file path."""
- return self._logger_file_path
-
- @logger_file_path.setter
- def logger_file_path(self, val):
- self._logger_file_path = val
-
- @property
- def logger_formatter(self):
- """Message format of the log entries.
- The default is ``'%(asctime)s:%(destination)s:%(extra)s%(levelname)-8s:%(message)s'``"""
- return self._logger_formatter
-
- @logger_formatter.setter
- def logger_formatter(self, val):
- self._logger_formatter = val
-
- @property
- def logger_datefmt(self):
- """Date format of the log entries.
- The default is ``'%Y/%m/%d %H.%M.%S'``"""
- return self._logger_datefmt
-
- @logger_datefmt.setter
- def logger_datefmt(self, val):
- self._logger_datefmt = val
-
- @property
- def enable_debug_edb_logger(self):
- """Flag for enabling and disabling the logger for any EDB API methods."""
- return self._enable_debug_edb_logger
-
- @enable_debug_edb_logger.setter
- def enable_debug_edb_logger(self, val):
- self._enable_debug_edb_logger = val
-
- @property
- def enable_debug_grpc_api_logger(self):
- """Flag for enabling and disabling the logging for the gRPC API calls."""
- return self._enable_debug_grpc_api_logger
-
- @enable_debug_grpc_api_logger.setter
- def enable_debug_grpc_api_logger(self, val):
- self._enable_debug_grpc_api_logger = val
-
- @property
- def enable_debug_geometry_operator_logger(self):
- """Flag for enabling and disabling the logging for the geometry operators.
- This setting is useful for debug purposes."""
- return self._enable_debug_geometry_operator_logger
-
- @enable_debug_geometry_operator_logger.setter
- def enable_debug_geometry_operator_logger(self, val):
- self._enable_debug_geometry_operator_logger = val
-
- @property
- def enable_debug_internal_methods_logger(self):
- """Flag for enabling and disabling the logging for internal methods.
- This setting is useful for debug purposes."""
- return self._enable_debug_internal_methods_logger
-
- @enable_debug_internal_methods_logger.setter
- def enable_debug_internal_methods_logger(self, val):
- self._enable_debug_internal_methods_logger = val
-
- @property
- def enable_debug_logger(self):
- """Flag for enabling and disabling the debug level logger."""
- return self._enable_debug_logger
-
- @enable_debug_logger.setter
- def enable_debug_logger(self, val):
- self._enable_debug_logger = val
+ self.__pyaedt_server_path = os.environ["PYAEDT_SERVER_AEDT_PATH"]
+
+ def load_yaml_configuration(self, path: str, raise_on_wrong_key: bool = False):
+ """Update default settings from a YAML configuration file."""
+ import yaml
+
+ def filter_settings(settings: dict, allowed_keys: List[str]):
+ """Filter the items of settings based on a list of allowed keys."""
+ return filter(lambda item: item[0] in allowed_keys, settings.items())
+
+ def filter_settings_with_raise(settings: dict, allowed_keys: List[str]):
+ """Filter the items of settings based on a list of allowed keys."""
+ for key, value in settings.items():
+ if key not in allowed_keys:
+ raise KeyError(f"Key '{key}' is not part of the allowed keys {allowed_keys}")
+ yield key, value
+
+ if os.path.exists(path):
+ with open(path, "r") as yaml_file:
+ local_settings = yaml.safe_load(yaml_file)
+ pairs = [
+ ("log", ALLOWED_LOG_SETTINGS),
+ ("lsf", ALLOWED_LSF_SETTINGS),
+ ("aedt_env_var", ALLOWED_AEDT_ENV_VAR_SETTINGS),
+ ("general", ALLOWED_GENERAL_SETTINGS),
+ ]
+ for setting_type, allowed_settings_key in pairs:
+ settings = local_settings.get(setting_type, {})
+ if raise_on_wrong_key:
+ for key, value in filter_settings_with_raise(settings, allowed_settings_key):
+ setattr(self, key, value)
+ else:
+ for key, value in filter_settings(settings, allowed_settings_key):
+ setattr(self, key, value)
+
+ def writte_yaml_configuration(self, path: str):
+ """Write the current settings into a YAML configuration file."""
+ import yaml
+
+ if os.path.exists(path):
+ yaml.safe_dump(settings, path)
settings = Settings()
+inner_project_settings = _InnerProjectSettings()
From 8309cb44331d7e591c969bb3434d1c0e27cf0d16 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Morais?=
<146729917+SMoraisAnsys@users.noreply.github.com>
Date: Wed, 28 Aug 2024 11:33:12 +0200
Subject: [PATCH 14/23] CHORE: Add git blame ignore revs file (#5095)
---
.git-blame-ignore-revs | 36 ++++++++++++++++++++++++++++++++++++
1 file changed, 36 insertions(+)
create mode 100644 .git-blame-ignore-revs
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 00000000000..ec0ee3183ad
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,36 @@
+# One commit hash per line, and all commits in the file will be ignored by git blame.
+
+# Apply flake8 policy for unused imports (#449)
+f2ebb7a80978e1ea82328e9b6afd3f9484362552
+# Organize module imports (#448)
+98bb0a1bfd27f8174ef86a131b3f6eed09f562ad
+# Format line length (#445)
+b82041bb75d71b8e11b54f9dcfdfc696a1bcf68c
+# Format modeler files (#441)
+0174ae92359f49fb8215f16d715f3afe8598745f
+# Format example files (#444)
+41eb277bc05b6736c674af33d5b601c3bc9a33d3
+# Format ipy unittests (#443)
+522b13c6ed123be60eb6220bfe6fed35ba059ab8
+# Format unittests (#442)
+11d60aa7e31f3494d8a73c0223c924ff17eec985
+# Format modules files (#436)
+0798f3f4c2252c1c3c486b8400540939c5b42dc6
+# Format application files (#434)
+19b758272a6d9551a8837b5f2d3a6283974d3367
+# Format pyaedt files with black (#433)
+3c3f0c6e01336eae66199bb9e461319d18a435d7
+# Formatting - fix line length part 2 (#415)
+58bbdc280545f7345b7999db0f17c3ff666e0be2
+# Formatting - fix line length part 1 (#414)
+af6af2e6b5db5e1485bfddd2b9d0e363614ecfce
+# Formatting - blank lines (#412)
+babf97db629f2901f40ae080ac3a7e24084a1e87
+# Formatting - whitespace on blank lines (#409)
+08e4ae515f7d5fd4a37e7baab7b321c907e66324
+# Formatting - indentations (#407)
+9ddfe050d95feaa022cc30cc64acabee16029c07
+# Remove trailing whitespace (#404)
+a4b5fe60c7d0a7db624d7ab160a0ce5e1f378759
+# Run autopep8 W291, remove trailing whitespace (#401)
+159bb43d42ad502c2440531d71cf7df6c780a8c9
From 0b7a8b9356aa3d1e887e3e10c55bf6ecf5bdc827 Mon Sep 17 00:00:00 2001
From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com>
Date: Wed, 28 Aug 2024 15:12:27 +0200
Subject: [PATCH 15/23] FIX: Cast GetChildNames("Groups") to list (#5098)
---
_unittest/example_models/T12/template.rpt | 379 ++++++++++++++++++
_unittest/test_12_PostProcessing.py | 9 +
.../aedt/core/modeler/cad/components_3d.py | 2 +-
src/ansys/aedt/core/modeler/cad/primitives.py | 2 +-
src/ansys/aedt/core/modeler/modeler_3d.py | 6 +-
src/ansys/aedt/core/modules/post_processor.py | 2 +-
.../aedt/core/modules/report_templates.py | 45 +++
7 files changed, 439 insertions(+), 6 deletions(-)
create mode 100644 _unittest/example_models/T12/template.rpt
diff --git a/_unittest/example_models/T12/template.rpt b/_unittest/example_models/T12/template.rpt
new file mode 100644
index 00000000000..bd301c52488
--- /dev/null
+++ b/_unittest/example_models/T12/template.rpt
@@ -0,0 +1,379 @@
+$begin 'ReportDefinitions'
+ $begin 'ReportSetup'
+ $begin 'Reports'
+ $begin 'Plot_6LKGC0'
+ ReportID=117
+ $begin 'Report2D'
+ name='Plot_6LKGC0'
+ ReportID=117
+ ReportType=9
+ DisplayType=4
+ Title=''
+ Domain=''
+ $begin 'Migration'
+ MigVersion(0, 0, 'mig(0.0)')
+ $end 'Migration'
+ $begin 'Graph2DsV2'
+ $begin 'Graph2D'
+ TraceDefID=116
+ Type='Continuous'
+ Axis='Y1'
+ $end 'Graph2D'
+ $end 'Graph2DsV2'
+ $begin 'PlotDisplayDataManager'
+ NextUniqueID=9
+ MoveBackwards=false
+ $begin 'PlotHeaderDataSource'
+ CompanyName=''
+ ShowDesignName=true
+ ProjectFileName=''
+ $end 'PlotHeaderDataSource'
+ StockNameIDMap(DataTable=3, Header=0, PrimarySweep=1)
+ $begin 'SourceList'
+ $end 'SourceList'
+ Version='17.0:20150830'
+ $begin 'DocAttributes'
+ $begin 'PlotAttributeStoreMap'
+ $end 'PlotAttributeStoreMap'
+ $end 'DocAttributes'
+ $begin 'DisplayTypeAttributes'
+ $begin 'PlotAttributeStoreMap'
+ $begin 'MainMapItem'
+ $begin 'SubMapItem'
+ DataSourceID=7
+ $begin 'CurveCartesianAttribute'
+ YAxis='Y1'
+ $end 'CurveCartesianAttribute'
+ $end 'SubMapItem'
+ $end 'MainMapItem'
+ $begin 'MainMapItem'
+ $begin 'SubMapItem'
+ DataSourceID=7
+ $begin 'CurveRenderAttribute'
+ $begin 'LineRenderAttribute'
+ LineStyle='Solid'
+ LineWidth=3
+ ColorVersion=1
+ LineColor(R=237, G=28, B=36)
+ $end 'LineRenderAttribute'
+ TraceType='Continuous'
+ SymbolType='HollowHorizontalLeftTriangle'
+ SymbolColor(R=155, G=93, B=112)
+ ShowSymbols=false
+ SymbolFrequency=15
+ ShowArrows=false
+ $end 'CurveRenderAttribute'
+ $end 'SubMapItem'
+ $end 'MainMapItem'
+ $begin 'MainMapItem'
+ $begin 'SubMapItem'
+ DataSourceID=0
+ $begin 'HeaderRenderAttribute'
+ $begin 'TitleFont'
+ $begin 'FontAttribute'
+ $begin 'Font'
+ HeightInPts=14
+ Width=0
+ Escapement=0
+ Orientation=0
+ Weight=400
+ Italic=0
+ Underline=0
+ StrikeOut=0
+ CharSet=0
+ OutPrecision=7
+ ClipPrecision=48
+ Quality=6
+ PitchAndFamily=0
+ FaceName='Arial'
+ $end 'Font'
+ ColorVersion=1
+ $end 'FontAttribute'
+ $end 'TitleFont'
+ $begin 'SubtitleFont'
+ $begin 'FontAttribute'
+ $begin 'Font'
+ HeightInPts=10
+ Width=0
+ Escapement=0
+ Orientation=0
+ Weight=400
+ Italic=0
+ Underline=0
+ StrikeOut=0
+ CharSet=0
+ OutPrecision=7
+ ClipPrecision=48
+ Quality=6
+ PitchAndFamily=0
+ FaceName='Arial'
+ $end 'Font'
+ ColorVersion=1
+ $end 'FontAttribute'
+ $end 'SubtitleFont'
+ $end 'HeaderRenderAttribute'
+ $end 'SubMapItem'
+ $end 'MainMapItem'
+ $end 'PlotAttributeStoreMap'
+ $end 'DisplayTypeAttributes'
+ $begin 'DocDefaultAttributes'
+ $begin 'PlotAttributeStoreMap'
+ $end 'PlotAttributeStoreMap'
+ $end 'DocDefaultAttributes'
+ $begin 'PerViewPlotAttributeStoreMap'
+ $begin 'MapItem'
+ ItemID=6
+ $begin 'PlotAttributeStoreMap'
+ $begin 'MainMapItem'
+ $begin 'SubMapItem'
+ DataSourceID=5
+ $begin 'BasicLayoutAttribute'
+ $begin 'LayoutRect'
+ Top=75
+ Left=75
+ Bottom=9925
+ Right=814
+ $end 'LayoutRect'
+ $end 'BasicLayoutAttribute'
+ $end 'SubMapItem'
+ $end 'MainMapItem'
+ $begin 'MainMapItem'
+ $begin 'SubMapItem'
+ DataSourceID=4
+ $begin 'OverlayLayoutAttribute'
+ $begin 'BoundingRect'
+ Top=225
+ Left=989
+ Bottom=9775
+ Right=9775
+ $end 'BoundingRect'
+ ModifySize=false
+ ModifyPosition=false
+ $end 'OverlayLayoutAttribute'
+ $end 'SubMapItem'
+ $end 'MainMapItem'
+ $end 'PlotAttributeStoreMap'
+ PlotType=25
+ $end 'MapItem'
+ $end 'PerViewPlotAttributeStoreMap'
+ IsViewAttribServer=false
+ ViewID=-1
+ $begin 'SourceIDMap'
+ IDMapItem(116, 0, -1, 7)
+ $end 'SourceIDMap'
+ $begin 'TraceCharacteristicsMgr'
+ $end 'TraceCharacteristicsMgr'
+ $begin 'CartesianXMarkerManager'
+ RefMarkerID=-1
+ CurrentMarkerID=-1
+ $begin 'ReferenceCurves'
+ $end 'ReferenceCurves'
+ $end 'CartesianXMarkerManager'
+ $begin 'CartesianYMarkerManager'
+ $end 'CartesianYMarkerManager'
+ XAxisStackID=-1
+ $begin 'AllTransSrcDwg'
+ $begin 'PT'
+ ID=25
+ TransSrcDwg(-1, 0, 5)
+ $end 'PT'
+ $end 'AllTransSrcDwg'
+ $begin 'AllPtSVID'
+ $end 'AllPtSVID'
+ $end 'PlotDisplayDataManager'
+ $end 'Report2D'
+ $end 'Plot_6LKGC0'
+ $end 'Reports'
+ $end 'ReportSetup'
+ $begin 'Reports'
+ $begin 'Plot_6LKGC0'
+ ReportID=117
+ ReportName='Plot_6LKGC0'
+ $begin 'TraceDef'
+ TraceDefinitionType='TraceDefinition'
+ $begin 'DesignSolnDefn'
+ $begin 'DESIGN_SOLUTION_SIM_VALUE_CONTEXT'
+ DesignID=4
+ SolutionID=5484
+ $begin 'REPORT_TYPE_SIM_VALUE_CONTEXT'
+ ReportType=9
+ SimValueContext(0, 0, 2, 0, false, false, 49, 1, 0, 1, 1, '', 0, 0, 'SourceContext', false, '0')
+ $end 'REPORT_TYPE_SIM_VALUE_CONTEXT'
+ $end 'DESIGN_SOLUTION_SIM_VALUE_CONTEXT'
+ $end 'DesignSolnDefn'
+ ID=116
+ VersionID=2217
+ Name='db(PeakRealizedGain)'
+ TieNameToExpr=true
+ $begin 'Components'
+ $begin 'TraceComponentDefinition'
+ Expr='Freq'
+ $end 'TraceComponentDefinition'
+ $begin 'TraceComponentDefinition'
+ Expr='db(PeakRealizedGain)'
+ $end 'TraceComponentDefinition'
+ $end 'Components'
+ $begin 'ExtendedTraceInfo'
+ NumPoints=0
+ TraceType=0
+ Offset=0
+ XLabel=''
+ SamplingPeriod='0'
+ SamplingPeriodOffset='0'
+ AutoDelay=true
+ DelayValue='0ps'
+ AutoCompCrossAmplitude=true
+ CrossingAmplitude='0mV'
+ YAxis=1
+ AutoCompEyeMeasurementPoint=true
+ EyeMeasurementPoint='0ps'
+ EyePamLow()
+ EyePamVRef()
+ EyePamHigh()
+ EyePamNames()
+ EyePamStrictVRef=false
+ $end 'ExtendedTraceInfo'
+ $begin 'TraceFamiliesDisplayDefinition'
+ DisplayFamiliesType='DisplayAll'
+ $end 'TraceFamiliesDisplayDefinition'
+ $begin 'PointsetDefinition'
+ $begin 'SubsweepDefParamsContainer'
+ $begin '0'
+ SubsweepType='Regular'
+ SubsweepChoiceType='All'
+ SweepVariableName='Freq'
+ AllowSelecteValues=true
+ SweepHasConsistentValues=true
+ $end '0'
+ $begin '1'
+ SubsweepType='Regular'
+ SubsweepChoiceType='Selected'
+ SweepVariableName='fc'
+ AllowSelecteValues=true
+ SweepHasConsistentValues=true
+ ColumnValues(30000000000)
+ ParameterType='DoubleParam'
+ Units='GHz'
+ $end '1'
+ $begin '2'
+ SubsweepType='Regular'
+ SubsweepChoiceType='Selected'
+ SweepVariableName='w'
+ AllowSelecteValues=true
+ SweepHasConsistentValues=true
+ ColumnValues(0.02)
+ ParameterType='DoubleParam'
+ Units=''
+ $end '2'
+ $begin '3'
+ SubsweepType='Regular'
+ SubsweepChoiceType='Selected'
+ SweepVariableName='rA'
+ AllowSelecteValues=true
+ SweepHasConsistentValues=true
+ ColumnValues(0.51)
+ ParameterType='DoubleParam'
+ Units=''
+ $end '3'
+ $begin '4'
+ SubsweepType='Regular'
+ SubsweepChoiceType='Selected'
+ SweepVariableName='hA'
+ AllowSelecteValues=true
+ SweepHasConsistentValues=true
+ ColumnValues(0.5)
+ ParameterType='DoubleParam'
+ Units=''
+ $end '4'
+ $begin '5'
+ SubsweepType='Regular'
+ SubsweepChoiceType='Selected'
+ SweepVariableName='rB'
+ AllowSelecteValues=true
+ SweepHasConsistentValues=true
+ ColumnValues(0.687)
+ ParameterType='DoubleParam'
+ Units=''
+ $end '5'
+ $begin '6'
+ SubsweepType='Regular'
+ SubsweepChoiceType='Selected'
+ SweepVariableName='hB'
+ AllowSelecteValues=true
+ SweepHasConsistentValues=true
+ ColumnValues(0.585)
+ ParameterType='DoubleParam'
+ Units=''
+ $end '6'
+ $begin '7'
+ SubsweepType='Regular'
+ SubsweepChoiceType='Selected'
+ SweepVariableName='rC'
+ AllowSelecteValues=true
+ SweepHasConsistentValues=true
+ ColumnValues(0.805)
+ ParameterType='DoubleParam'
+ Units=''
+ $end '7'
+ $begin '8'
+ SubsweepType='Regular'
+ SubsweepChoiceType='Selected'
+ SweepVariableName='hC'
+ AllowSelecteValues=true
+ SweepHasConsistentValues=true
+ ColumnValues(0.661)
+ ParameterType='DoubleParam'
+ Units=''
+ $end '8'
+ $begin '9'
+ SubsweepType='Regular'
+ SubsweepChoiceType='Selected'
+ SweepVariableName='rD'
+ AllowSelecteValues=true
+ SweepHasConsistentValues=true
+ ColumnValues(0.822)
+ ParameterType='DoubleParam'
+ Units=''
+ $end '9'
+ $begin '10'
+ SubsweepType='Regular'
+ SubsweepChoiceType='Selected'
+ SweepVariableName='hD'
+ AllowSelecteValues=true
+ SweepHasConsistentValues=true
+ ColumnValues(2.3)
+ ParameterType='DoubleParam'
+ Units=''
+ $end '10'
+ $begin '11'
+ SubsweepType='Specifiable'
+ SubsweepChoiceType='Selected'
+ SweepVariableName='PC'
+ AllowSelecteValues=true
+ SweepHasConsistentValues=true
+ ColumnValues(-0.073)
+ ParameterType='DoubleParam'
+ Units=''
+ $end '11'
+ $end 'SubsweepDefParamsContainer'
+ FamilyBlock()
+ $end 'PointsetDefinition'
+ DesignInstanceID=5
+ $end 'TraceDef'
+ $end 'Plot_6LKGC0'
+ $end 'Reports'
+ $begin 'AllTracesInterpreter'
+ $begin 'PerTraceInterpreter'
+ $begin 'TraceInterpreter'
+ ID=116
+ $begin 'ReportMgrTraceInterpreter'
+ SolutionName='Setup1 : LastAdaptive'
+ SolutionID=5484
+ $end 'ReportMgrTraceInterpreter'
+ $begin 'ProductTraceInterpreterBlock'
+ EMContext='3D'
+ $end 'ProductTraceInterpreterBlock'
+ $end 'TraceInterpreter'
+ $end 'PerTraceInterpreter'
+ $end 'AllTracesInterpreter'
+$end 'ReportDefinitions'
diff --git a/_unittest/test_12_PostProcessing.py b/_unittest/test_12_PostProcessing.py
index ecd36d9445d..181ef201e81 100644
--- a/_unittest/test_12_PostProcessing.py
+++ b/_unittest/test_12_PostProcessing.py
@@ -194,6 +194,15 @@ def test_09_manipulate_report_B(self, field_test):
)
new_report4.report_type = "Data Table"
assert new_report4.create()
+ if not config["NonGraphical"]:
+ local_path = os.path.dirname(os.path.realpath(__file__))
+ template = os.path.join(local_path, "example_models", test_subfolder, "template.rpt")
+ assert new_report4.apply_report_template(template)
+ template = os.path.join(local_path, "example_models", test_subfolder, "template_invented.rpt")
+ assert not new_report4.apply_report_template(template)
+ template = os.path.join(local_path, "example_models", test_subfolder, "template.csv")
+ assert not new_report4.apply_report_template(template)
+ assert not new_report4.apply_report_template(template, property_type="Dummy")
def test_09_manipulate_report_C(self, field_test):
variations = field_test.available_variations.nominal_w_values_dict
diff --git a/src/ansys/aedt/core/modeler/cad/components_3d.py b/src/ansys/aedt/core/modeler/cad/components_3d.py
index bf1ea28b32e..3b802188ea2 100644
--- a/src/ansys/aedt/core/modeler/cad/components_3d.py
+++ b/src/ansys/aedt/core/modeler/cad/components_3d.py
@@ -280,7 +280,7 @@ def group_name(self, name):
"""
if "Group" in self._primitives.oeditor.GetChildObject(self.name).GetPropNames() and name not in list(
- self._primitives.oeditor.GetChildNames("Groups")
+ list(self._primitives.oeditor.GetChildNames("Groups"))
):
arg = [
"NAME:GroupParameter",
diff --git a/src/ansys/aedt/core/modeler/cad/primitives.py b/src/ansys/aedt/core/modeler/cad/primitives.py
index 98a0637a25e..ad48d2dad2e 100644
--- a/src/ansys/aedt/core/modeler/cad/primitives.py
+++ b/src/ansys/aedt/core/modeler/cad/primitives.py
@@ -5622,7 +5622,7 @@ def create_group(self, objects=None, components=None, groups=None, group_name=No
if components is None and groups is None and objects is None:
raise AttributeError("At least one between ``objects``, ``components``, ``groups`` has to be defined.")
- all_objects = self.object_names[:] + self.oeditor.GetChildNames("Groups")[::]
+ all_objects = self.object_names[:] + list(self.oeditor.GetChildNames("Groups"))
if objects:
object_selection = self.convert_to_selections(objects, return_list=False)
else:
diff --git a/src/ansys/aedt/core/modeler/modeler_3d.py b/src/ansys/aedt/core/modeler/modeler_3d.py
index d7b912647cd..77d55e504b6 100644
--- a/src/ansys/aedt/core/modeler/modeler_3d.py
+++ b/src/ansys/aedt/core/modeler/modeler_3d.py
@@ -1108,7 +1108,7 @@ def import_nastran(
aedt_objs = self.object_names[::]
for assembly, _ in nas_to_dict["Assemblies"].items():
assembly_group_name = assembly
- if assembly in self.oeditor.GetChildNames("Groups"):
+ if assembly in list(self.oeditor.GetChildNames("Groups")):
assembly_group_name = generate_unique_name(assembly, n=2)
new_group = []
for el in nas_to_dict["Assemblies"][assembly]["Solids"].keys():
@@ -1121,7 +1121,7 @@ def import_nastran(
]
if obj_names:
new_group.append(self.create_group(obj_names, group_name=str(el)))
- if assembly_group_name in self.oeditor.GetChildNames("Groups"):
+ if assembly_group_name in list(self.oeditor.GetChildNames("Groups")):
self.oeditor.MoveEntityToGroup(
[
"Groups:=",
@@ -1180,7 +1180,7 @@ def import_nastran(
id += 1
if group_parts:
pids = [i.name for i in polys]
- if assembly_name in self.oeditor.GetChildNames("Groups"):
+ if assembly_name in list(self.oeditor.GetChildNames("Groups")):
self.oeditor.MoveEntityToGroup(
[
"Objects:=",
diff --git a/src/ansys/aedt/core/modules/post_processor.py b/src/ansys/aedt/core/modules/post_processor.py
index aee16da526a..1cb0b19c183 100644
--- a/src/ansys/aedt/core/modules/post_processor.py
+++ b/src/ansys/aedt/core/modules/post_processor.py
@@ -4478,7 +4478,7 @@ def power_budget(self, units="W", temperature=22, output_type="component"):
power_dict_obj = {}
group_hierarchy = {}
- groups = self._app.oeditor.GetChildNames("Groups")
+ groups = list(self._app.oeditor.GetChildNames("Groups"))
self._app.modeler.add_new_user_defined_component()
for g in groups:
g1 = self._app.oeditor.GetChildObject(g)
diff --git a/src/ansys/aedt/core/modules/report_templates.py b/src/ansys/aedt/core/modules/report_templates.py
index 5eae5560eca..60bbaa415e0 100644
--- a/src/ansys/aedt/core/modules/report_templates.py
+++ b/src/ansys/aedt/core/modules/report_templates.py
@@ -2062,6 +2062,51 @@ def update_trace_in_report(self, traces, setup_name=None, variations=None, conte
finally:
self.expressions = expr
+ @pyaedt_function_handler()
+ def apply_report_template(self, input_file, property_type="Graphical"): # pragma: no cover
+ """Apply report template.
+
+ .. note::
+ This method works in only in graphical mode.
+
+ Parameters
+ ----------
+ input_file : str
+ Path for the file to import. The extension supported is ``".rpt"``.
+ property_type : str, optional
+ Property types to apply. Options are ``"Graphical"``, ``"Data"``, and ``"All"``.
+
+ Returns
+ -------
+ bool
+ ``True`` when successful, ``False`` when failed.
+
+ References
+ ----------
+
+ >>> oModule.ApplyReportTemplate
+ """
+ if not os.path.exists(input_file): # pragma: no cover
+ msg = "File does not exist."
+ self._post.logger.error(msg)
+ return False
+
+ split_path = os.path.splitext(input_file)
+ extension = split_path[1]
+
+ supported_ext = [".rpt"]
+ if extension not in supported_ext: # pragma: no cover
+ msg = "Extension {} is not supported.".format(extension)
+ self._post.logger.error(msg)
+ return False
+
+ if property_type not in ["Graphical", "Data", "All"]: # pragma: no cover
+ msg = "Invalid value for `property_type`. The value must be 'Graphical', 'Data', or 'All'."
+ self._post.logger.error(msg)
+ return False
+ self._post.oreportsetup.ApplyReportTemplate(self.plot_name, input_file, property_type)
+ return True
+
class Standard(CommonReport):
"""Provides a reporting class that fits most of the app's standard reports."""
From aba3dba60be0657dcb849c34dea95d76f338690c Mon Sep 17 00:00:00 2001
From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com>
Date: Thu, 29 Aug 2024 00:14:34 +0200
Subject: [PATCH 16/23] FEAT: Circuit Netlist class (#5094)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Sébastien Morais <146729917+SMoraisAnsys@users.noreply.github.com>
---
_unittest/conftest.py | 18 +-
_unittest/example_models/T47/netlist.aedtz | Bin 0 -> 53553 bytes
_unittest/test_21_Circuit.py | 8 +-
_unittest/test_47_CircuitNetlist.py | 60 ++++++
_unittest/test_launch_desktop.py | 6 +
_unittest_solvers/conftest.py | 1 +
src/ansys/aedt/core/__init__.py | 1 +
.../aedt/core/application/aedt_objects.py | 24 ++-
src/ansys/aedt/core/application/analysis.py | 11 +-
.../application/analysis_circuit_netlist.py | 126 +++++++++++
.../application/analysis_maxwell_circuit.py | 14 +-
src/ansys/aedt/core/application/design.py | 47 +++--
.../aedt/core/application/design_solutions.py | 3 +
src/ansys/aedt/core/application/variables.py | 9 +-
src/ansys/aedt/core/circuit.py | 10 +-
src/ansys/aedt/core/circuit_netlist.py | 197 ++++++++++++++++++
src/ansys/aedt/core/desktop.py | 25 ++-
src/ansys/aedt/core/generic/design_types.py | 6 +-
src/ansys/aedt/core/maxwell.py | 4 +-
.../modeler/circuits/primitives_nexxim.py | 8 +-
src/ansys/aedt/core/modeler/modeler_3d.py | 2 +-
src/ansys/aedt/core/modeler/schematic.py | 4 +-
src/ansys/aedt/core/modules/post_processor.py | 32 +--
.../aedt/core/modules/report_templates.py | 5 +-
24 files changed, 540 insertions(+), 81 deletions(-)
create mode 100644 _unittest/example_models/T47/netlist.aedtz
create mode 100644 _unittest/test_47_CircuitNetlist.py
create mode 100644 src/ansys/aedt/core/application/analysis_circuit_netlist.py
create mode 100644 src/ansys/aedt/core/circuit_netlist.py
diff --git a/_unittest/conftest.py b/_unittest/conftest.py
index eef19475e3b..183590b6378 100644
--- a/_unittest/conftest.py
+++ b/_unittest/conftest.py
@@ -62,6 +62,7 @@
settings.enable_desktop_logs = False
settings.desktop_launch_timeout = 180
settings.release_on_exception = False
+settings.wait_for_license = True
from ansys.aedt.core import Edb
from ansys.aedt.core import Hfss
@@ -197,13 +198,16 @@ def _method(
test_project = None
if not application:
application = Hfss
- return application(
- project=test_project,
- design=design_name,
- solution_type=solution_type,
- version=desktop_version,
- non_graphical=NONGRAPHICAL,
- )
+
+ args = {
+ "project": test_project,
+ "design": design_name,
+ "version": desktop_version,
+ "non_graphical": NONGRAPHICAL,
+ }
+ if solution_type:
+ args["solution_type"] = solution_type
+ return application(**args)
return _method
diff --git a/_unittest/example_models/T47/netlist.aedtz b/_unittest/example_models/T47/netlist.aedtz
new file mode 100644
index 0000000000000000000000000000000000000000..2023910630de1c82c4c8b0d717481bd99071f7b2
GIT binary patch
literal 53553
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