From ce58aac920360458fb22a220ac822e197336e05a Mon Sep 17 00:00:00 2001 From: Hui Zhou Date: Fri, 28 Apr 2023 16:25:24 +0200 Subject: [PATCH 01/23] doc update (#2931) Co-authored-by: ring630 <@gmail.com> --- pyaedt/edb_core/edb_data/hfss_extent_info.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyaedt/edb_core/edb_data/hfss_extent_info.py b/pyaedt/edb_core/edb_data/hfss_extent_info.py index e32cc14adf1..7e78d52d4d2 100644 --- a/pyaedt/edb_core/edb_data/hfss_extent_info.py +++ b/pyaedt/edb_core/edb_data/hfss_extent_info.py @@ -84,6 +84,7 @@ def air_box_positive_vertical_extent(self): @air_box_positive_vertical_extent.setter def air_box_positive_vertical_extent(self, value): + value = float(value) info = self._edb_hfss_extent_info info.AirBoxPositiveVerticalExtent = convert_pytuple_to_nettuple( (value, self.air_box_positive_vertical_extent_enabled) @@ -108,6 +109,7 @@ def air_box_negative_vertical_extent(self): @air_box_negative_vertical_extent.setter def air_box_negative_vertical_extent(self, value): + value = float(value) info = self._edb_hfss_extent_info info.AirBoxNegativeVerticalExtent = convert_pytuple_to_nettuple( (value, self.air_box_negative_vertical_extent_enabled) From b5756b182d52f0d62e0d4cb459d6fa9f3175246f Mon Sep 17 00:00:00 2001 From: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Date: Fri, 28 Apr 2023 16:25:54 +0200 Subject: [PATCH 02/23] Improve documentation. (#2928) --- .pre-commit-config.yaml | 2 +- pyaedt/icepak.py | 16 ++++------- pyaedt/modeler/cad/Modeler.py | 3 -- pyaedt/modeler/cad/polylines.py | 43 ++++++++++++++-------------- pyaedt/modeler/geometry_operators.py | 34 +++++++++++----------- 5 files changed, 43 insertions(+), 55 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 898a0067025..42db1b74d4e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -69,4 +69,4 @@ repos: # additional_dependencies: [toml] # files: ^pyaedt/ # args: -# - --ignore=D412,D413,D213,D101,D107,D203,D102 \ No newline at end of file +# - --ignore=D412,D413,D213,D101,D107,D203,D102, D205 \ No newline at end of file diff --git a/pyaedt/icepak.py b/pyaedt/icepak.py index bb2abe9afd4..73e2ea8b883 100644 --- a/pyaedt/icepak.py +++ b/pyaedt/icepak.py @@ -1848,7 +1848,6 @@ def get_link_data(self, links_data, **kwargs): List containing the requested link data. """ - if "linkData" in kwargs: warnings.warn( "The ``linkData`` parameter was deprecated in 0.6.43. Use the ``links_data`` parameter instead.", @@ -2077,7 +2076,6 @@ def create_ipk_3dcomponent_pcb( >>> oModule.InsertNativeComponent """ - if "extenttype" in kwargs: warnings.warn( "The ``extenttype`` parameter was deprecated in 0.6.43. Use the ``extent_type`` parameter instead.", @@ -2184,16 +2182,16 @@ def create_pcb_from_3dlayout( Name of the design. resolution : int, optional Resolution of the mapping. The default is ``2``. - extent_type : + extent_type : str, optional Type of the extent. Options are ``"Polygon"`` and ``"Bounding Box"``. The default is ``"Bounding Box"``. outline_polygon : str, optional Name of the outline polygon if ``extent_type="Polygon"``. The default is ``""``. close_linked_project_after_import : bool, optional Whether to close the linked AEDT project after the import. The default is ``True``. - custom_x_resolution : + custom_x_resolution : int, optional The default is ``None``. - custom_y_resolution : + custom_y_resolution : int, optional The default is ``None``. power_in : float, optional Power in in Watt. @@ -2208,7 +2206,6 @@ def create_pcb_from_3dlayout( >>> oModule.InsertNativeComponent """ - if "extenttype" in kwargs: warnings.warn( "``extenttype`` was deprecated in 0.6.43. Use ``extent_type`` instead.", @@ -2270,7 +2267,6 @@ def copyGroupFrom(self, group_name, source_design, source_project_name=None, sou >>> oEditor.Copy >>> oeditor.Paste """ - if "groupName" in kwargs: warnings.warn( "The ``groupName`` parameter was deprecated in 0.6.43. Use the ``group_name`` parameter instead.", @@ -2360,7 +2356,6 @@ def globalMeshSettings( >>> oModule.EditGlobalMeshRegion """ - bounding_box = self.modeler.oeditor.GetModelBoundingBox() xsize = abs(float(bounding_box[0]) - float(bounding_box[3])) / (15 * meshtype * meshtype) ysize = abs(float(bounding_box[1]) - float(bounding_box[4])) / (15 * meshtype * meshtype) @@ -2688,9 +2683,9 @@ def apply_icepak_settings( If ``False``, full validation is performed. default_fluid : str, optional Type of fluid. The default is ``"Air"``. - default_solid : + default_solid : str, optional Type of solid. The default is ``"Al-Extruded"``. - default_surface : + default_surface : str, optional Type of surface. The default is ``"Steel-oxidised-surface"``. Returns @@ -2703,7 +2698,6 @@ def apply_icepak_settings( >>> oDesign.SetDesignSettings """ - ambient_temperature = self.modeler._arg_with_dim(ambienttemp, "cel") axes = ["X", "Y", "Z"] diff --git a/pyaedt/modeler/cad/Modeler.py b/pyaedt/modeler/cad/Modeler.py index 70131e41a76..c7b71879878 100644 --- a/pyaedt/modeler/cad/Modeler.py +++ b/pyaedt/modeler/cad/Modeler.py @@ -2639,7 +2639,6 @@ def mirror(self, objid, position, vector, duplicate=False, is_3d_comp=False, dup >>> oEditor.Mirror >>> oEditor.DuplicateMirror """ - selections = self.convert_to_selections(objid) Xpos, Ypos, Zpos = self._pos_with_arg(position) Xnorm, Ynorm, Znorm = self._pos_with_arg(vector) @@ -4982,7 +4981,6 @@ def move_face(self, faces, offset=1.0): >>> oEditor.MoveFaces """ - face_selection = self.convert_to_selections(faces, True) selection = {} for f in face_selection: @@ -5044,7 +5042,6 @@ def move_edge(self, edges, offset=1.0): >>> oEditor.MoveEdges """ - edge_selection = self.convert_to_selections(edges, True) selection = {} for f in edge_selection: diff --git a/pyaedt/modeler/cad/polylines.py b/pyaedt/modeler/cad/polylines.py index 581407618d3..8c35a1c437d 100644 --- a/pyaedt/modeler/cad/polylines.py +++ b/pyaedt/modeler/cad/polylines.py @@ -368,7 +368,8 @@ def _update_segments_and_points(self): It will be called only once after opening a new project, then the internal variables are maintained updated. It is a single update call for both properties because they are very similar, - and we can access to the history only once.""" + and we can access to the history only once. + """ def _convert_points(p_in, dest_unit): p_out = [] @@ -441,7 +442,6 @@ def _convert_points(p_in, dest_unit): @property def points(self): """Polyline Points.""" - if self._positions: return self._positions else: @@ -573,7 +573,7 @@ def _point_segment_string_array(self): @pyaedt_function_handler() def _evaluate_arc_angle_extra_points(self, segment, start_point): - """Evaluates the extra points for the ArcAngle segment type. + """Evaluate the extra points for the ArcAngle segment type. It also auto evaluates the arc_plane if it was not specified by the user. segment.extra_points[0] contains the arc mid point (on the arc). segment.extra_points[1] contains the arc end point. @@ -632,22 +632,22 @@ def _segment_array(self, segment_data, start_index=0, start_point=None): """Retrieve a property array for a polyline segment for use in the :class:`pyaedt.modeler.Primitives.Polyline` constructor. - Parameters - ---------- - segment_data : :class:`pyaedt.modeler.Primitives.PolylineSegment` or str - Pointer to the calling object that provides additional functionality - or a string with the segment type ``Line`` or ``Arc``. - start_index : int, string - Starting vertex index of the segment within a compound polyline. The - default is ``0``. - start_point : list, optional - Position of the first point for type ``AngularArc``. The default is - ``None``. Float values are considered in model units. - - Returns - ------ - list - List of properties defining a polyline segment. + Parameters + ---------- + segment_data : :class:`pyaedt.modeler.Primitives.PolylineSegment` or str + Pointer to the calling object that provides additional functionality + or a string with the segment type ``Line`` or ``Arc``. + start_index : int, string + Starting vertex index of the segment within a compound polyline. The + default is ``0``. + start_point : list, optional + Position of the first point for type ``AngularArc``. The default is + ``None``. Float values are considered in model units. + + Returns + ------- + list + List of properties defining a polyline segment. """ if isinstance(segment_data, str): @@ -1033,7 +1033,7 @@ def _get_segment_id_from_point_n(self, pn, at_start, allow_inner_points=False): Returns ------- int, bool - segment id when successful. ``False`` when failed. + Segment id when successful. ``False`` when failed. """ n_points = 0 for i, s in enumerate(self.segment_types): @@ -1068,7 +1068,7 @@ def insert_segment(self, position_list, segment=None): Definition of the segment to insert. For the types ``"Line"`` and ``"Arc"``, use their string values ``"Line"`` and ``"Arc"``. For the types ``"AngularArc"`` and ``"Spline"``, use the :class:`pyaedt.modeler.Primitives.PolylineSegment` - object to define the segment precisely. + object to define the segment precisely. The default is ``None``. Returns ------- @@ -1081,7 +1081,6 @@ def insert_segment(self, position_list, segment=None): >>> oEditor.InsertPolylineSegment """ - # Check for a valid number of points num_points = len(position_list) diff --git a/pyaedt/modeler/geometry_operators.py b/pyaedt/modeler/geometry_operators.py index 9b519f02e0c..7e6aca6e8ec 100644 --- a/pyaedt/modeler/geometry_operators.py +++ b/pyaedt/modeler/geometry_operators.py @@ -47,12 +47,13 @@ def parse_dim_arg(string, scale_to_unit=None, variable_manager=None): Parameters ---------- - string : str - String to convert. For example, ``"2mm"``. - scale_to_unit : str + string : str, optional + String to convert. For example, ``"2mm"``. The default is ``None``. + scale_to_unit : str, optional Units for the value to convert. For example, ``"mm"``. variable_manager : :class:`pyaedt.application.Variables.VariableManager`, optional Try to parse formula and returns numeric value. + The default is ``None``. Returns ------- @@ -73,7 +74,6 @@ def parse_dim_arg(string, scale_to_unit=None, variable_manager=None): >>> 2.0 """ - if type(string) is not str: try: return float(string) @@ -115,8 +115,8 @@ def cs_plane_to_axis_str(val): Parameters ---------- - val : - + val : int + ``PLANE`` enum vélo. Returns ------- @@ -161,7 +161,8 @@ def cs_axis_str(val): Parameters ---------- - val : + val : int + ``AXIS`` enum value. Returns @@ -184,8 +185,8 @@ def draft_type_str(val): Parameters ---------- - val : - + val : int + ``SWEEPDRAFT`` enum value. Returns ------- @@ -267,7 +268,6 @@ def v_cross(a, b): List List of ``[x, y, z]`` coordinates for the result vector. """ - c = [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]] return c @@ -431,8 +431,8 @@ def v_norm(a): Parameters ---------- - a : List - List of ``[x, y, z]`` coordinates for the vector. + a : List + List of ``[x, y, z]`` coordinates for the vector. Returns ------- @@ -529,7 +529,6 @@ def find_point_on_plane(pointlists, direction=0): List """ - if direction <= 2: point = 1e6 for p in pointlists: @@ -728,7 +727,7 @@ def is_projection_inside(a1, a2, b1, b2): @staticmethod @pyaedt_function_handler() def arrays_positions_sum(vertlist1, vertlist2): - """ADD DESCRIPTION. + """Return the sum of two vertices lists. Parameters ---------- @@ -792,7 +791,6 @@ def pointing_to_axis(x_pointing, y_pointing): tuple ``[Xx, Xy, Xz], [Yx, Yy, Yz], [Zx, Zy, Zz]`` of the three axes (normalized). """ - zpt = GeometryOperators.v_cross(x_pointing, y_pointing) ypt = GeometryOperators.v_cross(zpt, x_pointing) @@ -1364,16 +1362,16 @@ def get_numeric(s): @staticmethod @pyaedt_function_handler() def is_small(s): - """ - Return True if the number represented by s is zero (i.e very small). + """Return ``True`` if the number represented by s is zero (i.e very small). Parameters ---------- - s, numeric or str + s : numeric or str Variable value. Returns ------- + bool """ n = GeometryOperators.get_numeric(s) From 35cb5c01f49bfbb7bf83a969d9e2a400dcb8801f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Apr 2023 22:22:10 +0000 Subject: [PATCH 03/23] MAINT: Bump ipython from 8.12.0 to 8.13.0 (#2936) Bumps [ipython](https://github.com/ipython/ipython) from 8.12.0 to 8.13.0. - [Release notes](https://github.com/ipython/ipython/releases) - [Commits](https://github.com/ipython/ipython/compare/8.12.0...8.13.0) --- updated-dependencies: - dependency-name: ipython dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e6bc03f4766..045ca1900ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ [project.optional-dependencies] tests = [ - "ipython==8.12.0", + "ipython==8.13.0", "joblib==1.2.0", "matplotlib==3.5.3; python_version <= '3.7'", "matplotlib==3.7.1; python_version > '3.7'", @@ -59,7 +59,7 @@ doc = [ "ansys-sphinx-theme==0.9.8", "imageio==2.28.0", "imageio-ffmpeg==0.4.8", - "ipython==8.12.0", + "ipython==8.13.0", "ipywidgets==8.0.6", "joblib==1.2.0", "jupyterlab==3.6.3", From 735f03863dbe8e4cef4a161da0df745ccb1caf60 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Tue, 2 May 2023 08:49:53 +0200 Subject: [PATCH 04/23] Added python file to Installation.rst (#2932) * Added python file to Installation.rst * added image to commit * added image to commit * refactored index and Contributing.rst. Added versioning * fixed vale * fixed vale * fixed vale * fixed vale * fixed vale --------- Co-authored-by: maxcapodi78 --- .../{ => Getting_started}/Contributing.rst | 0 doc/source/Getting_started/Installation.rst | 24 +- doc/source/Getting_started/index.rst | 2 + doc/source/Getting_started/versioning.rst | 75 +++++ .../Resources/PyAEDTInstallerFromDesktop.py | 146 +++++++++ doc/source/Resources/toolkits_ribbon.png | Bin 0 -> 43794 bytes doc/source/index.rst | 284 +++++++++++++++++- doc/styles/Vocab/ANSYS/accept.txt | 9 + 8 files changed, 533 insertions(+), 7 deletions(-) rename doc/source/{ => Getting_started}/Contributing.rst (100%) create mode 100644 doc/source/Getting_started/versioning.rst create mode 100644 doc/source/Resources/PyAEDTInstallerFromDesktop.py create mode 100644 doc/source/Resources/toolkits_ribbon.png diff --git a/doc/source/Contributing.rst b/doc/source/Getting_started/Contributing.rst similarity index 100% rename from doc/source/Contributing.rst rename to doc/source/Getting_started/Contributing.rst diff --git a/doc/source/Getting_started/Installation.rst b/doc/source/Getting_started/Installation.rst index 0b28fe6b80a..8dc4b2c37af 100644 --- a/doc/source/Getting_started/Installation.rst +++ b/doc/source/Getting_started/Installation.rst @@ -57,12 +57,30 @@ For example, on Windows with Python 3.7, install PyAEDT and all its dependencies pip install --no-cache-dir --no-index --find-links=file:////PyAEDT-v-wheelhouse-Windows-3.7 pyaedt +Install from a Python file +~~~~~~~~~~~~~~~~~~~~~~~~~~ +AEDT already includes CPython 3.7, which can be used to run PyAEDT. +It is also possible to use CPython 3.7 (3.10 from AEDT 2023R2) as a virtual environment to run PyAEDT. +In order to do that you can download the following file +:download:`PyAEDT Installer python file <../Resources/PyAEDTInstallerFromDesktop.py>` +Open an Electronics Desktop Session and click on Tools->Run Script and execute the file. + +After installation a new menu appears in AEDT Menu as in the image below. + +.. image:: ../Resources/toolkits.png + :width: 800 + :alt: PyAEDT toolkit installed after batch run + + +Starting from 2023R2, a Ribbon button is available in Automation Tab as in the example below. + +.. image:: ../Resources/toolkits_ribbon.png + :width: 800 + :alt: PyAEDT toolkit buttons available in AEDT 2023.2 after batch run + Install from a batch file ~~~~~~~~~~~~~~~~~~~~~~~~~ -AEDT already includes CPython 3.7, which can be used to run PyAEDT. -It is also possible to use CPython 3.7 as a virtual environment to run PyAEDT. - If you are running on Windows, you can download :download:`PyAEDT Environment with IDE bat file <../Resources/pyaedt_with_IDE.bat>` and run this batch file on your local machine. Using this approach diff --git a/doc/source/Getting_started/index.rst b/doc/source/Getting_started/index.rst index f4940382187..86f861e09cd 100644 --- a/doc/source/Getting_started/index.rst +++ b/doc/source/Getting_started/index.rst @@ -31,4 +31,6 @@ on the Ansys website. Installation ClientServer + versioning + Contributing diff --git a/doc/source/Getting_started/versioning.rst b/doc/source/Getting_started/versioning.rst new file mode 100644 index 00000000000..f4ce39a4e28 --- /dev/null +++ b/doc/source/Getting_started/versioning.rst @@ -0,0 +1,75 @@ +.. _versions_and_interfaces: + +======================= +Versions and interfaces +======================= + +The PyAEDT project attempts to maintain compatibility with legacy +versions of AEDT while allowing for support of faster and better +interfaces with the latest versions of AEDT. + +There are two interfaces PyAEDT can use to connect to AEDT. +You can see a table with the AEDT version and the supported interfaces +in `Table of supported versions `_ + + +gRPC interface +============== + +This is the default and preferred interface to connect to AEDT. +Ansys 2022 R2 and later support the latest gRPC interface, allowing +for remote management of MAPDL with rapid streaming of mesh, results, +and files from the MAPDL service. + + +Legacy interfaces +================= + +COM interface +-------------- + +AnsysEM supports the legacy COM interface, enabled with the settings option. + +This interface works only on Windows and uses .NET COM objects. + + +.. code:: python + + + from pyaedt import settings + + settings.use_grpc_api = False + + + +Compatibility between AEDT and interfaces +========================================= + +The following table shows the supported versions of Ansys EDT and the recommended interface for each one of them in PyAEDT. + + +**Table of supported versions** + +.. _table_versions: + ++---------------------------+------------------------+-----------------------------------------------+ +| Ansys Version | Recommended interface | Support | +| | +-----------------------+-----------------------+ +| | | gRPC | COM | ++===========================+========================+=======================+=======================+ +| AnsysEM 2023 R2 | gRPC | YES | YES* | ++---------------------------+------------------------+-----------------------+-----------------------+ +| AnsysEM 2023 R1 | gRPC | YES | YES* | ++---------------------------+------------------------+-----------------------+-----------------------+ +| AnsysEM 2022 R2 | gRPC | YES* | YES | ++---------------------------+------------------------+-----------------------+-----------------------+ +| AnsysEM 2022 R1 | gRPC | NO | YES | ++---------------------------+------------------------+-----------------------+-----------------------+ +| AnsysEM 2021 R2 | gRPC | NO | YES | ++---------------------------+------------------------+-----------------------+-----------------------+ + +Where: + +* YES means that the interface is supported and recommended. +* YES* means that the interface is supported, but not recommended. Their support might be dropped in the future. +* NO means that the interface is not supported. \ No newline at end of file diff --git a/doc/source/Resources/PyAEDTInstallerFromDesktop.py b/doc/source/Resources/PyAEDTInstallerFromDesktop.py new file mode 100644 index 00000000000..c313aeedc6e --- /dev/null +++ b/doc/source/Resources/PyAEDTInstallerFromDesktop.py @@ -0,0 +1,146 @@ +import argparse +import os +import sys +import platform + +is_iron_python = platform.python_implementation().lower() == "ironpython" +is_linux = os.name == "posix" +is_windows = not is_linux + + +def run_pyinstaller_from_c_python(oDesktop): + # This is called when run from IronPython + version = oDesktop.GetVersion()[2:6].replace(".", "") + python_version = "3.10" + if version <= "231": + python_version = "3.7" + edt_root = os.path.normpath(oDesktop.GetExeDir()) + if is_windows: + python_exe = os.path.normpath(os.path.join(edt_root, "commonfiles", "CPython", python_version.replace(".", "_"), + "winx64", "Release", "python", "python.exe")) + else: + python_exe = os.path.normpath(os.path.join(edt_root, "commonfiles", "CPython", python_version.replace(".", "_"), + "linx64", "Release", "python", "runpython")) + command = ['"{}"'.format(python_exe), '"{}"'.format(os.path.normpath(__file__)), "--version=" + version] + if is_student_version(oDesktop): + command.append("--student") + if is_linux: + command.extend(['--edt_root="{}"'.format(edt_root), '--python_version="{}"'.format(python_version)]) + + oDesktop.AddMessage("", "", 0, "Installing PyAEDT.") + if is_windows: + import subprocess + process = subprocess.Popen(" ".join(command)) + process.wait() + return_code = process.returncode + err_msg = "There was an error while installing PyAEDT." + else: + return_code = run_command(" ".join(command)) + err_msg = "There was an error while installing PyAEDT. Refer to the Terminal window where AEDT was launched " \ + "from." + + if version >= "232": + oDesktop.RefreshToolkitUI() + + if str(return_code) != "0": + oDesktop.AddMessage("", "", 2, err_msg) + return + + msg = "PyAEDT setup complete." + if is_linux: + msg += " Please ensure Ansys Electronics Desktop is launched in gRPC mode (i.e. launch ansysedt with -grpcsrv" \ + " argument) to take advantage of the new toolkits." + from System.Windows.Forms import MessageBox, MessageBoxButtons, MessageBoxIcon + oDesktop.AddMessage("", "", 0, msg) + MessageBox.Show(msg, 'Info', MessageBoxButtons.OK, MessageBoxIcon.Information) + + +def parse_arguments_for_pyaedt_installer(args=None): + parser = argparse.ArgumentParser(description="Install PyAEDT") + if is_linux: + parser.add_argument("--edt_root", help="AEDT's path (required for Linux)", required=True) + parser.add_argument("--python_version", help="Python version (required for Linux)", required=True) + parser.add_argument("--version", "-v", help="AEDT's 3 digit version", required=True) + parser.add_argument("--student", "--student_version", "-sv", help="Is Student version", action="store_true") + args = parser.parse_args(args) + if len(sys.argv[1:]) == 0 and args is None: + parser.print_help() + parser.error("No arguments given!") + return args + + +def install_pyaedt(): + # This is called when run from CPython + args = parse_arguments_for_pyaedt_installer() + if is_windows: + venv_dir = os.path.join(os.environ["APPDATA"], "pyaedt_env_ide", "v" + args.version) + python_exe = os.path.join(venv_dir, "Scripts", "python.exe") + pip_exe = os.path.join(venv_dir, "Scripts", "pip.exe") + else: + venv_dir = os.path.join(os.environ["HOME"], "pyaedt_env_ide", "v" + args.version) + python_exe = os.path.join(venv_dir, "bin", "python") + pip_exe = os.path.join(venv_dir, "bin", "pip") + os.environ["ANSYSEM_ROOT{}".format(args.version)] = args.edt_root + ld_library_path_dirs_to_add = [ + "{}/commonfiles/CPython/{}/linx64/Release/python/lib".format(args.edt_root, + args.python_version.replace(".", "_")), + "{}/common/mono/Linux64/lib64".format(args.edt_root), + "{}/Delcross".format(args.edt_root), + "{}".format(args.edt_root), + ] + os.environ["LD_LIBRARY_PATH"] = ":".join(ld_library_path_dirs_to_add) + ":" + os.getenv("LD_LIBRARY_PATH", "") + + if not os.path.exists(venv_dir): + run_command('"{}" -m venv "{}"'.format(sys.executable, venv_dir)) + + run_command('"{}" -m pip install --upgrade pip'.format(python_exe)) + run_command('"{}" --default-timeout=1000 install wheel'.format(pip_exe)) + run_command('"{}" --default-timeout=1000 install pyaedt[full]'.format(pip_exe)) + # run_command('"{}" --default-timeout=1000 install git+https://github.com/pyansys/pyaedt.git@main'.format(pip_exe)) + run_command('"{}" --default-timeout=1000 install jupyterlab'.format(pip_exe)) + run_command('"{}" --default-timeout=1000 install ipython -U'.format(pip_exe)) + run_command('"{}" --default-timeout=1000 install ipyvtklink'.format(pip_exe)) + run_command('"{}" --default-timeout=1000 install pyside6==6.4.0'.format(pip_exe)) + run_command('"{}" --default-timeout=1000 install pyqtgraph'.format(pip_exe)) + run_command('"{}" --default-timeout=1000 install qdarkstyle'.format(pip_exe)) + else: + run_command('"{}" uninstall --yes pyaedt'.format(pip_exe)) + run_command('"{}" --default-timeout=1000 install pyaedt[full]'.format(pip_exe)) + + if is_windows: + pyaedt_setup_script = "{}/Lib/site-packages/pyaedt/misc/aedtlib_personalib_install.py".format(venv_dir) + else: + pyaedt_setup_script = "{}/lib/python{}/site-packages/pyaedt/misc/aedtlib_personalib_install.py".format( + venv_dir, args.python_version) + + if not os.path.isfile(pyaedt_setup_script): + sys.exit("[ERROR] PyAEDT was not setup properly since {} file does not exist.".format(pyaedt_setup_script)) + + command = '"{}" "{}" --version={}'.format(python_exe, pyaedt_setup_script, args.version) + if args.student: + command += " --student" + run_command(command) + sys.exit(0) + + +def is_student_version(oDesktop): + edt_root = os.path.normpath(oDesktop.GetExeDir()) + if is_windows and os.path.isdir(edt_root): + if any("ansysedtsv" in fn.lower() for fn in os.listdir(edt_root)): + return True + return False + + +def run_command(command): + if is_windows: + command = '"{}"'.format(command) + ret_code = os.system(command) + return ret_code + + +if __name__ == "__main__": + # When launched from Toolkit (i.e. IronPython) call this same file using CPython + if is_iron_python: + run_pyinstaller_from_c_python(oDesktop) + else: + install_pyaedt() diff --git a/doc/source/Resources/toolkits_ribbon.png b/doc/source/Resources/toolkits_ribbon.png new file mode 100644 index 0000000000000000000000000000000000000000..e6e6d500d54a20d8eef45241f521e087af064760 GIT binary patch literal 43794 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 literal 0 HcmV?d00001 diff --git a/doc/source/index.rst b/doc/source/index.rst index e2f977a6e4f..19cddc56516 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,10 +1,287 @@ PyAEDT documentation |version| =============================== -.. - Simply reuse the root readme +|pyansys| |PyPI| |PyPIact| |PythonVersion| |GH-CI| |coverage| |MIT| |black| |Anaconda| |pre-commit| + +.. |pyansys| image:: 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 + :target: https://docs.pyansys.com/ + :alt: PyAnsys + +.. |PyPI| image:: https://img.shields.io/pypi/v/pyaedt.svg?logo=python&logoColor=white + :target: https://pypi.org/project/pyaedt/ + +.. |PyPIact| image:: https://pepy.tech/badge/pyaedt/month + :target: https://pypi.org/project/pyaedt/ + +.. |PythonVersion| image:: https://img.shields.io/badge/python-3.7+-blue.svg + :target: https://www.python.org/downloads/ + +.. |GH-CI| image:: https://github.com/pyansys/pyaedt/actions/workflows/unit_tests.yml/badge.svg + :target: https://github.com/pyansys/pyaedt/actions/workflows/unit_tests.yml + +.. |coverage| image:: https://codecov.io/gh/pyansys/pyaedt/branch/main/graph/badge.svg + :target: https://codecov.io/gh/pyansys/pyaedt + +.. |MIT| image:: https://img.shields.io/badge/License-MIT-yellow.svg + :target: https://opensource.org/licenses/MIT + +.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=flat + :target: https://github.com/psf/black + :alt: black + +.. |Anaconda| image:: https://anaconda.org/conda-forge/pyaedt/badges/version.svg + :target: https://anaconda.org/conda-forge/pyaedt + +.. |pre-commit| image:: https://results.pre-commit.ci/badge/github/pyansys/pyaedt/main.svg + :target: https://results.pre-commit.ci/latest/github/pyansys/pyaedt/main + :alt: pre-commit.ci status + + +What is PyAEDT? +--------------- +PyAEDT is a Python library that interacts directly with the API for +Ansys Electronics Desktop (AEDT) to make scripting simpler. The architecture +for PyAEDT can be reused for all AEDT 3D products (HFSS, Icepak, Maxwell 3D, +and Q3D Extractor), 2D tools, and Ansys Mechanical. PyAEDT also provides +support for Circuit tools like Nexxim and system simulation tools like +Twin Builder. Finally, PyAEDT provides scripting capabilities in Ansys layout +tools like HFSS 3D Layout and EDB. The PyAEDT class and method structures +simplify operation while reusing information as much as possible across +the API. + +Install on CPython from PyPI +---------------------------- +You can install PyAEDT on CPython 3.7 through 3.10 from PyPI with this command: + +.. code:: python + + pip install pyaedt + +To install PyAEDT with all extra packages (matplotlib, numpy, pandas, and pyvista), +use this command: + +.. code:: python + + pip install pyaedt[full] + +You can also install PyAEDT from Conda-Forge with this command: + +.. code:: python + + conda install -c conda-forge pyaedt + +PyAEDT remains compatible with IronPython and can be still used in the AEDT Framework. + + +PyAnsys +------- + +PyAEDT is part of the larger `PyAnsys `_ +effort to facilitate the use of Ansys technologies directly from Python. + +PyAEDT is intended to consolidate and extend all existing +functionalities around scripting for AEDT to allow reuse of existing code, +sharing of best practices, and increased collaboration. + + +About AEDT +---------- + +`AEDT `_ is a platform that enables true +electronics system design. AEDT provides access to the Ansys gold-standard +electro-magnetics simulation solutions, such as Ansys HFSS, Ansys Maxwell, +Ansys Q3D Extractor, Ansys Siwave, and Ansys Icepak using electrical CAD (ECAD) and +Mechanical CAD (MCAD) workflows. + +In addition, AEDT includes direct links to the complete Ansys portfolio of thermal, fluid, +and mechanical solvers for comprehensive multiphysics analysis. +Tight integration among these solutions provides unprecedented ease of use for setup and +faster resolution of complex simulations for design and optimization. + +.. image:: https://images.ansys.com/is/image/ansys/ansys-electronics-technology-collage?wid=941&op_usm=0.9,1.0,20,0&fit=constrain,0 + :width: 800 + :alt: AEDT Applications + :target: https://www.ansys.com/products/electronics + + +PyAEDT is licensed under the `MIT License +`_. + +PyAEDT includes functionality for interacting with the following AEDT tools and Ansys products: + +- HFSS and HFSS 3D Layout +- Icepak +- Maxwell 2D, Maxwell 3D, and RMXprt +- 2D Extractor and Q3D Extractor +- Mechanical +- Nexxim +- EDB +- Twin Builder + + +Documentation and issues +------------------------ +Documentation for the latest stable release of PyAEDT is hosted at +`PyAEDT Documentation `_. + +In the upper right corner of the documentation's title bar, there is an option +for switching from viewing the documentation for the latest stable release +to viewing the documentation for the development version or previously +released versions. + +On the `PyAEDT Issues `_ page, you can +create issues to submit questions, report bugs, and request new features. + +To reach the project support team, email `pyansys.core@ansys.com `_. + +Dependencies +------------ +To run PyAEDT, you must have a local licensed copy of AEDT. +PyAEDT supports AEDT versions 2022 R1 and later. + +Student version +--------------- + +PyAEDT supports AEDT Student versions 2022 R1 and later. For more information, see +`Ansys Electronics Desktop Student - Free Software Download `_ on the Ansys website. + + +Why PyAEDT? +----------- +A quick and easy approach for automating a simple operation in the +AEDT UI is to record and reuse a script. However, here are some disadvantages of +this approach: + +- Recorded code is dirty and difficult to read and understand. +- Recorded scripts are difficult to reuse and adapt. +- Complex coding is required by many global users of AEDT. + +Here are the main advantages that PyAEDT provides: + +- Automatic initialization of all AEDT objects, such as desktop + objects like the editor, boundaries, and more +- Error management +- Log management +- Variable management +- Compatibility with IronPython and CPython +- Simplification of complex API syntax using data objects while + maintaining PEP8 compliance. +- Code reusability across different solvers +- Clear documentation on functions and API +- Unit tests of code to increase quality across different AEDT versions + + +Example workflow +----------------- +1. Initialize the ``Desktop`` class with the version of AEDT to use. +2. Initialize the application to use within AEDT. + + +Connect to AEDT from a Python IDE +--------------------------------- +PyAEDT works both inside AEDT and as a standalone app. +This Python library automatically detects whether it is running +in an IronPython or CPython environment and initializes AEDT accordingly. +PyAEDT also provides advanced error management. Usage examples follow. + +Explicit AEDT declaration and error management +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + # Launch AEDT 2023 R1 in non-graphical mode + + from pyaedt import Desktop, Circuit + with Desktop(specified_version="2023.1", + non_graphical=False, new_desktop_session=True, + close_on_exit=True, student_version=False): + circuit = Circuit() + ... + # Any error here will be caught by Desktop. + ... + + # Desktop is automatically released here. + + +Implicit AEDT declaration and error management +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + # Launch the latest installed version of AEDT in graphical mode + + from pyaedt import Circuit + with Circuit(specified_version="2023.1", + non_graphical=False) as circuit: + ... + # Any error here will be caught by Desktop. + ... + + # Desktop is automatically released here. + + +Remote application call +~~~~~~~~~~~~~~~~~~~~~~~ +You can make a remote application call on a CPython server +or any Windows client machine. + +On a CPython server: + +.. code:: python + + # Launch PyAEDT remote server on CPython + + from pyaedt.common_rpc import pyaedt_service_manager + pyaedt_service_manager() + + +On any Windows client machine: + +.. code:: python + + from pyaedt.common_rpc import create_session + cl1 = create_session("server_name") + cl1.aedt(port=50000, non_graphical=False) + hfss = Hfss(machine="server_name", port=50000) + # your code here + +Variables +~~~~~~~~~ + +.. code:: python + + from pyaedt.HFSS import HFSS + with HFSS as hfss: + hfss["dim"] = "1mm" # design variable + hfss["$dim"] = "1mm" # project variable + + +Modeler +~~~~~~~ + +.. code:: python + + # Create a box, assign variables, and assign materials. + + from pyaedt.hfss import Hfss + with Hfss as hfss: + hfss.modeler.create_box([0, 0, 0], [10, "dim", 10], + "mybox", "aluminum") + +License +------- +PyAEDT is licensed under the MIT license. + +This module makes no commercial claim over Ansys whatsoever. +PyAEDT extends the functionality of AEDT by adding +an additional Python interface to AEDT without changing the core +behavior or license of the original software. The use of the +interactive control of PyAEDT requires a legally licensed +local copy of AEDT. For more information about AEDT, +see the `Ansys Electronics `_ +page on the Ansys website. -.. include:: ../../README.rst .. toctree:: :hidden: @@ -14,7 +291,6 @@ PyAEDT documentation |version| API/index EDBAPI/index examples/index - Contributing Indices and tables diff --git a/doc/styles/Vocab/ANSYS/accept.txt b/doc/styles/Vocab/ANSYS/accept.txt index 8f803d0bf0e..86a9a8c5360 100644 --- a/doc/styles/Vocab/ANSYS/accept.txt +++ b/doc/styles/Vocab/ANSYS/accept.txt @@ -9,6 +9,7 @@ autosave busbar busbars Bz +circuit Circuit CPython DesignXploration @@ -71,3 +72,11 @@ vias _static pwl Conda +PyAnsys +codecov +mechanical +reusability +pypi +EDT +pyansys + From 7a6ee4c62367e3a749a5226386f8768628fd53e6 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Tue, 2 May 2023 11:21:22 +0200 Subject: [PATCH 05/23] Added help class to Pyaedt which open default browser (#2933) * Added help class to Pyaedt which open default browser * Update pyaedt/generic/general_methods.py Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> * fixed vale * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> --------- Co-authored-by: maxcapodi78 Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> --- pyaedt/__init__.py | 1 + pyaedt/generic/general_methods.py | 79 +++++++++++++++++++++++++++++++ pyproject.toml | 3 ++ 3 files changed, 83 insertions(+) diff --git a/pyaedt/__init__.py b/pyaedt/__init__.py index 1d2ce630d07..dc6add8bd8e 100644 --- a/pyaedt/__init__.py +++ b/pyaedt/__init__.py @@ -27,6 +27,7 @@ from pyaedt.generic.general_methods import is_ironpython from pyaedt.generic.general_methods import is_linux from pyaedt.generic.general_methods import is_windows +from pyaedt.generic.general_methods import online_help from pyaedt.generic.general_methods import pyaedt_function_handler from pyaedt.generic.general_methods import settings diff --git a/pyaedt/generic/general_methods.py b/pyaedt/generic/general_methods.py index a6781844a0a..dd70cae797c 100644 --- a/pyaedt/generic/general_methods.py +++ b/pyaedt/generic/general_methods.py @@ -1574,6 +1574,84 @@ def _check_installed_version(install_path, long_version): return False +class Help: # pragma: no cover + def __init__(self): + self._base_path = "https://aedt.docs.pyansys.com/version/stable" + self.browser = "default" + + def _launch_ur(self, url): + import webbrowser + + if self.browser != "default": + webbrowser.get(self.browser).open_new_tab(url) + else: + webbrowser.open_new_tab(url) + + def search(self, keywords, app_name=None, search_in_examples_only=False): + """Search for one or more keywords. + + Parameters + ---------- + keywords : str or list + app_name : str, optional + Name of a PyAEDT app. For example, ``"Hfss"``, ``"Circuit"``, ``"Icepak"``, or any other available app. + search_in_examples_only : bool, optional + Whether to search for the one or more keywords only in the PyAEDT examples. + The default is ``False``. + """ + if isinstance(keywords, str): + keywords = [keywords] + if search_in_examples_only: + keywords.append("This example") + if app_name: + keywords.append(app_name) + url = self._base_path + "/search.html?q={}".format("+".join(keywords)) + self._launch_ur(url) + + def getting_started(self): + """Open the PyAEDT User guide page.""" + url = self._base_path + "/User_guide/index.html" + self._launch_ur(url) + + def examples(self): + """Open the PyAEDT Examples page.""" + url = self._base_path + "/examples/index.html" + self._launch_ur(url) + + def github(self): + """Open the PyAEDT GitHub page.""" + url = "https://github.com/pyansys/pyaedt" + self._launch_ur(url) + + def changelog(self, release=None): + """Open the PyAEDT GitHub Changelog for a given release. + + Parameters + ---------- + release : str, optional + Release to get the changelog for. For example, ``"0.6.70"``. + """ + if release is None: + from pyaedt import __version__ as release + url = "https://github.com/pyansys/pyaedt/releases/tag/v" + release + self._launch_ur(url) + + def issues(self): + """Open the PyAEDT GitHub Issues page.""" + url = "https://github.com/pyansys/pyaedt/issues" + self._launch_ur(url) + + def ansys_forum(self): + """Open the PyAEDT GitHub Issues page.""" + url = "https://discuss.ansys.com/discussions/tagged/pyaedt" + self._launch_ur(url) + + def developer_forum(self): + """Open the Discussions page on the Ansys Developer site.""" + url = "https://developer.ansys.com/" + self._launch_ur(url) + + class Settings(object): """Manages all PyAEDT environment variables and global settings.""" @@ -2021,3 +2099,4 @@ def enable_debug_logger(self, val): settings = Settings() +online_help = Help() diff --git a/pyproject.toml b/pyproject.toml index 045ca1900ac..c8c2b34e992 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ [project.optional-dependencies] tests = [ "ipython==8.13.0", + "imageio==2.28.0", "joblib==1.2.0", "matplotlib==3.5.3; python_version <= '3.7'", "matplotlib==3.7.1; python_version > '3.7'", @@ -86,6 +87,7 @@ doc = [ "openpyxl==3.1.2", ] full = [ + "imageio==2.28.0", "matplotlib==3.5.3; python_version <= '3.7'", "matplotlib==3.7.1; python_version > '3.7'", "numpy==1.21.6; python_version <= '3.7'", @@ -100,6 +102,7 @@ full = [ "openpyxl==3.1.2", ] all = [ + "imageio==2.28.0", "matplotlib==3.5.3; python_version <= '3.7'", "matplotlib==3.7.1; python_version > '3.7'", "numpy==1.21.6; python_version <= '3.7'", From a03343dc1e2df29ba6500b0567d70b05e01e3cba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 May 2023 12:01:10 +0200 Subject: [PATCH 06/23] MAINT: Bump sphinx from 6.2.1 to 7.0.0 (#2940) Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 6.2.1 to 7.0.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v6.2.1...v7.0.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Co-authored-by: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c8c2b34e992..be42dea3329 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ doc = [ "pyvista==0.38.5", "recommonmark==0.7.1", "scikit-learn==1.2.2", - "Sphinx==6.2.1", + "Sphinx==7.0.0", "sphinx-autobuild==2021.3.14", "sphinx-autodoc-typehints==1.23.0", "sphinx-copybutton==0.5.2", From b0d172fb3f2b33c41d6a6e742462cab8548b935e Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Tue, 2 May 2023 12:24:15 +0200 Subject: [PATCH 07/23] Added support of Arch Heights to create_polygon and create_polygon_from_points methods. (#2943) * Added support of Arch Heights to create_polygon and create_polygon_from_points methods. * Update pyaedt/edb_core/layout.py Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> * Update pyaedt/edb_core/layout.py Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: maxcapodi78 Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- _unittest/test_00_EDB.py | 3 +- examples/00-EDB/00_EDB_Create_VIA.py | 4 +- pyaedt/edb_core/layout.py | 74 ++++++++++++++++++---------- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/_unittest/test_00_EDB.py b/_unittest/test_00_EDB.py index a8ed30988c4..ba4228b85b9 100644 --- a/_unittest/test_00_EDB.py +++ b/_unittest/test_00_EDB.py @@ -765,7 +765,8 @@ def test_068_create_polygon(self): [-0.001, -0.001], [0.001, -0.001, "ccw", 0.0, -0.0012], [0.001, 0.001], - [-0.001, 0.001], + [0.0015, 0.0015, 0.0001], + [-0.001, 0.0015], [-0.001, -0.001], ] void1 = self.edbapp.modeler.Shape("polygon", points=points) diff --git a/examples/00-EDB/00_EDB_Create_VIA.py b/examples/00-EDB/00_EDB_Create_VIA.py index 59926c21d8f..7377e911ff9 100644 --- a/examples/00-EDB/00_EDB_Create_VIA.py +++ b/examples/00-EDB/00_EDB_Create_VIA.py @@ -39,10 +39,10 @@ ] edb.modeler.create_trace(points, "TOP", width=1e-3) points = [[0.0, 1e-3], [0.0, 10e-3], [100e-3, 10e-3], [100e-3, 1e-3], [0.0, 1e-3]] -edb.modeler.create_polygon_from_points(points, "TOP") +edb.modeler.create_polygon(points, "TOP") points = [[0.0, -1e-3], [0.0, -10e-3], [100e-3, -10e-3], [100e-3, -1e-3], [0.0, -1e-3]] -edb.modeler.create_polygon_from_points(points, "TOP") +edb.modeler.create_polygon(points, "TOP") ####################################### # Create vias with parametric positions diff --git a/pyaedt/edb_core/layout.py b/pyaedt/edb_core/layout.py index 8d278604995..1fb2d930ee4 100644 --- a/pyaedt/edb_core/layout.py +++ b/pyaedt/edb_core/layout.py @@ -2,6 +2,7 @@ This module contains these classes: `EdbLayout` and `Shape`. """ import math +import warnings from pyaedt.edb_core.edb_data.primitives_data import EDBPrimitives from pyaedt.edb_core.edb_data.utilities import EDBStatistics @@ -508,7 +509,11 @@ def create_polygon(self, main_shape, layer_name, voids=[], net_name=""): Parameters ---------- main_shape : list of points or PolygonData or ``modeler.Shape`` - Shape or point lists of the main object. + Shape or point lists of the main object. Point list can be in the format of `[[x1,y1], [x2,y2],..,[xn,yn]]`. + Each point can be: + - [x, y] coordinate + - [x, y, height] for an arc with specific height (between previous point and actual point) + - [x, y, rotation, xc, yc] for an arc given a point, rotation and center. layer_name : str Name of the layer on which to create the polygon. voids : list, optional @@ -555,10 +560,17 @@ def create_polygon(self, main_shape, layer_name, voids=[], net_name=""): def create_polygon_from_points(self, point_list, layer_name, net_name=""): """Create a new polygon from a point list. + .. deprecated:: 0.6.73 + Use :func:`create_polygon` method instead. It now supports point lists as arguments. + Parameters ---------- point_list : list Point list in the format of `[[x1,y1], [x2,y2],..,[xn,yn]]`. + Each point can be: + - [x,y] coordinate + - [x,y, height] for an arc with specific height (between previous point and actual point) + - [x,y, rotation, xc,yc] for an arc given a point, rotation and center. layer_name : str Name of layer on which create the polygon. net_name : str, optional @@ -568,18 +580,10 @@ def create_polygon_from_points(self, point_list, layer_name, net_name=""): ------- :class:`pyaedt.edb_core.edb_data.primitives_data.EDBPrimitives` """ - net = self._pedb.nets.find_or_create_net(net_name) - plane = self.Shape("polygon", points=point_list) - _poly = self.shape_to_polygon_data(plane) - if _poly is None or _poly.IsNull() or _poly is False: - self._logger.error("Failed to create main shape polygon data") - return False - polygon = self._edb.Cell.Primitive.Polygon.Create(self._active_layout, layer_name, net, _poly) - if polygon.IsNull(): - self._logger.error("Null polygon created") - return False - else: - return EDBPrimitives(polygon, self._pedb) + warnings.warn( + "Use :func:`create_polygon` method instead. It now supports point lists as arguments.", DeprecationWarning + ) + return self.create_polygon(point_list, layer_name, net_name=net_name) @pyaedt_function_handler() def create_rectangle( @@ -870,6 +874,21 @@ def _createPolygonDataFromPolygon(self, shape): self._pedb.point_data(endPoint[0], endPoint[1]), ) arcs.append(arc) + elif len(endPoint) == 3: + is_parametric = ( + is_parametric + or startPoint[0].IsParametric() + or startPoint[1].IsParametric() + or endPoint[0].IsParametric() + or endPoint[1].IsParametric() + or endPoint[2].IsParametric() + ) + arc = self._edb.Geometry.ArcData( + self._pedb.point_data(startPoint[0], startPoint[1]), + self._pedb.point_data(endPoint[0], endPoint[1]), + endPoint[2].ToDouble(), + ) + arcs.append(arc) elif len(endPoint) == 5: is_parametric = ( is_parametric @@ -889,19 +908,10 @@ def _createPolygonDataFromPolygon(self, shape): self._logger.error("Invalid rotation direction %s is specified.", endPoint[2]) return None arc = self._edb.Geometry.ArcData( - self._edb.Geometry.PointData( - self._get_edb_value(startPoint[0].ToDouble()), - self._get_edb_value(startPoint[1].ToDouble()), - ), - self._edb.Geometry.PointData( - self._get_edb_value(endPoint[0].ToDouble()), - self._get_edb_value(endPoint[1].ToDouble()), - ), + self._pedb.point_data(startPoint[0], startPoint[1]), + self._pedb.point_data(endPoint[0], endPoint[1]), rotationDirection, - self._edb.Geometry.PointData( - self._get_edb_value(endPoint[3].ToDouble()), - self._get_edb_value(endPoint[4].ToDouble()), - ), + self._pedb.point_data(endPoint[3], endPoint[4]), ) arcs.append(arc) polygon = self._edb.Geometry.PolygonData.CreateFromArcs(convert_py_list_to_net_list(arcs), True) @@ -928,6 +938,20 @@ def _validatePoint(self, point, allowArcs=True): self._logger.error("Point Y value must be a number.") return False return True + elif len(point) == 3: + if not allowArcs: + self._logger.error("Arc found but arcs are not allowed in _validatePoint.") + return False + if not isinstance(point[0], (int, float, str)): + self._logger.error("Point X value must be a number.") + return False + if not isinstance(point[1], (int, float, str)): + self._logger.error("Point Y value must be a number.") + return False + if not isinstance(point[1], (int, float, str)): + self._logger.error("Invalid point height.") + return False + return True elif len(point) == 5: if not allowArcs: self._logger.error("Arc found but arcs are not allowed in _validatePoint.") From 0283421af39689b082fced404cde61830d13bcee Mon Sep 17 00:00:00 2001 From: jsalant22 <101826634+jsalant22@users.noreply.github.com> Date: Tue, 2 May 2023 06:55:32 -0400 Subject: [PATCH 08/23] Update ComputeProtectionLevels.py.back (#2937) * Update ComputeProtectionLevels.py.back modify example to ensure correct channel pair is used for computing powerAtRx * Update examples/07-EMIT/ComputeProtectionLevels.py.back Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> --------- Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> --- .../07-EMIT/ComputeProtectionLevels.py.back | 102 ++++++++---------- 1 file changed, 46 insertions(+), 56 deletions(-) diff --git a/examples/07-EMIT/ComputeProtectionLevels.py.back b/examples/07-EMIT/ComputeProtectionLevels.py.back index c53ce1adfd8..13eed5e7c37 100644 --- a/examples/07-EMIT/ComputeProtectionLevels.py.back +++ b/examples/07-EMIT/ComputeProtectionLevels.py.back @@ -138,7 +138,8 @@ for band in bands: rev = emitapp.results.analyze() modeRx = econsts.tx_rx_mode().rx modeTx = econsts.tx_rx_mode().tx -resultMode = econsts.result_type().powerAtRx +mode_power = econsts.result_type().powerAtRx +mode_emi = econsts.result_type().emi tx_interferer = econsts.interferer_type().transmitters ############################################################################### @@ -239,74 +240,63 @@ domain = emitapp.results.interaction_domain() # at the input to each receiver due to each of the transmitters. Computes # which, if any, protection levels are exceeded by these power levels. -emi_matrix=[] +power_matrix=[] all_colors=[] for tx_radio in tx_radios: - rx_emis = [] + rx_powers = [] rx_colors = [] for rx_radio in rx_radios: + # powerAtRx is the same for all Rx bands, so just + # use the first one + rx_band = rev.get_band_names(rx_radio, modeRx)[0] if tx_radio == rx_radio: # skip self-interaction - rx_emis.append('N/A') + rx_powers.append('N/A') rx_colors.append('green') continue - print("Power Thresholds for {tx} vs {rx}".format(tx=tx_radio,rx=rx_radio)) - for rx_band in rev.get_band_names(rx_radio, modeRx): - # if "L2 P(Y)" not in rx_band: - # # skip 'normal' Rx bands - # continue - # check for enabled Bands - cur_rx_radio = get_radio_node(rx_radio) - bands = cur_rx_radio.bands() - for band in bands: - if rx_band in band.node_name: - bandEnabled = band.enabled - break - if not bandEnabled: - continue - # get enabled tx band - cur_tx_radio = get_radio_node(tx_radio) - bands = cur_tx_radio.bands() - for band in bands: - if band.enabled: - tx_band = band.node_name - break - for tx_band_shortname in rev.get_band_names(tx_radio, modeTx): - if tx_band_shortname in tx_band: - break - - # Find the highest power level at the Rx input due - # to each Tx Radio - domain.set_receiver(rx_radio, rx_band) - domain.set_interferer(tx_radio,tx_band_shortname) + print("Power Thresholds for {tx} vs {rx}".format(tx=tx_radio,rx=rx_radio)) + max_power = -200 + for tx_band in rev.get_band_names(tx_radio, modeTx): + # Find the highest power level at the Rx input due to each Tx Radio. + # Can look at any Rx freq since susceptibility won't impact + # powerAtRx, but need to look at all tx channels since coupling + # can change over a transmitter's bandwidth + rx_freq = rev.get_active_frequencies(rx_radio, rx_band, modeRx) + domain.set_receiver(rx_radio, rx_band, rx_freq[0]) + domain.set_interferer(tx_radio, tx_band) interaction = rev.run(domain) - worst = interaction.get_worst_instance(resultMode) - - # If the worst case for the band-pair is below the EMI limit, then - # there are no interference issues and no offset is required. - if worst.has_valid_values(): - emi = worst.get_value(resultMode) - rx_emis.append(emi) - if (emi > damage_threshold): - rx_colors.append('red') - print("{} may damage {}".format(tx_radio, rx_radio)) - elif (emi > overload_threshold): - rx_colors.append('orange') - print("{} may overload {}".format(tx_radio, rx_radio)) - elif (emi > intermod_threshold): - rx_colors.append('yellow') - print("{} may cause intermodulation in {}".format(tx_radio, rx_radio)) - else: - rx_colors.append('green') - print("{} may cause desensitization in {}".format(tx_radio, rx_radio)) - else: - rx_emis.append(-200) + tx_freqs = rev.get_active_frequencies(tx_radio, tx_band, modeTx) + for tx_freq in tx_freqs: + domain.set_interferer(tx_radio, tx_band, tx_freq) + #interaction = rev.run(domain) + instance = interaction.get_instance(domain) + if instance.get_value(modePower) > max_power: + max_power = instance.get_value(modePower) + + # If the worst case for the band-pair is below the power thresholds, then + # there are no interference issues and no offset is required. + if max_power > -200: + rx_powers.append(max_power) + if (max_power > damage_threshold): rx_colors.append('red') + print("{} may damage {}".format(tx_radio, rx_radio)) + elif (max_power > overload_threshold): + rx_colors.append('orange') + print("{} may overload {}".format(tx_radio, rx_radio)) + elif (max_power > intermod_threshold): + rx_colors.append('yellow') + print("{} may cause intermodulation in {}".format(tx_radio, rx_radio)) + else: + rx_colors.append('green') + print("{} may cause desensitization in {}".format(tx_radio, rx_radio)) + else: + rx_powers.append(-200) + rx_colors.append('red') all_colors.append(rx_colors) - emi_matrix.append(rx_emis) + power_matrix.append(rx_powers) # Create a scenario matrix-like view for the protection levels -create_scenario_view(emi_matrix, all_colors, tx_radios, rx_radios) +create_scenario_view(power_matrix, all_colors, tx_radios, rx_radios) # Create a legend for the protection levels create_legend_table() From d5fc73525ca18cbe0ad56c1222de28f61d47654a Mon Sep 17 00:00:00 2001 From: svandenb-dev <74993647+svandenb-dev@users.noreply.github.com> Date: Tue, 2 May 2023 14:58:40 +0200 Subject: [PATCH 09/23] Bug fix create solder ball on polygon pad fixed (#2945) * create solder balls on polynomial pads * create solder balls on polynomial pads * Update pyaedt/edb_core/padstack.py * Update pyaedt/edb_core/padstack.py * Update pyaedt/edb_core/padstack.py * temp ipc broken * unit test fixed --------- Co-authored-by: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> --- pyaedt/edb_core/components.py | 9 ++++- pyaedt/edb_core/ipc2581/ecad/cad_data/step.py | 38 ++++++++++--------- pyaedt/edb_core/padstack.py | 36 +++++++++++++++--- 3 files changed, 57 insertions(+), 26 deletions(-) diff --git a/pyaedt/edb_core/components.py b/pyaedt/edb_core/components.py index e988feb3088..3b88933c475 100644 --- a/pyaedt/edb_core/components.py +++ b/pyaedt/edb_core/components.py @@ -800,8 +800,13 @@ def create_port_on_component( pin_layers = cmp_pins[0].GetPadstackDef().GetData().GetLayerNames() if port_type == SourceType.CoaxPort: pad_params = self._padstack.get_pad_parameters(pin=cmp_pins[0], layername=pin_layers[0], pad_type=0) - sball_diam = min([self._pedb.edb_value(val).ToDouble() for val in pad_params[1]]) - solder_ball_height = sball_diam + if not pad_params[0] == 7: + sball_diam = min([self._pedb.edb_value(val).ToDouble() for val in pad_params[1]]) + solder_ball_height = sball_diam / 2 + else: + bbox = pad_params[1] + sball_diam = min([abs(bbox[2] - bbox[0]), abs(bbox[3] - bbox[1])]) * 0.8 + solder_ball_height = sball_diam / 2 self.set_solder_ball(component, solder_ball_height, sball_diam) for pin in cmp_pins: self._padstack.create_coax_port(pin) diff --git a/pyaedt/edb_core/ipc2581/ecad/cad_data/step.py b/pyaedt/edb_core/ipc2581/ecad/cad_data/step.py index 4b08d2b1475..6029f004707 100644 --- a/pyaedt/edb_core/ipc2581/ecad/cad_data/step.py +++ b/pyaedt/edb_core/ipc2581/ecad/cad_data/step.py @@ -128,24 +128,26 @@ def add_component(self, component=None): # pragma no cover geometry_type, pad_parameters, pos_x, pos_y, rot = self._pedb.padstacks.get_pad_parameters( pin._edb_padstackinstance, component.placement_layer, 0 ) - position = pin._position if pin._position else pin.position - - pin_pos_x = self._ipc.from_meter_to_units(position[0], self.units) - pin_pos_y = self._ipc.from_meter_to_units(position[1], self.units) - primitive_ref = "" - if geometry_type == 1: - primitive_ref = "CIRC_{}".format(pad_parameters[0]) - elif geometry_type == 2: - primitive_ref = "RECT_{}_{}".format(pad_parameters[0], pad_parameters[0]) - elif geometry_type == 3: - primitive_ref = "RECT_{}_{}".format(pad_parameters[0], pad_parameters[1]) - elif geometry_type == 4: - primitive_ref = "OVAL_{}_{}_{}".format(pad_parameters[0], pad_parameters[1], pad_parameters[2]) - if primitive_ref: - package.add_pin( - number=pin_number, x=pin_pos_x, y=pin_pos_y, rotation=rot, primitive_ref=primitive_ref - ) - pin_number += 1 + if pad_parameters: + position = pin._position if pin._position else pin.position + pin_pos_x = self._ipc.from_meter_to_units(position[0], self.units) + pin_pos_y = self._ipc.from_meter_to_units(position[1], self.units) + primitive_ref = "" + if geometry_type == 1: + primitive_ref = "CIRC_{}".format(pad_parameters[0]) + elif geometry_type == 2: + primitive_ref = "RECT_{}_{}".format(pad_parameters[0], pad_parameters[0]) + elif geometry_type == 3: + primitive_ref = "RECT_{}_{}".format(pad_parameters[0], pad_parameters[1]) + elif geometry_type == 4: + primitive_ref = "OVAL_{}_{}_{}".format( + pad_parameters[0], pad_parameters[1], pad_parameters[2] + ) + if primitive_ref: + package.add_pin( + number=pin_number, x=pin_pos_x, y=pin_pos_y, rotation=rot, primitive_ref=primitive_ref + ) + pin_number += 1 self.packages[package.name] = package ipc_component = Component() ipc_component.type = component.type diff --git a/pyaedt/edb_core/padstack.py b/pyaedt/edb_core/padstack.py index c98c34c9635..65dc9b7f88b 100644 --- a/pyaedt/edb_core/padstack.py +++ b/pyaedt/edb_core/padstack.py @@ -571,12 +571,36 @@ def get_pad_parameters(self, pin, layername, pad_type=0): padparams = self._edb.Definition.PadstackDefData(pin.GetPadstackDef().GetData()).GetPadParametersValue( layername, self.int_to_pad_type(pad_type) ) - geom_type = int(padparams[1]) - parameters = [i.ToString() for i in padparams[2]] - offset_x = padparams[3].ToDouble() - offset_y = padparams[4].ToDouble() - rot = padparams[5].ToDouble() - return geom_type, parameters, offset_x, offset_y, rot + if padparams[2]: + geometry_type = int(padparams[1]) + parameters = [i.ToString() for i in padparams[2]] + offset_x = padparams[3].ToDouble() + offset_y = padparams[4].ToDouble() + rotation = padparams[5].ToDouble() + return geometry_type, parameters, offset_x, offset_y, rotation + else: + if isinstance(pin, self._edb.Definition.PadstackDef): + padparams = self._edb.Definition.PadstackDefData(pin.GetData()).GetPolygonalPadParameters( + layername, self.int_to_pad_type(pad_type) + ) + else: + padparams = self._edb.Definition.PadstackDefData( + pin.GetPadstackDef().GetData() + ).GetPolygonalPadParameters(layername, self.int_to_pad_type(pad_type)) + + if padparams[0]: + parameters = [ + padparams[1].GetBBox().Item1.X.ToDouble(), + padparams[1].GetBBox().Item1.Y.ToDouble(), + padparams[1].GetBBox().Item2.X.ToDouble(), + padparams[1].GetBBox().Item2.Y.ToDouble(), + ] + offset_x = padparams[2] + offset_y = padparams[3] + rotation = padparams[4] + geometry_type = 7 + return geometry_type, parameters, offset_x, offset_y, rotation + return 0, [0], 0, 0, 0 @pyaedt_function_handler def set_all_antipad_value(self, value): From 4671c741bb5e729a24a5dcf4d964216281e6418c Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Wed, 3 May 2023 09:21:33 +0200 Subject: [PATCH 10/23] added try except to shutil copy file for analyze method. In linux sometimes it gives SameFileError. (#2951) Co-authored-by: maxcapodi78 --- pyaedt/application/Analysis.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyaedt/application/Analysis.py b/pyaedt/application/Analysis.py index 35b21843a86..86d36bad904 100644 --- a/pyaedt/application/Analysis.py +++ b/pyaedt/application/Analysis.py @@ -1658,7 +1658,18 @@ def analyze_setup( if self.working_directory[0] != "\\" else os.path.join(self.working_directory, config_name + ".acf") ) - shutil.copy2(source_name, target_name) + try: + shutil.copy2(source_name, target_name) + + # If source and destination are same + except shutil.SameFileError: + self.logger.warning("Source and destination represents the same file.") + # If there is any permission issue + except PermissionError: + self.logger.error("Permission denied.") + # For other errors + except: + self.logger.error("Error occurred while copying file.") if num_cores: update_hpc_option(target_name, "NumCores", num_cores, False) From 5dac32772cc4e8f1313346964c12545fa0d8c3a7 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Wed, 3 May 2023 15:23:05 +0200 Subject: [PATCH 11/23] Add a new method install_with_pip in generic.general_methods (#2948) * Fixed imageio issue * removed print * Update pyaedt/generic/general_methods.py * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/generic/general_methods.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> --------- Co-authored-by: maxcapodi78 Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Co-authored-by: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> --- pyaedt/generic/general_methods.py | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pyaedt/generic/general_methods.py b/pyaedt/generic/general_methods.py index dd70cae797c..f5e75eda04b 100644 --- a/pyaedt/generic/general_methods.py +++ b/pyaedt/generic/general_methods.py @@ -1574,6 +1574,48 @@ def _check_installed_version(install_path, long_version): return False +def install_with_pip(package_name, package_path=None, upgrade=False, uninstall=False): # pragma: no cover + """Install a new package using pip. + This method is useful for installing a package from the AEDT Console without launching the Python environment. + + Parameters + ---------- + package_name : str + Name of the package to install. + package_path : str, optional + Path for the GitHub package to download and install. For example, ``git+https://.....``. + upgrade : bool, optional + Whether to upgrade the package. The default is ``False``. + uninstall : bool, optional + Whether to install the package or uninstall the package. + """ + if is_linux and is_ironpython: + import subprocessdotnet as subprocess + else: + import subprocess + executable = '"{}"'.format(sys.executable) if is_windows else sys.executable + + commands = [] + if uninstall: + commands.append([executable, "-m", "pip", "uninstall", "--yes", package_name]) + else: + if package_path and upgrade: + commands.append([executable, "-m", "pip", "uninstall", "--yes", package_name]) + command = [executable, "-m", "pip", "install", package_path] + else: + command = [executable, "-m", "pip", "install", package_name] + if upgrade: + command.append("-U") + + commands.append(command) + for command in commands: + if is_linux: + p = subprocess.Popen(command) + else: + p = subprocess.Popen(" ".join(command)) + p.wait() + + class Help: # pragma: no cover def __init__(self): self._base_path = "https://aedt.docs.pyansys.com/version/stable" From ad2e06f7bf4ff32572e514eb5c76382613086883 Mon Sep 17 00:00:00 2001 From: myoung301 <86373761+myoung301@users.noreply.github.com> Date: Wed, 3 May 2023 09:14:29 -0500 Subject: [PATCH 12/23] Require N-1 to match domain size when interferer(s) are defined (#2950) * Require N-1 to match domain size when interferer(s) are defined * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update pyaedt/emit_core/results/revision.py Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> --- pyaedt/emit_core/results/revision.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyaedt/emit_core/results/revision.py b/pyaedt/emit_core/results/revision.py index 98bff647542..5e512559302 100644 --- a/pyaedt/emit_core/results/revision.py +++ b/pyaedt/emit_core/results/revision.py @@ -126,6 +126,8 @@ def run(self, domain): """ self._load_revision() engine = self.emit_project._emit_api.get_engine() + if domain.interferer_names and engine.max_simultaneous_interferers != len(domain.interferer_names): + raise ValueError("The max_simultaneous_interferers must equal the number of interferers in the domain.") interaction = engine.run(domain) # save the revision self.emit_project._emit_api.save_project() From ed11e195252c78de6058f0fff9e2aa8380a2b1e9 Mon Sep 17 00:00:00 2001 From: myoung301 <86373761+myoung301@users.noreply.github.com> Date: Wed, 3 May 2023 10:48:34 -0500 Subject: [PATCH 13/23] Use GetResultDirectory in Revision (#2949) * Use GetResultDirectory in Revision Also: - Use Revision constructor for _add_revision in Results - Fix errors in ComputeChannelSeparation.py and ComputeProtectionLevels.py - Add some logging output to EMIT API import * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Remove import os from revision.py * Update revision.py * Update pyaedt/emit_core/results/results.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update pyaedt/emit_core/results/revision.py Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> * Update results.py * Update revision.py * Update results.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Co-authored-by: Kathy Pippert <84872299+PipKat@users.noreply.github.com> --- .../07-EMIT/ComputeChannelSeparation.py.back | 6 +- .../07-EMIT/ComputeProtectionLevels.py.back | 4 +- pyaedt/emit_core/__init__.py | 8 ++- pyaedt/emit_core/results/results.py | 20 +++---- pyaedt/emit_core/results/revision.py | 57 ++++++++----------- 5 files changed, 44 insertions(+), 51 deletions(-) diff --git a/examples/07-EMIT/ComputeChannelSeparation.py.back b/examples/07-EMIT/ComputeChannelSeparation.py.back index d30bfba8268..de6815f3a80 100644 --- a/examples/07-EMIT/ComputeChannelSeparation.py.back +++ b/examples/07-EMIT/ComputeChannelSeparation.py.back @@ -53,7 +53,7 @@ import numpy as np non_graphical = os.getenv("PYAEDT_NON_GRAPHICAL", "False").lower() in ("true", "1", "t") NewThread = False -desktop_version = "2022.2" +desktop_version = "2023.2" ############################################################################### # Launch AEDT with EMIT @@ -218,11 +218,11 @@ def minimum_tx_channel_separation(rx_band, tx_band, emi_threshold): # keeping the EMI below the threshold. # Freqs are used to set the domain, so they need to be in Hz rx_frequencies = rev.get_active_frequencies( - rx_band[0], rx_band[1], modeRx, "Hz" + rx_band[0], rx_band[1], modeRx ) rx_channel_count = len(rx_frequencies) tx_frequencies = rev.get_active_frequencies( - tx_band[0], tx_band[1], modeTx, "Hz" + tx_band[0], tx_band[1], modeTx ) tx_channel_count = len(tx_frequencies) chpair = domain diff --git a/examples/07-EMIT/ComputeProtectionLevels.py.back b/examples/07-EMIT/ComputeProtectionLevels.py.back index 13eed5e7c37..f28db1a6819 100644 --- a/examples/07-EMIT/ComputeProtectionLevels.py.back +++ b/examples/07-EMIT/ComputeProtectionLevels.py.back @@ -270,8 +270,8 @@ for tx_radio in tx_radios: domain.set_interferer(tx_radio, tx_band, tx_freq) #interaction = rev.run(domain) instance = interaction.get_instance(domain) - if instance.get_value(modePower) > max_power: - max_power = instance.get_value(modePower) + if instance.get_value(mode_power) > max_power: + max_power = instance.get_value(mode_power) # If the worst case for the band-pair is below the power thresholds, then # there are no interference issues and no offset is required. diff --git a/pyaedt/emit_core/__init__.py b/pyaedt/emit_core/__init__.py index 7f7d53f8448..7cd035f69c4 100644 --- a/pyaedt/emit_core/__init__.py +++ b/pyaedt/emit_core/__init__.py @@ -1,7 +1,10 @@ +import imp from importlib import import_module import os import sys +from pyaedt import pyaedt_logger as logger + EMIT_API_PYTHON = None delcross_python_path = os.environ.get("ANSYS_DELCROSS_PYTHON_PATH") @@ -24,6 +27,9 @@ def _set_api(aedt_version): desktop_path = os.environ.get(aedt_version) if desktop_path and numeric_version > 231: path = os.path.join(desktop_path, "Delcross") - sys.path.append(path) + sys.path.insert(0, path) + module_path = imp.find_module("EmitApiPython")[1] + logger.info("Importing EmitApiPython from: {}".format(module_path)) global EMIT_API_PYTHON EMIT_API_PYTHON = import_module("EmitApiPython") + logger.info("Loaded {}".format(EMIT_API_PYTHON.EmitApi().get_version(True))) diff --git a/pyaedt/emit_core/results/results.py b/pyaedt/emit_core/results/results.py index 2fb7ec29bac..84cfff83ae7 100644 --- a/pyaedt/emit_core/results/results.py +++ b/pyaedt/emit_core/results/results.py @@ -38,27 +38,23 @@ def __init__(self, emit_obj): @pyaedt_function_handler() def _add_revision(self, name=None): - """Add a new revision. + """Add a new revision or get the current revision if it already exists. Parameters ---------- name : str, optional - Name for the new revision. If None, it will - be named the current design revision. + Name for the new revision, if created. The default is ``None``, in which + case the name of the current design revision is used. + + Raises + ------ + RuntimeError if the name given is not the name of an existing result set and a current result set already + exists. Returns ------- ``Revision`` object that was created. """ - if name == None: - # check for a Current Revision that just hasn't been - # loaded by pyaedt - if self.design.GetCurrentResult() == "": - self.design.AddResult() - rev_num = self.design.GetRevision() - name = "Revision {}".format(rev_num) - else: - name = self.design.GetCurrentResult() revision = Revision(self, self.emit_project, name) self.revisions.append(revision) return revision diff --git a/pyaedt/emit_core/results/revision.py b/pyaedt/emit_core/results/revision.py index 5e512559302..5c3d1c5dd54 100644 --- a/pyaedt/emit_core/results/revision.py +++ b/pyaedt/emit_core/results/revision.py @@ -1,4 +1,3 @@ -import os import warnings import pyaedt.emit_core.EmitConstants as emitConsts @@ -16,8 +15,12 @@ class Revision: emit_obj : ``Emit`` object that this revision is associated with. name : str, optional - Name of the revision to create. The default is ``None``, in which case a - default name is given. + Name of the revision to create. The default is ``None``, in which + case the name of the current design revision is used. + + Raises + ------ + RuntimeError if the name given is not the name of an existing result set and a current result set already exists. Examples -------- @@ -29,28 +32,16 @@ class Revision: >>> rev.run(domain) """ - def __init__(self, parent_results, emit_obj, name=""): - design = emit_obj.odesktop.GetActiveProject().GetActiveDesign() - subfolder = "" - proj_name = emit_obj.oproject.GetName() - for f in os.scandir(emit_obj.oproject.GetPath()): - if os.path.splitext(f.name)[0] == proj_name and os.path.splitext(f.name)[1].lower() == ".aedtresults": - subfolder = os.path.join(f.path, "EmitDesign1") - default_behaviour = not os.path.exists(os.path.join(subfolder, "{}.emit".format(name))) - if default_behaviour: - print("The most recently generated revision will be used because the revision specified does not exist.") - if name == "" or default_behaviour: - # if there are no results yet, add a new Result - result_files = os.listdir(subfolder) - if len(result_files) == 0: - name = design.AddResult() - full = subfolder + "/{}.emit".format(name) - else: - file = max([f for f in os.scandir(subfolder)], key=lambda x: x.stat().st_mtime) - full = file.path - name = file.name + def __init__(self, parent_results, emit_obj, name=None): + if not name: + name = emit_obj.odesign.GetCurrentResult() + if not name: + name = emit_obj.odesign.AddResult("") else: - full = subfolder + "/{}.emit".format(name) + if name not in emit_obj.odesign.GetResultList(): + name = emit_obj.odesign.AddResult(name) + full = emit_obj.odesign.GetResultDirectory(name) + self.name = name """Name of the revision.""" @@ -60,14 +51,15 @@ def __init__(self, parent_results, emit_obj, name=""): self.emit_project = emit_obj """Emit project.""" - result_props = design.GetResultProperties(name) - # Strip off the Revision # - self.revision_number = result_props[0][9:] + raw_props = emit_obj.odesign.GetResultProperties(name) + key = lambda s: s.split("=", 1)[0] + val = lambda s: s.split("=", 1)[1] + props = {key(s): val(s) for s in raw_props} + + self.revision_number = int(props["Revision"]) """Unique revision number from the Emit design""" - result_props = design.GetResultProperties(name) - # Strip off the 'Timestamp=' - self.timestamp = result_props[1][10:] + self.timestamp = props["Timestamp"] """Unique timestamp for the revision""" self.parent_results = parent_results @@ -327,11 +319,10 @@ def notes(self): >>> aedtapp.results.current_revision.notes 'Added a filter to the WiFi Radio.' """ - design = self.emit_project.odesktop.GetActiveProject().GetActiveDesign() + design = self.emit_project.odesign return design.GetResultNotes(self.name) @notes.setter def notes(self, notes): - design = self.emit_project.odesktop.GetActiveProject().GetActiveDesign() - design.SetResultNotes(self.name, notes) + self.emit_project.odesign.SetResultNotes(self.name, notes) self.emit_project._emit_api.save_project() From 91f22f6924f8cd87880a0872698614dd7fe28906 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 May 2023 09:46:31 +0200 Subject: [PATCH 14/23] MAINT: Bump pyvista from 0.38.5 to 0.38.6 (#2954) Bumps [pyvista](https://github.com/pyvista/pyvista) from 0.38.5 to 0.38.6. - [Release notes](https://github.com/pyvista/pyvista/releases) - [Commits](https://github.com/pyvista/pyvista/compare/v0.38.5...v0.38.6) --- updated-dependencies: - dependency-name: pyvista dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index be42dea3329..15b92c865e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ tests = [ "pytest==7.3.1", "pytest-cov==4.0.0", "pytest-xdist==3.2.1", - "pyvista==0.38.5", + "pyvista==0.38.6", "scikit-learn==1.2.2", "SRTM.py", "utm", @@ -71,7 +71,7 @@ doc = [ "osmnx", "pypandoc==1.11", "pytest-sphinx==0.5.0", - "pyvista==0.38.5", + "pyvista==0.38.6", "recommonmark==0.7.1", "scikit-learn==1.2.2", "Sphinx==7.0.0", @@ -95,7 +95,7 @@ full = [ "pandas==1.3.5; python_version <= '3.7'", "pandas==2.0.1; python_version > '3.7'", "osmnx", - "pyvista==0.38.5", + "pyvista==0.38.6", "SRTM.py", "utm", "scikit-rf", @@ -110,7 +110,7 @@ all = [ "pandas==1.3.5; python_version <= '3.7'", "pandas==2.0.1; python_version > '3.7'", "osmnx", - "pyvista==0.38.5", + "pyvista==0.38.6", "SRTM.py", "utm", "scikit-rf", From 9a869eee27869cfb8e97da54c67ff6b0819a54e9 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Thu, 4 May 2023 10:15:17 +0200 Subject: [PATCH 15/23] Fixed port name setter in Hfss3dLayout (#2955) Co-authored-by: maxcapodi78 --- _unittest/test_40_3dlayout_edb.py | 8 +++++++- pyaedt/modules/Boundary.py | 11 +++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/_unittest/test_40_3dlayout_edb.py b/_unittest/test_40_3dlayout_edb.py index e023c4ab3cd..bb1bdb6aff4 100644 --- a/_unittest/test_40_3dlayout_edb.py +++ b/_unittest/test_40_3dlayout_edb.py @@ -312,12 +312,18 @@ def test_17_ports_on_components_nets(self): if "GND" not in self.aedtapp.modeler.pins[i].net_name ] ports_before = len(self.aedtapp.port_list) - assert self.aedtapp.create_ports_on_component_by_nets( + ports = self.aedtapp.create_ports_on_component_by_nets( "J1", nets, ) + assert ports ports_after = len(self.aedtapp.port_list) assert ports_after - ports_before == len(nets) + ports[0].name = "port_test" + assert ports[0].name == "port_test" + assert ports[0].props["Port"] == "port_test" + ports[0].props["Port"] = "port_test2" + assert ports[0].name == "port_test2" def test_18_set_variable(self): self.aedtapp.variable_manager.set_variable("var_test", expression="123") diff --git a/pyaedt/modules/Boundary.py b/pyaedt/modules/Boundary.py index c1c27f709fa..659e6b594ff 100644 --- a/pyaedt/modules/Boundary.py +++ b/pyaedt/modules/Boundary.py @@ -1603,8 +1603,12 @@ def name(self): @name.setter def name(self, value): - self._name = value + if "Port" in self.props: + self.auto_update = False + self.props["Port"] = value + self.auto_update = True self.update() + self._name = value @pyaedt_function_handler() def _get_args(self, props=None): @@ -1648,7 +1652,10 @@ def update(self): """ updated = False for el in list(self.props.keys()): - if el in self._app.oeditor.GetProperties("EM Design", "Excitations:{}".format(self.name)) and self.props[ + if el == "Port" and self._name != self.props[el]: + self._app.oeditor.SetPropertyValue("EM Design", "Excitations:" + self.name, el, self.props[el]) + self._name = self.props[el] + elif el in self._app.oeditor.GetProperties("EM Design", "Excitations:{}".format(self.name)) and self.props[ el ] != self._app.oeditor.GetPropertyValue("EM Design", "Excitations:" + self.name, el): self._app.oeditor.SetPropertyValue("EM Design", "Excitations:" + self.name, el, self.props[el]) From e0a38be89b206b3bb9a42e192bcebf4449ffde98 Mon Sep 17 00:00:00 2001 From: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> Date: Thu, 4 May 2023 11:41:02 +0200 Subject: [PATCH 16/23] DOC: Update intersphinx mapping for Python (#2956) --- doc/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 7da57e7907c..734d28410ce 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -103,7 +103,7 @@ def setup(app): # Intersphinx mapping intersphinx_mapping = { - "python": ("https://docs.python.org/dev", None), + "python": ("https://docs.python.org/3", None), "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), "numpy": ("https://numpy.org/devdocs", None), "matplotlib": ("https://matplotlib.org/stable", None), From 315c472ac37f06fca8d683fc61e9856406e16ec6 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Thu, 4 May 2023 11:56:11 +0200 Subject: [PATCH 17/23] Added dc_resistance_only property to Q3D setup (#2957) Co-authored-by: maxcapodi78 --- _unittest/test_31_Q3D.py | 5 +++++ pyaedt/modules/SolveSetup.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/_unittest/test_31_Q3D.py b/_unittest/test_31_Q3D.py index 2a73d4297f7..e1bd56abc67 100644 --- a/_unittest/test_31_Q3D.py +++ b/_unittest/test_31_Q3D.py @@ -53,6 +53,11 @@ def test_06a_create_setup(self): mysetup = self.aedtapp.create_setup() mysetup.props["SaveFields"] = True assert mysetup.update() + assert mysetup.dc_enabled + mysetup.dc_resistance_only = True + assert mysetup.dc_resistance_only + mysetup.dc_enabled = False + mysetup.dc_enabled = True sweep = self.aedtapp.create_discrete_sweep(mysetup.name, sweepname="mysweep", freqstart=1, units="GHz") assert sweep assert sweep.props["RangeStart"] == "1GHz" diff --git a/pyaedt/modules/SolveSetup.py b/pyaedt/modules/SolveSetup.py index 66ada7e7de8..a488d37a96b 100644 --- a/pyaedt/modules/SolveSetup.py +++ b/pyaedt/modules/SolveSetup.py @@ -3077,6 +3077,24 @@ def dc_enabled(self, value): self._dc_enabled = value self.update() + @property + def dc_resistance_only(self): + """Get/Set the DC Resistance Only or Resistance/Inductance calculatio in active Q3D setup. + + Returns + ------- + bool + """ + try: + return self.props["DC"]["SolveResOnly"] + except KeyError: + return False + + @dc_resistance_only.setter + def dc_resistance_only(self, value): + if self.dc_enabled: + self.props["DC"]["SolveResOnly"] = value + @pyaedt_function_handler() def update(self, update_dictionary=None): """Update the setup based on either the class argument or a dictionary. From a34cc6dee7727d4b3e323b36450761e675d6667b Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Thu, 4 May 2023 12:55:48 +0200 Subject: [PATCH 18/23] Fix Student Version 22R2 launcher (#2958) Co-authored-by: maxcapodi78 --- pyaedt/desktop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyaedt/desktop.py b/pyaedt/desktop.py index 1bb9ec75639..e301711a818 100644 --- a/pyaedt/desktop.py +++ b/pyaedt/desktop.py @@ -530,7 +530,7 @@ def __init__( if version_key < "2022.2": settings.use_grpc_api = False elif ( - version_key == "2022.2" + version_key.startswith("2022.2") and not self.port and not self.machine and settings.use_grpc_api is None From 46192761c4a14ed29fb61a5666a0a7a279010db2 Mon Sep 17 00:00:00 2001 From: lollipop <21281676+cutelolly@users.noreply.github.com> Date: Thu, 4 May 2023 19:00:29 +0800 Subject: [PATCH 19/23] Updated README documentation (#2660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 更新readme文档 * Update README.md * Update README_CN.md * Uodate README_CN.md * Update README.md * Updata README_CN.md * Updata README_CN.md * Update README.md * Update README_CN.md * Update README.md * Update README.md * Update README_CN.md * Update README_CN.md * Update README_CN.md * Update README.md * Update README.md * Update README_CN.md * Update README_CN.md * Update README_CN.md --------- Co-authored-by: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> --- README.md | 206 ++++++++++++++++++++++++++++++++++++++++++++++++ README_CN.md | 219 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 425 insertions(+) create mode 100644 README.md create mode 100644 README_CN.md diff --git a/README.md b/README.md new file mode 100644 index 00000000000..d6c9b24b547 --- /dev/null +++ b/README.md @@ -0,0 +1,206 @@ + + + + +# PyAEDT + +

+
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/pyansys/pyaedt/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/pyansys/pyaedt/actions/workflows/unit_tests.yml)[![codecov](https://codecov.io/gh/pyansys/pyaedt/branch/main/graph/badge.svg)](https://codecov.io/gh/pyansys/pyaedt)[![MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/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/pyansys/pyaedt/main.svg)](https://results.pre-commit.ci/latest/github/pyansys/pyaedt/main) + +## What is PyAEDT? + +`PyAEDT` is a Python library that interacts directly with the AEDT API to make scripting simpler for the end user. Its architecture can be reused for all AEDT 3D products (HFSS, Icepak, Maxwell 3D, and Q3D Extractor), 2D tools, and Ansys Mechanical. It also provides support for circuit tools like Nexxim and system simulation tools like Twin Builder. Finally it provides scripting capabilities in Ansys layout tools like HFSS 3D Layout and EDB. Its class and method structures simplify operation for the end user while reusing information as much as possible across the API. + +## Install on CPython from PyPI + +You can install `PyAEDT` on CPython 3.7 through 3.10 from PyPI: + +```sh + pip install pyaedt +``` + +Install `PyAEDT` with all extra packages (matplotlib, numpy, pandas, pyvista): + +```sh + pip install pyaedt[all] +``` + +You can also install PyAEDT from Conda-Forge: + +```sh + conda install -c conda-forge pyaedt +``` +`PyAEDT` is still compatible with Ironpython and can be still used in AEDT Framework. + +## About PyAnsys + +`PyAEDT` is part of the larger [PyAnsys](https://docs.pyansys.com "PyAnsys") effort to facilitate the use of Ansys technologies directly from Python. + +`PyAEDT` is intended to consolidate and extend all existing functionalities around scripting for `Ansys Electronics Desktop (AEDT)` to allow reuse of existing code, sharing of best practices, and increased collaboration. + +## About AEDT + +The `Ansys Electronics Desktop (AEDT)` is a platform that enables true electronics system design. [AEDT](https://www.ansys.com/products/electronics) provides access to the Ansys gold-standard electro-magnetics simulation solutions such as `Ansys HFSS`, `Ansys Maxwell`, `Ansys Q3D Extractor`, `Ansys Siwave`, and `Ansys Icepak` using `electrical CAD (ECAD)` and `Mechanical CAD (MCAD)` workflows. +In addition, it includes direct links to the complete Ansys portfolio of thermal, fluid, and Mechanical solvers for comprehensive multiphysics analysis. Tight integration among these solutions provides unprecedented ease of use for setup and faster resolution of complex simulations for design and optimization. + +

+ +

+ +`PyAEDT` is licensed under the [MIT License](https://github.com/pyansys/PyAEDT/blob/main/LICENSE) + +`PyAEDT` includes functionality for interacting with the following `AEDT tools` and `Ansys products`: + + - HFSS and HFSS 3D Layout + - Icepak + - Maxwell 2D, Maxwell 3D, and RMXprt + - 2D Extractor and Q3D Extractor + - Mechanical + - Nexxim + - EDB + - Twin Builder + +## Documentation and issues + +In addition to installation and usage information, the `PyAEDT` documentation provides [API reference](https://aedt.docs.pyansys.com/release/0.6/API/index.html), [Examples](https://aedt.docs.pyansys.com/release/0.6/examples/index.html), and [Contribute](https://aedt.docs.pyansys.com/release/0.6/Contributing.html) sections. + +On the [PyAEDT Issues](https://github.com/pyansys/PyAEDT/issues) page, you can create issues to submit questions, report bugs, and request new features. To reach the project support team, email [pyansys.support@ansys.com](pyansys.support@ansys.com). + +## Dependencies + +To run `PyAEDT`, you must have a local licenced copy of AEDT. +`PyAEDT` supports AEDT versions 2022 R1 or newer. + +## Student version + +`PyAEDT` supports `AEDT Student version 2022 R1` and later. For more information, see [Student Version page](https://www.ansys.com/academic/students/ansys-electronics-desktop-student). + +## Why PyAEDT? + +A quick and easy approach for automating a simple operation in the AEDT UI is to record and reuse a script. However, disadvantages of this approach are: + + - Recorded code is dirty and difficult to read and understand. + - Recorded scripts are difficult to reuse and adapt. + - Complex coding is required by many global users of `AEDT`. + +The main advantages of `PyAEDT` are: + + - Automatic initialization of all `AEDT` objects, such as desktop objects like the editor, boundaries, and so on + - Error management + - Log management + - Variable management + - Compatibility with IronPython and CPython + - Simplification of complex API syntax using data objects while maintaining PEP8 compliance. + - Code reusability across different solvers + - Clear documentation on functions and API + - Unit tests of code to increase quality across different AEDT versions + +## Example workflow + + 1. Initialize the ``Desktop`` class with the version of `AEDT` to use. + 2. Initialize the application to use within `AEDT`. + +## Connect to AEDT from Python IDE + +``PyAEDT`` works both inside AEDT and as a standalone application. This Python library automatically detects whether it is running in an IronPython or CPython environment and initializes AEDT accordingly. +``PyAEDT`` also provides advanced error management. Usage examples follow. + +## Explicit AEDT declaration and error management + +``` python + # Launch AEDT 2022 R2 in non-graphical mode + + from pyaedt import Desktop, Circuit + with Desktop(specified_version="2022.2", + non_graphical=False, new_desktop_session=True, + close_on_exit=True, student_version=False): + circuit = Circuit() + ... + # Any error here will be caught by Desktop. + ... + + # Desktop is automatically released here. +``` + +## Implicit AEDT declaration and error management + +``` python + # Launch the latest installed version of AEDT in graphical mode + + from pyaedt import Circuit + with Circuit(specified_version="2022.2", + non_graphical=False) as circuit: + ... + # Any error here will be caught by Desktop. + ... + + # Desktop is automatically released here. +``` + +## Remote application call + +You can make a remote application call on a CPython server +or any Windows client machine. + +On a CPython Server: + +``` python + # Launch PyAEDT remote server on CPython + + from pyaedt.common_rpc import pyaedt_service_manager + pyaedt_service_manager() +``` + +On any Windows client machine: + +``` python + from pyaedt.common_rpc import create_session + cl1 = create_session("server_name") + cl1.aedt(port=50000, non_graphical=False) + hfss = Hfss(machine="server_name", port=50000) + # your code here +``` + +## Variables + +``` python + from pyaedt.HFSS import HFSS + with HFSS as hfss: + hfss["dim"] = "1mm" # design variable + hfss["$dim"] = "1mm" # project variable +``` + +## Modeler + +``` python + # Create a box, assign variables, and assign materials. + + from pyaedt.hfss import Hfss + with Hfss as hfss: + hfss.modeler.create_box([0, 0, 0], [10, "dim", 10], + "mybox", "aluminum") +``` + +## License + +`PyAEDT` is licensed under the `MIT` license. + +This module makes no commercial claim over Ansys whatsoever. `PyAEDT` extends the functionality of `AEDT` by adding an additional Python interface to `AEDT` without changing the core behavior or license of the original software. The use of the interactive control of `PyAEDT` requires a legally licensed local copy of `AEDT`. For more information about `AEDT`, visit the [AEDT page](https://www.ansys.com/products/electronics) on the `Ansys` website. + +

back to top

+ +## Indices and tables + +- [Index](https://aedt.docs.pyansys.com/release/0.6/genindex.html) +- [Module Index](https://aedt.docs.pyansys.com/release/0.6/py-modindex.html) +- [Search Page](https://aedt.docs.pyansys.com/release/0.6/search.html) diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 00000000000..7337c7f32e1 --- /dev/null +++ b/README_CN.md @@ -0,0 +1,219 @@ + + + + +# PyAEDT + +

+
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/pyansys/pyaedt/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/pyansys/pyaedt/actions/workflows/unit_tests.yml)[![codecov](https://codecov.io/gh/pyansys/pyaedt/branch/main/graph/badge.svg)](https://codecov.io/gh/pyansys/pyaedt)[![MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/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/pyansys/pyaedt/main.svg)](https://results.pre-commit.ci/latest/github/pyansys/pyaedt/main) + +## PyAEDT 简介 + +`PyAEDT` 是一个直接与 `AEDT API` 交互的 `Python` 库。使最终用户的脚本编写更简易。它的框架可重复使用,适用于所有 AEDT 3D 产品(`HFSS`、`Icepak`、`Maxwell 3D` 和 `Q3D Extractor`)、`2D tools` 和 `Ansys Mechanical`。它支持像 `Nexxim` 这样的电路工具和 `Twin Builder` 这样的系统模拟工具。最后它为 `HFSS 3D Layout` 和 `EDB` 等 `Ansys` 布局工具提供脚本功能。它的类方法结构简化了最终用户的操作,同时尽可能在整个 API 中复用信息。 + +## 关于 PyAEDT 的安装 + +1. 您可以使用 `PyPI` 工具将 `PyAEDT` 安装在 `CPython 3.7-3.10` 版本上: +``` python + pip install pyaedt +``` + +2. 当您同时需要其他数据处理时可以选择安装 `PyAEDT` 和以下对应的库(`matplotlib`, `numpy`, `pandas`, `pyvista`): + +```sh + pip install pyaedt[all] + pip install matplotlib + pip install numpy + pip install pandas + pip install pyvista +``` + +3. 您也可以从 `Conda-Forge` 安装 `PyAEDT` : + +```sh + conda install -c conda-forge pyaedt +``` + +`PyAEDT` 与 `Ironpython` 兼容,仍然可以在 `AEDT` 框架中使用。 + +备注(2023年3月22日):`PyAEDT`的主要贡献者决定减少默认包的内存占用。如果你运行 pip install pyaedt,以下安装是必须的(在拥有PyAEDT基础功能的情况下): +``` +"cffi == 1.15.0;platform_system=='Linux'", +"pywin32 >= 303;platform_system=='Windows'", +"pythonnet == 3.0.1", +"rpyc==5.3.0", +"psutil", +"dotnetcore2 ==3.1.23;platform_system=='Linux'" +``` +4. 如果你需要其他库来做后期处理,可以使用以下方法来安装它们: +```sh +pip install pyaedt[full] +``` + +## 关于 PyAEDT + +`PyAEDT` 是 [PyAnsys](https://docs.pyansys.com "PyAnsys") 的一部分,后者更庞大。努力促进直接从 `Python` 使用 `Ansys` 技术。 + +`PyAEDT` 旨在整合和扩展 `Ansys Electronics Desktop (AEDT)` 脚本的所有现有功能,以允许重用现有代码、共享最佳实践并加强协作。 + + ## 关于 AEDT + +`Ansys Electronics Desktop (AEDT)` 是一个真正支持电子系统设计的平台。[AEDT](https://www.ansys.com/products/electronics) 使用 `电气CAD(ECAD)` 和 `机械CAD(MCAD)` 工作流程提供对 Ansys gold-standard electro-magnetics 仿真解决方案的访问,例如 `Ansys HFSS`、`Ansys Maxwell`、`Ansys Q3D Extractor`、`Ansys Siwave` 和 `Ansys Icepak`。此外,它还包括与完整的 `Ansys热求解器(Thermal)`、`流体求解器(Fluid)` 和 `机械求解器(Mechanical)` 产品组合的直接连接,用于全面的多物理场分析。这些解决方案之间的紧密集成提供了前所未有的设置易用性,并更快地解决了用于设计和优化的复杂仿真。 + +

+ +

+ +`PyAEDT` 遵循 [MIT](https://github.com/pyansys/PyAEDT/blob/main/LICENSE "MIT") 许可证,并包括与 `AEDT tools` 和 `Ansys products` 交互的功能: + +- HFSS and HFSS 3D Layout +- Icepak +- Maxwell 2D, Maxwell 3D, and RMXprt +- 2D Extractor and Q3D Extractor +- Mechanical +- Nexxim +- EDB +- Twin Builder + +## 文档与反馈 + +除了安装和使用信息外,`PyAEDT` 文档还提供了 [参考API](https://aedt.docs.pyansys.com/release/0.6/API/index.html "API reference"),[示例](https://aedt.docs.pyansys.com/release/0.6/examples/index.html "Examples"),和 [贡献](https://aedt.docs.pyansys.com/release/0.6/Contributing.html "Contribute") 。 + + +在 [PyAEDT Issues](https://github.com/pyansys/PyAEDT/issues) 页面,您可以提交所遇到的问题、反馈 bug、为新功能贡献您的想法与思路。如联系项目支持团队,请点击后面的链接发送电子邮件 [pyansys.support@ansys.com](pyansys.support@ansys.com). + +## 适用范围 + +`PyAEDT` 支持 `AEDT 2022 R1` 或更高的版本。 + +`PyAEDT` 支持 `AEDT Student version 2022 R1` 或更高的版本。 + +关于学生版本的更多信息请查阅 [Student Version page](https://www.ansys.com/academic/students/ansys-electronics-desktop-student). + +## 为什么选择 PyAEDT + +在 AEDT UI 中使用记录和重用脚本自动执行是简化操作的一种快速又简便的办法。但是,此方法有以下缺陷: + +- 记录的代码比较混乱,难以阅读与理解。 +- 录制的脚本难以复用和二次编辑。 +- AEDT 多个全局用户会生成复杂庞大的代码。 + +`PyAEDT` 的主要优点是: + +- 自动初始化所有AEDT项目,例如desktop项目中的编辑器、边界等等 +- 清晰的错误管理 +- 详细的日志管理 +- 自由的变量管理 +- 兼容 IronPython 和 CPython +- 使用数据对象简化复杂的 API 语法,同时保持 PEP8 合规性。 +- 在不同求解器之间的可复用代码 +- 在函数与API上拥有清晰的文档 +- 代码单元测试,以提高不同 AEDT 版本的质量 + +## 工作流程示例 + + 1. 在所需的`AEDT`版本中初始化`desktop`类。 + 2. 并初始化`AEDT`中所要使用的应用程序。 + +## 使用 Python IDE 连接 AEDT + +`PyAEDT` 既可以在AEDT内部工作,也可以作为独立应用程序使用。此 Python 库会自动检测它是否正在运行在 IronPython 或 CPython 环境中并相应地初始化 AEDT。 +`PyAEDT` 还提供高级错误管理。使用示例如下: + +### 明确 AEDT 声明 [INFO] 并管理报错 [ERROR] + +``` python + # 以非图形模式启动 AEDT 2022 R2 + + from pyaedt import Desktop, Circuit + with Desktop(specified_version="2022.2", + non_graphical=False, new_desktop_session=True, + close_on_exit=True, student_version=False): + circuit = Circuit() + ... + # 此处的任何错误都将被 Desktop 捕获。 + ... + + # Desktop 将在此处自动发布。 +``` + +### Implicit AEDT declaration and error management + +``` python + # Launch the latest installed version of AEDT in graphical mode + + from pyaedt import Circuit + with Circuit(specified_version="2022.2", + non_graphical=False) as circuit: + ... + # Any error here will be caught by Desktop. + ... + + # Desktop is automatically released here. +``` + +## 远程应用程序调用 + +您可以在 CPython 服务器或任何 Windows 客户端计算机上进行远程应用程序调用。 + +CPython 服务器: + +``` python + # 在CPython上启动PyAEDT远程服务器 + + from pyaedt.common_rpc import pyaedt_service_manager + pyaedt_service_manager() +``` + +任意的 Windows 客户端: + +``` python + from pyaedt.common_rpc import create_session + cl1 = create_session("server_name") + cl1.aedt(port=50000, non_graphical=False) + hfss = Hfss(machine="server_name", port=50000) + # 在这里编辑您的代码 +``` + +## 变量 Variables + +``` python + from pyaedt.HFSS import HFSS + with HFSS as hfss: + hfss["dim"] = "1mm" # design variable + hfss["$dim"] = "1mm" # project variable +``` + +## 模型 Modeler + +``` python + # 创建BOX、分配变量和指定材料。 + + from pyaedt.hfss import Hfss + with Hfss as hfss: + hfss.modeler.create_box([0, 0, 0], [10, "dim", 10], + "mybox", "aluminum") +``` + +## 许可 + +要运行`PyAEDT`,您必须拥有`AEDT`的本地许可证。 +`PyAEDT`在`MIT`许可下获得许可。这个模块对`Ansys`没有任何商业要求。`PyAEDT`通过向`AEDT`添加额外的Python接口扩展了`AEDT`的功能,而不改变原始软件的核心行为或许可。使用`PyAEDT`的交互式控件需要合法授权的`AEDT`本地license。有关`AEDT`的更多信息,请访问`Ansys`网站上的[AEDT页面](https://www.ansys.com/products/electronics)。 + +

回到顶部

+ +## 索引与目录 + +- [Index](https://aedt.docs.pyansys.com/release/0.6/genindex.html) +- [Module Index](https://aedt.docs.pyansys.com/release/0.6/py-modindex.html) +- [Search Page](https://aedt.docs.pyansys.com/release/0.6/search.html) From 46dbdc6e2e01d64e41362a5e93647dc4a05a0aff Mon Sep 17 00:00:00 2001 From: gmalinve <103059376+gmalinve@users.noreply.github.com> Date: Thu, 4 May 2023 15:14:48 +0200 Subject: [PATCH 20/23] Material name setter (#2961) * Now material setters check if argument is a material that exists or an array (checking for "[") * Now material setters check if argument is a material that exists or an array (checking for "[" or "(") * mat name + test --------- Co-authored-by: maxcapodi78 --- _unittest/test_07_Object3D.py | 12 ++++++++++-- pyaedt/modeler/cad/object3d.py | 9 +++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/_unittest/test_07_Object3D.py b/_unittest/test_07_Object3D.py index b55a7c8fc07..61c6f94d62e 100644 --- a/_unittest/test_07_Object3D.py +++ b/_unittest/test_07_Object3D.py @@ -169,16 +169,24 @@ def test_03_FacePrimitive(self): assert isinstance(o_box.longest_edge()[0], EdgePrimitive) assert isinstance(o_box.shortest_edge()[0], EdgePrimitive) - def test_04_object_material_property_invalid(self): + def test_04a_object_material_property_invalid(self): o_box = self.create_copper_box("Invalid1") o_box.material_name = "Copper1234Invalid" assert o_box.material_name == "copper" - def test_04_object_material_property_valid(self): + def test_04b_object_material_property_valid(self): o_box = self.create_copper_box("Valid2") o_box.material_name = "aluminum" assert o_box.material_name == "aluminum" + def test_04c_material_name_setter(self): + self.aedtapp.materials.add_material("myMat") + self.aedtapp.materials.add_material("myMat2") + self.aedtapp["mat_sweep_test"] = '["myMat", "myMat2"]' + box = self.aedtapp.modeler["MyBox"] + box.material_name = "mat_sweep_test[0]" + assert self.aedtapp.modeler.get_objects_by_material(materialname="mat_sweep_test[0]")[0].name == "MyBox" + def test_05_object3d_properties_transparency(self): o = self.create_copper_box("TransparencyBox") diff --git a/pyaedt/modeler/cad/object3d.py b/pyaedt/modeler/cad/object3d.py index 6b7bec09bf3..bf1bcc5508b 100644 --- a/pyaedt/modeler/cad/object3d.py +++ b/pyaedt/modeler/cad/object3d.py @@ -892,12 +892,17 @@ def material_name(self): @material_name.setter def material_name(self, mat): matobj = self._primitives._materials.checkifmaterialexists(mat) + mat_value = None if matobj: + mat_value = chr(34) + matobj.name + chr(34) + elif "[" in mat or "(" in mat: + mat_value = mat + if mat_value is not None: if not self.model: self.model = True - vMaterial = ["NAME:Material", "Value:=", chr(34) + matobj.name + chr(34)] + vMaterial = ["NAME:Material", "Value:=", mat_value] self._change_property(vMaterial) - self._material_name = matobj.name.lower() + self._material_name = mat_value.strip('"') self._solve_inside = None else: self.logger.warning("Material %s does not exist.", mat) From b9c4e328698e543e231a2a34687f2f6df60defb7 Mon Sep 17 00:00:00 2001 From: Massimo Capodiferro <77293250+maxcapodi78@users.noreply.github.com> Date: Thu, 4 May 2023 15:44:29 +0200 Subject: [PATCH 21/23] Removed Rst (#2960) * Removed Rst Improved chinese and english version * Removed Rst Improved chinese and english version * Removed Rst Improved chinese and english version * Removed Rst Improved chinese and english version --------- Co-authored-by: maxcapodi78 --- .github/labeler.yml | 3 +- README.md | 65 ++++++---- README.rst | 282 -------------------------------------------- README_CN.md | 20 ++-- pyproject.toml | 2 +- 5 files changed, 55 insertions(+), 317 deletions(-) delete mode 100644 README.rst diff --git a/.github/labeler.yml b/.github/labeler.yml index daadd414b4e..a6f0f67aee5 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,6 +1,7 @@ documentation: - doc/source/**/* -- README.rst +- README.md +- README_CN.md maintenance: - .github/**/* - .flake8 diff --git a/README.md b/README.md index d6c9b24b547..51cbed12771 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,13 @@ - + + # PyAEDT -

+


English | 中文

@@ -18,11 +15,19 @@ ## What is PyAEDT? -`PyAEDT` is a Python library that interacts directly with the AEDT API to make scripting simpler for the end user. Its architecture can be reused for all AEDT 3D products (HFSS, Icepak, Maxwell 3D, and Q3D Extractor), 2D tools, and Ansys Mechanical. It also provides support for circuit tools like Nexxim and system simulation tools like Twin Builder. Finally it provides scripting capabilities in Ansys layout tools like HFSS 3D Layout and EDB. Its class and method structures simplify operation for the end user while reusing information as much as possible across the API. +`PyAEDT` is a Python library that interacts directly with the API for +Ansys Electronics Desktop (AEDT) to make scripting simpler. The architecture +for PyAEDT can be reused for all AEDT 3D products (HFSS, Icepak, Maxwell 3D, +and Q3D Extractor), 2D tools, and Ansys Mechanical. PyAEDT also provides +support for circuit tools like Nexxim and system simulation tools like +Twin Builder. Finally, PyAEDT provides scripting capabilities in Ansys layout +tools like HFSS 3D Layout and EDB. The PyAEDT class and method structures +simplify operation while reusing information as much as possible across +the API. ## Install on CPython from PyPI -You can install `PyAEDT` on CPython 3.7 through 3.10 from PyPI: +You can install PyAEDT on CPython 3.7 through 3.10 from PyPI with this command: ```sh pip install pyaedt @@ -31,26 +36,36 @@ You can install `PyAEDT` on CPython 3.7 through 3.10 from PyPI: Install `PyAEDT` with all extra packages (matplotlib, numpy, pandas, pyvista): ```sh - pip install pyaedt[all] + pip install pyaedt[full] ``` -You can also install PyAEDT from Conda-Forge: +You can also install PyAEDT from Conda-Forge with this command: ```sh conda install -c conda-forge pyaedt ``` -`PyAEDT` is still compatible with Ironpython and can be still used in AEDT Framework. +PyAEDT remains compatible with IronPython and can be still used in the AEDT Framework. ## About PyAnsys `PyAEDT` is part of the larger [PyAnsys](https://docs.pyansys.com "PyAnsys") effort to facilitate the use of Ansys technologies directly from Python. -`PyAEDT` is intended to consolidate and extend all existing functionalities around scripting for `Ansys Electronics Desktop (AEDT)` to allow reuse of existing code, sharing of best practices, and increased collaboration. +PyAEDT is intended to consolidate and extend all existing +functionalities around scripting for AEDT to allow reuse of existing code, +sharing of best practices, and increased collaboration. ## About AEDT -The `Ansys Electronics Desktop (AEDT)` is a platform that enables true electronics system design. [AEDT](https://www.ansys.com/products/electronics) provides access to the Ansys gold-standard electro-magnetics simulation solutions such as `Ansys HFSS`, `Ansys Maxwell`, `Ansys Q3D Extractor`, `Ansys Siwave`, and `Ansys Icepak` using `electrical CAD (ECAD)` and `Mechanical CAD (MCAD)` workflows. -In addition, it includes direct links to the complete Ansys portfolio of thermal, fluid, and Mechanical solvers for comprehensive multiphysics analysis. Tight integration among these solutions provides unprecedented ease of use for setup and faster resolution of complex simulations for design and optimization. +[AEDT](https://www.ansys.com/products/electronics) is a platform that enables true +electronics system design. AEDT provides access to the Ansys gold-standard +electro-magnetics simulation solutions, such as Ansys HFSS, Ansys Maxwell, +Ansys Q3D Extractor, Ansys Siwave, and Ansys Icepak using electrical CAD (ECAD) and +Mechanical CAD (MCAD) workflows. + +In addition, AEDT includes direct links to the complete Ansys portfolio of thermal, fluid, +and mechanical solvers for comprehensive multiphysics analysis. +Tight integration among these solutions provides unprecedented ease of use for setup and +faster resolution of complex simulations for design and optimization.

`_. ## Dependencies @@ -197,10 +220,10 @@ On any Windows client machine: This module makes no commercial claim over Ansys whatsoever. `PyAEDT` extends the functionality of `AEDT` by adding an additional Python interface to `AEDT` without changing the core behavior or license of the original software. The use of the interactive control of `PyAEDT` requires a legally licensed local copy of `AEDT`. For more information about `AEDT`, visit the [AEDT page](https://www.ansys.com/products/electronics) on the `Ansys` website. -

back to top

+

back to top

## Indices and tables -- [Index](https://aedt.docs.pyansys.com/release/0.6/genindex.html) -- [Module Index](https://aedt.docs.pyansys.com/release/0.6/py-modindex.html) -- [Search Page](https://aedt.docs.pyansys.com/release/0.6/search.html) +- [Index](https://aedt.docs.pyansys.com/version/stable/genindex.html) +- [Module Index](https://aedt.docs.pyansys.com/version/stable/py-modindex.html) +- [Search Page](https://aedt.docs.pyansys.com/version/stable/search.html) diff --git a/README.rst b/README.rst deleted file mode 100644 index 221b7a163ab..00000000000 --- a/README.rst +++ /dev/null @@ -1,282 +0,0 @@ -PyAEDT -====== - -|pyansys| |pypi| |PyPIact| |PythonVersion| |GH-CI| |codecov| |MIT| |black| |Anaconda| |pre-commit| - -.. |pyansys| image:: 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 - :target: https://docs.pyansys.com/ - :alt: PyAnsys - -.. |pypi| image:: https://img.shields.io/pypi/v/pyaedt.svg?logo=python&logoColor=white - :target: https://pypi.org/project/pyaedt/ - -.. |PyPIact| image:: https://pepy.tech/badge/pyaedt/month - :target: https://pypi.org/project/pyaedt/ - -.. |PythonVersion| image:: https://img.shields.io/badge/python-3.7+-blue.svg - :target: https://www.python.org/downloads/ - -.. |GH-CI| image:: https://github.com/pyansys/pyaedt/actions/workflows/unit_tests.yml/badge.svg - :target: https://github.com/pyansys/pyaedt/actions/workflows/unit_tests.yml - -.. |codecov| image:: https://codecov.io/gh/pyansys/pyaedt/branch/main/graph/badge.svg - :target: https://codecov.io/gh/pyansys/pyaedt - -.. |MIT| image:: https://img.shields.io/badge/License-MIT-yellow.svg - :target: https://opensource.org/licenses/MIT - -.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=flat - :target: https://github.com/psf/black - :alt: black - -.. |Anaconda| image:: https://anaconda.org/conda-forge/pyaedt/badges/version.svg - :target: https://anaconda.org/conda-forge/pyaedt - -.. |pre-commit| image:: https://results.pre-commit.ci/badge/github/pyansys/pyaedt/main.svg - :target: https://results.pre-commit.ci/latest/github/pyansys/pyaedt/main - :alt: pre-commit.ci status - - -What is PyAEDT? ---------------- -PyAEDT is a Python library that interacts directly with the API for -Ansys Electronics Desktop (AEDT) to make scripting simpler. The architecture -for PyAEDT can be reused for all AEDT 3D products (HFSS, Icepak, Maxwell 3D, -and Q3D Extractor), 2D tools, and Ansys Mechanical. PyAEDT also provides -support for circuit tools like Nexxim and system simulation tools like -Twin Builder. Finally, PyAEDT provides scripting capabilities in Ansys layout -tools like HFSS 3D Layout and EDB. The PyAEDT class and method structures -simplify operation while reusing information as much as possible across -the API. - -Install on CPython from PyPI ----------------------------- -You can install PyAEDT on CPython 3.7 through 3.10 from PyPI with this command: - -.. code:: python - - pip install pyaedt - -To install PyAEDT with all extra packages (Matplotlib, NumPy, Pandas, and PyVista), -use this command: - -.. code:: python - - pip install pyaedt[full] - -You can also install PyAEDT from Conda-Forge with this command: - -.. code:: python - - conda install -c conda-forge pyaedt - -PyAEDT remains compatible with IronPython and can be still used in the AEDT Framework. - -About PyAnsys -------------- - -PyAEDT is part of the larger `PyAnsys `_ -effort to facilitate the use of Ansys technologies directly from Python. - -PyAEDT is intended to consolidate and extend all existing -functionalities around scripting for AEDT to allow reuse of existing code, -sharing of best practices, and increased collaboration. - - -About AEDT ----------- - -`AEDT `_ is a platform that enables true -electronics system design. AEDT provides access to the Ansys gold-standard -electro-magnetics simulation solutions, such as Ansys HFSS, Ansys Maxwell, -Ansys Q3D Extractor, Ansys Siwave, and Ansys Icepak using electrical CAD (ECAD) and -Mechanical CAD (MCAD) workflows. - -In addition, AEDT includes direct links to the complete Ansys portfolio of thermal, fluid, -and mechanical solvers for comprehensive multiphysics analysis. -Tight integration among these solutions provides unprecedented ease of use for setup and -faster resolution of complex simulations for design and optimization. - -.. image:: https://images.ansys.com/is/image/ansys/ansys-electronics-technology-collage?wid=941&op_usm=0.9,1.0,20,0&fit=constrain,0 - :width: 800 - :alt: AEDT Applications - :target: https://www.ansys.com/products/electronics - - -PyAEDT is licensed under the `MIT License -`_. - -PyAEDT includes functionality for interacting with the following AEDT tools and Ansys products: - -- HFSS and HFSS 3D Layout -- Icepak -- Maxwell 2D, Maxwell 3D, and RMXprt -- 2D Extractor and Q3D Extractor -- Mechanical -- Nexxim -- EDB -- Twin Builder - - -Documentation and issues ------------------------- -Documentation for the latest stable release of PyAEDT is hosted at -`PyAEDT Documentation `_. - -In the upper right corner of the documentation's title bar, there is an option -for switching from viewing the documentation for the latest stable release -to viewing the documentation for the development version or previously -released versions. - -On the `PyAEDT Issues `_ page, you can -create issues to submit questions, report bugs, and request new features. - -To reach the project support team, email `pyansys.core@ansys.com `_. - -Dependencies ------------- -To run PyAEDT, you must have a local licensed copy of AEDT. -PyAEDT supports AEDT versions 2022 R1 and later. - -Student version ---------------- - -PyAEDT supports AEDT Student versions 2022 R1 and later. For more information, see -`Ansys Electronics Desktop Student - Free Software Download `_ on the Ansys website. - - -Why PyAEDT? ------------ -A quick and easy approach for automating a simple operation in the -AEDT UI is to record and reuse a script. However, here are some disadvantages of -this approach: - -- Recorded code is dirty and difficult to read and understand. -- Recorded scripts are difficult to reuse and adapt. -- Complex coding is required by many global users of AEDT. - -Here are the main advantages that PyAEDT provides: - -- Automatic initialization of all AEDT objects, such as desktop - objects like the editor, boundaries, and more -- Error management -- Log management -- Variable management -- Compatibility with IronPython and CPython -- Simplification of complex API syntax using data objects while - maintaining PEP8 compliance. -- Code reusability across different solvers -- Clear documentation on functions and API -- Unit tests of code to increase quality across different AEDT versions - - -Example workflow ------------------ -1. Initialize the ``Desktop`` class with the version of AEDT to use. -2. Initialize the application to use within AEDT. - - -Connect to AEDT from a Python IDE ---------------------------------- -PyAEDT works both inside AEDT and as a standalone app. -This Python library automatically detects whether it is running -in an IronPython or CPython environment and initializes AEDT accordingly. -PyAEDT also provides advanced error management. Usage examples follow. - -Explicit AEDT declaration and error management -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code:: python - - # Launch AEDT 2023 R1 in non-graphical mode - - from pyaedt import Desktop, Circuit - with Desktop(specified_version="2023.1", - non_graphical=False, new_desktop_session=True, - close_on_exit=True, student_version=False): - circuit = Circuit() - ... - # Any error here will be caught by Desktop. - ... - - # Desktop is automatically released here. - - -Implicit AEDT declaration and error management -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code:: python - - # Launch the latest installed version of AEDT in graphical mode - - from pyaedt import Circuit - with Circuit(specified_version="2023.1", - non_graphical=False) as circuit: - ... - # Any error here will be caught by Desktop. - ... - - # Desktop is automatically released here. - - -Remote application call -~~~~~~~~~~~~~~~~~~~~~~~ -You can make a remote application call on a CPython server -or any Windows client machine. - -On a CPython server: - -.. code:: python - - # Launch PyAEDT remote server on CPython - - from pyaedt.common_rpc import pyaedt_service_manager - pyaedt_service_manager() - - -On any Windows client machine: - -.. code:: python - - from pyaedt.common_rpc import create_session - cl1 = create_session("server_name") - cl1.aedt(port=50000, non_graphical=False) - hfss = Hfss(machine="server_name", port=50000) - # your code here - -Variables -~~~~~~~~~ - -.. code:: python - - from pyaedt.HFSS import HFSS - with HFSS as hfss: - hfss["dim"] = "1mm" # design variable - hfss["$dim"] = "1mm" # project variable - - -Modeler -~~~~~~~ - -.. code:: python - - # Create a box, assign variables, and assign materials. - - from pyaedt.hfss import Hfss - with Hfss as hfss: - hfss.modeler.create_box([0, 0, 0], [10, "dim", 10], - "mybox", "aluminum") - -License -------- -PyAEDT is licensed under the MIT license. - -This module makes no commercial claim over Ansys whatsoever. -PyAEDT extends the functionality of AEDT by adding -an additional Python interface to AEDT without changing the core -behavior or license of the original software. The use of the -interactive control of PyAEDT requires a legally licensed -local copy of AEDT. For more information about AEDT, -see the `Ansys Electronics `_ -page on the Ansys website. diff --git a/README_CN.md b/README_CN.md index 7337c7f32e1..83c8e5b5844 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,16 +1,12 @@ - + # PyAEDT -

+


English | 中文

@@ -87,7 +83,7 @@ pip install pyaedt[full] ## 文档与反馈 -除了安装和使用信息外,`PyAEDT` 文档还提供了 [参考API](https://aedt.docs.pyansys.com/release/0.6/API/index.html "API reference"),[示例](https://aedt.docs.pyansys.com/release/0.6/examples/index.html "Examples"),和 [贡献](https://aedt.docs.pyansys.com/release/0.6/Contributing.html "Contribute") 。 +除了安装和使用信息外,`PyAEDT` 文档还提供了 [参考API](https://aedt.docs.pyansys.com/version/stable/API/index.html "API reference"),[示例](https://aedt.docs.pyansys.com/version/stable/examples/index.html "Examples"),和 [贡献](https://aedt.docs.pyansys.com/version/stable/Contributing.html "Contribute") 。 在 [PyAEDT Issues](https://github.com/pyansys/PyAEDT/issues) 页面,您可以提交所遇到的问题、反馈 bug、为新功能贡献您的想法与思路。如联系项目支持团队,请点击后面的链接发送电子邮件 [pyansys.support@ansys.com](pyansys.support@ansys.com). @@ -210,10 +206,10 @@ CPython 服务器: 要运行`PyAEDT`,您必须拥有`AEDT`的本地许可证。 `PyAEDT`在`MIT`许可下获得许可。这个模块对`Ansys`没有任何商业要求。`PyAEDT`通过向`AEDT`添加额外的Python接口扩展了`AEDT`的功能,而不改变原始软件的核心行为或许可。使用`PyAEDT`的交互式控件需要合法授权的`AEDT`本地license。有关`AEDT`的更多信息,请访问`Ansys`网站上的[AEDT页面](https://www.ansys.com/products/electronics)。 -

回到顶部

+

回到顶部

## 索引与目录 -- [Index](https://aedt.docs.pyansys.com/release/0.6/genindex.html) -- [Module Index](https://aedt.docs.pyansys.com/release/0.6/py-modindex.html) -- [Search Page](https://aedt.docs.pyansys.com/release/0.6/search.html) +- [Index](https://aedt.docs.pyansys.com/version/stable/genindex.html) +- [Module Index](https://aedt.docs.pyansys.com/version/stable/py-modindex.html) +- [Search Page](https://aedt.docs.pyansys.com/version/stable/search.html) diff --git a/pyproject.toml b/pyproject.toml index 15b92c865e7..201d7f0cb4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "flit_core.buildapi" name = "pyaedt" dynamic = ["version"] description = "Higher-Level Pythonic Ansys Electronics Desktop Framework" -readme = "README.rst" +readme = "README.md" requires-python = ">=3.7,<4" license = {file = "LICENSE"} authors = [{name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"}] From e187f8db0a07e979e13b3bd4fe7330555d7853e2 Mon Sep 17 00:00:00 2001 From: svandenb-dev <74993647+svandenb-dev@users.noreply.github.com> Date: Thu, 4 May 2023 16:07:57 +0200 Subject: [PATCH 22/23] Create port on pin (#2952) * temp * create port on pin * ipython fix * ipython fix --- _unittest/test_00_EDB.py | 10 ++++ pyaedt/edb_core/components.py | 90 ++++++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/_unittest/test_00_EDB.py b/_unittest/test_00_EDB.py index ba4228b85b9..874cfb570ee 100644 --- a/_unittest/test_00_EDB.py +++ b/_unittest/test_00_EDB.py @@ -2398,3 +2398,13 @@ def test_134_hfss_extent_info(self): assert j.tofloat == hfss_extent_info._get_edb_value(config[i]).ToDouble() else: assert j == config[i] + + def test_134_create_port_on_pin(self): + source_path = os.path.join(local_path, "example_models", test_subfolder, "Galileo.aedb") + target_path = os.path.join(self.local_scratch.path, "test_0134.aedb") + self.local_scratch.copyfolder(source_path, target_path) + edbapp = Edb(target_path, edbversion=desktop_version) + pin = "AJ6" + ref_pins = [pin for pin in list(edbapp.components["U2A5"].pins.values()) if pin.net_name == "GND"] + term = edbapp.components.create_port_on_pins(refdes="U2A5", pins=pin, reference_pins=ref_pins) + assert term diff --git a/pyaedt/edb_core/components.py b/pyaedt/edb_core/components.py index 3b88933c475..e550710f99d 100644 --- a/pyaedt/edb_core/components.py +++ b/pyaedt/edb_core/components.py @@ -730,6 +730,86 @@ def create_source_on_component(self, sources=None): ) return True + @pyaedt_function_handler() + def create_port_on_pins(self, refdes, pins, reference_pins, impedance=50.0): + """Create circuit port between pins and reference ones. + + Parameters + ---------- + refdes : Component reference designator + str or EDBComponent object. + pins : pin name where the terminal has to be created. Single pin or several ones can be provided.If several + pins are provided a pin group will is created. Pin names can be the EDB name or the EDBPadstackInstance one. + For instance the pin called ``Pin1`` located on component ``U1``, ``U1-Pin1`` or ``Pin1`` can be provided and + will be handled. + str, [str], EDBPadstackInstance, [EDBPadstackInstance] + reference_pins : reference pin name used for terminal reference. Single pin or several ones can be provided. + If several pins are provided a pin group will is created. Pin names can be the EDB name or the + EDBPadstackInstance one. For instance the pin called ``Pin1`` located on component ``U1``, ``U1-Pin1`` + or ``Pin1`` can be provided and will be handled. + str, [str], EDBPadstackInstance, [EDBPadstackInstance] + impedance : Port impedance + str, float + + Returns + ------- + EDB terminal created, or False if failed to create. + + Example: + >>> from pyaedt import Edb + >>> edb = Edb(path_to_edb_file) + >>> pin = "AJ6" + >>> ref_pins = ["AM7", "AM4"] + Or to take all reference pins + >>> ref_pins = [pin for pin in list(edb.components["U2A5"].pins.values()) if pin.net_name == "GND"] + >>> edb.components.create_port_on_pins(refdes="U2A5", pins=pin, reference_pins=ref_pins) + >>> edb.save_edb() + >>> edb.close_edb() + """ + + if isinstance(pins, str) or isinstance(pins, EDBPadstackInstance): + pins = [pins] + if isinstance(reference_pins, str): + reference_pins = [reference_pins] + if isinstance(refdes, str) or isinstance(refdes, EDBComponent): + refdes = self.instances[refdes] + if len([pin for pin in pins if isinstance(pin, str)]) == len(pins): + cmp_pins = [] + for pin_name in pins: + cmp_pin = [pin for pin in list(refdes.pins.values()) if pin_name in pin.name] + if cmp_pin: + cmp_pins.append(cmp_pin[0]) + if not cmp_pins: + return + pins = cmp_pins + if not len([pin for pin in pins if isinstance(pin, EDBPadstackInstance)]) == len(pins): + self._logger.error("Pin list must contain only pins instances") + return + if len([pin for pin in reference_pins if isinstance(pin, str)]) == len(reference_pins): + ref_cmp_pins = [] + for ref_pin_name in reference_pins: + cmp_ref_pin = [pin for pin in list(refdes.pins.values()) if ref_pin_name in pin.name] + if cmp_ref_pin: + ref_cmp_pins.append(cmp_ref_pin[0]) + if not ref_cmp_pins: + return + reference_pins = ref_cmp_pins + if not len([pin for pin in reference_pins if isinstance(pin, EDBPadstackInstance)]) == len(reference_pins): + return + group_name = "group_{}_{}".format(pins[0].net_name, pins[0].name) + ref_group_name = "group_{}_{}_ref".format(pins[0].net_name, pins[0].name) + pin_group = self.create_pingroup_from_pins(pins, group_name) + ref_pin_group = self.create_pingroup_from_pins(reference_pins, ref_group_name) + term = self._create_pin_group_terminal(pingroup=pin_group, component=refdes.refdes) + term.SetIsCircuitPort(True) + ref_term = self._create_pin_group_terminal(pingroup=ref_pin_group, component=refdes.refdes) + ref_term.SetIsCircuitPort(True) + term.SetImpedance(self._edb.Utility.Value(impedance)) + term.SetReferenceTerminal(ref_term) + if term: + return term + return False + @pyaedt_function_handler() def create_port_on_component( self, @@ -1395,7 +1475,7 @@ def create_pingroup_from_pins(self, pins, group_name=None): Parameters ---------- pins : list - List of EDB core pins. + List of EDB pins. group_name : str, optional Name for the group. The default is ``None``, in which case a default name is assigned as follows: ``[component Name] [NetName]``. @@ -1415,8 +1495,16 @@ def create_pingroup_from_pins(self, pins, group_name=None): if len(pins) < 1: self._logger.error("No pins specified for pin group %s", group_name) return (False, None) + if len([pin for pin in pins if isinstance(pin, EDBPadstackInstance)]): + _pins = [pin._edb_padstackinstance for pin in pins] + if _pins: + pins = _pins if group_name is None: group_name = self._edb.Cell.Hierarchy.PinGroup.GetUniqueName(self._active_layout) + for pin in pins: + pin.SetIsLayoutPin(True) + forbiden_car = "-><" + group_name = group_name.translate({ord(i): "_" for i in forbiden_car}) pingroup = _retry_ntimes( 10, self._edb.Cell.Hierarchy.PinGroup.Create, From 4b9d1a8305804973c61601fba9e32ced6879aacf Mon Sep 17 00:00:00 2001 From: Samuel Lopez <85613111+Samuelopez-ansys@users.noreply.github.com> Date: Fri, 5 May 2023 15:06:43 +0200 Subject: [PATCH 23/23] Insert Layout component (#2968) * Read Layout component in User defined component class * Insert Layout Component * Add unittest * Add example * Fix bug * Fix UT and codevoc * Fix UT --- .github/workflows/ironpython.yml | 1 + .../T08/Layoutcomponent_231.aedbcomp | Bin 0 -> 14861 bytes _unittest/test_08_Primitives3D.py | 28 +++ pyaedt/__init__.py | 1 + pyaedt/generic/general_methods.py | 1 + pyaedt/modeler/cad/Primitives3D.py | 236 ++++++++++++++++++ pyaedt/modeler/cad/components_3d.py | 72 +++++- 7 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 _unittest/example_models/T08/Layoutcomponent_231.aedbcomp diff --git a/.github/workflows/ironpython.yml b/.github/workflows/ironpython.yml index 70f1b6ea4e7..658d948108f 100644 --- a/.github/workflows/ironpython.yml +++ b/.github/workflows/ironpython.yml @@ -31,6 +31,7 @@ jobs: Set-Item -Path env:ANSYSEM_FEATURE_SF159726_SCRIPTOBJECT_ENABLE -Value "1" Set-Item -Path env:ANSYSEM_FEATURE_SF222134_CABLE_MODELING_ENHANCEMENTS_ENABLE -Value "1" Set-Item -Path env:ANSYSEM_FEATURE_F395486_RIGID_FLEX_BENDING_ENABLE -Value "1" + Set-Item -Path env:ANSYSEM_FEATURE_S432616_LAYOUT_COMPONENT_IN_3D_ENABLE -Value "1" $processA = start-process 'cmd' -ArgumentList '/c .\_unittest_ironpython\run_unittests_batchmode.cmd --test-filter test_0*.py' -PassThru $processB = start-process 'cmd' -ArgumentList '/c .\_unittest_ironpython\run_unittests_batchmode.cmd --test-filter test_1*.py' -PassThru $processC = start-process 'cmd' -ArgumentList '/c .\_unittest_ironpython\run_unittests_batchmode.cmd --test-filter test_2*.py' -PassThru diff --git a/_unittest/example_models/T08/Layoutcomponent_231.aedbcomp b/_unittest/example_models/T08/Layoutcomponent_231.aedbcomp new file mode 100644 index 0000000000000000000000000000000000000000..6817d49bdf91a9169bc09f25e7ebbe389b22a7b6 GIT binary patch literal 14861 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 "2022.2": assembly = "assembly_231" @@ -57,6 +58,7 @@ def setup_class(self): self.flatten = BasisTest.add_app(self, project_name=components_flatten, subfolder=test_subfolder) test_54b_project = os.path.join(local_path, "example_models", test_subfolder, polyline + ".aedt") self.test_54b_project = self.local_scratch.copyfile(test_54b_project) + self.layout_component = os.path.join(local_path, "example_models", test_subfolder, layout_comp) def teardown_class(self): BasisTest.my_teardown(self) @@ -1731,3 +1733,29 @@ def test_84_replace_3dcomponent(self): object_list=[box2.name], ) assert len(self.aedtapp.modeler.user_defined_components) == 2 + + @pytest.mark.skipif( + config["desktopVersion"] < "2023.1" or is_ironpython, reason="Method available in beta from 2023.1" + ) + def test_85_insert_layoutcomponent(self): + self.aedtapp.insert_design("LayoutComponent") + self.aedtapp.solution_type = "Modal" + assert not self.aedtapp.modeler.insert_layout_component( + self.layout_component, name=None, parameter_mapping=False + ) + self.aedtapp.solution_type = "Terminal" + comp = self.aedtapp.modeler.insert_layout_component(self.layout_component, name=None, parameter_mapping=False) + assert isinstance(comp, UserDefinedComponent) + assert len(self.aedtapp.modeler.modeler.user_defined_components[comp.name].parts) == 3 + comp2 = self.aedtapp.modeler.insert_layout_component( + self.layout_component, name="new_layout", parameter_mapping=True + ) + assert isinstance(comp2, UserDefinedComponent) + assert len(comp2.parameters) == 2 + assert comp2.show_layout + comp2.show_layout = False + assert not comp2.show_layout + comp2.show_layout = True + assert comp2.fast_transformation + comp2.fast_transformation = False + assert not comp2.fast_transformation diff --git a/pyaedt/__init__.py b/pyaedt/__init__.py index dc6add8bd8e..7377bc9999b 100644 --- a/pyaedt/__init__.py +++ b/pyaedt/__init__.py @@ -8,6 +8,7 @@ os.environ["ANSYSEM_FEATURE_SF159726_SCRIPTOBJECT_ENABLE"] = "1" os.environ["ANSYSEM_FEATURE_SF222134_CABLE_MODELING_ENHANCEMENTS_ENABLE"] = "1" os.environ["ANSYSEM_FEATURE_F395486_RIGID_FLEX_BENDING_ENABLE"] = "1" +os.environ["ANSYSEM_FEATURE_S432616_LAYOUT_COMPONENT_IN_3D_ENABLE"] = "1" pyaedt_path = os.path.dirname(__file__) diff --git a/pyaedt/generic/general_methods.py b/pyaedt/generic/general_methods.py index f5e75eda04b..0e646753dbf 100644 --- a/pyaedt/generic/general_methods.py +++ b/pyaedt/generic/general_methods.py @@ -1745,6 +1745,7 @@ def __init__(self): "ANSYSEM_FEATURE_SF159726_SCRIPTOBJECT_ENABLE": "1", "ANSYSEM_FEATURE_SF222134_CABLE_MODELING_ENHANCEMENTS_ENABLE": "1", "ANSYSEM_FEATURE_F395486_RIGID_FLEX_BENDING_ENABLE": "1", + "ANSYSEM_FEATURE_S432616_LAYOUT_COMPONENT_IN_3D_ENABLE": "1", } if is_linux: self._aedt_environment_variables["ANS_NODEPCHECK"] = "1" diff --git a/pyaedt/modeler/cad/Primitives3D.py b/pyaedt/modeler/cad/Primitives3D.py index 6f0b151e6a3..de3ed7e9ed7 100644 --- a/pyaedt/modeler/cad/Primitives3D.py +++ b/pyaedt/modeler/cad/Primitives3D.py @@ -11,6 +11,7 @@ from math import tan import os +from pyaedt import Edb from pyaedt import Icepak from pyaedt.generic import LoadAEDTFile from pyaedt.generic.general_methods import _retry_ntimes @@ -1426,6 +1427,241 @@ def insert_3d_component( else: return udm_obj + @pyaedt_function_handler() + def insert_layout_component( + self, + comp_file, + coordinate_system="Global", + name=None, + parameter_mapping=False, + ): + """Insert a new layout component. + + Parameters + ---------- + comp_file : str + Path of the component file. Either ``".aedb"`` and ``".aedbcomp"`` are allowed. + coordinate_system : str, optional + Target coordinate system. The default is ``"Global"``. + name : str, optional + 3D component name. The default is ``None``. + parameter_mapping : bool, optional + Map the layout parameters in the target HFSS design. The default is ``False``. + + Returns + ------- + :class:`pyaedt.modeler.components_3d.UserDefinedComponent` + User defined component object. + + References + ---------- + + >>> oEditor.InsertNativeComponent + + Examples + -------- + >>> from pyaedt import Hfss + >>> app = Hfss() + >>> layout_component = "path/to/layout_component/component.aedbcomp" + >>> comp = app.modeler.insert_layout_component(layout_component) + + """ + if self._app.solution_type != "Terminal" and self._app.solution_type != "TransientAPhiFormulation": + self.logger.warning("Solution type must be terminal in HFSS or APhi in Maxwell") + return False + + component_name = os.path.splitext(os.path.basename(comp_file))[0] + aedt_component_name = component_name + if component_name not in self._app.o_component_manager.GetNames(): + compInfo = ["NAME:" + str(component_name), "Info:=", []] + + compInfo.extend( + [ + "CircuitEnv:=", + 0, + "Refbase:=", + "U", + "NumParts:=", + 1, + "ModSinceLib:=", + True, + "Terminal:=", + [], + "CompExtID:=", + 9, + "ModelEDBFilePath:=", + comp_file, + "EDBCompPassword:=", + "", + ] + ) + + aedt_component_name = self._app.o_component_manager.Add(compInfo) + + if not name or name in self.modeler.user_defined_component_names: + name = generate_unique_name("LC") + + # Open Layout component and get information + aedb_component_path = comp_file + if os.path.splitext(os.path.basename(comp_file))[1] == ".aedbcomp": + aedb_project_path = os.path.join(self._app.project_path, self._app.project_name + ".aedb") + aedb_component_path = os.path.join( + aedb_project_path, "LayoutComponents", aedt_component_name, aedt_component_name + ".aedb" + ) + aedb_component_path = aedb_component_path.replace("/", "\\") + + component_obj = Edb( + edbpath=aedb_component_path, + isreadonly=True, + edbversion=self._app._aedt_version, + student_version=self._app.student_version, + ) + + # Extract and map parameters + parameters = {} + for param in component_obj.design_variables: + parameters[param] = [param + "_" + name, component_obj.design_variables[param].value_string] + if parameter_mapping: + self._app[param + "_" + name] = component_obj.design_variables[param].value_string + + # Get coordinate systems + component_cs = list(component_obj.components.components.keys()) + component_obj.close_edb() + + vArg1 = ["NAME:InsertNativeComponentData"] + vArg1.append("TargetCS:=") + vArg1.append(coordinate_system) + vArg1.append("SubmodelDefinitionName:=") + vArg1.append("LC") + varg2 = ["NAME:ComponentPriorityLists"] + vArg1.append(varg2) + vArg1.append("NextUniqueID:=") + vArg1.append(0) + vArg1.append("MoveBackwards:=") + vArg1.append(False) + vArg1.append("DatasetType:=") + vArg1.append("ComponentDatasetType") + varg3 = ["NAME:DatasetDefinitions"] + vArg1.append(varg3) + varg4 = [ + "NAME:BasicComponentInfo", + "ComponentName:=", + "LC", + "Company:=", + "", + "Company URL:=", + "", + "Model Number:=", + "", + "Help URL:=", + "", + "Version:=", + "1.0", + "Notes:=", + "", + "IconTypeL:=", + "Layout Component", + ] + vArg1.append(varg4) + varg5 = [ + "NAME:GeometryDefinitionParameters", + ] + if parameters and parameter_mapping: + for param in parameters: + varg5.append("VariableProp:=") + varg5.append([parameters[param][0], "D", "", parameters[param][1]]) + + varg5.append(["NAME:VariableOrders"]) + vArg1.append(varg5) + + varg6 = ["NAME:DesignDefinitionParameters", ["NAME:VariableOrders"]] + vArg1.append(varg6) + + varg7 = ["NAME:MaterialDefinitionParameters", ["NAME:VariableOrders"]] + vArg1.append(varg7) + + vArg1.append("DefReferenceCSID:=") + vArg1.append(1) + vArg1.append("MapInstanceParameters:=") + vArg1.append("DesignVariable") + vArg1.append("UniqueDefinitionIdentifier:=") + vArg1.append("") + vArg1.append("OriginFilePath:=") + vArg1.append("") + vArg1.append("IsLocal:=") + vArg1.append(False) + vArg1.append("ChecksumString:=") + vArg1.append("") + vArg1.append("ChecksumHistory:=") + vArg1.append([]) + vArg1.append("VersionHistory:=") + vArg1.append([]) + + varg8 = ["NAME:VariableMap"] + + for param in parameters: + varg8.append(param + ":=") + if parameter_mapping: + varg8.append(parameters[param][0]) + else: + varg8.append(parameters[param][1]) + + varg9 = [ + "NAME:NativeComponentDefinitionProvider", + "Type:=", + "Layout Component", + "Unit:=", + "mm", + "Version:=", + 1.1, + "EDBDefinition:=", + aedt_component_name, + varg8, + "ReferenceCS:=", + "Global", + "CSToImport:=", + ] + + if component_cs: + varg10 = component_cs + varg10.append("Global") + else: + varg10 = ["Global"] + varg9.append(varg10) + vArg1.append(varg9) + + varg11 = ["NAME:InstanceParameters"] + varg11.append("GeometryParameters:=") + + if parameters and parameter_mapping: + varg12 = "" + for param in parameters: + varg12 += " {0}='{1}'".format(parameters[param][0], parameters[param][0]) + else: + varg12 = "" + varg11.append(varg12[1:]) + + varg11.append("MaterialParameters:=") + varg11.append("") + varg11.append("DesignParameters:=") + varg11.append("") + vArg1.append(varg11) + + try: + new_object_name = self.oeditor.InsertNativeComponent(vArg1) + udm_obj = False + if new_object_name: + obj_list = list(self.oeditor.Get3DComponentPartNames(new_object_name)) + for new_name in obj_list: + self._create_object(new_name) + + udm_obj = self._create_user_defined_component(new_object_name) + if name: + udm_obj.name = name + except Exception: # pragma: no cover + udm_obj = False + return udm_obj + @pyaedt_function_handler() def get_3d_component_object_list(self, componentname): """Retrieve all objects belonging to a 3D component. diff --git a/pyaedt/modeler/cad/components_3d.py b/pyaedt/modeler/cad/components_3d.py index fb583934572..1fb8186dbd3 100644 --- a/pyaedt/modeler/cad/components_3d.py +++ b/pyaedt/modeler/cad/components_3d.py @@ -102,6 +102,8 @@ def __init__(self, primitives, name=None, props=None, component_type=None): self._group_name = None self._is3dcomponent = None self._mesh_assembly = None + self._show_layout = None + self._fast_transformation = None if name: self._m_name = name else: @@ -123,7 +125,7 @@ def __init__(self, primitives, name=None, props=None, component_type=None): OrderedDict( { "TargetCS": self._target_coordinate_system, - "SubmodelDefinitionName": component, + "SubmodelDefinitionName": self.definition_name, "ComponentPriorityLists": OrderedDict({}), "NextUniqueID": 0, "MoveBackwards": False, @@ -131,7 +133,7 @@ def __init__(self, primitives, name=None, props=None, component_type=None): "DatasetDefinitions": OrderedDict({}), "BasicComponentInfo": OrderedDict( { - "ComponentName": component, + "ComponentName": self.definition_name, "Company": "", "Company URL": "", "Model Number": "", @@ -300,6 +302,66 @@ def mesh_assembly(self, ma): self._primitives.oeditor.GetChildObject(self.name).SetPropValue(key, ma) self._mesh_assembly = ma + @property + def show_layout(self): + """Show layout flag. + + Returns + ------- + bool + ``True`` if show layout is checked and if the component is a Layout Component, + ``None`` if other cases. + + """ + key = "Show Layout" + if self.is3dcomponent and key in self._primitives.oeditor.GetChildObject(self.name).GetPropNames(): + show_layout = self._primitives.oeditor.GetChildObject(self.name).GetPropValue(key) + self._show_layout = show_layout + return show_layout + else: + return None + + @show_layout.setter + def show_layout(self, show_layout): + key = "Show Layout" + if ( + self.is3dcomponent + and isinstance(show_layout, bool) + and key in self._primitives.oeditor.GetChildObject(self.name).GetPropNames() + ): + self._primitives.oeditor.GetChildObject(self.name).SetPropValue(key, show_layout) + self._show_layout = show_layout + + @property + def fast_transformation(self): + """Show layout flag. + + Returns + ------- + bool + ``True`` if fast transformation is checked and if the component is a Layout Component, + ``None`` if other cases. + + """ + key = "Fast Transformation" + if self.is3dcomponent and key in self._primitives.oeditor.GetChildObject(self.name).GetPropNames(): + fast_transformation = self._primitives.oeditor.GetChildObject(self.name).GetPropValue(key) + self._fast_transformation = fast_transformation + return fast_transformation + else: + return None + + @fast_transformation.setter + def fast_transformation(self, fast_transformation): + key = "Fast Transformation" + if ( + self.is3dcomponent + and isinstance(fast_transformation, bool) + and key in self._primitives.oeditor.GetChildObject(self.name).GetPropNames() + ): + self._primitives.oeditor.GetChildObject(self.name).SetPropValue(key, fast_transformation) + self._fast_transformation = fast_transformation + @property def name(self): """Name of the object. @@ -378,7 +440,11 @@ def parts(self): :class:`pyaedt.modeler.Object3d` """ - component_parts = list(self._primitives.oeditor.GetChildObject(self.name).GetChildNames()) + if self.is3dcomponent: + component_parts = list(self._primitives.oeditor.Get3DComponentPartNames(self.name)) + else: + component_parts = list(self._primitives.oeditor.GetChildObject(self.name).GetChildNames()) + parts_id = [ self._primitives._object_names_to_ids[part] for part in self._primitives._object_names_to_ids