diff --git a/.github/labeler.yml b/.github/labeler.yml index 1e3b030048..b1d0b71830 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -20,4 +20,4 @@ testing: grpc-transition: - changed-files: - - any-glob-to-any-file: ['src/pyedb/dotnet/edb_core/*',] + - any-glob-to-any-file: ['src/pyedb/dotnet/database/*',] diff --git a/codecov.yml b/codecov.yml index 1f7fdf5cd4..0a2bc5b179 100644 --- a/codecov.yml +++ b/codecov.yml @@ -15,5 +15,5 @@ coverage: ignore: - "examples" # ignore folders and all its contents - "tests" # ignore folders and all its contents - - "src/pyedb/legacy/edb_core/siwave.py" # ignore folders and all its contents + - "src/pyedb/legacy/database/siwave.py" # ignore folders and all its contents - "src/pyedb/misc/*.py" # ignore folders and all its contents diff --git a/doc/source/api/CoreEdb.rst b/doc/source/api/CoreEdb.rst index d93a829421..42bbf3609e 100644 --- a/doc/source/api/CoreEdb.rst +++ b/doc/source/api/CoreEdb.rst @@ -31,7 +31,7 @@ This section lists the core EDB modules for reading and writing information to AEDB files. -.. currentmodule:: pyedb.dotnet.edb_core +.. currentmodule:: pyedb.dotnet.database .. autosummary:: :toctree: _autosummary diff --git a/doc/source/api/SimulationConfigurationEdb.rst b/doc/source/api/SimulationConfigurationEdb.rst index 48e914b890..30a1f4b5a3 100644 --- a/doc/source/api/SimulationConfigurationEdb.rst +++ b/doc/source/api/SimulationConfigurationEdb.rst @@ -3,7 +3,7 @@ Simulation configuration These classes are the containers of simulation configuration constructors for the EDB. -.. currentmodule:: pyedb.dotnet.edb_core.edb_data.simulation_configuration +.. currentmodule:: pyedb.dotnet.database.edb_data.simulation_configuration .. autosummary:: :toctree: _autosummary diff --git a/examples/legacy_standalone/10_GDS_workflow.py b/examples/legacy_standalone/10_GDS_workflow.py index 13525ee49b..53443bfe52 100644 --- a/examples/legacy_standalone/10_GDS_workflow.py +++ b/examples/legacy_standalone/10_GDS_workflow.py @@ -11,7 +11,7 @@ import tempfile import pyedb -from pyedb.dotnet.edb_core.edb_data.control_file import ControlFile +from pyedb.dotnet.database.edb_data.control_file import ControlFile from pyedb.misc.downloads import download_file # - diff --git a/pyproject.toml b/pyproject.toml index c478aa980d..d8a46b036c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,10 @@ dependencies = [ "pydantic>=2.6.4,<2.11", "Rtree >= 1.2.0", "toml == 0.10.2", - "scikit-rf" + "scikit-rf", + "ansys-edb-core", + "ansys-api-edb", + "psutil", ] [project.optional-dependencies] diff --git a/src/pyedb/common/nets.py b/src/pyedb/common/nets.py index dec1a6d3d1..c1c81713be 100644 --- a/src/pyedb/common/nets.py +++ b/src/pyedb/common/nets.py @@ -1,44 +1,9 @@ import math -import os import time from pyedb.generic.constants import CSS4_COLORS -def is_notebook(): - """Check if pyaedt is running in Jupyter or not. - - Returns - ------- - bool - """ - try: - shell = get_ipython().__class__.__name__ - if shell in ["ZMQInteractiveShell"]: # pragma: no cover - return True # Jupyter notebook or qtconsole - else: - return False - except NameError: - return False # Probably standard Python interpreter - - -def is_ipython(): - """Check if pyaedt is running in Jupyter or not. - - Returns - ------- - bool - """ - try: - shell = get_ipython().__class__.__name__ - if shell in ["TerminalInteractiveShell", "SpyderShell"]: - return True # Jupyter notebook or qtconsole - else: # pragma: no cover - return False - except NameError: - return False # Probably standard Python interpreter - - class CommonNets: def __init__(self, _pedb): self._pedb = _pedb @@ -57,8 +22,6 @@ def plot( show=True, annotate_component_names=True, plot_vias=False, - include_outline=True, - plot_edges=True, **kwargs, ): """Plot a Net to Matplotlib 2D Chart. @@ -79,7 +42,7 @@ def plot( If a path is specified the plot will be saved in this location. If ``save_plot`` is provided, the ``show`` parameter is ignored. outline : list, optional - Add a customer outline from a list of points of the outline to plot. + List of points of the outline to plot. size : tuple, int, optional Image size in pixel (width, height). Default value is ``(6000, 3000)`` top_view : bool, optional @@ -96,10 +59,6 @@ def plot( Default is ``False``. show : bool, optional Whether to show the plot or not. Default is `True`. - include_outline : bool, optional - Whether to include the internal layout outline or not. Default is `True`. - plot_edges : bool, optional - Whether to plot polygon edges or not. Default is `True`. Returns ------- @@ -118,30 +77,22 @@ def mirror_poly(poly): sign = -1 return [[sign * i[0], i[1]] for i in poly] - try: - import matplotlib.pyplot as plt - except ImportError: # pragma: no cover - self._pedb.logger.error("Matplotlib is needed. Please, install it first.") - return False + import matplotlib.pyplot as plt dpi = 100.0 figsize = (size[0] / dpi, size[1] / dpi) fig = plt.figure(figsize=figsize) ax = fig.add_subplot(1, 1, 1) - try: - from shapely import affinity, union_all - from shapely.geometry import ( - LinearRing, - MultiLineString, - MultiPolygon, - Point, - Polygon, - ) - from shapely.plotting import plot_line, plot_polygon - except ImportError: # pragma: no cover - self._pedb.logger.error("Shapely is needed. Please, install it first.") - return False + from shapely import affinity + from shapely.geometry import ( + LinearRing, + MultiLineString, + MultiPolygon, + Point, + Polygon, + ) + from shapely.plotting import plot_line, plot_polygon start_time = time.time() if not nets: @@ -154,33 +105,19 @@ def mirror_poly(poly): layers = [layers] color_index = 0 label_colors = {} - edge_colors = {} if outline: poly = Polygon(outline) plot_line(poly.boundary, add_points=False, color=(0.7, 0, 0), linewidth=4) - elif include_outline: - prims = self._pedb.modeler.primitives_by_layer.get("Outline", []) - if prims: - for prim in prims: - if prim.is_void: - continue - xt, yt = prim.points() - p1 = [(i, j) for i, j in zip(xt[::-1], yt[::-1])] - p1 = mirror_poly(p1) - poly = LinearRing(p1) - plot_line(poly, add_points=False, color=(0.7, 0, 0), linewidth=4) - else: - bbox = self._pedb.hfss.get_layout_bounding_box() - if not bbox: - return False, False - x1 = bbox[0] - x2 = bbox[2] - y1 = bbox[1] - y2 = bbox[3] - p = [(x1, y1), (x1, y2), (x2, y2), (x2, y1), (x1, y1)] - p = mirror_poly(p) - poly = LinearRing(p) - plot_line(poly, add_points=False, color=(0.7, 0, 0), linewidth=4) + else: + bbox = self._pedb.hfss.get_layout_bounding_box() + x1 = bbox[0] + x2 = bbox[2] + y1 = bbox[1] + y2 = bbox[3] + p = [(x1, y1), (x1, y2), (x2, y2), (x2, y1), (x1, y1)] + p = mirror_poly(p) + poly = LinearRing(p) + plot_line(poly, add_points=False, color=(0.7, 0, 0), linewidth=4) layer_colors = {i: k.color for i, k in self._pedb.stackup.layers.items()} top_layer = list(self._pedb.stackup.signal_layers.keys())[0] bottom_layer = list(self._pedb.stackup.signal_layers.keys())[-1] @@ -310,24 +247,14 @@ def create_poly(prim, polys, lines): # poly = LineString(line).buffer(prim.width / 2) # else: xt, yt = prim.points() - if len(xt) < 3: - return p1 = [(i, j) for i, j in zip(xt[::-1], yt[::-1])] p1 = mirror_poly(p1) holes = [] for void in prim.voids: xvt, yvt = void.points(arc_segments=3) - if len(xvt) < 3: - continue h1 = mirror_poly([(i, j) for i, j in zip(xvt, yvt)]) holes.append(h1) - if len(holes) > 1: - holes = union_all([Polygon(i) for i in holes]) - if isinstance(holes, MultiPolygon): - holes = [i.boundary for i in list(holes.geoms)] - else: - holes = [holes.boundary] poly = Polygon(p1, holes) if layer_name == "Outline": if label_colors[label] in lines: @@ -338,36 +265,33 @@ def create_poly(prim, polys, lines): if color_by_net: for net in nets: - prims = self._pedb.nets.nets[net].primitives - polys = [] - lines = [] - if net not in nets: - continue - label = "Net " + net - label_colors[label] = list(CSS4_COLORS.keys())[color_index] - try: - edge_colors[label] = [i * 0.5 for i in label_colors[label]] - except TypeError: - edge_colors[label] = label_colors[label] - color_index += 1 - if color_index >= len(CSS4_COLORS): - color_index = 0 - for prim in prims: - create_poly(prim, polys, lines) - if polys: - ob = MultiPolygon(polys) - plot_polygon( - ob, - ax=ax, - color=label_colors[label], - add_points=False, - alpha=0.7, - label=label, - edgecolor="none" if not plot_edges else edge_colors[label], - ) - if lines: - ob = MultiLineString(p) - plot_line(ob, ax=ax, add_points=False, color=label_colors[label], linewidth=1, label=label) + if net in self._pedb.nets.nets: + prims = self._pedb.nets.nets[net].primitives + polys = [] + lines = [] + if net not in nets: + continue + label = "Net " + net + label_colors[label] = list(CSS4_COLORS.keys())[color_index] + color_index += 1 + if color_index >= len(CSS4_COLORS): + color_index = 0 + for prim in prims: + create_poly(prim, polys, lines) + if polys: + ob = MultiPolygon(polys) + plot_polygon( + ob, + ax=ax, + color=label_colors[label], + add_points=False, + alpha=0.7, + label=label, + edgecolor="none", + ) + if lines: + ob = MultiLineString(p) + plot_line(ob, ax=ax, add_points=False, color=label_colors[label], linewidth=1, label=label) else: prims_by_layers_dict = {i: j for i, j in self._pedb.modeler.primitives_by_layer.items()} if not top_view: @@ -397,10 +321,6 @@ def create_poly(prim, polys, lines): if color_index >= len(CSS4_COLORS): color_index = 0 label_colors[label] = c - try: - edge_colors[label] = [i * 0.5 for i in c] - except TypeError: - edge_colors[label] = label_colors[label] for prim in prims: create_poly(prim, polys, lines) if polys: @@ -412,7 +332,7 @@ def create_poly(prim, polys, lines): add_points=False, alpha=alpha, label=label, - edgecolor="none" if not plot_edges else edge_colors[label], + edgecolor="none", ) if lines: ob = MultiLineString(p) @@ -480,15 +400,10 @@ def create_poly(prim, polys, lines): plt.title(message, size=20) if show_legend: plt.legend(loc="upper left", fontsize="x-large") - end_time = time.time() - start_time - self._logger.info(f"Plot Generation time {round(end_time, 3)}") if save_plot: plt.savefig(save_plot) - if show: # pragma: no cover - if is_notebook(): - pass - elif is_ipython() or "PYTEST_CURRENT_TEST" in os.environ: - fig.show() - else: - plt.show() + elif show: + plt.show() + end_time = time.time() - start_time + self._logger.info(f"Plot Generation time {round(end_time, 3)}") return fig, ax diff --git a/src/pyedb/configuration/cfg_components.py b/src/pyedb/configuration/cfg_components.py index b3c371b43e..6b1be46121 100644 --- a/src/pyedb/configuration/cfg_components.py +++ b/src/pyedb/configuration/cfg_components.py @@ -21,7 +21,7 @@ # SOFTWARE. from pyedb.configuration.cfg_common import CfgBase -from pyedb.dotnet.edb_core.general import pascal_to_snake, snake_to_pascal +from pyedb.dotnet.database.general import pascal_to_snake, snake_to_pascal class CfgComponent(CfgBase): diff --git a/src/pyedb/configuration/cfg_general.py b/src/pyedb/configuration/cfg_general.py index 4665ec4bf9..8601803513 100644 --- a/src/pyedb/configuration/cfg_general.py +++ b/src/pyedb/configuration/cfg_general.py @@ -32,8 +32,10 @@ def __init__(self, pedb, data): self.suppress_pads = data.get("suppress_pads", True) def apply(self): - self._pedb.design_options.antipads_always_on = self.anti_pads_always_on - self._pedb.design_options.suppress_pads = self.suppress_pads + # TODO check if design_options features exists in grpc + # self._pedb.design_options.antipads_always_on = self.anti_pads_always_on + # self._pedb.design_options.suppress_pads = self.suppress_pads + pass def get_data_from_db(self): self.anti_pads_always_on = self._pedb.design_options.antipads_always_on diff --git a/src/pyedb/configuration/cfg_modeler.py b/src/pyedb/configuration/cfg_modeler.py index 0ba257a2d9..a26e0149d3 100644 --- a/src/pyedb/configuration/cfg_modeler.py +++ b/src/pyedb/configuration/cfg_modeler.py @@ -22,7 +22,7 @@ from pyedb.configuration.cfg_components import CfgComponent from pyedb.configuration.cfg_padstacks import CfgPadstackDefinition, CfgPadstackInstance -from pyedb.dotnet.edb_core.edb_data.padstacks_data import EDBPadstack +from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstack class CfgTrace: diff --git a/src/pyedb/configuration/cfg_package_definition.py b/src/pyedb/configuration/cfg_package_definition.py index bf8fa26b4d..68dc735955 100644 --- a/src/pyedb/configuration/cfg_package_definition.py +++ b/src/pyedb/configuration/cfg_package_definition.py @@ -21,7 +21,7 @@ # SOFTWARE. from pyedb.configuration.cfg_common import CfgBase -from pyedb.dotnet.edb_core.definition.package_def import PackageDef +from pyedb.dotnet.database.definition.package_def import PackageDef class CfgPackage(CfgBase): diff --git a/src/pyedb/configuration/cfg_padstacks.py b/src/pyedb/configuration/cfg_padstacks.py index 062c9b99e4..c30c2901af 100644 --- a/src/pyedb/configuration/cfg_padstacks.py +++ b/src/pyedb/configuration/cfg_padstacks.py @@ -21,7 +21,7 @@ # SOFTWARE. from pyedb.configuration.cfg_common import CfgBase -from pyedb.dotnet.edb_core.general import ( +from pyedb.dotnet.database.general import ( convert_py_list_to_net_list, pascal_to_snake, snake_to_pascal, diff --git a/src/pyedb/configuration/cfg_ports_sources.py b/src/pyedb/configuration/cfg_ports_sources.py index 6e58f8d097..b9bf3a6ff9 100644 --- a/src/pyedb/configuration/cfg_ports_sources.py +++ b/src/pyedb/configuration/cfg_ports_sources.py @@ -21,9 +21,9 @@ # SOFTWARE. from pyedb.configuration.cfg_common import CfgBase -from pyedb.dotnet.edb_core.edb_data.ports import WavePort -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list -from pyedb.dotnet.edb_core.geometry.point_data import PointData +from pyedb.dotnet.database.edb_data.ports import WavePort +from pyedb.dotnet.database.general import convert_py_list_to_net_list +from pyedb.dotnet.database.geometry.point_data import PointData class CfgTerminalInfo(CfgBase): diff --git a/src/pyedb/configuration/configuration.py b/src/pyedb/configuration/configuration.py index 21c41d6669..a817bf990d 100644 --- a/src/pyedb/configuration/configuration.py +++ b/src/pyedb/configuration/configuration.py @@ -27,7 +27,7 @@ import toml from pyedb.configuration.cfg_data import CfgData -from pyedb.dotnet.edb_core.definition.package_def import PackageDef +from pyedb.dotnet.database.definition.package_def import PackageDef class Configuration: diff --git a/src/pyedb/dotnet/application/Variables.py b/src/pyedb/dotnet/database/Variables.py similarity index 98% rename from src/pyedb/dotnet/application/Variables.py rename to src/pyedb/dotnet/database/Variables.py index 694fc66be5..59a9ccc909 100644 --- a/src/pyedb/dotnet/application/Variables.py +++ b/src/pyedb/dotnet/database/Variables.py @@ -368,7 +368,7 @@ class VariableManager(object): This class provides access to all variables or a subset of the variables. Manipulation of the numerical or string definitions of variable values is provided in the - :class:`pyedb.dotnet.application.Variables.Variable` class. + :class:`pyedb.dotnet.database.Variables.Variable` class. Parameters ---------- @@ -410,7 +410,7 @@ class VariableManager(object): See Also -------- - pyedb.dotnet.application.Variables.Variable + pyedb.dotnet.database.Variables.Variable Examples -------- @@ -434,23 +434,23 @@ class VariableManager(object): Get a dictionary of all project and design variables. >>> v.variables - {'Var1': , - 'Var2': , - 'Var3': , - '$PrjVar1': } + {'Var1': , + 'Var2': , + 'Var3': , + '$PrjVar1': } Get a dictionary of only the design variables. >>> v.design_variables - {'Var1': , - 'Var2': , - 'Var3': } + {'Var1': , + 'Var2': , + 'Var3': } Get a dictionary of only the independent design variables. >>> v.independent_design_variables - {'Var1': , - 'Var2': } + {'Var1': , + 'Var2': } """ @@ -1287,7 +1287,7 @@ class Variable(object): Examples -------- - >>> from pyedb.dotnet.application.Variables import Variable + >>> from pyedb.dotnet.database.Variables import Variable Define a variable using a string value consistent with the AEDT properties. @@ -1719,7 +1719,7 @@ def rescale_to(self, units): # pragma: no cover Examples -------- - >>> from pyedb.dotnet.application.Variables import Variable + >>> from pyedb.dotnet.database.Variables import Variable >>> v = Variable("10W") >>> assert v.numeric_value == 10 @@ -1752,7 +1752,7 @@ def format(self, format): # pragma: no cover Examples -------- - >>> from pyedb.dotnet.application.Variables import Variable + >>> from pyedb.dotnet.database.Variables import Variable >>> v = Variable("10W") >>> assert v.format("f") == '10.000000W' @@ -1777,7 +1777,7 @@ def __mul__(self, other): # pragma: no cover Examples -------- - >>> from pyedb.dotnet.application.Variables import Variable + >>> from pyedb.dotnet.database.Variables import Variable Multiply ``'Length1'`` by unitless ``'None'``` to obtain ``'Length'``. A numerical value is also considered to be unitless. @@ -1827,7 +1827,7 @@ def __add__(self, other): # pragma: no cover Parameters ---------- - other : class:`pyedb.dotnet.application.Variables.Variable` + other : class:`pyedb.dotnet.database.Variables.Variable` Object to be multiplied. Returns @@ -1837,7 +1837,7 @@ def __add__(self, other): # pragma: no cover Examples -------- - >>> from pyedb.dotnet.application.Variables import Variable + >>> from pyedb.dotnet.database.Variables import Variable >>> import ansys.aedt.core.generic.constants >>> v1 = Variable("3mA") >>> v2 = Variable("10A") @@ -1867,7 +1867,7 @@ def __sub__(self, other): # pragma: no cover Parameters ---------- - other : class:`pyedb.dotnet.application.Variables.Variable` + other : class:`pyedb.dotnet.database.Variables.Variable` Object to be subtracted. Returns @@ -1879,7 +1879,7 @@ def __sub__(self, other): # pragma: no cover -------- >>> import ansys.aedt.core.generic.constants - >>> from pyedb.dotnet.application.Variables import Variable + >>> from pyedb.dotnet.database.Variables import Variable >>> v3 = Variable("3mA") >>> v4 = Variable("10A") >>> result_2 = v3 - v4 @@ -1923,7 +1923,7 @@ def __truediv__(self, other): # pragma: no cover Divide a variable with units ``"W"`` by a variable with units ``"V"`` and automatically resolve the new units to ``"A"``. - >>> from pyedb.dotnet.application.Variables import Variable + >>> from pyedb.dotnet.database.Variables import Variable >>> import ansys.aedt.core.generic.constants >>> v1 = Variable("10W") >>> v2 = Variable("40V") @@ -1967,7 +1967,7 @@ def __rtruediv__(self, other): # pragma: no cover the result is in ``"Hz"``. >>> import ansys.aedt.core.generic.constants - >>> from pyedb.dotnet.application.Variables import Variable + >>> from pyedb.dotnet.database.Variables import Variable >>> v = Variable("1s") >>> result = 3.0 / v >>> assert result.numeric_value == 3.0 diff --git a/src/pyedb/dotnet/edb_core/__init__.py b/src/pyedb/dotnet/database/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/__init__.py rename to src/pyedb/dotnet/database/__init__.py diff --git a/src/pyedb/dotnet/application/__init__.py b/src/pyedb/dotnet/database/cell/__init__.py similarity index 100% rename from src/pyedb/dotnet/application/__init__.py rename to src/pyedb/dotnet/database/cell/__init__.py diff --git a/src/pyedb/dotnet/edb_core/cell/connectable.py b/src/pyedb/dotnet/database/cell/connectable.py similarity index 88% rename from src/pyedb/dotnet/edb_core/cell/connectable.py rename to src/pyedb/dotnet/database/cell/connectable.py index 4bc3fdafc0..2d8066e1bc 100644 --- a/src/pyedb/dotnet/edb_core/cell/connectable.py +++ b/src/pyedb/dotnet/database/cell/connectable.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.cell.layout_obj import LayoutObj +from pyedb.dotnet.database.cell.layout_obj import LayoutObj class Connectable(LayoutObj): @@ -35,9 +35,9 @@ def net(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.nets_data.EDBNetsData` + :class:`pyedb.dotnet.database.edb_data.nets_data.EDBNetsData` """ - from pyedb.dotnet.edb_core.edb_data.nets_data import EDBNetsData + from pyedb.dotnet.database.edb_data.nets_data import EDBNetsData return EDBNetsData(self._edb_object.GetNet(), self._pedb) @@ -74,9 +74,9 @@ def component(self): Returns ------- - :class:`dotnet.edb_core.edb_data.nets_data.EDBComponent` + :class:`dotnet.database.edb_data.nets_data.EDBComponent` """ - from pyedb.dotnet.edb_core.cell.hierarchy.component import EDBComponent + from pyedb.dotnet.database.cell.hierarchy.component import EDBComponent edb_comp = self._edb_object.GetComponent() if edb_comp.IsNull(): diff --git a/src/pyedb/dotnet/edb_core/cell/__init__.py b/src/pyedb/dotnet/database/cell/hierarchy/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/cell/__init__.py rename to src/pyedb/dotnet/database/cell/hierarchy/__init__.py diff --git a/src/pyedb/dotnet/edb_core/cell/hierarchy/component.py b/src/pyedb/dotnet/database/cell/hierarchy/component.py similarity index 97% rename from src/pyedb/dotnet/edb_core/cell/hierarchy/component.py rename to src/pyedb/dotnet/database/cell/hierarchy/component.py index 95517f6f9f..5e8fc38ebe 100644 --- a/src/pyedb/dotnet/edb_core/cell/hierarchy/component.py +++ b/src/pyedb/dotnet/database/cell/hierarchy/component.py @@ -25,14 +25,14 @@ from typing import Optional import warnings -from pyedb.dotnet.edb_core.cell.hierarchy.hierarchy_obj import Group -from pyedb.dotnet.edb_core.cell.hierarchy.model import PinPairModel, SPICEModel -from pyedb.dotnet.edb_core.cell.hierarchy.netlist_model import NetlistModel -from pyedb.dotnet.edb_core.cell.hierarchy.pin_pair_model import PinPair -from pyedb.dotnet.edb_core.cell.hierarchy.s_parameter_model import SparamModel -from pyedb.dotnet.edb_core.cell.hierarchy.spice_model import SpiceModel -from pyedb.dotnet.edb_core.definition.package_def import PackageDef -from pyedb.dotnet.edb_core.edb_data.padstacks_data import EDBPadstackInstance +from pyedb.dotnet.database.cell.hierarchy.hierarchy_obj import Group +from pyedb.dotnet.database.cell.hierarchy.model import PinPairModel, SPICEModel +from pyedb.dotnet.database.cell.hierarchy.netlist_model import NetlistModel +from pyedb.dotnet.database.cell.hierarchy.pin_pair_model import PinPair +from pyedb.dotnet.database.cell.hierarchy.s_parameter_model import SparamModel +from pyedb.dotnet.database.cell.hierarchy.spice_model import SpiceModel +from pyedb.dotnet.database.definition.package_def import PackageDef +from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance try: import numpy as np @@ -49,7 +49,7 @@ class EDBComponent(Group): Parameters ---------- - parent : :class:`pyedb.dotnet.edb_core.components.Components` + parent : :class:`pyedb.dotnet.database.components.Components` Components object. component : object Edb Component Object @@ -170,7 +170,7 @@ def create_package_def(self, name="", component_part_name=None): self._pedb.definitions.add_package_def(name, component_part_name=component_part_name) self.package_def = name - from pyedb.dotnet.edb_core.dotnet.database import PolygonDataDotNet + from pyedb.dotnet.database.dotnet.database import PolygonDataDotNet polygon = PolygonDataDotNet(self._pedb).create_from_bbox(self.component_instance.GetBBox()) self.package_def._edb_object.SetExteriorBoundary(polygon) @@ -641,7 +641,7 @@ def pins(self): Returns ------- - dic[str, :class:`dotnet.edb_core.edb_data.definitions.EDBPadstackInstance`] + dic[str, :class:`dotnet.database.edb_data.definitions.EDBPadstackInstance`] Dictionary of EDBPadstackInstance Components. """ pins = {} diff --git a/src/pyedb/dotnet/edb_core/cell/hierarchy/hierarchy_obj.py b/src/pyedb/dotnet/database/cell/hierarchy/hierarchy_obj.py similarity index 97% rename from src/pyedb/dotnet/edb_core/cell/hierarchy/hierarchy_obj.py rename to src/pyedb/dotnet/database/cell/hierarchy/hierarchy_obj.py index 23f26ecd97..90851034f6 100644 --- a/src/pyedb/dotnet/edb_core/cell/hierarchy/hierarchy_obj.py +++ b/src/pyedb/dotnet/database/cell/hierarchy/hierarchy_obj.py @@ -22,7 +22,7 @@ import logging -from pyedb.dotnet.edb_core.cell.connectable import Connectable +from pyedb.dotnet.database.cell.connectable import Connectable class HierarchyObj(Connectable): diff --git a/src/pyedb/dotnet/edb_core/cell/hierarchy/model.py b/src/pyedb/dotnet/database/cell/hierarchy/model.py similarity index 98% rename from src/pyedb/dotnet/edb_core/cell/hierarchy/model.py rename to src/pyedb/dotnet/database/cell/hierarchy/model.py index f06909de7b..0b1faef0a1 100644 --- a/src/pyedb/dotnet/edb_core/cell/hierarchy/model.py +++ b/src/pyedb/dotnet/database/cell/hierarchy/model.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.utilities.obj_base import ObjBase +from pyedb.dotnet.database.utilities.obj_base import ObjBase class Model(ObjBase): diff --git a/src/pyedb/dotnet/edb_core/cell/hierarchy/netlist_model.py b/src/pyedb/dotnet/database/cell/hierarchy/netlist_model.py similarity index 100% rename from src/pyedb/dotnet/edb_core/cell/hierarchy/netlist_model.py rename to src/pyedb/dotnet/database/cell/hierarchy/netlist_model.py diff --git a/src/pyedb/dotnet/edb_core/cell/hierarchy/pin_pair_model.py b/src/pyedb/dotnet/database/cell/hierarchy/pin_pair_model.py similarity index 100% rename from src/pyedb/dotnet/edb_core/cell/hierarchy/pin_pair_model.py rename to src/pyedb/dotnet/database/cell/hierarchy/pin_pair_model.py diff --git a/src/pyedb/dotnet/edb_core/cell/hierarchy/s_parameter_model.py b/src/pyedb/dotnet/database/cell/hierarchy/s_parameter_model.py similarity index 100% rename from src/pyedb/dotnet/edb_core/cell/hierarchy/s_parameter_model.py rename to src/pyedb/dotnet/database/cell/hierarchy/s_parameter_model.py diff --git a/src/pyedb/dotnet/edb_core/cell/hierarchy/spice_model.py b/src/pyedb/dotnet/database/cell/hierarchy/spice_model.py similarity index 100% rename from src/pyedb/dotnet/edb_core/cell/hierarchy/spice_model.py rename to src/pyedb/dotnet/database/cell/hierarchy/spice_model.py diff --git a/src/pyedb/dotnet/edb_core/cell/layout.py b/src/pyedb/dotnet/database/cell/layout.py similarity index 91% rename from src/pyedb/dotnet/edb_core/cell/layout.py rename to src/pyedb/dotnet/database/cell/layout.py index cee9e7fb54..4ba37f8e21 100644 --- a/src/pyedb/dotnet/edb_core/cell/layout.py +++ b/src/pyedb/dotnet/database/cell/layout.py @@ -25,33 +25,33 @@ """ from typing import Union -from pyedb.dotnet.edb_core.cell.hierarchy.component import EDBComponent -from pyedb.dotnet.edb_core.cell.primitive.bondwire import Bondwire -from pyedb.dotnet.edb_core.cell.primitive.path import Path -from pyedb.dotnet.edb_core.cell.terminal.bundle_terminal import BundleTerminal -from pyedb.dotnet.edb_core.cell.terminal.edge_terminal import EdgeTerminal -from pyedb.dotnet.edb_core.cell.terminal.padstack_instance_terminal import ( +from pyedb.dotnet.database.cell.hierarchy.component import EDBComponent +from pyedb.dotnet.database.cell.primitive.bondwire import Bondwire +from pyedb.dotnet.database.cell.primitive.path import Path +from pyedb.dotnet.database.cell.terminal.bundle_terminal import BundleTerminal +from pyedb.dotnet.database.cell.terminal.edge_terminal import EdgeTerminal +from pyedb.dotnet.database.cell.terminal.padstack_instance_terminal import ( PadstackInstanceTerminal, ) -from pyedb.dotnet.edb_core.cell.terminal.pingroup_terminal import PinGroupTerminal -from pyedb.dotnet.edb_core.cell.terminal.point_terminal import PointTerminal -from pyedb.dotnet.edb_core.cell.voltage_regulator import VoltageRegulator -from pyedb.dotnet.edb_core.edb_data.nets_data import ( +from pyedb.dotnet.database.cell.terminal.pingroup_terminal import PinGroupTerminal +from pyedb.dotnet.database.cell.terminal.point_terminal import PointTerminal +from pyedb.dotnet.database.cell.voltage_regulator import VoltageRegulator +from pyedb.dotnet.database.edb_data.nets_data import ( EDBDifferentialPairData, EDBExtendedNetData, EDBNetClassData, EDBNetsData, ) -from pyedb.dotnet.edb_core.edb_data.padstacks_data import EDBPadstackInstance -from pyedb.dotnet.edb_core.edb_data.primitives_data import ( +from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance +from pyedb.dotnet.database.edb_data.primitives_data import ( EdbCircle, EdbPolygon, EdbRectangle, EdbText, ) -from pyedb.dotnet.edb_core.edb_data.sources import PinGroup -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list -from pyedb.dotnet.edb_core.utilities.obj_base import ObjBase +from pyedb.dotnet.database.edb_data.sources import PinGroup +from pyedb.dotnet.database.general import convert_py_list_to_net_list +from pyedb.dotnet.database.utilities.obj_base import ObjBase def primitive_cast(pedb, edb_object): @@ -178,7 +178,7 @@ def terminals(self): Returns ------- - Terminal dictionary : Dict[str, pyedb.dotnet.edb_core.edb_data.terminals.Terminal] + Terminal dictionary : Dict[str, pyedb.dotnet.database.edb_data.terminals.Terminal] """ temp = [] for i in list(self._edb_object.Terminals): @@ -228,7 +228,7 @@ def primitives(self): Returns ------- - list of :class:`dotnet.edb_core.dotnet.primitive.PrimitiveDotNet` cast objects. + list of :class:`dotnet.database.dotnet.primitive.PrimitiveDotNet` cast objects. """ return [primitive_cast(self._pedb, p) for p in self._edb_object.Primitives] diff --git a/src/pyedb/dotnet/edb_core/cell/layout_obj.py b/src/pyedb/dotnet/database/cell/layout_obj.py similarity index 93% rename from src/pyedb/dotnet/edb_core/cell/layout_obj.py rename to src/pyedb/dotnet/database/cell/layout_obj.py index 73adecf869..53ea0ec617 100644 --- a/src/pyedb/dotnet/edb_core/cell/layout_obj.py +++ b/src/pyedb/dotnet/database/cell/layout_obj.py @@ -20,8 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.layout_obj_instance import LayoutObjInstance -from pyedb.dotnet.edb_core.utilities.obj_base import ObjBase +from pyedb.dotnet.database.layout_obj_instance import LayoutObjInstance +from pyedb.dotnet.database.utilities.obj_base import ObjBase class LayoutObj(ObjBase): @@ -42,7 +42,7 @@ def _edb(self): @property def _layout_obj_instance(self): - """Returns :class:`dotnet.edb_core.edb_data.connectable.LayoutObjInstance`.""" + """Returns :class:`dotnet.database.edb_data.connectable.LayoutObjInstance`.""" obj = self._pedb.layout_instance.GetLayoutObjInstance(self._edb_object, None) return LayoutObjInstance(self._pedb, obj) diff --git a/src/pyedb/dotnet/edb_core/cell/primitive/__init__.py b/src/pyedb/dotnet/database/cell/primitive/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/cell/primitive/__init__.py rename to src/pyedb/dotnet/database/cell/primitive/__init__.py diff --git a/src/pyedb/dotnet/edb_core/cell/primitive/bondwire.py b/src/pyedb/dotnet/database/cell/primitive/bondwire.py similarity index 99% rename from src/pyedb/dotnet/edb_core/cell/primitive/bondwire.py rename to src/pyedb/dotnet/database/cell/primitive/bondwire.py index 9cf465de33..21fcb91e2c 100644 --- a/src/pyedb/dotnet/edb_core/cell/primitive/bondwire.py +++ b/src/pyedb/dotnet/database/cell/primitive/bondwire.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.cell.primitive.primitive import Primitive +from pyedb.dotnet.database.cell.primitive.primitive import Primitive class Bondwire(Primitive): diff --git a/src/pyedb/dotnet/edb_core/cell/primitive/path.py b/src/pyedb/dotnet/database/cell/primitive/path.py similarity index 97% rename from src/pyedb/dotnet/edb_core/cell/primitive/path.py rename to src/pyedb/dotnet/database/cell/primitive/path.py index b4921be6a7..dfbc609b61 100644 --- a/src/pyedb/dotnet/edb_core/cell/primitive/path.py +++ b/src/pyedb/dotnet/database/cell/primitive/path.py @@ -21,9 +21,9 @@ # SOFTWARE. import math -from pyedb.dotnet.edb_core.cell.primitive.primitive import Primitive -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list -from pyedb.dotnet.edb_core.geometry.point_data import PointData +from pyedb.dotnet.database.cell.primitive.primitive import Primitive +from pyedb.dotnet.database.general import convert_py_list_to_net_list +from pyedb.dotnet.database.geometry.point_data import PointData class Path(Primitive): @@ -203,7 +203,7 @@ def create_edge_port( Returns ------- - :class:`dotnet.edb_core.edb_data.sources.ExcitationPorts` + :class:`dotnet.database.edb_data.sources.ExcitationPorts` Examples -------- diff --git a/src/pyedb/dotnet/edb_core/cell/primitive/primitive.py b/src/pyedb/dotnet/database/cell/primitive/primitive.py similarity index 90% rename from src/pyedb/dotnet/edb_core/cell/primitive/primitive.py rename to src/pyedb/dotnet/database/cell/primitive/primitive.py index 023930210e..1d0f25c077 100644 --- a/src/pyedb/dotnet/edb_core/cell/primitive/primitive.py +++ b/src/pyedb/dotnet/database/cell/primitive/primitive.py @@ -20,9 +20,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.cell.connectable import Connectable -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list -from pyedb.dotnet.edb_core.geometry.polygon_data import PolygonData +from pyedb.dotnet.database.cell.connectable import Connectable +from pyedb.dotnet.database.general import convert_py_list_to_net_list +from pyedb.dotnet.database.geometry.polygon_data import PolygonData from pyedb.misc.utilities import compute_arc_points from pyedb.modeler.geometry_operators import GeometryOperators @@ -267,7 +267,7 @@ def convert_to_polygon(self): Returns ------- - bool, :class:`dotnet.edb_core.edb_data.primitives.EDBPrimitives` + bool, :class:`dotnet.database.edb_data.primitives.EDBPrimitives` Polygon when successful, ``False`` when failed. """ @@ -284,7 +284,7 @@ def intersection_type(self, primitive): Parameters ---------- - primitive : :class:`pyaeedt.edb_core.edb_data.primitives_data.EDBPrimitives` or `PolygonData` + primitive : :class:`pyaeedt.database.edb_data.primitives_data.EDBPrimitives` or `PolygonData` Returns ------- @@ -308,7 +308,7 @@ def is_intersecting(self, primitive): Parameters ---------- - primitive : :class:`pyaeedt.edb_core.edb_data.primitives_data.EDBPrimitives` or `PolygonData` + primitive : :class:`pyaeedt.database.edb_data.primitives_data.EDBPrimitives` or `PolygonData` Returns ------- @@ -354,11 +354,11 @@ def subtract(self, primitives): Parameters ---------- - primitives : :class:`dotnet.edb_core.edb_data.EDBPrimitives` or EDB PolygonData or EDB Primitive or list + primitives : :class:`dotnet.database.edb_data.EDBPrimitives` or EDB PolygonData or EDB Primitive or list Returns ------- - List of :class:`dotnet.edb_core.edb_data.EDBPrimitives` + List of :class:`dotnet.database.edb_data.EDBPrimitives` """ poly = self.primitive_object.GetPolygonData() if not isinstance(primitives, list): @@ -404,11 +404,11 @@ def intersect(self, primitives): Parameters ---------- - primitives : :class:`dotnet.edb_core.edb_data.EDBPrimitives` or EDB PolygonData or EDB Primitive or list + primitives : :class:`dotnet.database.edb_data.EDBPrimitives` or EDB PolygonData or EDB Primitive or list Returns ------- - List of :class:`dotnet.edb_core.edb_data.EDBPrimitives` + List of :class:`dotnet.database.edb_data.EDBPrimitives` """ poly = self._edb_object.GetPolygonData() if not isinstance(primitives, list): @@ -475,11 +475,11 @@ def unite(self, primitives): Parameters ---------- - primitives : :class:`dotnet.edb_core.edb_data.EDBPrimitives` or EDB PolygonData or EDB Primitive or list + primitives : :class:`dotnet.database.edb_data.EDBPrimitives` or EDB PolygonData or EDB Primitive or list Returns ------- - List of :class:`dotnet.edb_core.edb_data.EDBPrimitives` + List of :class:`dotnet.database.edb_data.EDBPrimitives` """ poly = self._edb_object.GetPolygonData() if not isinstance(primitives, list): @@ -600,7 +600,7 @@ def aedt_name(self, value): @property def polygon_data(self): - """:class:`pyedb.dotnet.edb_core.dotnet.database.PolygonDataDotNet`: Outer contour of the Polygon object.""" + """:class:`pyedb.dotnet.database.dotnet.database.PolygonDataDotNet`: Outer contour of the Polygon object.""" return PolygonData(self._pedb, self._edb_object.GetPolygonData()) def add_void(self, point_list): @@ -608,7 +608,7 @@ def add_void(self, point_list): Parameters ---------- - point_list : list or :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.EDBPrimitives` \ + point_list : list or :class:`pyedb.dotnet.database.edb_data.primitives_data.EDBPrimitives` \ or EDB Primitive Object. Point list in the format of `[[x1,y1], [x2,y2],..,[xn,yn]]`. Returns @@ -787,3 +787,54 @@ def scale(self, factor, center=None): self.polygon_data = polygon_data return True return False + + def plot(self, plot_net=False, show=True, save_plot=None): + """Plot the current polygon on matplotlib. + + Parameters + ---------- + plot_net : bool, optional + Whether if plot the entire net or only the selected polygon. Default is ``False``. + show : bool, optional + Whether if show the plot or not. Default is ``True``. + save_plot : str, optional + Save the plot path. + + Returns + ------- + (ax, fig) + Matplotlib ax and figures. + """ + import matplotlib.pyplot as plt + from shapely.geometry import Polygon + from shapely.plotting import plot_polygon + + dpi = 100.0 + figsize = (2000 / dpi, 1000 / dpi) + if plot_net and self.net_name: + fig, ax = self._pedb.nets.plot([self.net_name], color_by_net=True, show=False, show_legend=False) + else: + fig = plt.figure(figsize=figsize) + ax = fig.add_subplot(1, 1, 1) + xt, yt = self.points() + p1 = [(i, j) for i, j in zip(xt[::-1], yt[::-1])] + + holes = [] + for void in self.voids: + xvt, yvt = void.points(arc_segments=3) + h1 = [(i, j) for i, j in zip(xvt, yvt)] + holes.append(h1) + poly = Polygon(p1, holes) + plot_polygon(poly, add_points=False, color=(1, 0, 0)) + ax.grid(False) + ax.set_axis_off() + # Hide axes ticks + ax.set_xticks([]) + ax.set_yticks([]) + message = f"Polygon {self.id} on net {self.net_name}" + plt.title(message, size=20) + if save_plot: + plt.savefig(save_plot) + elif show: + plt.show() + return ax, fig diff --git a/src/pyedb/dotnet/edb_core/cell/hierarchy/__init__.py b/src/pyedb/dotnet/database/cell/terminal/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/cell/hierarchy/__init__.py rename to src/pyedb/dotnet/database/cell/terminal/__init__.py diff --git a/src/pyedb/dotnet/edb_core/cell/terminal/bundle_terminal.py b/src/pyedb/dotnet/database/cell/terminal/bundle_terminal.py similarity index 93% rename from src/pyedb/dotnet/edb_core/cell/terminal/bundle_terminal.py rename to src/pyedb/dotnet/database/cell/terminal/bundle_terminal.py index aaddacc0ec..292c5e7d53 100644 --- a/src/pyedb/dotnet/edb_core/cell/terminal/bundle_terminal.py +++ b/src/pyedb/dotnet/database/cell/terminal/bundle_terminal.py @@ -20,8 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.cell.terminal.edge_terminal import EdgeTerminal -from pyedb.dotnet.edb_core.cell.terminal.terminal import Terminal +from pyedb.dotnet.database.cell.terminal.edge_terminal import EdgeTerminal +from pyedb.dotnet.database.cell.terminal.terminal import Terminal class BundleTerminal(Terminal): diff --git a/src/pyedb/dotnet/edb_core/cell/terminal/edge_terminal.py b/src/pyedb/dotnet/database/cell/terminal/edge_terminal.py similarity index 86% rename from src/pyedb/dotnet/edb_core/cell/terminal/edge_terminal.py rename to src/pyedb/dotnet/database/cell/terminal/edge_terminal.py index 2568247c23..97727f40a7 100644 --- a/src/pyedb/dotnet/edb_core/cell/terminal/edge_terminal.py +++ b/src/pyedb/dotnet/database/cell/terminal/edge_terminal.py @@ -20,8 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.cell.terminal.terminal import Terminal -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list +from pyedb.dotnet.database.cell.terminal.terminal import Terminal +from pyedb.dotnet.database.general import convert_py_list_to_net_list class EdgeTerminal(Terminal): @@ -33,12 +33,12 @@ def couple_ports(self, port): Parameters ---------- - port : :class:`dotnet.edb_core.ports.WavePort`, :class:`dotnet.edb_core.ports.GapPort`, list, optional + port : :class:`dotnet.database.ports.WavePort`, :class:`dotnet.database.ports.GapPort`, list, optional Ports to be added. Returns ------- - :class:`dotnet.edb_core.ports.BundleWavePort` + :class:`dotnet.database.ports.BundleWavePort` """ if not isinstance(port, (list, tuple)): diff --git a/src/pyedb/dotnet/edb_core/cell/terminal/padstack_instance_terminal.py b/src/pyedb/dotnet/database/cell/terminal/padstack_instance_terminal.py similarity index 97% rename from src/pyedb/dotnet/edb_core/cell/terminal/padstack_instance_terminal.py rename to src/pyedb/dotnet/database/cell/terminal/padstack_instance_terminal.py index e0e9fd4717..b77ae28d63 100644 --- a/src/pyedb/dotnet/edb_core/cell/terminal/padstack_instance_terminal.py +++ b/src/pyedb/dotnet/database/cell/terminal/padstack_instance_terminal.py @@ -20,8 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.cell.terminal.terminal import Terminal -from pyedb.dotnet.edb_core.edb_data.padstacks_data import EDBPadstackInstance +from pyedb.dotnet.database.cell.terminal.terminal import Terminal +from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance from pyedb.generic.general_methods import generate_unique_name diff --git a/src/pyedb/dotnet/edb_core/cell/terminal/pingroup_terminal.py b/src/pyedb/dotnet/database/cell/terminal/pingroup_terminal.py similarity index 95% rename from src/pyedb/dotnet/edb_core/cell/terminal/pingroup_terminal.py rename to src/pyedb/dotnet/database/cell/terminal/pingroup_terminal.py index 9914f32d34..d0c1b5ff51 100644 --- a/src/pyedb/dotnet/edb_core/cell/terminal/pingroup_terminal.py +++ b/src/pyedb/dotnet/database/cell/terminal/pingroup_terminal.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.cell.terminal.terminal import Terminal +from pyedb.dotnet.database.cell.terminal.terminal import Terminal class PinGroupTerminal(Terminal): @@ -45,7 +45,7 @@ def create(self, name, net_name, pin_group_name, is_ref=False): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PinGroupTerminal` + :class:`pyedb.dotnet.database.edb_data.terminals.PinGroupTerminal` """ net_obj = self._pedb.layout.find_net_by_name(net_name) term = self._pedb.edb_api.cell.terminal.PinGroupTerminal.Create( diff --git a/src/pyedb/dotnet/edb_core/cell/terminal/point_terminal.py b/src/pyedb/dotnet/database/cell/terminal/point_terminal.py similarity index 95% rename from src/pyedb/dotnet/edb_core/cell/terminal/point_terminal.py rename to src/pyedb/dotnet/database/cell/terminal/point_terminal.py index 266b55fbfb..514661a6c7 100644 --- a/src/pyedb/dotnet/edb_core/cell/terminal/point_terminal.py +++ b/src/pyedb/dotnet/database/cell/terminal/point_terminal.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.cell.terminal.terminal import Terminal +from pyedb.dotnet.database.cell.terminal.terminal import Terminal class PointTerminal(Terminal): @@ -48,7 +48,7 @@ def create(self, name, net, location, layer, is_ref=False): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PointTerminal` + :class:`pyedb.dotnet.database.edb_data.terminals.PointTerminal` """ terminal = self._pedb.edb_api.cell.terminal.PointTerminal.Create( self._pedb.active_layout, diff --git a/src/pyedb/dotnet/edb_core/cell/terminal/terminal.py b/src/pyedb/dotnet/database/cell/terminal/terminal.py similarity index 96% rename from src/pyedb/dotnet/edb_core/cell/terminal/terminal.py rename to src/pyedb/dotnet/database/cell/terminal/terminal.py index 1f8404284d..bf54114064 100644 --- a/src/pyedb/dotnet/edb_core/cell/terminal/terminal.py +++ b/src/pyedb/dotnet/database/cell/terminal/terminal.py @@ -22,9 +22,9 @@ import re -from pyedb.dotnet.edb_core.cell.connectable import Connectable -from pyedb.dotnet.edb_core.edb_data.padstacks_data import EDBPadstackInstance -from pyedb.dotnet.edb_core.edb_data.primitives_data import cast +from pyedb.dotnet.database.cell.connectable import Connectable +from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance +from pyedb.dotnet.database.edb_data.primitives_data import cast class Terminal(Connectable): @@ -230,8 +230,8 @@ def reference_object(self): # pragma : no cover Returns ------- - :class:`dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance` or - :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.EDBPrimitives` + :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance` or + :class:`pyedb.dotnet.database.edb_data.primitives_data.EDBPrimitives` """ if not self._reference_object: term = self._edb_object @@ -274,7 +274,7 @@ def get_padstack_terminal_reference_pin(self, gnd_net_name_preference=None): # Returns ------- - :class:`dotnet.edb_core.edb_data.padstack_data.EDBPadstackInstance` + :class:`dotnet.database.edb_data.padstack_data.EDBPadstackInstance` """ if self._edb_object.GetIsCircuitPort(): @@ -296,7 +296,7 @@ def get_pin_group_terminal_reference_pin(self, gnd_net_name_preference=None): # Returns ------- - :class:`dotnet.edb_core.edb_data.padstack_data.EDBPadstackInstance` + :class:`dotnet.database.edb_data.padstack_data.EDBPadstackInstance` """ refTerm = self._edb_object.GetReferenceTerminal() @@ -327,7 +327,7 @@ def get_edge_terminal_reference_primitive(self): # pragma : no cover Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.EDBPrimitives` + :class:`pyedb.dotnet.database.edb_data.primitives_data.EDBPrimitives` """ ref_layer = self._edb_object.GetReferenceLayer() @@ -349,8 +349,8 @@ def get_point_terminal_reference_primitive(self): # pragma : no cover Returns ------- - :class:`dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance` or - :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.EDBPrimitives` + :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance` or + :class:`pyedb.dotnet.database.edb_data.primitives_data.EDBPrimitives` """ ref_term = self._edb_object.GetReferenceTerminal() # return value is type terminal @@ -384,7 +384,7 @@ def get_pad_edge_terminal_reference_pin(self, gnd_net_name_preference=None): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance` + :class:`pyedb.dotnet.database.edb_data.padstacks_data.EDBPadstackInstance` """ comp_inst = self._edb_object.GetComponent() pins = self._pedb.components.get_pin_from_component(comp_inst.GetName()) diff --git a/src/pyedb/dotnet/edb_core/cell/voltage_regulator.py b/src/pyedb/dotnet/database/cell/voltage_regulator.py similarity index 97% rename from src/pyedb/dotnet/edb_core/cell/voltage_regulator.py rename to src/pyedb/dotnet/database/cell/voltage_regulator.py index cbf378d7fc..ec576fc9aa 100644 --- a/src/pyedb/dotnet/edb_core/cell/voltage_regulator.py +++ b/src/pyedb/dotnet/database/cell/voltage_regulator.py @@ -20,8 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.cell.connectable import Connectable -from pyedb.dotnet.edb_core.edb_data.padstacks_data import EDBPadstackInstance +from pyedb.dotnet.database.cell.connectable import Connectable +from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance class VoltageRegulator(Connectable): diff --git a/src/pyedb/dotnet/edb_core/components.py b/src/pyedb/dotnet/database/components.py similarity index 99% rename from src/pyedb/dotnet/edb_core/components.py rename to src/pyedb/dotnet/database/components.py index 93ad4a899a..d35634b09c 100644 --- a/src/pyedb/dotnet/edb_core/components.py +++ b/src/pyedb/dotnet/database/components.py @@ -36,12 +36,12 @@ Series, ) from pyedb.dotnet.clr_module import String -from pyedb.dotnet.edb_core.cell.hierarchy.component import EDBComponent -from pyedb.dotnet.edb_core.definition.component_def import EDBComponentDef -from pyedb.dotnet.edb_core.edb_data.padstacks_data import EDBPadstackInstance -from pyedb.dotnet.edb_core.edb_data.sources import Source, SourceType -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list -from pyedb.dotnet.edb_core.padstack import EdbPadstacks +from pyedb.dotnet.database.cell.hierarchy.component import EDBComponent +from pyedb.dotnet.database.definition.component_def import EDBComponentDef +from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance +from pyedb.dotnet.database.edb_data.sources import Source, SourceType +from pyedb.dotnet.database.general import convert_py_list_to_net_list +from pyedb.dotnet.database.padstack import EdbPadstacks from pyedb.generic.general_methods import ( _retry_ntimes, generate_unique_name, @@ -99,7 +99,7 @@ def __getitem__(self, name): Returns ------- - :class:`pyedb.dotnet.edb_core.cell.hierarchy.component.EDBComponent` + :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent` """ if name in self.instances: @@ -157,7 +157,7 @@ def components(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.cell.hierarchy.component.EDBComponent`] + dict[str, :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] Default dictionary for the EDB component. Examples @@ -177,7 +177,7 @@ def instances(self): Returns ------- - Dict[str, :class:`pyedb.dotnet.edb_core.cell.hierarchy.component.EDBComponent`] + Dict[str, :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] Default dictionary for the EDB component. Examples @@ -322,7 +322,7 @@ def resistors(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.cell.hierarchy.component.EDBComponent`] + dict[str, :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] Dictionary of resistors. Examples @@ -340,7 +340,7 @@ def capacitors(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.cell.hierarchy.component.EDBComponent`] + dict[str, :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] Dictionary of capacitors. Examples @@ -358,7 +358,7 @@ def inductors(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.cell.hierarchy.component.EDBComponent`] + dict[str, :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] Dictionary of inductors. Examples @@ -377,7 +377,7 @@ def ICs(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.cell.hierarchy.component.EDBComponent`] + dict[str, :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] Dictionary of integrated circuits. Examples @@ -396,7 +396,7 @@ def IOs(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.cell.hierarchy.component.EDBComponent`] + dict[str, :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] Dictionary of circuit inputs and outputs. Examples @@ -415,7 +415,7 @@ def Others(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.cell.hierarchy.component.EDBComponent`] + dict[str, :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] Dictionary of other core components. Examples @@ -1518,7 +1518,7 @@ def create_rlc_component( ---------- pins : list List of EDB pins, length must be 2, since only 2 pins component are currently supported. - It can be an `dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance` object or + It can be an `dotnet.database.edb_data.padstacks_data.EDBPadstackInstance` object or an Edb Padstack Instance object. component_name : str Component definition name. diff --git a/src/pyedb/dotnet/edb_core/cell/terminal/__init__.py b/src/pyedb/dotnet/database/definition/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/cell/terminal/__init__.py rename to src/pyedb/dotnet/database/definition/__init__.py diff --git a/src/pyedb/dotnet/edb_core/definition/component_def.py b/src/pyedb/dotnet/database/definition/component_def.py similarity index 95% rename from src/pyedb/dotnet/edb_core/definition/component_def.py rename to src/pyedb/dotnet/database/definition/component_def.py index 269a4cd325..825e2ebf14 100644 --- a/src/pyedb/dotnet/edb_core/definition/component_def.py +++ b/src/pyedb/dotnet/database/definition/component_def.py @@ -22,9 +22,9 @@ import os -from pyedb.dotnet.edb_core.definition.component_model import NPortComponentModel -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list -from pyedb.dotnet.edb_core.utilities.obj_base import ObjBase +from pyedb.dotnet.database.definition.component_model import NPortComponentModel +from pyedb.dotnet.database.general import convert_py_list_to_net_list +from pyedb.dotnet.database.utilities.obj_base import ObjBase class EDBComponentDef(ObjBase): @@ -84,7 +84,7 @@ def components(self): ------- dict of :class:`EDBComponent` """ - from pyedb.dotnet.edb_core.cell.hierarchy.component import EDBComponent + from pyedb.dotnet.database.cell.hierarchy.component import EDBComponent comp_list = [ EDBComponent(self._pedb, l) @@ -185,7 +185,7 @@ def add_n_port_model(self, fpath, name=None): if not name: name = os.path.splitext(os.path.basename(fpath)[0]) - from pyedb.dotnet.edb_core.definition.component_model import NPortComponentModel + from pyedb.dotnet.database.definition.component_model import NPortComponentModel edb_object = self._pedb.definition.NPortComponentModel.Create(name) n_port_comp_model = NPortComponentModel(self._pedb, edb_object) diff --git a/src/pyedb/dotnet/edb_core/definition/component_model.py b/src/pyedb/dotnet/database/definition/component_model.py similarity index 96% rename from src/pyedb/dotnet/edb_core/definition/component_model.py rename to src/pyedb/dotnet/database/definition/component_model.py index da07f43fc1..981596b2bd 100644 --- a/src/pyedb/dotnet/edb_core/definition/component_model.py +++ b/src/pyedb/dotnet/database/definition/component_model.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.utilities.obj_base import ObjBase +from pyedb.dotnet.database.utilities.obj_base import ObjBase class ComponentModel(ObjBase): diff --git a/src/pyedb/dotnet/edb_core/definition/definition_obj.py b/src/pyedb/dotnet/database/definition/definition_obj.py similarity index 96% rename from src/pyedb/dotnet/edb_core/definition/definition_obj.py rename to src/pyedb/dotnet/database/definition/definition_obj.py index 2a55eacd51..68e46dabb0 100644 --- a/src/pyedb/dotnet/edb_core/definition/definition_obj.py +++ b/src/pyedb/dotnet/database/definition/definition_obj.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.utilities.obj_base import ObjBase +from pyedb.dotnet.database.utilities.obj_base import ObjBase class DefinitionObj(ObjBase): diff --git a/src/pyedb/dotnet/edb_core/definition/definitions.py b/src/pyedb/dotnet/database/definition/definitions.py similarity index 94% rename from src/pyedb/dotnet/edb_core/definition/definitions.py rename to src/pyedb/dotnet/database/definition/definitions.py index baf5361a58..8d5909e482 100644 --- a/src/pyedb/dotnet/edb_core/definition/definitions.py +++ b/src/pyedb/dotnet/database/definition/definitions.py @@ -20,8 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.definition.component_def import EDBComponentDef -from pyedb.dotnet.edb_core.definition.package_def import PackageDef +from pyedb.dotnet.database.definition.component_def import EDBComponentDef +from pyedb.dotnet.database.definition.package_def import PackageDef class Definitions: diff --git a/src/pyedb/dotnet/edb_core/definition/package_def.py b/src/pyedb/dotnet/database/definition/package_def.py similarity index 95% rename from src/pyedb/dotnet/edb_core/definition/package_def.py rename to src/pyedb/dotnet/database/definition/package_def.py index 9455c78996..736d32cbdd 100644 --- a/src/pyedb/dotnet/edb_core/definition/package_def.py +++ b/src/pyedb/dotnet/database/definition/package_def.py @@ -20,8 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.geometry.polygon_data import PolygonData -from pyedb.dotnet.edb_core.utilities.obj_base import ObjBase +from pyedb.dotnet.database.geometry.polygon_data import PolygonData +from pyedb.dotnet.database.utilities.obj_base import ObjBase from pyedb.edb_logger import pyedb_logger @@ -145,7 +145,7 @@ def height(self, value): self._edb_object.SetHeight(value) def set_heatsink(self, fin_base_height, fin_height, fin_orientation, fin_spacing, fin_thickness): - from pyedb.dotnet.edb_core.utilities.heatsink import HeatSink + from pyedb.dotnet.database.utilities.heatsink import HeatSink heatsink = HeatSink(self._pedb) heatsink.fin_base_height = fin_base_height @@ -158,7 +158,7 @@ def set_heatsink(self, fin_base_height, fin_height, fin_orientation, fin_spacing @property def heatsink(self): """Component heatsink.""" - from pyedb.dotnet.edb_core.utilities.heatsink import HeatSink + from pyedb.dotnet.database.utilities.heatsink import HeatSink flag, edb_object = self._edb_object.GetHeatSink() if flag: diff --git a/src/pyedb/dotnet/edb_core/definition/__init__.py b/src/pyedb/dotnet/database/dotnet/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/definition/__init__.py rename to src/pyedb/dotnet/database/dotnet/__init__.py diff --git a/src/pyedb/dotnet/edb_core/dotnet/database.py b/src/pyedb/dotnet/database/dotnet/database.py similarity index 98% rename from src/pyedb/dotnet/edb_core/dotnet/database.py rename to src/pyedb/dotnet/database/dotnet/database.py index ece1012b96..7e78650bf2 100644 --- a/src/pyedb/dotnet/edb_core/dotnet/database.py +++ b/src/pyedb/dotnet/database/dotnet/database.py @@ -26,7 +26,7 @@ import sys from pyedb import __version__ -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list +from pyedb.dotnet.database.general import convert_py_list_to_net_list from pyedb.edb_logger import pyedb_logger from pyedb.generic.general_methods import ( env_path, @@ -443,7 +443,7 @@ def hierarchy(self): Returns ------- - :class:`dotnet.edb_core.dotnet.HierarchyDotNet` + :class:`dotnet.database.dotnet.HierarchyDotNet` """ return HierarchyDotNet(self._app) @@ -480,7 +480,7 @@ def layout_object_type(self): @property def primitive(self): """Edb Dotnet Api Database `Edb.Cell.Primitive`.""" - from pyedb.dotnet.edb_core.dotnet.primitive import PrimitiveDotNet + from pyedb.dotnet.database.dotnet.primitive import PrimitiveDotNet return PrimitiveDotNet(self._app) @@ -588,7 +588,7 @@ def polygon_data(self): Returns ------- - :class:`dotnet.edb_core.dotnet.PolygonDataDotNet` + :class:`dotnet.database.dotnet.PolygonDataDotNet` """ return PolygonDataDotNet(self._app) @@ -659,7 +659,7 @@ def cell(self): Returns ------- - :class:`pyedb.dotnet.edb_core.dotnet.database.CellClassDotNet`""" + :class:`pyedb.dotnet.database.dotnet.database.CellClassDotNet`""" return CellClassDotNet(self._app) @property @@ -668,7 +668,7 @@ def utility(self): Returns ------- - :class:`pyedb.dotnet.edb_core.dotnet.database.UtilityDotNet`""" + :class:`pyedb.dotnet.database.dotnet.database.UtilityDotNet`""" return UtilityDotNet(self._app) @@ -678,7 +678,7 @@ def geometry(self): Returns ------- - :class:`pyedb.dotnet.edb_core.dotnet.database.GeometryDotNet`""" + :class:`pyedb.dotnet.database.dotnet.database.GeometryDotNet`""" return GeometryDotNet(self._app) @@ -779,7 +779,7 @@ def edb_api(self): Returns ------- - :class:`pyedb.dotnet.edb_core.dotnet.database.CellDotNet` + :class:`pyedb.dotnet.database.dotnet.database.CellDotNet` """ return CellDotNet(self) diff --git a/src/pyedb/dotnet/edb_core/dotnet/primitive.py b/src/pyedb/dotnet/database/dotnet/primitive.py similarity index 98% rename from src/pyedb/dotnet/edb_core/dotnet/primitive.py rename to src/pyedb/dotnet/database/dotnet/primitive.py index 27362c1f0f..ce51e614a2 100644 --- a/src/pyedb/dotnet/edb_core/dotnet/primitive.py +++ b/src/pyedb/dotnet/database/dotnet/primitive.py @@ -22,8 +22,8 @@ """Primitive.""" -from pyedb.dotnet.edb_core.dotnet.database import NetDotNet -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list +from pyedb.dotnet.database.dotnet.database import NetDotNet +from pyedb.dotnet.database.general import convert_py_list_to_net_list from pyedb.misc.utilities import compute_arc_points from pyedb.modeler.geometry_operators import GeometryOperators @@ -119,7 +119,7 @@ def add_void(self, point_list): Parameters ---------- - point_list : list or :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.EDBPrimitives` \ + point_list : list or :class:`pyedb.dotnet.database.edb_data.primitives_data.EDBPrimitives` \ or EDB Primitive Object. Point list in the format of `[[x1,y1], [x2,y2],..,[xn,yn]]`. Returns @@ -414,7 +414,7 @@ def create(self, layout, layer, net, rep_type, param1, param2, param3, param4, c Returns ------- - :class:`pyedb.dotnet.edb_core.dotnet.primitive.RectangleDotNet` + :class:`pyedb.dotnet.database.dotnet.primitive.RectangleDotNet` Rectangle that was created. """ @@ -539,7 +539,7 @@ def create(self, layout, layer, net, center_x, center_y, radius): Returns ------- - :class:`pyedb.dotnet.edb_core.dotnet.primitive.CircleDotNet` + :class:`pyedb.dotnet.database.dotnet.primitive.CircleDotNet` Circle object created. """ if isinstance(net, NetDotNet): @@ -652,7 +652,7 @@ def create(self, layout, layer, center_x, center_y, text): Returns ------- - :class:`pyedb.dotnet.edb_core.dotnet.primitive.TextDotNet` + :class:`pyedb.dotnet.database.dotnet.primitive.TextDotNet` The text Object that was created. """ return TextDotNet( @@ -737,7 +737,7 @@ def create(self, layout, layer, net, width, end_cap1, end_cap2, corner_style, po Returns ------- - :class:`pyedb.dotnet.edb_core.dotnet.primitive.PathDotNet` + :class:`pyedb.dotnet.database.dotnet.primitive.PathDotNet` Path object created. """ if isinstance(net, NetDotNet): @@ -915,7 +915,7 @@ def create( Returns ------- - :class:`pyedb.dotnet.edb_core.dotnet.primitive.BondwireDotNet` + :class:`pyedb.dotnet.database.dotnet.primitive.BondwireDotNet` Bondwire object created. """ if isinstance(net, NetDotNet): @@ -1168,7 +1168,7 @@ def create( Returns ------- - :class:`pyedb.dotnet.edb_core.dotnet.primitive.PadstackInstanceDotNet` + :class:`pyedb.dotnet.database.dotnet.primitive.PadstackInstanceDotNet` Padstack instance object created. """ if isinstance(net, NetDotNet): diff --git a/src/pyedb/dotnet/edb_core/dotnet/__init__.py b/src/pyedb/dotnet/database/edb_data/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/dotnet/__init__.py rename to src/pyedb/dotnet/database/edb_data/__init__.py diff --git a/src/pyedb/dotnet/edb_core/edb_data/control_file.py b/src/pyedb/dotnet/database/edb_data/control_file.py similarity index 98% rename from src/pyedb/dotnet/edb_core/edb_data/control_file.py rename to src/pyedb/dotnet/database/edb_data/control_file.py index 076b85f575..5ec6e496f0 100644 --- a/src/pyedb/dotnet/edb_core/edb_data/control_file.py +++ b/src/pyedb/dotnet/database/edb_data/control_file.py @@ -278,7 +278,7 @@ def vias(self): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.control_file.ControlFileVia` + list of :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileVia` """ return self._vias @@ -289,7 +289,7 @@ def materials(self): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.control_file.ControlFileMaterial` + list of :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileMaterial` """ return self._materials @@ -300,7 +300,7 @@ def dielectrics(self): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.control_file.ControlFileLayer` + list of :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileLayer` """ return self._dielectrics @@ -311,7 +311,7 @@ def layers(self): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.control_file.ControlFileLayer` + list of :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileLayer` """ return self._layers @@ -345,7 +345,7 @@ def add_material( Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.control_file.ControlFileMaterial` + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileMaterial` """ if isinstance(properties, dict): self._materials[material_name] = ControlFileMaterial(material_name, properties) @@ -399,7 +399,7 @@ def add_layer( Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.control_file.ControlFileLayer` + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileLayer` """ if isinstance(properties, dict): self._layers.append(ControlFileLayer(layer_name, properties)) @@ -452,7 +452,7 @@ def add_dielectric( Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.control_file.ControlFileDielectric` + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileDielectric` """ if isinstance(properties, dict): self._dielectrics.append(ControlFileDielectric(layer_name, properties)) @@ -530,7 +530,7 @@ def add_via( Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.control_file.ControlFileVia` + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileVia` """ if isinstance(properties, dict): self._vias.append(ControlFileVia(layer_name, properties)) @@ -816,7 +816,7 @@ def add_port(self, name, x1, y1, layer1, x2, y2, layer2, z0=50): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.control_file.ControlCircuitPt` + :class:`pyedb.dotnet.database.edb_data.control_file.ControlCircuitPt` """ self.ports[name] = ControlCircuitPt(name, str(x1), str(y1), layer1, str(x2), str(y2), layer2, str(z0)) return self.ports[name] @@ -1003,7 +1003,7 @@ def add_sweep(self, name, start, stop, step, sweep_type="Interpolating", step_ty Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.control_file.ControlFileSweep` + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileSweep` """ self.sweeps.append(ControlFileSweep(name, start, stop, step, sweep_type, step_type, use_q3d)) return self.sweeps[-1] @@ -1024,7 +1024,7 @@ def add_mesh_operation(self, name, region, type, nets_layers): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.control_file.ControlFileMeshOp` + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileMeshOp` """ mop = ControlFileMeshOp(name, region, type, nets_layers) @@ -1094,7 +1094,7 @@ def add_setup(self, name, frequency): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.control_file.ControlFileSetup` + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileSetup` """ setup = ControlFileSetup(name) setup.frequency = frequency diff --git a/src/pyedb/dotnet/edb_core/edb_data/design_options.py b/src/pyedb/dotnet/database/edb_data/design_options.py similarity index 100% rename from src/pyedb/dotnet/edb_core/edb_data/design_options.py rename to src/pyedb/dotnet/database/edb_data/design_options.py diff --git a/src/pyedb/dotnet/edb_core/edb_data/edbvalue.py b/src/pyedb/dotnet/database/edb_data/edbvalue.py similarity index 100% rename from src/pyedb/dotnet/edb_core/edb_data/edbvalue.py rename to src/pyedb/dotnet/database/edb_data/edbvalue.py diff --git a/src/pyedb/dotnet/edb_core/edb_data/hfss_extent_info.py b/src/pyedb/dotnet/database/edb_data/hfss_extent_info.py similarity index 96% rename from src/pyedb/dotnet/edb_core/edb_data/hfss_extent_info.py rename to src/pyedb/dotnet/database/edb_data/hfss_extent_info.py index c33a01243f..a48272116f 100644 --- a/src/pyedb/dotnet/edb_core/edb_data/hfss_extent_info.py +++ b/src/pyedb/dotnet/database/edb_data/hfss_extent_info.py @@ -20,9 +20,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.edb_data.edbvalue import EdbValue -from pyedb.dotnet.edb_core.edb_data.primitives_data import cast -from pyedb.dotnet.edb_core.general import convert_pytuple_to_nettuple, pascal_to_snake +from pyedb.dotnet.database.edb_data.edbvalue import EdbValue +from pyedb.dotnet.database.edb_data.primitives_data import cast +from pyedb.dotnet.database.general import convert_pytuple_to_nettuple, pascal_to_snake class HfssExtentInfo: @@ -75,7 +75,7 @@ def air_box_horizontal_extent(self): """Size of horizontal extent for the air box. Returns: - dotnet.edb_core.edb_data.edbvalue.EdbValue + dotnet.database.edb_data.edbvalue.EdbValue """ return self._edb_hfss_extent_info.AirBoxHorizontalExtent.Item1 @@ -141,7 +141,7 @@ def base_polygon(self): Returns ------- - :class:`dotnet.edb_core.edb_data.primitives_data.EDBPrimitive` + :class:`dotnet.database.edb_data.primitives_data.EDBPrimitive` """ return cast(self._edb_hfss_extent_info.BasePolygon, self._pedb) @@ -157,7 +157,7 @@ def dielectric_base_polygon(self): Returns ------- - :class:`dotnet.edb_core.edb_data.primitives_data.EDBPrimitive` + :class:`dotnet.database.edb_data.primitives_data.EDBPrimitive` """ return cast(self._edb_hfss_extent_info.DielectricBasePolygon, self._pedb) @@ -251,7 +251,7 @@ def operating_freq(self): Returns ------- - pyedb.dotnet.edb_core.edb_data.edbvalue.EdbValue + pyedb.dotnet.database.edb_data.edbvalue.EdbValue """ return EdbValue(self._edb_hfss_extent_info.OperatingFreq) diff --git a/src/pyedb/dotnet/edb_core/edb_data/layer_data.py b/src/pyedb/dotnet/database/edb_data/layer_data.py similarity index 100% rename from src/pyedb/dotnet/edb_core/edb_data/layer_data.py rename to src/pyedb/dotnet/database/edb_data/layer_data.py diff --git a/src/pyedb/dotnet/edb_core/edb_data/nets_data.py b/src/pyedb/dotnet/database/edb_data/nets_data.py similarity index 93% rename from src/pyedb/dotnet/edb_core/edb_data/nets_data.py rename to src/pyedb/dotnet/database/edb_data/nets_data.py index 9744824822..bd2cb23af1 100644 --- a/src/pyedb/dotnet/edb_core/edb_data/nets_data.py +++ b/src/pyedb/dotnet/database/edb_data/nets_data.py @@ -19,13 +19,13 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.dotnet.database import ( +from pyedb.dotnet.database.dotnet.database import ( DifferentialPairDotNet, ExtendedNetDotNet, NetClassDotNet, NetDotNet, ) -from pyedb.dotnet.edb_core.edb_data.padstacks_data import EDBPadstackInstance +from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance class EDBNetsData(NetDotNet): @@ -55,9 +55,9 @@ def primitives(self): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.EDBPrimitives` + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.EDBPrimitives` """ - from pyedb.dotnet.edb_core.cell.layout import primitive_cast + from pyedb.dotnet.database.cell.layout import primitive_cast return [primitive_cast(self._app, i) for i in self.net_object.Primitives] # return [self._app.layout.find_object_by_id(i.GetId()) for i in self.net_object.Primitives] @@ -68,7 +68,7 @@ def padstack_instances(self): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance`""" + list of :class:`pyedb.dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`""" # name = self.name # return [ # EDBPadstackInstance(i, self._app) for i in self.net_object.PadstackInstances if i.GetNet().GetName() == name @@ -81,7 +81,7 @@ def components(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.cell.hierarchy.component.EDBComponent`] + dict[str, :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] """ comps = {} for p in self.padstack_instances: @@ -113,9 +113,8 @@ def plot( show_legend=True, save_plot=None, outline=None, - size=(6000, 3000), + size=(2000, 1000), show=True, - plot_vias=True, ): """Plot a net to Matplotlib 2D chart. @@ -135,9 +134,6 @@ def plot( Image size in pixel (width, height). show : bool, optional Whether to show the plot or not. Default is `True`. - plot_vias : bool, optional - Whether to plot vias or not. It may impact on performances. - Default is `True`. """ return self._app.nets.plot( @@ -148,7 +144,8 @@ def plot( outline=outline, size=size, show=show, - plot_vias=plot_vias, + plot_vias=True, + plot_components=True, ) def get_smallest_trace_width(self): @@ -173,7 +170,7 @@ def extended_net(self): Returns ------- - :class:` :class:`pyedb.dotnet.edb_core.edb_data.nets_data.EDBExtendedNetData` + :class:` :class:`pyedb.dotnet.database.edb_data.nets_data.EDBExtendedNetData` Examples -------- diff --git a/src/pyedb/dotnet/edb_core/edb_data/padstacks_data.py b/src/pyedb/dotnet/database/edb_data/padstacks_data.py similarity index 98% rename from src/pyedb/dotnet/edb_core/edb_data/padstacks_data.py rename to src/pyedb/dotnet/database/edb_data/padstacks_data.py index bdcf7f2886..754ccd74b0 100644 --- a/src/pyedb/dotnet/edb_core/edb_data/padstacks_data.py +++ b/src/pyedb/dotnet/database/edb_data/padstacks_data.py @@ -26,10 +26,10 @@ import warnings from pyedb.dotnet.clr_module import String -from pyedb.dotnet.edb_core.cell.primitive.primitive import Primitive -from pyedb.dotnet.edb_core.dotnet.database import PolygonDataDotNet -from pyedb.dotnet.edb_core.edb_data.edbvalue import EdbValue -from pyedb.dotnet.edb_core.general import ( +from pyedb.dotnet.database.cell.primitive.primitive import Primitive +from pyedb.dotnet.database.dotnet.database import PolygonDataDotNet +from pyedb.dotnet.database.edb_data.edbvalue import EdbValue +from pyedb.dotnet.database.general import ( PadGeometryTpe, convert_py_list_to_net_list, pascal_to_snake, @@ -911,7 +911,7 @@ def split_to_microvias(self): Returns ------- - List of :class:`pyedb.dotnet.edb_core.padstackEDBPadstack` + List of :class:`pyedb.dotnet.database.padstackEDBPadstack` """ if self.via_start_layer == self.via_stop_layer: self._ppadstack._pedb.logger.error("Microvias cannot be applied when Start and Stop Layers are the same.") @@ -1180,13 +1180,13 @@ def get_terminal(self, name=None, create_new_terminal=False): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.terminals` + :class:`pyedb.dotnet.database.edb_data.terminals` """ warnings.warn("Use new property :func:`terminal` instead.", DeprecationWarning) if create_new_terminal: term = self._create_terminal(name) else: - from pyedb.dotnet.edb_core.cell.terminal.padstack_instance_terminal import ( + from pyedb.dotnet.database.cell.terminal.padstack_instance_terminal import ( PadstackInstanceTerminal, ) @@ -1197,7 +1197,7 @@ def get_terminal(self, name=None, create_new_terminal=False): @property def terminal(self): """Terminal.""" - from pyedb.dotnet.edb_core.cell.terminal.padstack_instance_terminal import ( + from pyedb.dotnet.database.cell.terminal.padstack_instance_terminal import ( PadstackInstanceTerminal, ) @@ -1211,7 +1211,7 @@ def _create_terminal(self, name=None): def create_terminal(self, name=None): """Create a padstack instance terminal""" - from pyedb.dotnet.edb_core.cell.terminal.padstack_instance_terminal import ( + from pyedb.dotnet.database.cell.terminal.padstack_instance_terminal import ( PadstackInstanceTerminal, ) @@ -1231,9 +1231,9 @@ def create_port(self, name=None, reference=None, is_circuit_port=False): ---------- name : str, optional Name of the port. The default is ``None``, in which case a name is automatically assigned. - reference : class:`pyedb.dotnet.edb_core.edb_data.nets_data.EDBNetsData`, \ - class:`pyedb.dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance`, \ - class:`pyedb.dotnet.edb_core.edb_data.sources.PinGroup`, optional + reference : class:`pyedb.dotnet.database.edb_data.nets_data.EDBNetsData`, \ + class:`pyedb.dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`, \ + class:`pyedb.dotnet.database.edb_data.sources.PinGroup`, optional Negative terminal of the port. is_circuit_port : bool, optional Whether it is a circuit port. @@ -1662,7 +1662,7 @@ def is_pin(self, pin): @property def component(self): """Component.""" - from pyedb.dotnet.edb_core.cell.hierarchy.component import EDBComponent + from pyedb.dotnet.database.cell.hierarchy.component import EDBComponent comp = EDBComponent(self._pedb, self._edb_object.GetComponent()) return comp if not comp.is_null else False @@ -1933,7 +1933,7 @@ def create_rectangle_in_pad(self, layer_name, return_points=False, partition_max Returns ------- - bool, List, :class:`pyedb.dotnet.edb_core.edb_data.primitives.EDBPrimitives` + bool, List, :class:`pyedb.dotnet.database.edb_data.primitives.EDBPrimitives` Polygon when successful, ``False`` when failed, list of list if `return_points=True`. Examples @@ -2131,7 +2131,7 @@ def get_reference_pins(self, reference_net="GND", search_radius=5e-3, max_limit= Returns ------- list - List of :class:`dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance`. + List of :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`. Examples -------- diff --git a/src/pyedb/dotnet/edb_core/edb_data/ports.py b/src/pyedb/dotnet/database/edb_data/ports.py similarity index 97% rename from src/pyedb/dotnet/edb_core/edb_data/ports.py rename to src/pyedb/dotnet/database/edb_data/ports.py index 055cbc9b09..426cffa26a 100644 --- a/src/pyedb/dotnet/edb_core/edb_data/ports.py +++ b/src/pyedb/dotnet/database/edb_data/ports.py @@ -20,12 +20,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.cell.terminal.bundle_terminal import BundleTerminal -from pyedb.dotnet.edb_core.cell.terminal.edge_terminal import EdgeTerminal -from pyedb.dotnet.edb_core.cell.terminal.padstack_instance_terminal import ( +from pyedb.dotnet.database.cell.terminal.bundle_terminal import BundleTerminal +from pyedb.dotnet.database.cell.terminal.edge_terminal import EdgeTerminal +from pyedb.dotnet.database.cell.terminal.padstack_instance_terminal import ( PadstackInstanceTerminal, ) -from pyedb.dotnet.edb_core.cell.terminal.terminal import Terminal +from pyedb.dotnet.database.cell.terminal.terminal import Terminal class GapPort(EdgeTerminal): diff --git a/src/pyedb/dotnet/edb_core/edb_data/primitives_data.py b/src/pyedb/dotnet/database/edb_data/primitives_data.py similarity index 98% rename from src/pyedb/dotnet/edb_core/edb_data/primitives_data.py rename to src/pyedb/dotnet/database/edb_data/primitives_data.py index 687017f0e5..458781c2d6 100644 --- a/src/pyedb/dotnet/edb_core/edb_data/primitives_data.py +++ b/src/pyedb/dotnet/database/edb_data/primitives_data.py @@ -22,14 +22,14 @@ import math -from pyedb.dotnet.edb_core.cell.primitive.primitive import Primitive -from pyedb.dotnet.edb_core.dotnet.primitive import ( +from pyedb.dotnet.database.cell.primitive.primitive import Primitive +from pyedb.dotnet.database.dotnet.primitive import ( BondwireDotNet, CircleDotNet, RectangleDotNet, TextDotNet, ) -from pyedb.dotnet.edb_core.geometry.polygon_data import PolygonData +from pyedb.dotnet.database.geometry.polygon_data import PolygonData from pyedb.modeler.geometry_operators import GeometryOperators @@ -283,7 +283,7 @@ def in_polygon( # # Parameters # ---------- - # point_list : list or :class:`dotnet.edb_core.edb_data.primitives_data.Primitive` or EDB Primitive Object + # point_list : list or :class:`dotnet.database.edb_data.primitives_data.Primitive` or EDB Primitive Object # Point list in the format of `[[x1,y1], [x2,y2],..,[xn,yn]]`. # # Returns @@ -308,7 +308,7 @@ def in_polygon( @property def polygon_data(self): - """:class:`pyedb.dotnet.edb_core.dotnet.database.PolygonDataDotNet`: Outer contour of the Polygon object.""" + """:class:`pyedb.dotnet.database.dotnet.database.PolygonDataDotNet`: Outer contour of the Polygon object.""" return PolygonData(self._pedb, self._edb_object.GetPolygonData()) @polygon_data.setter diff --git a/src/pyedb/dotnet/edb_core/edb_data/raptor_x_simulation_setup_data.py b/src/pyedb/dotnet/database/edb_data/raptor_x_simulation_setup_data.py similarity index 98% rename from src/pyedb/dotnet/edb_core/edb_data/raptor_x_simulation_setup_data.py rename to src/pyedb/dotnet/database/edb_data/raptor_x_simulation_setup_data.py index f0f557d3b7..a8363fffd3 100644 --- a/src/pyedb/dotnet/edb_core/edb_data/raptor_x_simulation_setup_data.py +++ b/src/pyedb/dotnet/database/edb_data/raptor_x_simulation_setup_data.py @@ -19,9 +19,9 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list -from pyedb.dotnet.edb_core.sim_setup_data.data.sweep_data import SweepData -from pyedb.dotnet.edb_core.utilities.simulation_setup import SimulationSetup +from pyedb.dotnet.database.general import convert_py_list_to_net_list +from pyedb.dotnet.database.sim_setup_data.data.sweep_data import SweepData +from pyedb.dotnet.database.utilities.simulation_setup import SimulationSetup from pyedb.generic.general_methods import generate_unique_name @@ -80,7 +80,7 @@ def add_frequency_sweep(self, name=None, frequency_sweep=None): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.simulation_setup.EdbFrequencySweep` + :class:`pyedb.dotnet.database.edb_data.simulation_setup.EdbFrequencySweep` Examples -------- diff --git a/src/pyedb/dotnet/edb_core/edb_data/simulation_configuration.py b/src/pyedb/dotnet/database/edb_data/simulation_configuration.py similarity index 99% rename from src/pyedb/dotnet/edb_core/edb_data/simulation_configuration.py rename to src/pyedb/dotnet/database/edb_data/simulation_configuration.py index 8bfbd85077..f907d1175b 100644 --- a/src/pyedb/dotnet/edb_core/edb_data/simulation_configuration.py +++ b/src/pyedb/dotnet/database/edb_data/simulation_configuration.py @@ -25,8 +25,8 @@ import os from pyedb.dotnet.clr_module import Dictionary -from pyedb.dotnet.edb_core.edb_data.sources import Source, SourceType -from pyedb.dotnet.edb_core.utilities.simulation_setup import AdaptiveType +from pyedb.dotnet.database.edb_data.sources import Source, SourceType +from pyedb.dotnet.database.utilities.simulation_setup import AdaptiveType from pyedb.generic.constants import ( BasisOrder, CutoutSubdesignType, @@ -513,7 +513,7 @@ def sources(self): # pragma: no cover Returns ------- - :class:`dotnet.edb_core.edb_data.sources.Source` + :class:`dotnet.database.edb_data.sources.Source` """ return self._sources @@ -530,7 +530,7 @@ def add_source(self, source=None): # pragma: no cover Parameters ---------- - source : :class:`pyedb.dotnet.edb_core.edb_data.sources.Source` + source : :class:`pyedb.dotnet.database.edb_data.sources.Source` """ if isinstance(source, Source): @@ -1911,7 +1911,7 @@ def adaptive_type(self): Returns ------- - class: pyedb.dotnet.edb_core.edb_data.simulation_setup.AdaptiveType + class: pyedb.dotnet.database.edb_data.simulation_setup.AdaptiveType """ return self._adaptive_type @@ -2328,7 +2328,7 @@ def dc_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.simulation_configuration.SimulationConfigurationDc` + :class:`pyedb.dotnet.database.edb_data.simulation_configuration.SimulationConfigurationDc` """ return self._dc_settings @@ -2339,7 +2339,7 @@ def ac_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.simulation_configuration.SimulationConfigurationAc` + :class:`pyedb.dotnet.database.edb_data.simulation_configuration.SimulationConfigurationAc` """ return self._ac_settings @@ -2350,7 +2350,7 @@ def batch_solve_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.simulation_configuration.SimulationConfigurationBatch` + :class:`pyedb.dotnet.database.edb_data.simulation_configuration.SimulationConfigurationBatch` """ return self._batch_solve_settings @@ -2699,7 +2699,7 @@ def export_json(self, output_file): Examples -------- - >>> from dotnet.edb_core.edb_data.simulation_configuration import SimulationConfiguration + >>> from dotnet.database.edb_data.simulation_configuration import SimulationConfiguration >>> config = SimulationConfiguration() >>> config.export_json(r"C:\Temp\test_json\test.json") """ @@ -2727,7 +2727,7 @@ def import_json(self, input_file): Examples -------- - >>> from dotnet.edb_core.edb_data.simulation_configuration import SimulationConfiguration + >>> from dotnet.database.edb_data.simulation_configuration import SimulationConfiguration >>> test = SimulationConfiguration() >>> test.import_json(r"C:\Temp\test_json\test.json") """ diff --git a/src/pyedb/dotnet/edb_core/edb_data/sources.py b/src/pyedb/dotnet/database/edb_data/sources.py similarity index 98% rename from src/pyedb/dotnet/edb_core/edb_data/sources.py rename to src/pyedb/dotnet/database/edb_data/sources.py index 2ed4c26b05..3e487cd736 100644 --- a/src/pyedb/dotnet/edb_core/edb_data/sources.py +++ b/src/pyedb/dotnet/database/edb_data/sources.py @@ -279,7 +279,7 @@ def component(self, value): @property def pins(self): """Gets the pins belong to this pin group.""" - from pyedb.dotnet.edb_core.edb_data.padstacks_data import EDBPadstackInstance + from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance return {i.GetName(): EDBPadstackInstance(i, self._pedb) for i in list(self._edb_object.GetPins())} @@ -317,7 +317,7 @@ def get_terminal(self, name=None, create_new_terminal=False): @property def terminal(self): """Terminal.""" - from pyedb.dotnet.edb_core.cell.terminal.pingroup_terminal import ( + from pyedb.dotnet.database.cell.terminal.pingroup_terminal import ( PinGroupTerminal, ) @@ -335,7 +335,7 @@ def _create_terminal(self, name=None): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PinGroupTerminal` + :class:`pyedb.dotnet.database.edb_data.terminals.PinGroupTerminal` """ warnings.warn("`_create_terminal` is deprecated. Use `create_terminal` instead.", DeprecationWarning) @@ -355,7 +355,7 @@ def create_terminal(self, name=None): """ if not name: name = generate_unique_name(self.name) - from pyedb.dotnet.edb_core.cell.terminal.pingroup_terminal import ( + from pyedb.dotnet.database.cell.terminal.pingroup_terminal import ( PinGroupTerminal, ) diff --git a/src/pyedb/dotnet/edb_core/edb_data/utilities.py b/src/pyedb/dotnet/database/edb_data/utilities.py similarity index 100% rename from src/pyedb/dotnet/edb_core/edb_data/utilities.py rename to src/pyedb/dotnet/database/edb_data/utilities.py diff --git a/src/pyedb/dotnet/edb_core/edb_data/variables.py b/src/pyedb/dotnet/database/edb_data/variables.py similarity index 98% rename from src/pyedb/dotnet/edb_core/edb_data/variables.py rename to src/pyedb/dotnet/database/edb_data/variables.py index 7b7dc3b912..5faca3d293 100644 --- a/src/pyedb/dotnet/edb_core/edb_data/variables.py +++ b/src/pyedb/dotnet/database/edb_data/variables.py @@ -65,7 +65,7 @@ def value_object(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.edbvalue.EdbValue` + :class:`pyedb.dotnet.database.edb_data.edbvalue.EdbValue` """ return self._pedb.get_variable(self.name) diff --git a/src/pyedb/dotnet/edb_core/general.py b/src/pyedb/dotnet/database/general.py similarity index 100% rename from src/pyedb/dotnet/edb_core/general.py rename to src/pyedb/dotnet/database/general.py diff --git a/src/pyedb/dotnet/edb_core/edb_data/__init__.py b/src/pyedb/dotnet/database/geometry/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/edb_data/__init__.py rename to src/pyedb/dotnet/database/geometry/__init__.py diff --git a/src/pyedb/dotnet/edb_core/geometry/point_data.py b/src/pyedb/dotnet/database/geometry/point_data.py similarity index 100% rename from src/pyedb/dotnet/edb_core/geometry/point_data.py rename to src/pyedb/dotnet/database/geometry/point_data.py diff --git a/src/pyedb/dotnet/edb_core/geometry/polygon_data.py b/src/pyedb/dotnet/database/geometry/polygon_data.py similarity index 95% rename from src/pyedb/dotnet/edb_core/geometry/polygon_data.py rename to src/pyedb/dotnet/database/geometry/polygon_data.py index 23d4189344..6ec310ea7e 100644 --- a/src/pyedb/dotnet/edb_core/geometry/polygon_data.py +++ b/src/pyedb/dotnet/database/geometry/polygon_data.py @@ -20,9 +20,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list -from pyedb.dotnet.edb_core.geometry.point_data import PointData -from pyedb.dotnet.edb_core.utilities.obj_base import BBox +from pyedb.dotnet.database.general import convert_py_list_to_net_list +from pyedb.dotnet.database.geometry.point_data import PointData +from pyedb.dotnet.database.utilities.obj_base import BBox class PolygonData: @@ -67,7 +67,7 @@ def bounding_box(self): @property def arcs(self): """Get the Primitive Arc Data.""" - from pyedb.dotnet.edb_core.edb_data.primitives_data import EDBArcs + from pyedb.dotnet.database.edb_data.primitives_data import EDBArcs arcs = [EDBArcs(self._pedb, i) for i in self._edb_object.GetArcData()] return arcs diff --git a/src/pyedb/dotnet/edb_core/hfss.py b/src/pyedb/dotnet/database/hfss.py similarity index 99% rename from src/pyedb/dotnet/edb_core/hfss.py rename to src/pyedb/dotnet/database/hfss.py index 24131bd235..75bb8b3799 100644 --- a/src/pyedb/dotnet/edb_core/hfss.py +++ b/src/pyedb/dotnet/database/hfss.py @@ -25,13 +25,13 @@ """ import math -from pyedb.dotnet.edb_core.edb_data.hfss_extent_info import HfssExtentInfo -from pyedb.dotnet.edb_core.edb_data.ports import BundleWavePort, WavePort -from pyedb.dotnet.edb_core.edb_data.primitives_data import Primitive -from pyedb.dotnet.edb_core.edb_data.simulation_configuration import ( +from pyedb.dotnet.database.edb_data.hfss_extent_info import HfssExtentInfo +from pyedb.dotnet.database.edb_data.ports import BundleWavePort, WavePort +from pyedb.dotnet.database.edb_data.primitives_data import Primitive +from pyedb.dotnet.database.edb_data.simulation_configuration import ( SimulationConfiguration, ) -from pyedb.dotnet.edb_core.general import ( +from pyedb.dotnet.database.general import ( convert_py_list_to_net_list, convert_pytuple_to_nettuple, ) @@ -514,7 +514,7 @@ def create_differential_wave_port( Returns ------- tuple - The tuple contains: (port_name, pyedb.dotnet.edb_core.edb_data.sources.ExcitationDifferential). + The tuple contains: (port_name, pyedb.dotnet.database.edb_data.sources.ExcitationDifferential). Examples -------- @@ -584,7 +584,7 @@ def create_bundle_wave_port( Returns ------- tuple - The tuple contains: (port_name, pyedb.egacy.edb_core.edb_data.sources.ExcitationDifferential). + The tuple contains: (port_name, pyedb.egacy.database.edb_data.sources.ExcitationDifferential). Examples -------- @@ -789,7 +789,7 @@ def create_wave_port( Returns ------- tuple - The tuple contains: (Port name, pyedb.dotnet.edb_core.edb_data.sources.Excitation). + The tuple contains: (Port name, pyedb.dotnet.database.edb_data.sources.Excitation). Examples -------- diff --git a/src/pyedb/dotnet/edb_core/layout_obj_instance.py b/src/pyedb/dotnet/database/layout_obj_instance.py similarity index 95% rename from src/pyedb/dotnet/edb_core/layout_obj_instance.py rename to src/pyedb/dotnet/database/layout_obj_instance.py index c3651ba026..8ee446ace0 100644 --- a/src/pyedb/dotnet/edb_core/layout_obj_instance.py +++ b/src/pyedb/dotnet/database/layout_obj_instance.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.utilities.obj_base import ObjBase +from pyedb.dotnet.database.utilities.obj_base import ObjBase class LayoutObjInstance(ObjBase): diff --git a/src/pyedb/dotnet/edb_core/layout_validation.py b/src/pyedb/dotnet/database/layout_validation.py similarity index 99% rename from src/pyedb/dotnet/edb_core/layout_validation.py rename to src/pyedb/dotnet/database/layout_validation.py index 70b489faac..50b394425c 100644 --- a/src/pyedb/dotnet/edb_core/layout_validation.py +++ b/src/pyedb/dotnet/database/layout_validation.py @@ -23,8 +23,8 @@ import re from pyedb.dotnet.clr_module import String -from pyedb.dotnet.edb_core.edb_data.padstacks_data import EDBPadstackInstance -from pyedb.dotnet.edb_core.edb_data.primitives_data import Primitive +from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance +from pyedb.dotnet.database.edb_data.primitives_data import Primitive from pyedb.generic.general_methods import generate_unique_name diff --git a/src/pyedb/dotnet/edb_core/materials.py b/src/pyedb/dotnet/database/materials.py similarity index 98% rename from src/pyedb/dotnet/edb_core/materials.py rename to src/pyedb/dotnet/database/materials.py index 5d553f8e64..82f7ded0de 100644 --- a/src/pyedb/dotnet/edb_core/materials.py +++ b/src/pyedb/dotnet/database/materials.py @@ -32,7 +32,7 @@ from pydantic import BaseModel, confloat from pyedb import Edb -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list +from pyedb.dotnet.database.general import convert_py_list_to_net_list from pyedb.exceptions import MaterialModelException logger = logging.getLogger(__name__) @@ -493,7 +493,7 @@ def add_material(self, name: str, **kwargs): Returns ------- - :class:`pyedb.dotnet.edb_core.materials.Material` + :class:`pyedb.dotnet.database.materials.Material` """ curr_materials = self.materials if name in curr_materials: @@ -532,7 +532,7 @@ def add_conductor_material(self, name, conductivity, **kwargs): Returns ------- - :class:`pyedb.dotnet.edb_core.materials.Material` + :class:`pyedb.dotnet.database.materials.Material` """ extended_kwargs = {key: value for (key, value) in kwargs.items()} @@ -555,7 +555,7 @@ def add_dielectric_material(self, name, permittivity, dielectric_loss_tangent, * Returns ------- - :class:`pyedb.dotnet.edb_core.materials.Material` + :class:`pyedb.dotnet.database.materials.Material` """ extended_kwargs = {key: value for (key, value) in kwargs.items()} extended_kwargs["permittivity"] = permittivity @@ -589,7 +589,7 @@ def add_djordjevicsarkar_dielectric( Returns ------- - :class:`pyedb.dotnet.edb_core.materials.Material` + :class:`pyedb.dotnet.database.materials.Material` """ curr_materials = self.materials if name in curr_materials: @@ -657,7 +657,7 @@ def add_debye_material( Returns ------- - :class:`pyedb.dotnet.edb_core.materials.Material` + :class:`pyedb.dotnet.database.materials.Material` """ curr_materials = self.materials if name in curr_materials: @@ -711,7 +711,7 @@ def add_multipole_debye_material( Returns ------- - :class:`pyedb.dotnet.edb_core.materials.Material` + :class:`pyedb.dotnet.database.materials.Material` Examples -------- @@ -786,7 +786,7 @@ def duplicate(self, material_name, new_material_name): Returns ------- - :class:`pyedb.dotnet.edb_core.materials.Material` + :class:`pyedb.dotnet.database.materials.Material` """ curr_materials = self.materials if new_material_name in curr_materials: diff --git a/src/pyedb/dotnet/edb_core/modeler.py b/src/pyedb/dotnet/database/modeler.py similarity index 96% rename from src/pyedb/dotnet/edb_core/modeler.py rename to src/pyedb/dotnet/database/modeler.py index f4ce332642..13f33a9552 100644 --- a/src/pyedb/dotnet/edb_core/modeler.py +++ b/src/pyedb/dotnet/database/modeler.py @@ -26,11 +26,11 @@ import math import warnings -from pyedb.dotnet.edb_core.cell.primitive.bondwire import Bondwire -from pyedb.dotnet.edb_core.dotnet.primitive import CircleDotNet, RectangleDotNet -from pyedb.dotnet.edb_core.edb_data.primitives_data import Primitive, cast -from pyedb.dotnet.edb_core.edb_data.utilities import EDBStatistics -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list +from pyedb.dotnet.database.cell.primitive.bondwire import Bondwire +from pyedb.dotnet.database.dotnet.primitive import CircleDotNet, RectangleDotNet +from pyedb.dotnet.database.edb_data.primitives_data import Primitive, cast +from pyedb.dotnet.database.edb_data.utilities import EDBStatistics +from pyedb.dotnet.database.general import convert_py_list_to_net_list class Modeler(object): @@ -52,7 +52,7 @@ def __getitem__(self, name): Returns ------- - :class:`pyedb.dotnet.edb_core.cell.hierarchy.component.EDBComponent` + :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent` """ for i in self.primitives: @@ -123,7 +123,7 @@ def get_primitive(self, primitive_id): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.Primitive` + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` List of primitives. """ for p in self._layout.primitives: @@ -140,7 +140,7 @@ def primitives(self): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.Primitive` + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` List of primitives. """ return self._pedb.layout.primitives @@ -202,7 +202,7 @@ def rectangles(self): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.Primitive` + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` List of rectangles. """ @@ -214,7 +214,7 @@ def circles(self): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.Primitive` + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` List of circles. """ @@ -226,7 +226,7 @@ def paths(self): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.Primitive` + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` List of paths. """ return [i for i in self.primitives if i.primitive_type == "path"] @@ -237,7 +237,7 @@ def polygons(self): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.Primitive` + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` List of polygons. """ return [i for i in self.primitives if i.primitive_type == "polygon"] @@ -282,7 +282,7 @@ def get_primitive_by_layer_and_point(self, point=None, layer=None, nets=None): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.Primitive` + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` List of primitives, polygons, paths and rectangles. """ if isinstance(layer, str) and layer not in list(self._pedb.stackup.signal_layers.keys()): @@ -339,8 +339,8 @@ def get_polygon_bounding_box(self, polygon): Examples -------- - >>> poly = edb_core.modeler.get_polygons_by_layer("GND") - >>> bounding = edb_core.modeler.get_polygon_bounding_box(poly[0]) + >>> poly = database.modeler.get_polygons_by_layer("GND") + >>> bounding = database.modeler.get_polygon_bounding_box(poly[0]) """ bounding = [] try: @@ -364,7 +364,7 @@ def get_polygon_points(self, polygon): Parameters ---------- polygon : - class: `dotnet.edb_core.edb_data.primitives_data.Primitive` + class: `dotnet.database.edb_data.primitives_data.Primitive` Returns ------- @@ -378,8 +378,8 @@ def get_polygon_points(self, polygon): Examples -------- - >>> poly = edb_core.modeler.get_polygons_by_layer("GND") - >>> points = edb_core.modeler.get_polygon_points(poly[0]) + >>> poly = database.modeler.get_polygons_by_layer("GND") + >>> points = database.modeler.get_polygon_points(poly[0]) """ points = [] @@ -499,7 +499,7 @@ def _create_path( Parameters ---------- - path_list : :class:`dotnet.edb_core.layout.Shape` + path_list : :class:`dotnet.database.layout.Shape` List of points. layer_name : str Name of the layer on which to create the path. @@ -521,7 +521,7 @@ def _create_path( Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.Primitive` + :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` ``True`` when successful, ``False`` when failed. """ net = self._pedb.nets.find_or_create_net(net_name) @@ -600,7 +600,7 @@ def create_trace( Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.Primitive` + :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` """ path = self.Shape("Polygon", points=path_list) primitive = self._create_path( @@ -635,7 +635,7 @@ def create_polygon(self, main_shape, layer_name, voids=[], net_name=""): Returns ------- - bool, :class:`dotnet.edb_core.edb_data.primitives.Primitive` + bool, :class:`dotnet.database.edb_data.primitives.Primitive` Polygon when successful, ``False`` when failed. """ net = self._pedb.nets.find_or_create_net(net_name) @@ -706,7 +706,7 @@ def create_polygon_from_points(self, point_list, layer_name, net_name=""): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.Primitive` + :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` """ warnings.warn( "Use :func:`create_polygon` method instead. It now supports point lists as arguments.", DeprecationWarning @@ -754,7 +754,7 @@ def create_rectangle( Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.Primitive` + :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` Rectangle when successful, ``False`` when failed. """ edb_net = self._pedb.nets.find_or_create_net(net_name) @@ -809,7 +809,7 @@ def create_circle(self, layer_name, x, y, radius, net_name=""): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.primitives_data.Primitive` + :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` Objects of the circle created when successful. """ edb_net = self._pedb.nets.find_or_create_net(net_name) @@ -948,7 +948,7 @@ def shape_to_polygon_data(self, shape): Parameters ---------- - shape : :class:`pyedb.dotnet.edb_core.modeler.Modeler.Shape` + shape : :class:`pyedb.dotnet.database.modeler.Modeler.Shape` Type of the shape to convert. Options are ``"rectangle"`` and ``"polygon"``. """ if shape.type == "polygon": @@ -1387,7 +1387,7 @@ def create_bondwire( Returns ------- - :class:`pyedb.dotnet.edb_core.dotnet.primitive.BondwireDotNet` + :class:`pyedb.dotnet.database.dotnet.primitive.BondwireDotNet` Bondwire object created. """ diff --git a/src/pyedb/dotnet/edb_core/net_class.py b/src/pyedb/dotnet/database/net_class.py similarity index 95% rename from src/pyedb/dotnet/edb_core/net_class.py rename to src/pyedb/dotnet/database/net_class.py index 4fce317c31..3bab0d8a15 100644 --- a/src/pyedb/dotnet/edb_core/net_class.py +++ b/src/pyedb/dotnet/database/net_class.py @@ -24,7 +24,7 @@ import re -from pyedb.dotnet.edb_core.edb_data.nets_data import ( +from pyedb.dotnet.database.edb_data.nets_data import ( EDBDifferentialPairData, EDBExtendedNetData, EDBNetClassData, @@ -59,7 +59,7 @@ def __getitem__(self, name): Returns ------- - :class:` :class:`pyedb.dotnet.edb_core.edb_data.nets_data.EDBExtendedNetsData` + :class:` :class:`pyedb.dotnet.database.edb_data.nets_data.EDBExtendedNetsData` """ if name in self.items: @@ -86,7 +86,7 @@ def items(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.edb_data.nets_data.EDBDifferentialPairData`] + dict[str, :class:`pyedb.dotnet.database.edb_data.nets_data.EDBDifferentialPairData`] Dictionary of extended nets. """ temp = {} @@ -107,7 +107,7 @@ def create(self, name, net): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.nets_data.EDBNetClassData` + :class:`pyedb.dotnet.database.edb_data.nets_data.EDBNetClassData` """ if name in self.items: self._pedb.logger.error("{} already exists.".format(name)) @@ -142,7 +142,7 @@ def items(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.edb_data.nets_data.EDBExtendedNetsData`] + dict[str, :class:`pyedb.dotnet.database.edb_data.nets_data.EDBExtendedNetsData`] Dictionary of extended nets. """ nets = {} @@ -163,7 +163,7 @@ def create(self, name, net): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.nets_data.EDBExtendedNetsData` + :class:`pyedb.dotnet.database.edb_data.nets_data.EDBExtendedNetsData` """ if name in self.items: self._pedb.logger.error("{} already exists.".format(name)) @@ -267,7 +267,7 @@ def items(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.edb_data.nets_data.EDBDifferentialPairData`] + dict[str, :class:`pyedb.dotnet.database.edb_data.nets_data.EDBDifferentialPairData`] Dictionary of extended nets. """ diff_pairs = {} @@ -290,7 +290,7 @@ def create(self, name, net_p, net_n): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.nets_data.EDBDifferentialPairData` + :class:`pyedb.dotnet.database.edb_data.nets_data.EDBDifferentialPairData` """ if name in self.items: self._pedb.logger.error("{} already exists.".format(name)) diff --git a/src/pyedb/dotnet/edb_core/nets.py b/src/pyedb/dotnet/database/nets.py similarity index 97% rename from src/pyedb/dotnet/edb_core/nets.py rename to src/pyedb/dotnet/database/nets.py index 12c7aae664..46547e3986 100644 --- a/src/pyedb/dotnet/edb_core/nets.py +++ b/src/pyedb/dotnet/database/nets.py @@ -25,7 +25,7 @@ import warnings from pyedb.common.nets import CommonNets -from pyedb.dotnet.edb_core.edb_data.nets_data import EDBNetsData +from pyedb.dotnet.database.edb_data.nets_data import EDBNetsData from pyedb.generic.general_methods import generate_unique_name from pyedb.misc.utilities import compute_arc_points @@ -49,7 +49,7 @@ def __getitem__(self, name): Returns ------- - :class:` :class:`pyedb.dotnet.edb_core.edb_data.nets_data.EDBNetsData` + :class:` :class:`pyedb.dotnet.database.edb_data.nets_data.EDBNetsData` """ return self._pedb.layout.find_net_by_name(name) @@ -70,7 +70,7 @@ def __contains__(self, name): return name in self.nets def __init__(self, p_edb): - self._pedb = p_edb + CommonNets.__init__(self, p_edb) self._nets_by_comp_dict = {} self._comps_by_nets_dict = {} @@ -110,7 +110,7 @@ def nets(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.edb_data.nets_data.EDBNetsData`] + dict[str, :class:`pyedb.dotnet.database.edb_data.nets_data.EDBNetsData`] Dictionary of nets. """ return {i.name: i for i in self._pedb.layout.nets} @@ -135,7 +135,7 @@ def signal_nets(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.edb_data.EDBNetsData`] + dict[str, :class:`pyedb.dotnet.database.edb_data.EDBNetsData`] Dictionary of signal nets. """ warnings.warn("Use :func:`signal` instead.", DeprecationWarning) @@ -150,7 +150,7 @@ def power_nets(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.edb_data.EDBNetsData`] + dict[str, :class:`pyedb.dotnet.database.edb_data.EDBNetsData`] Dictionary of power nets. """ warnings.warn("Use :func:`power` instead.", DeprecationWarning) @@ -162,7 +162,7 @@ def signal(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.edb_data.EDBNetsData`] + dict[str, :class:`pyedb.dotnet.database.edb_data.EDBNetsData`] Dictionary of signal nets. """ nets = {} @@ -177,7 +177,7 @@ def power(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.edb_data.EDBNetsData`] + dict[str, :class:`pyedb.dotnet.database.edb_data.EDBNetsData`] Dictionary of power nets. """ nets = {} @@ -197,7 +197,7 @@ def eligible_power_nets(self, threshold=0.3): Returns ------- - list of :class:`pyedb.dotnet.edb_core.edb_data.EDBNetsData` + list of :class:`pyedb.dotnet.database.edb_data.EDBNetsData` """ pwr_gnd_nets = [] for net in self._layout.nets[:]: @@ -564,7 +564,7 @@ def delete_nets(self, netlist): Examples -------- - >>> deleted_nets = edb_core.nets.delete(["Net1","Net2"]) + >>> deleted_nets = database.nets.delete(["Net1","Net2"]) """ warnings.warn("Use :func:`delete` method instead.", DeprecationWarning) return self.delete(netlist=netlist) @@ -585,7 +585,7 @@ def delete(self, netlist): Examples -------- - >>> deleted_nets = edb_core.nets.delete(["Net1","Net2"]) + >>> deleted_nets = database.nets.delete(["Net1","Net2"]) """ if isinstance(netlist, str): netlist = [netlist] @@ -726,7 +726,7 @@ def find_and_fix_disjoint_nets( Examples -------- - >>> renamed_nets = edb_core.nets.find_and_fix_disjoint_nets(["GND","Net2"]) + >>> renamed_nets = database.nets.find_and_fix_disjoint_nets(["GND","Net2"]) """ warnings.warn("Use new function :func:`edb.layout_validation.disjoint_nets` instead.", DeprecationWarning) return self._pedb.layout_validation.disjoint_nets( diff --git a/src/pyedb/dotnet/edb_core/padstack.py b/src/pyedb/dotnet/database/padstack.py similarity index 98% rename from src/pyedb/dotnet/edb_core/padstack.py rename to src/pyedb/dotnet/database/padstack.py index 7a16534e1d..7e4211e1b6 100644 --- a/src/pyedb/dotnet/edb_core/padstack.py +++ b/src/pyedb/dotnet/database/padstack.py @@ -31,12 +31,12 @@ from scipy.spatial import ConvexHull from pyedb.dotnet.clr_module import Array -from pyedb.dotnet.edb_core.edb_data.padstacks_data import ( +from pyedb.dotnet.database.edb_data.padstacks_data import ( EDBPadstack, EDBPadstackInstance, ) -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list -from pyedb.dotnet.edb_core.geometry.polygon_data import PolygonData +from pyedb.dotnet.database.general import convert_py_list_to_net_list +from pyedb.dotnet.database.geometry.polygon_data import PolygonData from pyedb.generic.general_methods import generate_unique_name from pyedb.modeler.geometry_operators import GeometryOperators @@ -60,7 +60,7 @@ def __getitem__(self, name): Returns ------- - :class:`pyedb.dotnet.edb_core.cell.hierarchy.component.EDBComponent` + :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent` """ if isinstance(name, int) and name in self.instances: @@ -183,7 +183,7 @@ def definitions(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.edb_data.padstacks_data.EdbPadstack`] + dict[str, :class:`pyedb.dotnet.database.edb_data.padstacks_data.EdbPadstack`] List of definitions via padstack definitions. """ @@ -205,7 +205,7 @@ def padstacks(self): Returns ------- - dict[str, :class:`pyedb.dotnet.edb_core.edb_data.EdbPadstack`] + dict[str, :class:`pyedb.dotnet.database.edb_data.EdbPadstack`] List of definitions via padstack definitions. """ @@ -218,7 +218,7 @@ def instances(self): Returns ------- - dict[int, :class:`dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance`] + dict[int, :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`] List of padstack instances. """ @@ -234,7 +234,7 @@ def instances_by_name(self): Returns ------- - dict[str, :class:`dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance`] + dict[str, :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`] List of padstack instances. """ @@ -259,7 +259,7 @@ def pins(self): Returns ------- - dic[str, :class:`dotnet.edb_core.edb_data.definitions.EDBPadstackInstance`] + dic[str, :class:`dotnet.database.edb_data.definitions.EDBPadstackInstance`] Dictionary of EDBPadstackInstance Components. @@ -280,7 +280,7 @@ def vias(self): Returns ------- - dic[str, :class:`dotnet.edb_core.edb_data.definitions.EDBPadstackInstance`] + dic[str, :class:`dotnet.database.edb_data.definitions.EDBPadstackInstance`] Dictionary of EDBPadstackInstance Components. @@ -302,7 +302,7 @@ def padstack_instances(self): Returns ------- - dict[str, :class:`dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance`] + dict[str, :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`] List of padstack instances. """ @@ -1167,7 +1167,7 @@ def place( Returns ------- - :class:`dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance` + :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance` """ padstack = None for pad in list(self.definitions.keys()): @@ -1428,7 +1428,7 @@ def get_instances( Returns ------- list - List of :class:`dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance`. + List of :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`. """ instances_by_id = self.instances @@ -1468,7 +1468,7 @@ def get_padstack_instance_by_net_name(self, net_name): Returns ------- list - List of :class:`dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance`. + List of :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`. """ warnings.warn("Use new property :func:`get_padstack_instance` instead.", DeprecationWarning) return self.get_instances(net_name=net_name) @@ -1497,7 +1497,7 @@ def get_reference_pins( Returns ------- list - List of :class:`dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance`. + List of :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`. Examples -------- diff --git a/src/pyedb/dotnet/edb_core/sim_setup_data/__init__.py b/src/pyedb/dotnet/database/sim_setup_data/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/sim_setup_data/__init__.py rename to src/pyedb/dotnet/database/sim_setup_data/__init__.py diff --git a/src/pyedb/dotnet/edb_core/sim_setup_data/data/__init__.py b/src/pyedb/dotnet/database/sim_setup_data/data/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/sim_setup_data/data/__init__.py rename to src/pyedb/dotnet/database/sim_setup_data/data/__init__.py diff --git a/src/pyedb/dotnet/edb_core/sim_setup_data/data/adaptive_frequency_data.py b/src/pyedb/dotnet/database/sim_setup_data/data/adaptive_frequency_data.py similarity index 100% rename from src/pyedb/dotnet/edb_core/sim_setup_data/data/adaptive_frequency_data.py rename to src/pyedb/dotnet/database/sim_setup_data/data/adaptive_frequency_data.py diff --git a/src/pyedb/dotnet/edb_core/sim_setup_data/data/mesh_operation.py b/src/pyedb/dotnet/database/sim_setup_data/data/mesh_operation.py similarity index 99% rename from src/pyedb/dotnet/edb_core/sim_setup_data/data/mesh_operation.py rename to src/pyedb/dotnet/database/sim_setup_data/data/mesh_operation.py index dfb6ba2eef..a17250b3db 100644 --- a/src/pyedb/dotnet/edb_core/sim_setup_data/data/mesh_operation.py +++ b/src/pyedb/dotnet/database/sim_setup_data/data/mesh_operation.py @@ -24,7 +24,7 @@ from System import Tuple -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list +from pyedb.dotnet.database.general import convert_py_list_to_net_list class MeshOpType(Enum): diff --git a/src/pyedb/dotnet/edb_core/sim_setup_data/data/settings.py b/src/pyedb/dotnet/database/sim_setup_data/data/settings.py similarity index 99% rename from src/pyedb/dotnet/edb_core/sim_setup_data/data/settings.py rename to src/pyedb/dotnet/database/sim_setup_data/data/settings.py index 001e13816b..dd12761580 100644 --- a/src/pyedb/dotnet/edb_core/sim_setup_data/data/settings.py +++ b/src/pyedb/dotnet/database/sim_setup_data/data/settings.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.sim_setup_data.data.adaptive_frequency_data import ( +from pyedb.dotnet.database.sim_setup_data.data.adaptive_frequency_data import ( AdaptiveFrequencyData, ) @@ -43,7 +43,7 @@ def adaptive_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.hfss_simulation_setup_data.AdaptiveSettings` + :class:`pyedb.dotnet.database.edb_data.hfss_simulation_setup_data.AdaptiveSettings` """ return self._parent.sim_setup_info.simulation_settings.AdaptiveSettings @@ -53,7 +53,7 @@ def adaptive_frequency_data_list(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.hfss_simulation_setup_data.AdaptiveFrequencyData` + :class:`pyedb.dotnet.database.edb_data.hfss_simulation_setup_data.AdaptiveFrequencyData` """ return [AdaptiveFrequencyData(i) for i in list(self.adaptive_settings.AdaptiveFrequencyDataList)] diff --git a/src/pyedb/dotnet/edb_core/sim_setup_data/data/sim_setup_info.py b/src/pyedb/dotnet/database/sim_setup_data/data/sim_setup_info.py similarity index 97% rename from src/pyedb/dotnet/edb_core/sim_setup_data/data/sim_setup_info.py rename to src/pyedb/dotnet/database/sim_setup_data/data/sim_setup_info.py index 9644fba071..612b1d8c8e 100644 --- a/src/pyedb/dotnet/edb_core/sim_setup_data/data/sim_setup_info.py +++ b/src/pyedb/dotnet/database/sim_setup_data/data/sim_setup_info.py @@ -20,10 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.sim_setup_data.data.simulation_settings import ( # HFSSSimulationSettings +from pyedb.dotnet.database.sim_setup_data.data.simulation_settings import ( # HFSSSimulationSettings HFSSPISimulationSettings, ) -from pyedb.dotnet.edb_core.sim_setup_data.data.sweep_data import SweepData +from pyedb.dotnet.database.sim_setup_data.data.sweep_data import SweepData class SimSetupInfo: diff --git a/src/pyedb/dotnet/edb_core/sim_setup_data/data/simulation_settings.py b/src/pyedb/dotnet/database/sim_setup_data/data/simulation_settings.py similarity index 99% rename from src/pyedb/dotnet/edb_core/sim_setup_data/data/simulation_settings.py rename to src/pyedb/dotnet/database/sim_setup_data/data/simulation_settings.py index 27ee5e5163..e0173aea1c 100644 --- a/src/pyedb/dotnet/edb_core/sim_setup_data/data/simulation_settings.py +++ b/src/pyedb/dotnet/database/sim_setup_data/data/simulation_settings.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list +from pyedb.dotnet.database.general import convert_py_list_to_net_list class BaseSimulationSettings: diff --git a/src/pyedb/dotnet/edb_core/sim_setup_data/data/siw_dc_ir_settings.py b/src/pyedb/dotnet/database/sim_setup_data/data/siw_dc_ir_settings.py similarity index 99% rename from src/pyedb/dotnet/edb_core/sim_setup_data/data/siw_dc_ir_settings.py rename to src/pyedb/dotnet/database/sim_setup_data/data/siw_dc_ir_settings.py index bd7edc14b1..ce010bde9d 100644 --- a/src/pyedb/dotnet/edb_core/sim_setup_data/data/siw_dc_ir_settings.py +++ b/src/pyedb/dotnet/database/sim_setup_data/data/siw_dc_ir_settings.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from pyedb.dotnet.edb_core.general import ( +from pyedb.dotnet.database.general import ( convert_netdict_to_pydict, convert_pydict_to_netdict, ) diff --git a/src/pyedb/dotnet/edb_core/sim_setup_data/data/sweep_data.py b/src/pyedb/dotnet/database/sim_setup_data/data/sweep_data.py similarity index 99% rename from src/pyedb/dotnet/edb_core/sim_setup_data/data/sweep_data.py rename to src/pyedb/dotnet/database/sim_setup_data/data/sweep_data.py index 74af3f51e9..8806d7c656 100644 --- a/src/pyedb/dotnet/edb_core/sim_setup_data/data/sweep_data.py +++ b/src/pyedb/dotnet/database/sim_setup_data/data/sweep_data.py @@ -28,7 +28,7 @@ class SweepData(object): Parameters ---------- - sim_setup : :class:`pyedb.dotnet.edb_core.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup` + sim_setup : :class:`pyedb.dotnet.database.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup` name : str, optional Name of the frequency sweep. edb_object : :class:`Ansys.Ansoft.Edb.Utility.SIWDCIRSimulationSettings`, optional diff --git a/src/pyedb/dotnet/edb_core/geometry/__init__.py b/src/pyedb/dotnet/database/sim_setup_data/io/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/geometry/__init__.py rename to src/pyedb/dotnet/database/sim_setup_data/io/__init__.py diff --git a/src/pyedb/dotnet/edb_core/sim_setup_data/io/siwave.py b/src/pyedb/dotnet/database/sim_setup_data/io/siwave.py similarity index 100% rename from src/pyedb/dotnet/edb_core/sim_setup_data/io/siwave.py rename to src/pyedb/dotnet/database/sim_setup_data/io/siwave.py diff --git a/src/pyedb/dotnet/edb_core/siwave.py b/src/pyedb/dotnet/database/siwave.py similarity index 99% rename from src/pyedb/dotnet/edb_core/siwave.py rename to src/pyedb/dotnet/database/siwave.py index 7a0a4fe481..8d05d46368 100644 --- a/src/pyedb/dotnet/edb_core/siwave.py +++ b/src/pyedb/dotnet/database/siwave.py @@ -27,19 +27,19 @@ import os import time -from pyedb.dotnet.edb_core.edb_data.padstacks_data import EDBPadstackInstance -from pyedb.dotnet.edb_core.edb_data.simulation_configuration import ( +from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance +from pyedb.dotnet.database.edb_data.simulation_configuration import ( SimulationConfiguration, SourceType, ) -from pyedb.dotnet.edb_core.edb_data.sources import ( +from pyedb.dotnet.database.edb_data.sources import ( CircuitPort, CurrentSource, DCTerminal, ResistorSource, VoltageSource, ) -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list +from pyedb.dotnet.database.general import convert_py_list_to_net_list from pyedb.generic.constants import SolverType, SweepType from pyedb.generic.general_methods import _retry_ntimes, generate_unique_name from pyedb.misc.siw_feature_config.xtalk_scan.scan_config import SiwaveScanConfig @@ -846,7 +846,7 @@ def add_siwave_syz_analysis( Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup` + :class:`pyedb.dotnet.database.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup` Setup object class. """ setup = self._pedb.create_siwave_syz_setup() @@ -888,7 +888,7 @@ def add_siwave_dc_analysis(self, name=None): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.siwave_simulation_setup_data.SiwaveDCSimulationSetup` + :class:`pyedb.dotnet.database.edb_data.siwave_simulation_setup_data.SiwaveDCSimulationSetup` Setup object class. Examples @@ -1200,7 +1200,7 @@ def create_rlc_component( Returns ------- - class:`pyedb.dotnet.edb_core.components.Components` + class:`pyedb.dotnet.database.components.Components` Created EDB component. """ @@ -1453,16 +1453,16 @@ def create_vrm_module( Set the voltage regulator active or not. Default value is ``True``. voltage ; str, float Set the voltage value. - positive_sensor_pin : int, .class pyedb.dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance + positive_sensor_pin : int, .class pyedb.dotnet.database.edb_data.padstacks_data.EDBPadstackInstance defining the positive sensor pin. - negative_sensor_pin : int, .class pyedb.dotnet.edb_core.edb_data.padstacks_data.EDBPadstackInstance + negative_sensor_pin : int, .class pyedb.dotnet.database.edb_data.padstacks_data.EDBPadstackInstance defining the negative sensor pin. load_regulation_current : str or float definition the load regulation current value. load_regulation_percent : float definition the load regulation percent value. """ - from pyedb.dotnet.edb_core.cell.voltage_regulator import VoltageRegulator + from pyedb.dotnet.database.cell.voltage_regulator import VoltageRegulator voltage = self._pedb.edb_value(voltage) load_regulation_current = self._pedb.edb_value(load_regulation_current) diff --git a/src/pyedb/dotnet/edb_core/stackup.py b/src/pyedb/dotnet/database/stackup.py similarity index 99% rename from src/pyedb/dotnet/edb_core/stackup.py rename to src/pyedb/dotnet/database/stackup.py index a6a332cf5e..6955facf21 100644 --- a/src/pyedb/dotnet/edb_core/stackup.py +++ b/src/pyedb/dotnet/database/stackup.py @@ -33,12 +33,12 @@ import math import warnings -from pyedb.dotnet.edb_core.edb_data.layer_data import ( +from pyedb.dotnet.database.edb_data.layer_data import ( LayerEdbClass, StackupLayerEdbClass, layer_cast, ) -from pyedb.dotnet.edb_core.general import convert_py_list_to_net_list +from pyedb.dotnet.database.general import convert_py_list_to_net_list from pyedb.generic.general_methods import ET, generate_unique_name from pyedb.misc.aedtlib_personalib_install import write_pretty_xml @@ -300,7 +300,7 @@ def layers(self): Returns ------- - Dict[str, :class:`pyedb.dotnet.edb_core.edb_data.layer_data.LayerEdbClass`] + Dict[str, :class:`pyedb.dotnet.database.edb_data.layer_data.LayerEdbClass`] """ return {name: obj for name, obj in self.all_layers.items() if obj.is_stackup_layer} @@ -636,7 +636,7 @@ def signal_layers(self): Returns ------- - Dict[str, :class:`pyedb.dotnet.edb_core.edb_data.layer_data.LayerEdbClass`] + Dict[str, :class:`pyedb.dotnet.database.edb_data.layer_data.LayerEdbClass`] """ layer_type = self._pedb.edb_api.cell.layer_type.SignalLayer _lays = OrderedDict() @@ -651,7 +651,7 @@ def dielectric_layers(self): Returns ------- - dict[str, :class:`dotnet.edb_core.edb_data.layer_data.EDBLayer`] + dict[str, :class:`dotnet.database.edb_data.layer_data.EDBLayer`] Dictionary of dielectric layers. """ layer_type = self._pedb.edb_api.cell.layer_type.DielectricLayer @@ -669,7 +669,7 @@ def _set_layout_stackup(self, layer_clone, operation, base_layer=None, method=1) Parameters ---------- - layer_clone : :class:`dotnet.edb_core.EDB_Data.EDBLayer` + layer_clone : :class:`dotnet.database.EDB_Data.EDBLayer` operation : str Options are ``"change_attribute"``, ``"change_name"``,``"change_position"``, ``"insert_below"``, ``"insert_above"``, ``"add_on_top"``, ``"add_on_bottom"``, ``"non_stackup"``, ``"add_at_elevation"``. @@ -837,7 +837,7 @@ def add_layer( Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.layer_data.LayerEdbClass` + :class:`pyedb.dotnet.database.edb_data.layer_data.LayerEdbClass` """ if layer_name in self.layers: logger.error("layer {} exists.".format(layer_name)) @@ -2236,7 +2236,7 @@ def _add_materials_from_dictionary(self, material_dict): def _import_xml(self, file_path, rename=False): """Read external xml file and convert into json file. You can use xml file to import layer stackup but using json file is recommended. - see :class:`pyedb.dotnet.edb_core.edb_data.simulation_configuration.SimulationConfiguration´ class to + see :class:`pyedb.dotnet.database.edb_data.simulation_configuration.SimulationConfiguration´ class to generate files`. Parameters @@ -2417,9 +2417,9 @@ def plot( plot_definitions : str, list, optional List of padstack definitions to plot on the stackup. It is supported only for Laminate mode. - first_layer : str or :class:`pyedb.dotnet.edb_core.edb_data.layer_data.LayerEdbClass` + first_layer : str or :class:`pyedb.dotnet.database.edb_data.layer_data.LayerEdbClass` First layer to plot from the bottom. Default is `None` to start plotting from bottom. - last_layer : str or :class:`pyedb.dotnet.edb_core.edb_data.layer_data.LayerEdbClass` + last_layer : str or :class:`pyedb.dotnet.database.edb_data.layer_data.LayerEdbClass` Last layer to plot from the bottom. Default is `None` to plot up to top layer. scale_elevation : bool, optional The real layer thickness is scaled so that max_thickness = 3 * min_thickness. @@ -2443,7 +2443,7 @@ def plot( elif isinstance(first_layer, LayerEdbClass): bottom_layer = first_layer.name else: - raise AttributeError("first_layer must be str or class `dotnet.edb_core.edb_data.layer_data.LayerEdbClass`") + raise AttributeError("first_layer must be str or class `dotnet.database.edb_data.layer_data.LayerEdbClass`") if last_layer is None or last_layer not in layer_names: top_layer = layer_names[0] elif isinstance(last_layer, str): @@ -2451,7 +2451,7 @@ def plot( elif isinstance(last_layer, LayerEdbClass): top_layer = last_layer.name else: - raise AttributeError("last_layer must be str or class `dotnet.edb_core.edb_data.layer_data.LayerEdbClass`") + raise AttributeError("last_layer must be str or class `dotnet.database.edb_data.layer_data.LayerEdbClass`") stackup_mode = self.mode if stackup_mode not in ["Laminate", "Overlapping"]: diff --git a/src/pyedb/dotnet/edb_core/utilities/__init__.py b/src/pyedb/dotnet/database/utilities/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/utilities/__init__.py rename to src/pyedb/dotnet/database/utilities/__init__.py diff --git a/src/pyedb/dotnet/edb_core/utilities/heatsink.py b/src/pyedb/dotnet/database/utilities/heatsink.py similarity index 100% rename from src/pyedb/dotnet/edb_core/utilities/heatsink.py rename to src/pyedb/dotnet/database/utilities/heatsink.py diff --git a/src/pyedb/dotnet/edb_core/utilities/hfss_simulation_setup.py b/src/pyedb/dotnet/database/utilities/hfss_simulation_setup.py similarity index 93% rename from src/pyedb/dotnet/edb_core/utilities/hfss_simulation_setup.py rename to src/pyedb/dotnet/database/utilities/hfss_simulation_setup.py index e17d833635..c8ca3a41b1 100644 --- a/src/pyedb/dotnet/edb_core/utilities/hfss_simulation_setup.py +++ b/src/pyedb/dotnet/database/utilities/hfss_simulation_setup.py @@ -21,11 +21,11 @@ # SOFTWARE. -from pyedb.dotnet.edb_core.sim_setup_data.data.mesh_operation import ( +from pyedb.dotnet.database.sim_setup_data.data.mesh_operation import ( LengthMeshOperation, SkinDepthMeshOperation, ) -from pyedb.dotnet.edb_core.sim_setup_data.data.settings import ( +from pyedb.dotnet.database.sim_setup_data.data.settings import ( AdaptiveSettings, AdvancedMeshSettings, CurveApproxSettings, @@ -35,8 +35,8 @@ HfssSolverSettings, ViaSettings, ) -from pyedb.dotnet.edb_core.sim_setup_data.data.sim_setup_info import SimSetupInfo -from pyedb.dotnet.edb_core.utilities.simulation_setup import SimulationSetup +from pyedb.dotnet.database.sim_setup_data.data.sim_setup_info import SimSetupInfo +from pyedb.dotnet.database.utilities.simulation_setup import SimulationSetup from pyedb.generic.general_methods import generate_unique_name @@ -101,7 +101,7 @@ def hfss_solver_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.hfss_simulation_setup_data.HfssSolverSettings` + :class:`pyedb.dotnet.database.edb_data.hfss_simulation_setup_data.HfssSolverSettings` """ return HfssSolverSettings(self) @@ -112,7 +112,7 @@ def adaptive_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.hfss_simulation_setup_data.AdaptiveSettings` + :class:`pyedb.dotnet.database.edb_data.hfss_simulation_setup_data.AdaptiveSettings` """ return AdaptiveSettings(self) @@ -123,7 +123,7 @@ def defeature_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.hfss_simulation_setup_data.DefeatureSettings` + :class:`pyedb.dotnet.database.edb_data.hfss_simulation_setup_data.DefeatureSettings` """ return DefeatureSettings(self) @@ -134,7 +134,7 @@ def via_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.hfss_simulation_setup_data.ViaSettings` + :class:`pyedb.dotnet.database.edb_data.hfss_simulation_setup_data.ViaSettings` """ return ViaSettings(self) @@ -145,7 +145,7 @@ def advanced_mesh_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.hfss_simulation_setup_data.AdvancedMeshSettings` + :class:`pyedb.dotnet.database.edb_data.hfss_simulation_setup_data.AdvancedMeshSettings` """ return AdvancedMeshSettings(self) @@ -156,7 +156,7 @@ def curve_approx_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.hfss_simulation_setup_data.CurveApproxSettings` + :class:`pyedb.dotnet.database.edb_data.hfss_simulation_setup_data.CurveApproxSettings` """ return CurveApproxSettings(self) @@ -167,7 +167,7 @@ def dcr_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.hfss_simulation_setup_data.DcrSettings` + :class:`pyedb.dotnet.database.edb_data.hfss_simulation_setup_data.DcrSettings` """ return DcrSettings(self) @@ -178,7 +178,7 @@ def hfss_port_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.hfss_simulation_setup_data.HfssPortSettings` + :class:`pyedb.dotnet.database.edb_data.hfss_simulation_setup_data.HfssPortSettings` """ return HfssPortSettings(self) @@ -189,7 +189,7 @@ def mesh_operations(self): Returns ------- - List of :class:`dotnet.edb_core.edb_data.hfss_simulation_setup_data.MeshOperation` + List of :class:`dotnet.database.edb_data.hfss_simulation_setup_data.MeshOperation` """ settings = self.sim_setup_info.simulation_settings.MeshOperations @@ -238,7 +238,7 @@ def add_length_mesh_operation( Returns ------- - :class:`dotnet.edb_core.edb_data.hfss_simulation_setup_data.LengthMeshOperation` + :class:`dotnet.database.edb_data.hfss_simulation_setup_data.LengthMeshOperation` """ if not name: name = generate_unique_name("skin") @@ -292,7 +292,7 @@ def add_skin_depth_mesh_operation( Returns ------- - :class:`dotnet.edb_core.edb_data.hfss_simulation_setup_data.LengthMeshOperation` + :class:`dotnet.database.edb_data.hfss_simulation_setup_data.LengthMeshOperation` """ if not name: name = generate_unique_name("length") diff --git a/src/pyedb/dotnet/edb_core/utilities/obj_base.py b/src/pyedb/dotnet/database/utilities/obj_base.py similarity index 97% rename from src/pyedb/dotnet/edb_core/utilities/obj_base.py rename to src/pyedb/dotnet/database/utilities/obj_base.py index c8ea27e86a..a9e18b3c3c 100644 --- a/src/pyedb/dotnet/edb_core/utilities/obj_base.py +++ b/src/pyedb/dotnet/database/utilities/obj_base.py @@ -21,7 +21,7 @@ # SOFTWARE. from pyedb.dotnet.clr_module import Tuple -from pyedb.dotnet.edb_core.geometry.point_data import PointData +from pyedb.dotnet.database.geometry.point_data import PointData class BBox: diff --git a/src/pyedb/dotnet/edb_core/utilities/simulation_setup.py b/src/pyedb/dotnet/database/utilities/simulation_setup.py similarity index 98% rename from src/pyedb/dotnet/edb_core/utilities/simulation_setup.py rename to src/pyedb/dotnet/database/utilities/simulation_setup.py index dbce4ab5d5..57eceba498 100644 --- a/src/pyedb/dotnet/edb_core/utilities/simulation_setup.py +++ b/src/pyedb/dotnet/database/utilities/simulation_setup.py @@ -24,8 +24,8 @@ from enum import Enum import warnings -from pyedb.dotnet.edb_core.sim_setup_data.data.sim_setup_info import SimSetupInfo -from pyedb.dotnet.edb_core.sim_setup_data.data.sweep_data import SweepData +from pyedb.dotnet.database.sim_setup_data.data.sim_setup_info import SimSetupInfo +from pyedb.dotnet.database.sim_setup_data.data.sweep_data import SweepData from pyedb.generic.general_methods import generate_unique_name @@ -334,7 +334,7 @@ def add_frequency_sweep(self, name=None, frequency_sweep=None): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.simulation_setup_data.EdbFrequencySweep` + :class:`pyedb.dotnet.database.edb_data.simulation_setup_data.EdbFrequencySweep` Examples -------- diff --git a/src/pyedb/dotnet/edb_core/utilities/siwave_simulation_setup.py b/src/pyedb/dotnet/database/utilities/siwave_simulation_setup.py similarity index 97% rename from src/pyedb/dotnet/edb_core/utilities/siwave_simulation_setup.py rename to src/pyedb/dotnet/database/utilities/siwave_simulation_setup.py index bb0397b92a..e91078d7ef 100644 --- a/src/pyedb/dotnet/edb_core/utilities/siwave_simulation_setup.py +++ b/src/pyedb/dotnet/database/utilities/siwave_simulation_setup.py @@ -1,19 +1,19 @@ import warnings -from pyedb.dotnet.edb_core.general import ( +from pyedb.dotnet.database.general import ( convert_netdict_to_pydict, convert_pydict_to_netdict, ) -from pyedb.dotnet.edb_core.sim_setup_data.data.sim_setup_info import SimSetupInfo -from pyedb.dotnet.edb_core.sim_setup_data.data.siw_dc_ir_settings import ( +from pyedb.dotnet.database.sim_setup_data.data.sim_setup_info import SimSetupInfo +from pyedb.dotnet.database.sim_setup_data.data.siw_dc_ir_settings import ( SiwaveDCIRSettings, ) -from pyedb.dotnet.edb_core.sim_setup_data.io.siwave import ( +from pyedb.dotnet.database.sim_setup_data.io.siwave import ( AdvancedSettings, DCAdvancedSettings, DCSettings, ) -from pyedb.dotnet.edb_core.utilities.simulation_setup import SimulationSetup +from pyedb.dotnet.database.utilities.simulation_setup import SimulationSetup from pyedb.generic.general_methods import is_linux @@ -335,7 +335,7 @@ def dc_advanced_settings(self): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.siwave_simulation_setup_data.SiwaveDCAdvancedSettings` + :class:`pyedb.dotnet.database.edb_data.siwave_simulation_setup_data.SiwaveDCAdvancedSettings` """ return DCAdvancedSettings(self) diff --git a/src/pyedb/dotnet/edb.py b/src/pyedb/dotnet/edb.py index eb7b7ac4f4..a9e8db4a2c 100644 --- a/src/pyedb/dotnet/edb.py +++ b/src/pyedb/dotnet/edb.py @@ -42,18 +42,18 @@ import rtree from pyedb.configuration.configuration import Configuration -from pyedb.dotnet.application.Variables import decompose_variable_value -from pyedb.dotnet.edb_core.cell.layout import Layout -from pyedb.dotnet.edb_core.cell.terminal.terminal import Terminal -from pyedb.dotnet.edb_core.components import Components -from pyedb.dotnet.edb_core.dotnet.database import Database -from pyedb.dotnet.edb_core.edb_data.control_file import ( +from pyedb.dotnet.database.Variables import decompose_variable_value +from pyedb.dotnet.database.cell.layout import Layout +from pyedb.dotnet.database.cell.terminal.terminal import Terminal +from pyedb.dotnet.database.components import Components +from pyedb.dotnet.database.dotnet.database import Database +from pyedb.dotnet.database.edb_data.control_file import ( ControlFile, convert_technology_file, ) -from pyedb.dotnet.edb_core.edb_data.design_options import EdbDesignOptions -from pyedb.dotnet.edb_core.edb_data.edbvalue import EdbValue -from pyedb.dotnet.edb_core.edb_data.ports import ( +from pyedb.dotnet.database.edb_data.design_options import EdbDesignOptions +from pyedb.dotnet.database.edb_data.edbvalue import EdbValue +from pyedb.dotnet.database.edb_data.ports import ( BundleWavePort, CircuitPort, CoaxPort, @@ -61,37 +61,37 @@ GapPort, WavePort, ) -from pyedb.dotnet.edb_core.edb_data.raptor_x_simulation_setup_data import ( +from pyedb.dotnet.database.edb_data.raptor_x_simulation_setup_data import ( RaptorXSimulationSetup, ) -from pyedb.dotnet.edb_core.edb_data.simulation_configuration import ( +from pyedb.dotnet.database.edb_data.simulation_configuration import ( SimulationConfiguration, ) -from pyedb.dotnet.edb_core.edb_data.sources import SourceType -from pyedb.dotnet.edb_core.edb_data.variables import Variable -from pyedb.dotnet.edb_core.general import ( +from pyedb.dotnet.database.edb_data.sources import SourceType +from pyedb.dotnet.database.edb_data.variables import Variable +from pyedb.dotnet.database.general import ( LayoutObjType, Primitives, convert_py_list_to_net_list, ) -from pyedb.dotnet.edb_core.hfss import EdbHfss -from pyedb.dotnet.edb_core.layout_validation import LayoutValidation -from pyedb.dotnet.edb_core.materials import Materials -from pyedb.dotnet.edb_core.modeler import Modeler -from pyedb.dotnet.edb_core.net_class import ( +from pyedb.dotnet.database.hfss import EdbHfss +from pyedb.dotnet.database.layout_validation import LayoutValidation +from pyedb.dotnet.database.materials import Materials +from pyedb.dotnet.database.modeler import Modeler +from pyedb.dotnet.database.net_class import ( EdbDifferentialPairs, EdbExtendedNets, EdbNetClasses, ) -from pyedb.dotnet.edb_core.nets import EdbNets -from pyedb.dotnet.edb_core.padstack import EdbPadstacks -from pyedb.dotnet.edb_core.siwave import EdbSiwave -from pyedb.dotnet.edb_core.stackup import Stackup -from pyedb.dotnet.edb_core.utilities.hfss_simulation_setup import ( +from pyedb.dotnet.database.nets import EdbNets +from pyedb.dotnet.database.padstack import EdbPadstacks +from pyedb.dotnet.database.siwave import EdbSiwave +from pyedb.dotnet.database.stackup import Stackup +from pyedb.dotnet.database.utilities.hfss_simulation_setup import ( HFSSPISimulationSetup, HfssSimulationSetup, ) -from pyedb.dotnet.edb_core.utilities.siwave_simulation_setup import ( +from pyedb.dotnet.database.utilities.siwave_simulation_setup import ( SiwaveDCSimulationSetup, SiwaveSimulationSetup, ) @@ -278,6 +278,7 @@ def __init__( self.logger.info("EDB initialized.") else: self.logger.info("Failed to initialize DLLs.") + self._layout_instance = None def __enter__(self): return self @@ -296,7 +297,7 @@ def __getitem__(self, variable_name): Returns ------- - variable object : :class:`pyedb.dotnet.edb_core.edb_data.variables.Variable` + variable object : :class:`pyedb.dotnet.database.edb_data.variables.Variable` """ if self.variable_exists(variable_name)[0]: @@ -348,6 +349,7 @@ def _check_remove_project_files(self, edbpath: str, remove_existing_aedt: bool) def _clean_variables(self): """Initialize internal variables and perform garbage collection.""" + self.grpc = False self._materials = None self._components = None self._core_primitives = None @@ -393,7 +395,7 @@ def design_variables(self): Returns ------- - variable dictionary : Dict[str, :class:`pyedb.dotnet.edb_core.edb_data.variables.Variable`] + variable dictionary : Dict[str, :class:`pyedb.dotnet.database.edb_data.variables.Variable`] """ d_var = dict() for i in self.active_cell.GetVariableServer().GetAllVariableNames(): @@ -406,7 +408,7 @@ def project_variables(self): Returns ------- - variables dictionary : Dict[str, :class:`pyedb.dotnet.edb_core.edb_data.variables.Variable`] + variables dictionary : Dict[str, :class:`pyedb.dotnet.database.edb_data.variables.Variable`] """ p_var = dict() @@ -416,11 +418,11 @@ def project_variables(self): @property def layout_validation(self): - """:class:`pyedb.dotnet.edb_core.edb_data.layout_validation.LayoutValidation`. + """:class:`pyedb.dotnet.database.edb_data.layout_validation.LayoutValidation`. Returns ------- - layout validation object : :class: 'pyedb.dotnet.edb_core.layout_validation.LayoutValidation' + layout validation object : :class: 'pyedb.dotnet.database.layout_validation.LayoutValidation' """ return LayoutValidation(self) @@ -430,7 +432,7 @@ def variables(self): Returns ------- - variables dictionary : Dict[str, :class:`pyedb.dotnet.edb_core.edb_data.variables.Variable`] + variables dictionary : Dict[str, :class:`pyedb.dotnet.database.edb_data.variables.Variable`] """ all_vars = dict() @@ -469,8 +471,8 @@ def ports(self): Returns ------- - port dictionary : Dict[str, [:class:`pyedb.dotnet.edb_core.edb_data.ports.GapPort`, - :class:`pyedb.dotnet.edb_core.edb_data.ports.WavePort`,]] + port dictionary : Dict[str, [:class:`pyedb.dotnet.database.edb_data.ports.GapPort`, + :class:`pyedb.dotnet.database.edb_data.ports.WavePort`,]] """ temp = [term for term in self.layout.terminals if not term.is_reference_terminal] @@ -765,7 +767,7 @@ def core_components(self): # pragma: no cover Returns ------- - Instance of :class:`pyedb.dotnet.edb_core.Components.Components` + Instance of :class:`pyedb.dotnet.database.Components.Components` Examples -------- @@ -782,7 +784,7 @@ def components(self): Returns ------- - Instance of :class:`pyedb.dotnet.edb_core.components.Components` + Instance of :class:`pyedb.dotnet.database.components.Components` Examples -------- @@ -815,7 +817,7 @@ def design_options(self): Returns ------- - Instance of :class:`pyedb.dotnet.edb_core.edb_data.design_options.EdbDesignOptions` + Instance of :class:`pyedb.dotnet.database.edb_data.design_options.EdbDesignOptions` """ return EdbDesignOptions(self.active_cell) @@ -825,7 +827,7 @@ def stackup(self): Returns ------- - Instance of :class: 'pyedb.dotnet.edb_core.Stackup` + Instance of :class: 'pyedb.dotnet.database.Stackup` Examples -------- @@ -843,7 +845,7 @@ def materials(self): Returns ------- - Instance of :class: `pyedb.dotnet.edb_core.Materials` + Instance of :class: `pyedb.dotnet.database.Materials` Examples -------- @@ -867,7 +869,7 @@ def core_padstack(self): # pragma: no cover Returns ------- - Instance of :class: `pyedb.dotnet.edb_core.padstack.EdbPadstack` + Instance of :class: `pyedb.dotnet.database.padstack.EdbPadstack` Examples -------- @@ -889,7 +891,7 @@ def padstacks(self): Returns ------- - Instance of :class: `legacy.edb_core.padstack.EdbPadstack` + Instance of :class: `legacy.database.padstack.EdbPadstack` Examples -------- @@ -914,7 +916,7 @@ def core_siwave(self): # pragma: no cover Returns ------- - Instance of :class: `pyedb.dotnet.edb_core.siwave.EdbSiwave` + Instance of :class: `pyedb.dotnet.database.siwave.EdbSiwave` Examples -------- @@ -931,7 +933,7 @@ def siwave(self): Returns ------- - Instance of :class: `pyedb.dotnet.edb_core.siwave.EdbSiwave` + Instance of :class: `pyedb.dotnet.database.siwave.EdbSiwave` Examples -------- @@ -952,7 +954,7 @@ def core_hfss(self): # pragma: no cover Returns ------- - Instance of :class:`legacy.edb_core.hfss.EdbHfss` + Instance of :class:`legacy.database.hfss.EdbHfss` Examples -------- @@ -969,11 +971,11 @@ def hfss(self): Returns ------- - :class:`pyedb.dotnet.edb_core.hfss.EdbHfss` + :class:`pyedb.dotnet.database.hfss.EdbHfss` See Also -------- - :class:`legacy.edb_core.edb_data.simulation_configuration.SimulationConfiguration` + :class:`legacy.database.edb_data.simulation_configuration.SimulationConfiguration` Examples -------- @@ -996,7 +998,7 @@ def core_nets(self): # pragma: no cover Returns ------- - :class:`pyedb.dotnet.edb_core.nets.EdbNets` + :class:`pyedb.dotnet.database.nets.EdbNets` Examples -------- @@ -1014,7 +1016,7 @@ def nets(self): Returns ------- - :class:`legacy.edb_core.nets.EdbNets` + :class:`legacy.database.nets.EdbNets` Examples -------- @@ -1035,7 +1037,7 @@ def net_classes(self): Returns ------- - :class:`legacy.edb_core.nets.EdbNetClasses` + :class:`legacy.database.nets.EdbNetClasses` Examples -------- @@ -1053,7 +1055,7 @@ def extended_nets(self): Returns ------- - :class:`legacy.edb_core.nets.EdbExtendedNets` + :class:`legacy.database.nets.EdbExtendedNets` Examples -------- @@ -1071,7 +1073,7 @@ def differential_pairs(self): Returns ------- - :class:`legacy.edb_core.nets.EdbDifferentialPairs` + :class:`legacy.database.nets.EdbDifferentialPairs` Examples -------- @@ -1093,7 +1095,7 @@ def core_primitives(self): # pragma: no cover Returns ------- - Instance of :class: `legacy.edb_core.layout.EdbLayout` + Instance of :class: `legacy.database.layout.EdbLayout` Examples -------- @@ -1110,7 +1112,7 @@ def modeler(self): Returns ------- - Instance of :class: `legacy.edb_core.layout.EdbLayout` + Instance of :class: `legacy.database.layout.EdbLayout` Examples -------- @@ -1128,7 +1130,7 @@ def layout(self): Returns ------- - :class:`legacy.edb_core.dotnet.layout.Layout` + :class:`legacy.database.dotnet.layout.Layout` """ return Layout(self, self._active_cell.GetLayout()) @@ -1145,7 +1147,9 @@ def active_layout(self): @property def layout_instance(self): """Edb Layout Instance.""" - return self.layout._edb_object.GetLayoutInstance() + if not self._layout_instance: + self._layout_instance = self.layout._edb_object.GetLayoutInstance() + return self._layout_instance def get_connected_objects(self, layout_object_instance): """Get connected objects. @@ -1163,7 +1167,7 @@ def get_connected_objects(self, layout_object_instance): ): obj_type = i.GetObjType().ToString() if obj_type == LayoutObjType.PadstackInstance.name: - from pyedb.dotnet.edb_core.edb_data.padstacks_data import ( + from pyedb.dotnet.database.edb_data.padstacks_data import ( EDBPadstackInstance, ) @@ -1171,21 +1175,21 @@ def get_connected_objects(self, layout_object_instance): elif obj_type == LayoutObjType.Primitive.name: prim_type = i.GetPrimitiveType().ToString() if prim_type == Primitives.Path.name: - from pyedb.dotnet.edb_core.cell.primitive.path import Path + from pyedb.dotnet.database.cell.primitive.path import Path temp.append(Path(self, i)) elif prim_type == Primitives.Rectangle.name: - from pyedb.dotnet.edb_core.edb_data.primitives_data import ( + from pyedb.dotnet.database.edb_data.primitives_data import ( EdbRectangle, ) temp.append(EdbRectangle(i, self)) elif prim_type == Primitives.Circle.name: - from pyedb.dotnet.edb_core.edb_data.primitives_data import EdbCircle + from pyedb.dotnet.database.edb_data.primitives_data import EdbCircle temp.append(EdbCircle(i, self)) elif prim_type == Primitives.Polygon.name: - from pyedb.dotnet.edb_core.edb_data.primitives_data import ( + from pyedb.dotnet.database.edb_data.primitives_data import ( EdbPolygon, ) @@ -1205,7 +1209,7 @@ def pins(self): Returns ------- - dic[str, :class:`legacy.edb_core.edb_data.definitions.EDBPadstackInstance`] + dic[str, :class:`legacy.database.edb_data.definitions.EDBPadstackInstance`] Dictionary of EDBPadstackInstance Components. @@ -3171,7 +3175,7 @@ def get_variable(self, variable_name): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.edbvalue.EdbValue` + :class:`pyedb.dotnet.database.edb_data.edbvalue.EdbValue` """ var_server = self.variable_exists(variable_name) if var_server[0]: @@ -3315,7 +3319,7 @@ def build_simulation_project(self, simulation_setup): Parameters ---------- - simulation_setup : :class:`pyedb.dotnet.edb_core.edb_data.simulation_configuration.SimulationConfiguration`. + simulation_setup : :class:`pyedb.dotnet.database.edb_data.simulation_configuration.SimulationConfiguration`. SimulationConfiguration object that can be instantiated or directly loaded with a configuration file. @@ -3328,7 +3332,7 @@ def build_simulation_project(self, simulation_setup): -------- >>> from pyedb import Edb - >>> from pyedb.dotnet.edb_core.edb_data.simulation_configuration import SimulationConfiguration + >>> from pyedb.dotnet.database.edb_data.simulation_configuration import SimulationConfiguration >>> config_file = path_configuration_file >>> source_file = path_to_edb_folder >>> edb = Edb(source_file) @@ -3601,7 +3605,7 @@ def new_simulation_configuration(self, filename=None): Returns ------- - :class:`legacy.edb_core.edb_data.simulation_configuration.SimulationConfiguration` + :class:`legacy.database.edb_data.simulation_configuration.SimulationConfiguration` """ return SimulationConfiguration(filename, self) @@ -3611,9 +3615,9 @@ def setups(self): Returns ------- - Dict[str, :class:`legacy.edb_core.edb_data.hfss_simulation_setup_data.HfssSimulationSetup`] or - Dict[str, :class:`legacy.edb_core.edb_data.siwave_simulation_setup_data.SiwaveDCSimulationSetup`] or - Dict[str, :class:`legacy.edb_core.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup`] + Dict[str, :class:`legacy.database.edb_data.hfss_simulation_setup_data.HfssSimulationSetup`] or + Dict[str, :class:`legacy.database.edb_data.siwave_simulation_setup_data.SiwaveDCSimulationSetup`] or + Dict[str, :class:`legacy.database.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup`] """ setups = {} @@ -3636,7 +3640,7 @@ def hfss_setups(self): Returns ------- - Dict[str, :class:`legacy.edb_core.edb_data.hfss_simulation_setup_data.HfssSimulationSetup`] + Dict[str, :class:`legacy.database.edb_data.hfss_simulation_setup_data.HfssSimulationSetup`] """ return {name: i for name, i in self.setups.items() if i.setup_type == "kHFSS"} @@ -3647,7 +3651,7 @@ def siwave_dc_setups(self): Returns ------- - Dict[str, :class:`legacy.edb_core.edb_data.siwave_simulation_setup_data.SiwaveDCSimulationSetup`] + Dict[str, :class:`legacy.database.edb_data.siwave_simulation_setup_data.SiwaveDCSimulationSetup`] """ return {name: i for name, i in self.setups.items() if isinstance(i, SiwaveDCSimulationSetup)} @@ -3657,7 +3661,7 @@ def siwave_ac_setups(self): Returns ------- - Dict[str, :class:`legacy.edb_core.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup`] + Dict[str, :class:`legacy.database.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup`] """ return {name: i for name, i in self.setups.items() if isinstance(i, SiwaveSimulationSetup)} @@ -3671,7 +3675,7 @@ def create_hfss_setup(self, name=None): Returns ------- - :class:`legacy.edb_core.edb_data.hfss_simulation_setup_data.HfssSimulationSetup` + :class:`legacy.database.edb_data.hfss_simulation_setup_data.HfssSimulationSetup` Examples -------- @@ -3699,7 +3703,7 @@ def create_raptorx_setup(self, name=None): Returns ------- - :class:`legacy.edb_core.edb_data.raptor_x_simulation_setup_data.RaptorXSimulationSetup` + :class:`legacy.database.edb_data.raptor_x_simulation_setup_data.RaptorXSimulationSetup` """ if name in self.setups: @@ -3723,7 +3727,7 @@ def create_hfsspi_setup(self, name=None): Returns ------- - :class:`legacy.edb_core.edb_data.hfss_pi_simulation_setup_data.HFSSPISimulationSetup when succeeded, ``False`` + :class:`legacy.database.edb_data.hfss_pi_simulation_setup_data.HFSSPISimulationSetup when succeeded, ``False`` when failed. """ @@ -3746,7 +3750,7 @@ def create_siwave_syz_setup(self, name=None, **kwargs): Returns ------- - :class:`pyedb.dotnet.edb_core.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup` + :class:`pyedb.dotnet.database.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup` Examples -------- @@ -3778,7 +3782,7 @@ def create_siwave_dc_setup(self, name=None, **kwargs): Returns ------- - :class:`legacy.edb_core.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup` + :class:`legacy.database.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup` Examples -------- @@ -4015,15 +4019,15 @@ def create_port(self, terminal, ref_terminal=None, is_circuit_port=False, name=N Parameters ---------- - terminal : class:`pyedb.dotnet.edb_core.edb_data.terminals.EdgeTerminal`, - class:`pyedb.dotnet.edb_core.edb_data.terminals.PadstackInstanceTerminal`, - class:`pyedb.dotnet.edb_core.edb_data.terminals.PointTerminal`, - class:`pyedb.dotnet.edb_core.edb_data.terminals.PinGroupTerminal`, + terminal : class:`pyedb.dotnet.database.edb_data.terminals.EdgeTerminal`, + class:`pyedb.dotnet.database.edb_data.terminals.PadstackInstanceTerminal`, + class:`pyedb.dotnet.database.edb_data.terminals.PointTerminal`, + class:`pyedb.dotnet.database.edb_data.terminals.PinGroupTerminal`, Positive terminal of the port. - ref_terminal : class:`pyedb.dotnet.edb_core.edb_data.terminals.EdgeTerminal`, - class:`pyedb.dotnet.edb_core.edb_data.terminals.PadstackInstanceTerminal`, - class:`pyedb.dotnet.edb_core.edb_data.terminals.PointTerminal`, - class:`pyedb.dotnet.edb_core.edb_data.terminals.PinGroupTerminal`, + ref_terminal : class:`pyedb.dotnet.database.edb_data.terminals.EdgeTerminal`, + class:`pyedb.dotnet.database.edb_data.terminals.PadstackInstanceTerminal`, + class:`pyedb.dotnet.database.edb_data.terminals.PointTerminal`, + class:`pyedb.dotnet.database.edb_data.terminals.PinGroupTerminal`, optional Negative terminal of the port. is_circuit_port : bool, optional @@ -4032,8 +4036,8 @@ def create_port(self, terminal, ref_terminal=None, is_circuit_port=False, name=N Name of the created port. The default is None, a random name is generated. Returns ------- - list: [:class:`pyedb.dotnet.edb_core.edb_data.ports.GapPort`, - :class:`pyedb.dotnet.edb_core.edb_data.ports.WavePort`,]. + list: [:class:`pyedb.dotnet.database.edb_data.ports.GapPort`, + :class:`pyedb.dotnet.database.edb_data.ports.WavePort`,]. """ terminal.boundary_type = "PortBoundary" @@ -4051,20 +4055,20 @@ def create_voltage_probe(self, terminal, ref_terminal): Parameters ---------- - terminal : :class:`pyedb.dotnet.edb_core.edb_data.terminals.EdgeTerminal`, - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PadstackInstanceTerminal`, - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PointTerminal`, - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PinGroupTerminal`, + terminal : :class:`pyedb.dotnet.database.edb_data.terminals.EdgeTerminal`, + :class:`pyedb.dotnet.database.edb_data.terminals.PadstackInstanceTerminal`, + :class:`pyedb.dotnet.database.edb_data.terminals.PointTerminal`, + :class:`pyedb.dotnet.database.edb_data.terminals.PinGroupTerminal`, Positive terminal of the port. - ref_terminal : :class:`pyedb.dotnet.edb_core.edb_data.terminals.EdgeTerminal`, - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PadstackInstanceTerminal`, - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PointTerminal`, - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PinGroupTerminal`, + ref_terminal : :class:`pyedb.dotnet.database.edb_data.terminals.EdgeTerminal`, + :class:`pyedb.dotnet.database.edb_data.terminals.PadstackInstanceTerminal`, + :class:`pyedb.dotnet.database.edb_data.terminals.PointTerminal`, + :class:`pyedb.dotnet.database.edb_data.terminals.PinGroupTerminal`, Negative terminal of the probe. Returns ------- - pyedb.dotnet.edb_core.edb_data.terminals.Terminal + pyedb.dotnet.database.edb_data.terminals.Terminal """ term = Terminal(self, terminal._edb_object) term.boundary_type = "kVoltageProbe" @@ -4080,20 +4084,20 @@ def create_voltage_source(self, terminal, ref_terminal): Parameters ---------- - terminal : :class:`pyedb.dotnet.edb_core.edb_data.terminals.EdgeTerminal`, \ - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PadstackInstanceTerminal`, \ - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PointTerminal`, \ - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PinGroupTerminal` + terminal : :class:`pyedb.dotnet.database.edb_data.terminals.EdgeTerminal`, \ + :class:`pyedb.dotnet.database.edb_data.terminals.PadstackInstanceTerminal`, \ + :class:`pyedb.dotnet.database.edb_data.terminals.PointTerminal`, \ + :class:`pyedb.dotnet.database.edb_data.terminals.PinGroupTerminal` Positive terminal of the port. - ref_terminal : class:`pyedb.dotnet.edb_core.edb_data.terminals.EdgeTerminal`, \ - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PadstackInstanceTerminal`, \ - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PointTerminal`, \ - :class:`pyedb.dotnet.edb_core.edb_data.terminals.PinGroupTerminal` + ref_terminal : class:`pyedb.dotnet.database.edb_data.terminals.EdgeTerminal`, \ + :class:`pyedb.dotnet.database.edb_data.terminals.PadstackInstanceTerminal`, \ + :class:`pyedb.dotnet.database.edb_data.terminals.PointTerminal`, \ + :class:`pyedb.dotnet.database.edb_data.terminals.PinGroupTerminal` Negative terminal of the source. Returns ------- - class:`legacy.edb_core.edb_data.ports.ExcitationSources` + class:`legacy.database.edb_data.ports.ExcitationSources` """ term = Terminal(self, terminal._edb_object) term.boundary_type = "kVoltageSource" @@ -4109,20 +4113,20 @@ def create_current_source(self, terminal, ref_terminal): Parameters ---------- - terminal : :class:`legacy.edb_core.edb_data.terminals.EdgeTerminal`, - :class:`legacy.edb_core.edb_data.terminals.PadstackInstanceTerminal`, - :class:`legacy.edb_core.edb_data.terminals.PointTerminal`, - :class:`legacy.edb_core.edb_data.terminals.PinGroupTerminal`, + terminal : :class:`legacy.database.edb_data.terminals.EdgeTerminal`, + :class:`legacy.database.edb_data.terminals.PadstackInstanceTerminal`, + :class:`legacy.database.edb_data.terminals.PointTerminal`, + :class:`legacy.database.edb_data.terminals.PinGroupTerminal`, Positive terminal of the port. - ref_terminal : class:`legacy.edb_core.edb_data.terminals.EdgeTerminal`, - :class:`legacy.edb_core.edb_data.terminals.PadstackInstanceTerminal`, - :class:`legacy.edb_core.edb_data.terminals.PointTerminal`, - :class:`legacy.edb_core.edb_data.terminals.PinGroupTerminal`, + ref_terminal : class:`legacy.database.edb_data.terminals.EdgeTerminal`, + :class:`legacy.database.edb_data.terminals.PadstackInstanceTerminal`, + :class:`legacy.database.edb_data.terminals.PointTerminal`, + :class:`legacy.database.edb_data.terminals.PinGroupTerminal`, Negative terminal of the source. Returns ------- - :class:`legacy.edb_core.edb_data.ports.ExcitationSources` + :class:`legacy.database.edb_data.ports.ExcitationSources` """ term = Terminal(self, terminal._edb_object) term.boundary_type = "kCurrentSource" @@ -4149,9 +4153,9 @@ def get_point_terminal(self, name, net_name, location, layer): Returns ------- - :class:`legacy.edb_core.edb_data.terminals.PointTerminal` + :class:`legacy.database.edb_data.terminals.PointTerminal` """ - from pyedb.dotnet.edb_core.cell.terminal.point_terminal import PointTerminal + from pyedb.dotnet.database.cell.terminal.point_terminal import PointTerminal point_terminal = PointTerminal(self) return point_terminal.create(name, net_name, location, layer) @@ -4598,7 +4602,7 @@ def create_model_for_arbitrary_wave_ports( @property def definitions(self): """Definitions class.""" - from pyedb.dotnet.edb_core.definition.definitions import Definitions + from pyedb.dotnet.database.definition.definitions import Definitions return Definitions(self) diff --git a/src/pyedb/generic/general_methods.py b/src/pyedb/generic/general_methods.py index a06f2c3eb6..9757cf9858 100644 --- a/src/pyedb/generic/general_methods.py +++ b/src/pyedb/generic/general_methods.py @@ -250,7 +250,7 @@ def _log_method(func, new_args, new_kwargs): # pragma: no cover if ( not settings.enable_debug_edb_logger and "Edb" in str(func) + str(new_args) - or "edb_core" in str(func) + str(new_args) + or "database" in str(func) + str(new_args) ): return line_begin = "ARGUMENTS: " diff --git a/src/pyedb/generic/plot.py b/src/pyedb/generic/plot.py index 5b436f8338..4a4039081a 100644 --- a/src/pyedb/generic/plot.py +++ b/src/pyedb/generic/plot.py @@ -1,4 +1,3 @@ -import ast import os import warnings @@ -9,7 +8,6 @@ "The NumPy module is required to run some functionalities of PostProcess.\n" "Install with \n\npip install numpy\n\nRequires CPython." ) - try: from matplotlib.patches import PathPatch from matplotlib.path import Path diff --git a/src/pyedb/dotnet/edb_core/sim_setup_data/io/__init__.py b/src/pyedb/grpc/__init__.py similarity index 100% rename from src/pyedb/dotnet/edb_core/sim_setup_data/io/__init__.py rename to src/pyedb/grpc/__init__.py diff --git a/src/pyedb/grpc/database/__init__.py b/src/pyedb/grpc/database/__init__.py new file mode 100644 index 0000000000..b95c82c0b3 --- /dev/null +++ b/src/pyedb/grpc/database/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import # noreorder diff --git a/src/pyedb/grpc/database/components.py b/src/pyedb/grpc/database/components.py new file mode 100644 index 0000000000..7e9a3c3aef --- /dev/null +++ b/src/pyedb/grpc/database/components.py @@ -0,0 +1,2354 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""This module contains the `Components` class. + +""" +import codecs +import json +import math +import os +import re +import warnings + +from ansys.edb.core.definition.die_property import DieOrientation as GrpDieOrientation +from ansys.edb.core.definition.die_property import DieType as GrpcDieType +from ansys.edb.core.definition.solder_ball_property import ( + SolderballShape as GrpcSolderballShape, +) +from ansys.edb.core.hierarchy.component_group import ComponentType as GrpcComponentType +from ansys.edb.core.hierarchy.spice_model import SPICEModel as GrpcSPICEModel +from ansys.edb.core.utility.rlc import Rlc as GrpcRlc +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.component_libraries.ansys_components import ( + ComponentLib, + ComponentPart, + Series, +) +from pyedb.generic.general_methods import ( + generate_unique_name, + get_filename_without_extension, +) +from pyedb.grpc.database.definition.component_def import ComponentDef +from pyedb.grpc.database.definition.component_pins import ComponentPin +from pyedb.grpc.database.hierarchy.component import Component +from pyedb.grpc.database.hierarchy.pin_pair_model import PinPairModel +from pyedb.grpc.database.hierarchy.pingroup import PinGroup +from pyedb.grpc.database.utility.sources import SourceType +from pyedb.modeler.geometry_operators import GeometryOperators + + +def resistor_value_parser(r_value): + """Convert a resistor value. + + Parameters + ---------- + r_value : float + Resistor value. + + Returns + ------- + float + Resistor value. + + """ + if isinstance(r_value, str): + r_value = r_value.replace(" ", "") + r_value = r_value.replace("meg", "m") + r_value = r_value.replace("Ohm", "") + r_value = r_value.replace("ohm", "") + r_value = r_value.replace("k", "e3") + r_value = r_value.replace("m", "e-3") + r_value = r_value.replace("M", "e6") + r_value = float(r_value) + return r_value + + +class Components(object): + """Manages EDB components and related method accessible from `Edb.components` property. + + Parameters + ---------- + edb_class : :class:`pyedb.grpc.edb.Edb` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components + """ + + def __getitem__(self, name): + """Get a component or component definition from the Edb project. + + Parameters + ---------- + name : str + + Returns + ------- + :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent` + + """ + if name in self.instances: + return self.instances[name] + elif name in self.definitions: + return self.definitions[name] + self._pedb.logger.error("Component or definition not found.") + return + + def __init__(self, p_edb): + self._pedb = p_edb + self._cmp = {} + self._res = {} + self._cap = {} + self._ind = {} + self._ios = {} + self._ics = {} + self._others = {} + self._pins = {} + self._comps_by_part = {} + self._init_parts() + # self._padstack = Padstacks(self._pedb) + # self._excitations = self._pedb.excitations + + @property + def _logger(self): + """Logger.""" + return self._pedb.logger + + def _init_parts(self): + a = self.instances + a = self.resistors + a = self.ICs + a = self.Others + a = self.inductors + a = self.IOs + a = self.components_by_partname + return True + + @property + def _active_layout(self): + return self._pedb.active_layout + + @property + def _layout(self): + return self._pedb.layout + + @property + def _cell(self): + return self._pedb.cell + + @property + def _db(self): + return self._pedb.active_db + + @property + def instances(self): + """All Cell components objects. + + Returns + ------- + Dict[str, :class:`pyedb.grpc.database.cell.hierarchy.component.Component`] + Default dictionary for the EDB component. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.instances + + """ + if not self._cmp: + self.refresh_components() + return self._cmp + + @property + def definitions(self): + """Retrieve component definition list. + + Returns + ------- + dict of :class:`EDBComponentDef`""" + return {l.name: ComponentDef(self._pedb, l) for l in self._pedb.component_defs} + + @property + def nport_comp_definition(self): + """Retrieve Nport component definition list.""" + m = "Ansys.Ansoft.Edb.Definition.NPortComponentModel" + return {name: l for name, l in self.definitions.items() if m in [i for i in l.model]} + + def import_definition(self, file_path): + """Import component definition from json file. + + Parameters + ---------- + file_path : str + File path of json file. + """ + with codecs.open(file_path, "r", encoding="utf-8") as f: + data = json.load(f) + for part_name, p in data["Definitions"].items(): + model_type = p["Model_type"] + if part_name not in self.definitions: + continue + comp_definition = self.definitions[part_name] + comp_definition.type = p["Component_type"] + + if model_type == "RLC": + comp_definition.assign_rlc_model(p["Res"], p["Ind"], p["Cap"], p["Is_parallel"]) + else: + model_name = p["Model_name"] + file_path = data[model_type][model_name] + if model_type == "SParameterModel": + if "Reference_net" in p: + reference_net = p["Reference_net"] + else: + reference_net = None + comp_definition.assign_s_param_model(file_path, model_name, reference_net) + elif model_type == "SPICEModel": + comp_definition.assign_spice_model(file_path, model_name) + else: + pass + return True + + def export_definition(self, file_path): + """Export component definitions to json file. + + Parameters + ---------- + file_path : str + File path of json file. + + Returns + ------- + + """ + data = { + "SParameterModel": {}, + "SPICEModel": {}, + "Definitions": {}, + } + for part_name, props in self.definitions.items(): + comp_list = list(props.components.values()) + if comp_list: + data["Definitions"][part_name] = {} + data["Definitions"][part_name]["Component_type"] = props.type + comp = comp_list[0] + data["Definitions"][part_name]["Model_type"] = comp.model_type + if comp.model_type == "RLC": + rlc_values = [i if i else 0 for i in comp.rlc_values] + data["Definitions"][part_name]["Res"] = rlc_values[0] + data["Definitions"][part_name]["Ind"] = rlc_values[1] + data["Definitions"][part_name]["Cap"] = rlc_values[2] + data["Definitions"][part_name]["Is_parallel"] = True if comp.is_parallel_rlc else False + else: + if comp.model_type == "SParameterModel": + model = comp.s_param_model + data["Definitions"][part_name]["Model_name"] = model.name + data["Definitions"][part_name]["Reference_net"] = model.reference_net + if not model.name in data["SParameterModel"]: + data["SParameterModel"][model.name] = model.file_path + elif comp.model_type == "SPICEModel": + model = comp.spice_model + data["Definitions"][part_name]["Model_name"] = model.name + if not model.name in data["SPICEModel"]: + data["SPICEModel"][model.name] = model.file_path + else: + model = comp.netlist_model + data["Definitions"][part_name]["Model_name"] = model.netlist + + with codecs.open(file_path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) + return file_path + + def refresh_components(self): + """Refresh the component dictionary.""" + self._logger.info("Refreshing the Components dictionary.") + self._cmp = {} + for i in self._pedb.layout.groups: + if isinstance(i, Component): + if not i.is_null: + self._cmp[i.name] = i + return True + + @property + def resistors(self): + """Resistors. + + Returns + ------- + dict[str, .:class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] + Dictionary of resistors. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.resistors + """ + self._res = {} + for el, val in self.instances.items(): + if not val.is_null: + try: + if val.type == "resistor": + self._res[el] = val + except: + pass + return self._res + + @property + def capacitors(self): + """Capacitors. + + Returns + ------- + dict[str, .:class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] + Dictionary of capacitors. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.capacitors + """ + self._cap = {} + for el, val in self.instances.items(): + if not val.is_null: + try: + if val.type == "capacitor": + self._cap[el] = val + except: + pass + return self._cap + + @property + def inductors(self): + """Inductors. + + Returns + ------- + dict[str, .:class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] + Dictionary of inductors. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.inductors + + """ + self._ind = {} + for el, val in self.instances.items(): + if not val.is_null: + try: + if val.type == "inductor": + self._ind[el] = val + except: + pass + return self._ind + + @property + def ICs(self): + """Integrated circuits. + + Returns + ------- + dict[str, .:class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] + Dictionary of integrated circuits. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.ICs + + """ + self._ics = {} + for el, val in self.instances.items(): + if not val.is_null: + try: + if val.type == "ic": + self._ics[el] = val + except: + pass + return self._ics + + @property + def IOs(self): + """Circuit inupts and outputs. + + Returns + ------- + dict[str, .:class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] + Dictionary of circuit inputs and outputs. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.IOs + + """ + self._ios = {} + for el, val in self.instances.items(): + if not val.is_null: + try: + if val.type == "io": + self._ios[el] = val + except: + pass + return self._ios + + @property + def Others(self): + """Other core components. + + Returns + ------- + dict[str, .:class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] + Dictionary of other core components. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.others + + """ + self._others = {} + for el, val in self.instances.items(): + if not val.is_null: + try: + if val.type == "other": + self._others[el] = val + except: + pass + return self._others + + @property + def components_by_partname(self): + """Components by part name. + + Returns + ------- + dict + Dictionary of components by part name. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.components_by_partname + + """ + self._comps_by_part = {} + for el, val in self.instances.items(): + if val.partname in self._comps_by_part.keys(): + self._comps_by_part[val.partname].append(val) + else: + self._comps_by_part[val.partname] = [val] + return self._comps_by_part + + def get_component_by_name(self, name): + """Retrieve a component by name. + + Parameters + ---------- + name : str + Name of the component. + + Returns + ------- + bool + Component object. + + """ + return self.instances[name] + + def get_pin_from_component(self, component, net_name=None, pin_name=None): + """Return component pins. + Parameters + ---------- + component: .:class: `Component` or str. + Component object or component name. + net_name : str, List[str], optional + Apply filter on net name. + pin_name : str, optional + Apply filter on specific pin name. + Return + ------ + List[:clas: `PadstackInstance`] + + + + """ + if isinstance(component, Component): + component = component.name + pins = [pin for pin in list(self.instances[component].pins.values())] + if net_name: + if isinstance(net_name, str): + net_name = [net_name] + pins = [pin for pin in pins if pin.net_name in net_name] + if pin_name: + pins = [pin for pin in pins if pin.name == pin_name] + return pins + + def get_components_from_nets(self, netlist=None): + """Retrieve components from a net list. + + Parameters + ---------- + netlist : str, optional + Name of the net list. The default is ``None``. + + Returns + ------- + list + List of components that belong to the signal nets. + + """ + cmp_list = [] + if isinstance(netlist, str): + netlist = [netlist] + components = list(self.instances.keys()) + for refdes in components: + cmpnets = self._cmp[refdes].nets + if set(cmpnets).intersection(set(netlist)): + cmp_list.append(refdes) + return cmp_list + + def _get_edb_pin_from_pin_name(self, cmp, pin): + if not isinstance(cmp, Component): + return False + if not isinstance(pin, str): + return False + if pin in cmp.pins: + return cmp.pins[pin] + return False + + def get_component_placement_vector( + self, + mounted_component, + hosting_component, + mounted_component_pin1, + mounted_component_pin2, + hosting_component_pin1, + hosting_component_pin2, + flipped=False, + ): + """Get the placement vector between 2 components. + + Parameters + ---------- + mounted_component : `edb.cell.hierarchy._hierarchy.Component` + Mounted component name. + hosting_component : `edb.cell.hierarchy._hierarchy.Component` + Hosting component name. + mounted_component_pin1 : str + Mounted component Pin 1 name. + mounted_component_pin2 : str + Mounted component Pin 2 name. + hosting_component_pin1 : str + Hosted component Pin 1 name. + hosting_component_pin2 : str + Hosted component Pin 2 name. + flipped : bool, optional + Either if the mounted component will be flipped or not. + + Returns + ------- + tuple + Tuple of Vector offset, rotation and solder height. + + Examples + -------- + >>> edb1 = Edb(edbpath=targetfile1, edbversion="2021.2") + >>> hosting_cmp = edb1.components.get_component_by_name("U100") + >>> mounted_cmp = edb2.components.get_component_by_name("BGA") + >>> vector, rotation, solder_ball_height = edb1.components.get_component_placement_vector( + ... mounted_component=mounted_cmp, + ... hosting_component=hosting_cmp, + ... mounted_component_pin1="A12", + ... mounted_component_pin2="A14", + ... hosting_component_pin1="A12", + ... hosting_component_pin2="A14") + """ + m_pin1_pos = [0.0, 0.0] + m_pin2_pos = [0.0, 0.0] + h_pin1_pos = [0.0, 0.0] + h_pin2_pos = [0.0, 0.0] + if not isinstance(mounted_component, Component): + return False + if not isinstance(hosting_component, Component): + return False + + if mounted_component_pin1: + m_pin1 = self._get_edb_pin_from_pin_name(mounted_component, mounted_component_pin1) + m_pin1_pos = self.get_pin_position(m_pin1) + if mounted_component_pin2: + m_pin2 = self._get_edb_pin_from_pin_name(mounted_component, mounted_component_pin2) + m_pin2_pos = self.get_pin_position(m_pin2) + + if hosting_component_pin1: + h_pin1 = self._get_edb_pin_from_pin_name(hosting_component, hosting_component_pin1) + h_pin1_pos = self.get_pin_position(h_pin1) + + if hosting_component_pin2: + h_pin2 = self._get_edb_pin_from_pin_name(hosting_component, hosting_component_pin2) + h_pin2_pos = self.get_pin_position(h_pin2) + # + vector = [h_pin1_pos[0] - m_pin1_pos[0], h_pin1_pos[1] - m_pin1_pos[1]] + vector1 = GeometryOperators.v_points(m_pin1_pos, m_pin2_pos) + vector2 = GeometryOperators.v_points(h_pin1_pos, h_pin2_pos) + multiplier = 1 + if flipped: + multiplier = -1 + vector1[1] = multiplier * vector1[1] + + rotation = GeometryOperators.v_angle_sign_2D(vector1, vector2, False) + if rotation != 0.0: + layinst = mounted_component.layout_instance + cmpinst = layinst.GetLayoutObjInstance(mounted_component, None) + center = cmpinst.center + # center_double = [center.X.ToDouble(), center.Y.ToDouble()] + vector_center = GeometryOperators.v_points(center, m_pin1_pos) + x_v2 = vector_center[0] * math.cos(rotation) + multiplier * vector_center[1] * math.sin(rotation) + y_v2 = -1 * vector_center[0] * math.sin(rotation) + multiplier * vector_center[1] * math.cos(rotation) + new_vector = [x_v2 + center[0], y_v2 + center[1]] + vector = [h_pin1_pos[0] - new_vector[0], h_pin1_pos[1] - new_vector[1]] + + if vector: + solder_ball_height = self.get_solder_ball_height(mounted_component) + return True, vector, rotation, solder_ball_height + self._logger.warning("Failed to compute vector.") + return False, [0, 0], 0, 0 + + def get_solder_ball_height(self, cmp): + """Get component solder ball height. + + Parameters + ---------- + cmp : str or `Component` object. + EDB component or str component name. + + Returns + ------- + double, bool + Salder ball height vale, ``False`` when failed. + + """ + if isinstance(cmp, str): + cmp = self.get_component_by_name(cmp) + return cmp.solder_ball_height + + def get_vendor_libraries(self): + """Retrieve all capacitors and inductors libraries from ANSYS installation (used by Siwave). + + Returns + ------- + ComponentLib object contains nested dictionaries to navigate through [component type][vendors][series] + :class: `pyedb.component_libraries.ansys_components.ComponentPart` + + Examples + -------- + >>> edbapp = Edb() + >>> comp_lib = edbapp.components.get_vendor_libraries() + >>> network = comp_lib.capacitors["AVX"]["AccuP01005"]["C005YJ0R1ABSTR"].s_parameters + >>> network.write_touchstone(os.path.join(edbapp.directory, "test_export.s2p")) + + """ + comp_lib_path = os.path.join(self._pedb.base_path, "complib", "Locked") + comp_types = ["Capacitors", "Inductors"] + comp_lib = ComponentLib() + comp_lib.path = comp_lib_path + for cmp_type in comp_types: + folder = os.path.join(comp_lib_path, cmp_type) + vendors = {f.name: "" for f in os.scandir(folder) if f.is_dir()} + for vendor in list(vendors.keys()): + series = {f.name: Series() for f in os.scandir(os.path.join(folder, vendor)) if f.is_dir()} + for serie_name, _ in series.items(): + _serie = {} + index_file = os.path.join(folder, vendor, serie_name, "index.txt") + sbin_file = os.path.join(folder, vendor, serie_name, "sdata.bin") + if os.path.isfile(index_file): + with open(index_file, "r") as f: + for line in f.readlines(): + part_name, index = line.split() + _serie[part_name] = ComponentPart(part_name, int(index), sbin_file) + _serie[part_name].type = cmp_type[:-1] + f.close() + series[serie_name] = _serie + vendors[vendor] = series + if cmp_type == "Capacitors": + comp_lib.capacitors = vendors + elif cmp_type == "Inductors": + comp_lib.inductors = vendors + return comp_lib + + def create_source_on_component(self, sources=None): + """Create voltage, current source, or resistor on component. + + . deprecated:: pyedb 0.28.0 + Use .:func:`pyedb.grpc.core.excitations.create_source_on_component` instead. + + Parameters + ---------- + sources : list[Source] + List of ``edb_data.sources.Source`` objects. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + warnings.warn( + "`create_source_on_component` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_source_on_component` instead.", + DeprecationWarning, + ) + self._pedb.excitations.create_source_on_component(self, sources=sources) + + def create_port_on_pins( + self, + refdes, + pins, + reference_pins, + impedance=50.0, + port_name=None, + pec_boundary=False, + pingroup_on_single_pin=False, + ): + """Create circuit port between pins and reference ones. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_port_on_pins` instead. + + 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 + port_name : str, optional + Port name. The default is ``None``, in which case a name is automatically assigned. + pec_boundary : bool, optional + Whether to define the PEC boundary, The default is ``False``. If set to ``True``, + a perfect short is created between the pin and impedance is ignored. This + parameter is only supported on a port created between two pins, such as + when there is no pin group. + pingroup_on_single_pin : bool + If ``True`` force using pingroup definition on single pin to have the port created at the pad center. If + ``False`` the port is created at the pad edge. Default value is ``False``. + + Returns + ------- + EDB terminal created, or False if failed to create. + """ + warnings.warn( + "`create_port_on_pins` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_port_on_pins` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_port_on_pins( + refdes, + pins, + reference_pins, + impedance=impedance, + port_name=port_name, + pec_boundary=pec_boundary, + pingroup_on_single_pin=pingroup_on_single_pin, + ) + + def create_port_on_component( + self, + component, + net_list, + port_type=SourceType.CoaxPort, + do_pingroup=True, + reference_net="gnd", + port_name=None, + solder_balls_height=None, + solder_balls_size=None, + solder_balls_mid_size=None, + extend_reference_pins_outside_component=False, + ): + """Create ports on a component. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_port_on_component` instead. + + Parameters + ---------- + component : str or self._pedb.component + EDB component or str component name. + net_list : str or list of string. + List of nets where ports must be created on the component. + If the net is not part of the component, this parameter is skipped. + port_type : SourceType enumerator, CoaxPort or CircuitPort + Type of port to create. ``CoaxPort`` generates solder balls. + ``CircuitPort`` generates circuit ports on pins belonging to the net list. + do_pingroup : bool + True activate pingroup during port creation (only used with combination of CircPort), + False will take the closest reference pin and generate one port per signal pin. + refnet : string or list of string. + list of the reference net. + port_name : str + Port name for overwriting the default port-naming convention, + which is ``[component][net][pin]``. The port name must be unique. + If a port with the specified name already exists, the + default naming convention is used so that port creation does + not fail. + solder_balls_height : float, optional + Solder balls height used for the component. When provided default value is overwritten and must be + provided in meter. + solder_balls_size : float, optional + Solder balls diameter. When provided auto evaluation based on padstack size will be disabled. + solder_balls_mid_size : float, optional + Solder balls mid-diameter. When provided if value is different than solder balls size, spheroid shape will + be switched. + extend_reference_pins_outside_component : bool + When no reference pins are found on the component extend the pins search with taking the closest one. If + `do_pingroup` is `True` will be set to `False`. Default value is `False`. + + Returns + ------- + double, bool + Salder ball height vale, ``False`` when failed. + + """ + warnings.warn( + "`create_port_on_component` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_port_on_component` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_port_on_component( + component, + net_list, + port_type=port_type, + do_pingroup=do_pingroup, + reference_net=reference_net, + port_name=port_name, + solder_balls_height=solder_balls_height, + solder_balls_size=solder_balls_size, + solder_balls_mid_size=solder_balls_mid_size, + extend_reference_pins_outside_component=extend_reference_pins_outside_component, + ) + + def _create_terminal(self, pin, term_name=None): + """Create terminal on component pin. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations._create_terminal` instead. + + Parameters + ---------- + pin : Edb padstack instance. + + term_name : Terminal name (Optional). + str. + + Returns + ------- + EDB terminal. + """ + warnings.warn( + "`_create_terminal` is deprecated and is now located here " + "`pyedb.grpc.core.excitations._create_terminal` instead.", + DeprecationWarning, + ) + self._pedb.excitations._create_terminal(pin, term_name=term_name) + + def _get_closest_pin_from(self, pin, ref_pinlist): + """Returns the closest pin from given pin among the list of reference pins. + + Parameters + ---------- + pin : Edb padstack instance. + + ref_pinlist : list of reference edb pins. + + Returns + ------- + Edb pin. + + """ + distance = 1e3 + pin_position = pin.position + closest_pin = ref_pinlist[0] + for ref_pin in ref_pinlist: + temp_distance = pin_position.distance(ref_pin.position) + if temp_distance < distance: + distance = temp_distance + closest_pin = ref_pin + return closest_pin + + def replace_rlc_by_gap_boundaries(self, component=None): + """Replace RLC component by RLC gap boundaries. These boundary types are compatible with 3D modeler export. + Only 2 pins RLC components are supported in this command. + + Parameters + ---------- + component : str + Reference designator of the RLC component. + + Returns + ------- + bool + ``True`` when succeed, ``False`` if it failed. + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb(edb_file) + >>> for refdes, cmp in edb.components.capacitors.items(): + >>> edb.components.replace_rlc_by_gap_boundaries(refdes) + >>> edb.save_edb() + >>> edb.close_edb() + """ + if not component: + return False + if isinstance(component, str): + component = self.instances[component] + if not component: + self._logger.error("component %s not found.", component) + return False + if component.type in ["other", "ic", "io"]: + self._logger.info(f"Component {component.refdes} skipped to deactivate is not an RLC.") + return False + component.enabled = False + return self._pedb.source_excitation.add_rlc_boundary(component.refdes, False) + + def deactivate_rlc_component(self, component=None, create_circuit_port=False, pec_boundary=False): + """Deactivate RLC component with a possibility to convert it to a circuit port. + + Parameters + ---------- + component : str + Reference designator of the RLC component. + + create_circuit_port : bool, optional + Whether to replace the deactivated RLC component with a circuit port. The default + is ``False``. + pec_boundary : bool, optional + Whether to define the PEC boundary, The default is ``False``. If set to ``True``, + a perfect short is created between the pin and impedance is ignored. This + parameter is only supported on a port created between two pins, such as + when there is no pin group. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + >>> from pyedb import Edb + >>> edb_file = r'C:\my_edb_file.aedb' + >>> edb = Edb(edb_file) + >>> for cmp in list(edb.components.instances.keys()): + >>> edb.components.deactivate_rlc_component(component=cmp, create_circuit_port=False) + >>> edb.save_edb() + >>> edb.close_edb() + """ + if not component: + return False + if isinstance(component, str): + component = self.instances[component] + if not component: + self._logger.error("component %s not found.", component) + return False + if component.type in ["other", "ic", "io"]: + self._logger.info(f"Component {component.refdes} passed to deactivate is not an RLC.") + return False + component.is_enabled = False + return self._pedb.source_excitation.add_port_on_rlc_component( + component=component.refdes, circuit_ports=create_circuit_port, pec_boundary=pec_boundary + ) + + def add_port_on_rlc_component(self, component=None, circuit_ports=True, pec_boundary=False): + """Deactivate RLC component and replace it with a circuit port. + The circuit port supports only two-pin components. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.add_port_on_rlc_component` instead. + + Parameters + ---------- + component : str + Reference designator of the RLC component. + + circuit_ports : bool + ``True`` will replace RLC component by circuit ports, ``False`` gap ports compatible with HFSS 3D modeler + export. + + pec_boundary : bool, optional + Whether to define the PEC boundary, The default is ``False``. If set to ``True``, + a perfect short is created between the pin and impedance is ignored. This + parameter is only supported on a port created between two pins, such as + when there is no pin group. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + warnings.warn( + "`add_port_on_rlc_component` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.add_port_on_rlc_component` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.add_port_on_rlc_component( + component=component, circuit_ports=circuit_ports, pec_boundary=pec_boundary + ) + + def add_rlc_boundary(self, component=None, circuit_type=True): + """Add RLC gap boundary on component and replace it with a circuit port. + The circuit port supports only 2-pin components. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.add_rlc_boundary` instead. + + Parameters + ---------- + component : str + Reference designator of the RLC component. + circuit_type : bool + When ``True`` circuit type are defined, if ``False`` gap type will be used instead (compatible with HFSS 3D + modeler). Default value is ``True``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + warnings.warn( + "`add_rlc_boundary` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.add_rlc_boundary` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.add_rlc_boundary(self, component=component, circuit_type=circuit_type) + + def _create_pin_group_terminal(self, pingroup, isref=False, term_name=None, term_type="circuit"): + """Creates an EDB pin group terminal from a given EDB pin group. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations._create_pin_group_terminal` instead. + + Parameters + ---------- + pingroup : Edb pin group. + + isref : bool + Specify if this terminal a reference terminal. + + term_name : Terminal name (Optional). If not provided default name is Component name, Pin name, Net name. + str. + + term_type: Type of terminal, gap, circuit or auto. + str. + Returns + ------- + Edb pin group terminal. + """ + warnings.warn( + "`_create_pin_group_terminal` is deprecated and is now located here " + "`pyedb.grpc.core.excitations._create_pin_group_terminal` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation._create_pin_group_terminal( + pingroup=pingroup, term_name=term_name, term_type=term_type, isref=isref + ) + + def _is_top_component(self, cmp): + """Test the component placement layer. + + Parameters + ---------- + cmp : self._pedb.component + Edb component. + + Returns + ------- + bool + ``True`` when component placed on top layer, ``False`` on bottom layer. + + + """ + top_layer = self._pedb.stackup.signal[0].name + if cmp.placement_layer == top_layer: + return True + else: + return False + + def _get_component_definition(self, name, pins): + component_definition = ComponentDef.find(self._db, name) + if component_definition.is_null: + from ansys.edb.core.layout.cell import Cell as GrpcCell + from ansys.edb.core.layout.cell import CellType as GrpcCellType + + foot_print_cell = GrpcCell.create(self._pedb.active_db, GrpcCellType.FOOTPRINT_CELL, name) + component_definition = ComponentDef.create(self._db, name, fp=foot_print_cell) + if component_definition.is_null: + self._logger.error(f"Failed to create component definition {name}") + return False + ind = 1 + for pin in pins: + if not pin.name: + pin.name = str(ind) + ind += 1 + component_definition_pin = ComponentPin.create(component_definition, pin.name) + if component_definition_pin.is_null: + self._logger.error(f"Failed to create component definition pin {name}-{pin.name}") + return None + else: + self._logger.warning("Found existing component definition for footprint {}".format(name)) + return component_definition + + def create( + self, + pins, + component_name=None, + placement_layer=None, + component_part_name=None, + is_rlc=False, + r_value=None, + c_value=None, + l_value=None, + is_parallel=False, + ): + """Create a component from pins. + + Parameters + ---------- + pins : list + List of EDB core pins. + component_name : str + Name of the reference designator for the component. + placement_layer : str, optional + Name of the layer used for placing the component. + component_part_name : str, optional + Part name of the component. + is_rlc : bool, optional + Whether if the new component will be an RLC or not. + r_value : float + Resistor value. + c_value : float + Capacitance value. + l_value : float + Inductor value. + is_parallel : bool + Using parallel model when ``True``, series when ``False``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> pins = edbapp.components.get_pin_from_component("A1") + >>> edbapp.components.create(pins, "A1New") + + """ + from ansys.edb.core.hierarchy.component_group import ( + ComponentGroup as GrpcComponentGroup, + ) + + if not component_name: + component_name = generate_unique_name("Comp_") + if component_part_name: + compdef = self._get_component_definition(component_part_name, pins) + else: + compdef = self._get_component_definition(component_name, pins) + if not compdef: + return False + new_cmp = GrpcComponentGroup.create(self._active_layout, component_name, compdef.name) + hosting_component_location = pins[0].component.transform + if not len(pins) == len(compdef.component_pins): + self._pedb.logger.error( + f"Number on pins {len(pins)} does not match component definition number " + f"of pins {len(compdef.component_pins)}" + ) + return False + for padstack_instance, component_pin in zip(pins, compdef.component_pins): + padstack_instance.is_layout_pin = True + padstack_instance.name = component_pin.name + new_cmp.add_member(padstack_instance) + if not placement_layer: + new_cmp_layer_name = pins[0].padstack_def.data.layer_names[0] + else: + new_cmp_layer_name = placement_layer + if new_cmp_layer_name in self._pedb.stackup.signal_layers: + new_cmp_placement_layer = self._pedb.stackup.signal_layers[new_cmp_layer_name] + new_cmp.placement_layer = new_cmp_placement_layer + new_cmp.component_type = GrpcComponentType.OTHER + if is_rlc and len(pins) == 2: + rlc = GrpcRlc() + rlc.is_parallel = is_parallel + if not r_value: + rlc.r_enabled = False + else: + rlc.r_enabled = True + rlc.r = GrpcValue(r_value) + if l_value is None: + rlc.l_enabled = False + else: + rlc.l_enabled = True + rlc.l = GrpcValue(l_value) + if c_value is None: + rlc.c_enabled = False + else: + rlc.c_enabled = True + rlc.C = GrpcValue(c_value) + if rlc.r_enabled and not rlc.c_enabled and not rlc.l_enabled: + new_cmp.component_type = GrpcComponentType.RESISTOR + elif rlc.c_enabled and not rlc.r_enabled and not rlc.l_enabled: + new_cmp.component_type = GrpcComponentType.CAPACITOR + elif rlc.l_enabled and not rlc.r_enabled and not rlc.c_enabled: + new_cmp.component_type = GrpcComponentType.INDUCTOR + else: + new_cmp.component_type = GrpcComponentType.RESISTOR + pin_pair = (pins[0].name, pins[1].name) + rlc_model = PinPairModel(self._pedb, new_cmp.component_property.model) + rlc_model.set_rlc(pin_pair, rlc) + component_property = new_cmp.component_property + component_property.model = rlc_model + new_cmp.component_property = component_property + new_cmp.transform = hosting_component_location + new_edb_comp = Component(self._pedb, new_cmp) + self._cmp[new_cmp.name] = new_edb_comp + return new_edb_comp + + def create_component_from_pins( + self, pins, component_name, placement_layer=None, component_part_name=None + ): # pragma: no cover + """Create a component from pins. + + .. deprecated:: 0.6.62 + Use :func:`create` method instead. + + Parameters + ---------- + pins : list + List of EDB core pins. + component_name : str + Name of the reference designator for the component. + placement_layer : str, optional + Name of the layer used for placing the component. + component_part_name : str, optional + Part name of the component. It's created a new definition if doesn't exists. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> pins = edbapp.components.get_pin_from_component("A1") + >>> edbapp.components.create(pins, "A1New") + + """ + warnings.warn("`create_component_from_pins` is deprecated use `create` instead..", DeprecationWarning) + return self.create( + pins=pins, + component_name=component_name, + placement_layer=placement_layer, + component_part_name=component_part_name, + is_rlc=False, + ) + + def set_component_model(self, componentname, model_type="Spice", modelpath=None, modelname=None): + """Assign a Spice or Touchstone model to a component. + + Parameters + ---------- + componentname : str + Name of the component. + model_type : str, optional + Type of the model. Options are ``"Spice"`` and + ``"Touchstone"``. The default is ``"Spice"``. + modelpath : str, optional + Full path to the model file. The default is ``None``. + modelname : str, optional + Name of the model. The default is ``None``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.set_component_model("A1", model_type="Spice", + ... modelpath="pathtospfile", + ... modelname="spicemodelname") + + """ + if not modelname: + modelname = get_filename_without_extension(modelpath) + if componentname not in self.instances: + self._pedb.logger.error(f"Component {componentname} not found.") + return False + component = self.instances[componentname] + pin_number = len(component.pins) + if model_type == "Spice": + with open(modelpath, "r") as f: + for line in f: + if "subckt" in line.lower(): + pin_names = [i.strip() for i in re.split(" |\t", line) if i] + pin_names.remove(pin_names[0]) + pin_names.remove(pin_names[0]) + break + if len(pin_names) == pin_number: + spice_mod = GrpcSPICEModel.create(name=modelname, path=modelpath, sub_circuit=f"{modelname}_sub") + terminal = 1 + for pn in pin_names: + spice_mod.add_terminal(terminal=str(terminal), pin=pn) + terminal += 1 + component.component_property.model = spice_mod + else: + self._logger.error("Wrong number of Pins") + return False + + elif model_type == "Touchstone": # pragma: no cover + n_port_model_name = modelname + from ansys.edb.core.definition.component_model import ( + NPortComponentModel as GrpcNPortComponentModel, + ) + from ansys.edb.core.hierarchy.sparameter_model import ( + SParameterModel as GrpcSParameterModel, + ) + + n_port_model = GrpcNPortComponentModel.find_by_name(component.component_def, n_port_model_name) + if n_port_model.is_null: + n_port_model = GrpcNPortComponentModel.create(n_port_model_name) + n_port_model.reference_file = modelpath + component.component_def.add_component_model(n_port_model) + gndnets = list(filter(lambda x: "gnd" in x.lower(), component.nets)) + if len(gndnets) > 0: # pragma: no cover + net = gndnets[0] + else: # pragma: no cover + net = component.nets[len(component.nets) - 1] + s_parameter_mod = GrpcSParameterModel.create(name=n_port_model_name, ref_net=net) + component.component_property.model = s_parameter_mod + return True + + def create_pingroup_from_pins(self, pins, group_name=None): + """Create a pin group on a component. + + Parameters + ---------- + pins : list + 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]``. + + Returns + ------- + tuple + The tuple is structured as: (bool, pingroup). + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.create_pingroup_from_pins(gndpinlist, "MyGNDPingroup") + + """ + if len(pins) < 1: + self._logger.error("No pins specified for pin group %s", group_name) + return (False, None) + if group_name is None: + group_name = PinGroup.unique_name(self._active_layout, "pin_group") + for pin in pins: + pin.is_layout_pin = True + forbiden_car = "-><" + group_name = group_name.translate({ord(i): "_" for i in forbiden_car}) + for pgroup in list(self._pedb.active_layout.pin_groups): + if pgroup.name == group_name: + pin_group_exists = True + if len(pgroup.pins) == len(pins): + pnames = [i.name for i in pins] + for p in pgroup.pins: + if p.name in pnames: + continue + else: + group_name = PinGroup.unique_name(self._active_layout, group_name) + pin_group_exists = False + else: + group_name = PinGroup.unique_name(self._active_layout, group_name) + pin_group_exists = False + if pin_group_exists: + return pgroup + pin_group = PinGroup.create(self._active_layout, group_name, pins) + if pin_group.is_null: + return False + else: + pin_group.net = pins[0].net + return pin_group + + def delete_single_pin_rlc(self, deactivate_only=False): + # type: (bool) -> list + """Delete all RLC components with a single pin. + Single pin component model type will be reverted to ``"RLC"``. + + Parameters + ---------- + deactivate_only : bool, optional + Whether to only deactivate RLC components with a single point rather than + delete them. The default is ``False``, in which case they are deleted. + + Returns + ------- + list + List of deleted RLC components. + + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> list_of_deleted_rlcs = edbapp.components.delete_single_pin_rlc() + >>> print(list_of_deleted_rlcs) + + """ + deleted_comps = [] + for comp, val in self.instances.items(): + if val.numpins < 2 and val.type in ["Resistor", "Capacitor", "Inductor"]: + if deactivate_only: + val.is_enabled = False + val.model_type = "RLC" + else: + val.edbcomponent.delete() + deleted_comps.append(comp) + if not deactivate_only: + self.refresh_components() + self._pedb.logger.info("Deleted {} components".format(len(deleted_comps))) + return deleted_comps + + def delete(self, component_name): + """Delete a component. + + Parameters + ---------- + component_name : str + Name of the component. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.delete("A1") + + """ + edb_cmp = self.get_component_by_name(component_name) + if edb_cmp is not None: + edb_cmp.delete() + if edb_cmp in list(self.instances.keys()): + del self.instances[edb_cmp] + return True + return False + + def disable_rlc_component(self, component_name): + """Disable a RLC component. + + Parameters + ---------- + component_name : str + Name of the RLC component. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.disable_rlc_component("A1") + + """ + cmp = self.get_component_by_name(component_name) + if cmp is not None: + component_property = cmp.component_property + pin_pair_model = component_property.model + for pin_pair in pin_pair_model.pin_pairs(): + rlc = pin_pair_model.rlc(pin_pair) + rlc.c_enabled = False + rlc.l_enabled = False + rlc.r_enabled = False + pin_pair_model.set_rlc(pin_pair, rlc) + component_property.model = pin_pair_model + cmp.component_property = component_property + return True + return False + + def set_solder_ball( + self, + component="", + sball_diam=None, + sball_height=None, + shape="Cylinder", + sball_mid_diam=None, + chip_orientation="chip_down", + auto_reference_size=True, + reference_size_x=0, + reference_size_y=0, + reference_height=0, + ): + """Set cylindrical solder balls on a given component. + + Parameters + ---------- + component : str or EDB component, optional + Name of the discrete component. + sball_diam : str, float, optional + Diameter of the solder ball. + sball_height : str, float, optional + Height of the solder ball. + shape : str, optional + Shape of solder ball. Options are ``"Cylinder"``, + ``"Spheroid"``. The default is ``"Cylinder"``. + sball_mid_diam : str, float, optional + Mid diameter of the solder ball. + chip_orientation : str, optional + Give the chip orientation, ``"chip_down"`` or ``"chip_up"``. Default is ``"chip_down"``. Only applicable on + IC model. + auto_reference_size : bool, optional + Whether to automatically set reference size. + reference_size_x : int, str, float, optional + X size of the reference. Applicable when auto_reference_size is False. + reference_size_y : int, str, float, optional + Y size of the reference. Applicable when auto_reference_size is False. + reference_height : int, str, float, optional + Height of the reference. Applicable when auto_reference_size is False. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.set_solder_ball("A1") + + """ + if isinstance(component, str): + if component in self.instances: + cmp = self.instances[component] + else: + cmp = self.instances[component.name] + if not sball_diam: + pin1 = list(cmp.pins.values())[0] + pin_layers = pin1.padstack_def.data.layer_names + pad_params = self._pedb.padstacks.get_pad_parameters(pin=pin1, layername=pin_layers[0], pad_type=0) + _sb_diam = min([abs(GrpcValue(val).value) for val in pad_params[1]]) + sball_diam = 0.8 * _sb_diam + if sball_height: + sball_height = round(GrpcValue(sball_height).value, 9) + else: + sball_height = round(GrpcValue(sball_diam).value, 9) / 2 + + if not sball_mid_diam: + sball_mid_diam = sball_diam + + if shape.lower() == "cylinder": + sball_shape = GrpcSolderballShape.SOLDERBALL_CYLINDER + else: + sball_shape = GrpcSolderballShape.SOLDERBALL_SPHEROID + + cmp_property = cmp.component_property + if cmp.type == GrpcComponentType.IC: + ic_die_prop = cmp_property.die_property + ic_die_prop.die_type = GrpcDieType.FLIPCHIP + if chip_orientation.lower() == "chip_up": + ic_die_prop.orientation = GrpDieOrientation.CHIP_UP + else: + ic_die_prop.orientation = GrpDieOrientation.CHIP_DOWN + cmp_property.die_property = ic_die_prop + + solder_ball_prop = cmp_property.solder_ball_property + solder_ball_prop.set_diameter(GrpcValue(sball_diam), GrpcValue(sball_mid_diam)) + solder_ball_prop.height = GrpcValue(sball_height) + + solder_ball_prop.shape = sball_shape + cmp_property.solder_ball_property = solder_ball_prop + + port_prop = cmp_property.port_property + port_prop.reference_height = GrpcValue(reference_height) + port_prop.reference_size_auto = auto_reference_size + if not auto_reference_size: + port_prop.set_reference_size(GrpcValue(reference_size_x), GrpcValue(reference_size_y)) + cmp_property.port_property = port_prop + cmp.component_property = cmp_property + return True + + def set_component_rlc( + self, + componentname, + res_value=None, + ind_value=None, + cap_value=None, + isparallel=False, + ): + """Update values for an RLC component. + + Parameters + ---------- + componentname : + Name of the RLC component. + res_value : float, optional + Resistance value. The default is ``None``. + ind_value : float, optional + Inductor value. The default is ``None``. + cap_value : float optional + Capacitor value. The default is ``None``. + isparallel : bool, optional + Whether the RLC component is parallel. The default is ``False``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.set_component_rlc( + ... "R1", res_value=50, ind_value=1e-9, cap_value=1e-12, isparallel=False + ... ) + + """ + if res_value is None and ind_value is None and cap_value is None: + self.instances[componentname].enabled = False + self._logger.info(f"No parameters passed, component {componentname} is disabled.") + return True + component = self.get_component_by_name(componentname) + pin_number = len(component.pins) + if pin_number == 2: + from_pin = list(component.pins.values())[0] + to_pin = list(component.pins.values())[1] + rlc = GrpcRlc() + rlc.is_parallel = isparallel + if res_value is not None: + rlc.r_enabled = True + rlc.r = GrpcValue(res_value) + else: + rlc.r_enabled = False + if ind_value is not None: + rlc.l_enabled = True + rlc.l = GrpcValue(ind_value) + else: + rlc.l_enabled = False + if cap_value is not None: + rlc.c_enabled = True + rlc.c = GrpcValue(cap_value) + else: + rlc.CEnabled = False + pin_pair = (from_pin.name, to_pin.name) + component_property = component.component_property + model = component_property.model + model.set_rlc(pin_pair, rlc) + component_property.model = model + component.component_property = component_property + else: + self._logger.warning( + f"Component {componentname} has not been assigned because either it is not present in the layout " + "or it contains a number of pins not equal to 2." + ) + return False + self._logger.info(f"RLC properties for Component {componentname} has been assigned.") + return True + + def update_rlc_from_bom( + self, + bom_file, + delimiter=";", + valuefield="Func des", + comptype="Prod name", + refdes="Pos / Place", + ): + """Update the EDC core component values (RLCs) with values coming from a BOM file. + + Parameters + ---------- + bom_file : str + Full path to the BOM file, which is a delimited text file. + Header values needed inside the BOM reader must + be explicitly set if different from the defaults. + delimiter : str, optional + Value to use for the delimiter. The default is ``";"``. + valuefield : str, optional + Field header containing the value of the component. The default is ``"Func des"``. + The value for this parameter must being with the value of the component + followed by a space and then the rest of the value. For example, ``"22pF"``. + comptype : str, optional + Field header containing the type of component. The default is ``"Prod name"``. For + example, you might enter ``"Inductor"``. + refdes : str, optional + Field header containing the reference designator of the component. The default is + ``"Pos / Place"``. For example, you might enter ``"C100"``. + + Returns + ------- + bool + ``True`` if the file contains the header and it is correctly parsed. ``True`` is + returned even if no values are assigned. + + """ + with open(bom_file, "r") as f: + Lines = f.readlines() + found = False + refdescolumn = None + comptypecolumn = None + valuecolumn = None + unmount_comp_list = list(self.instances.keys()) + for line in Lines: + content_line = [i.strip() for i in line.split(delimiter)] + if valuefield in content_line: + valuecolumn = content_line.index(valuefield) + if comptype in content_line: + comptypecolumn = content_line.index(comptype) + if refdes in content_line: + refdescolumn = content_line.index(refdes) + elif refdescolumn: + found = True + new_refdes = content_line[refdescolumn].split(" ")[0] + new_value = content_line[valuecolumn].split(" ")[0] + new_type = content_line[comptypecolumn] + if "resistor" in new_type.lower(): + self.set_component_rlc(new_refdes, res_value=new_value) + unmount_comp_list.remove(new_refdes) + elif "capacitor" in new_type.lower(): + self.set_component_rlc(new_refdes, cap_value=new_value) + unmount_comp_list.remove(new_refdes) + elif "inductor" in new_type.lower(): + self.set_component_rlc(new_refdes, ind_value=new_value) + unmount_comp_list.remove(new_refdes) + for comp in unmount_comp_list: + self.instances[comp].enabled = False + return found + + def import_bom( + self, + bom_file, + delimiter=",", + refdes_col=0, + part_name_col=1, + comp_type_col=2, + value_col=3, + ): + """Load external BOM file. + + Parameters + ---------- + bom_file : str + Full path to the BOM file, which is a delimited text file. + delimiter : str, optional + Value to use for the delimiter. The default is ``","``. + refdes_col : int, optional + Column index of reference designator. The default is ``"0"``. + part_name_col : int, optional + Column index of part name. The default is ``"1"``. Set to ``None`` if + the column does not exist. + comp_type_col : int, optional + Column index of component type. The default is ``"2"``. + value_col : int, optional + Column index of value. The default is ``"3"``. Set to ``None`` + if the column does not exist. + + Returns + ------- + bool + """ + with open(bom_file, "r") as f: + lines = f.readlines() + unmount_comp_list = list(self.instances.keys()) + for l in lines[1:]: + l = l.replace(" ", "").replace("\n", "") + if not l: + continue + l = l.split(delimiter) + + refdes = l[refdes_col] + comp = self.instances[refdes] + if not part_name_col == None: + part_name = l[part_name_col] + if comp.partname == part_name: + pass + else: + pinlist = self._pedb.padstacks.get_instances(refdes) + if not part_name in self.definitions: + comp_def = ComponentDef.create(self._db, part_name, None) + # for pin in range(len(pinlist)): + # ComponentPin.create(comp_def, str(pin)) + + p_layer = comp.placement_layer + refdes_temp = comp.refdes + "_temp" + comp.refdes = refdes_temp + + unmount_comp_list.remove(refdes) + comp.ungroup(True) + self.create(pinlist, refdes, p_layer, part_name) + self.refresh_components() + comp = self.instances[refdes] + + comp_type = l[comp_type_col] + if comp_type.capitalize() in ["Resistor", "Capacitor", "Inductor", "Other"]: + comp.type = comp_type.capitalize() + else: + comp.type = comp_type.upper() + + if comp_type.capitalize() in ["Resistor", "Capacitor", "Inductor"] and refdes in unmount_comp_list: + unmount_comp_list.remove(refdes) + if not value_col == None: + try: + value = l[value_col] + except: + value = None + if value: + if comp_type == "Resistor": + self.set_component_rlc(refdes, res_value=value) + elif comp_type == "Capacitor": + self.set_component_rlc(refdes, cap_value=value) + elif comp_type == "Inductor": + self.set_component_rlc(refdes, ind_value=value) + for comp in unmount_comp_list: + self.instances[comp].enabled = False + return True + + def export_bom(self, bom_file, delimiter=","): + """Export Bom file from layout. + + Parameters + ---------- + bom_file : str + Full path to the BOM file, which is a delimited text file. + delimiter : str, optional + Value to use for the delimiter. The default is ``","``. + """ + with open(bom_file, "w") as f: + f.writelines([delimiter.join(["RefDes", "Part name", "Type", "Value\n"])]) + for refdes, comp in self.instances.items(): + if not comp.is_enabled and comp.type in ["Resistor", "Capacitor", "Inductor"]: + continue + part_name = comp.partname + comp_type = comp.type + if comp_type == "Resistor": + value = comp.res_value + elif comp_type == "Capacitor": + value = comp.cap_value + elif comp_type == "Inductor": + value = comp.ind_value + else: + value = "" + if not value: + value = "" + f.writelines([delimiter.join([refdes, part_name, comp_type, value + "\n"])]) + return True + + def find_by_reference_designator(self, reference_designator): + """Find a component. + + Parameters + ---------- + reference_designator : str + Reference designator of the component. + """ + return self.instances[reference_designator] + + def get_aedt_pin_name(self, pin): + """Retrieve the pin name that is shown in AEDT. + + .. note:: + To obtain the EDB core pin name, use `pin.GetName()`. + + Parameters + ---------- + pin : str + Name of the pin in EDB core. + + Returns + ------- + str + Name of the pin in AEDT. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> edbapp.components.get_aedt_pin_name(pin) + + """ + return pin.aedt_name + + def get_pins(self, reference_designator, net_name=None, pin_name=None): + """Get component pins. + + Parameters + ---------- + reference_designator : str + Reference designator of the component. + net_name : str, optional + Name of the net. + pin_name : str, optional + Name of the pin. + + Returns + ------- + + """ + comp = self.find_by_reference_designator(reference_designator) + + pins = comp.pins + if net_name: + pins = {i: j for i, j in pins.items() if j.net_name == net_name} + + if pin_name: + pins = {i: j for i, j in pins.items() if i == pin_name} + + return pins + + def get_pin_position(self, pin): + """Retrieve the pin position in meters. + + Parameters + ---------- + pin : str + Name of the pin. + + Returns + ------- + list + Pin position as a list of float values in the form ``[x, y]``. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> edbapp.components.get_pin_position(pin) + + """ + + pt_pos = pin.position + if pin.component.is_null: + transformed_pt_pos = pt_pos + else: + transformed_pt_pos = pin.component.transform.transform_point(pt_pos) + return [transformed_pt_pos[0].value, transformed_pt_pos[1].value] + + def get_pins_name_from_net(self, net_name, pin_list=None): + """Retrieve pins belonging to a net. + + Parameters + ---------- + pin_list : list of EDBPadstackInstance, optional + List of pins to check. The default is ``None``, in which case all pins are checked + net_name : str + Name of the net. + + Returns + ------- + list of str names: + Pins belonging to the net. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> edbapp.components.get_pins_name_from_net(pin_list, net_name) + + """ + pin_names = [] + if not pin_list: + pin_list = [] + for i in [*self.components.values()]: + for j in [*i.pins.values()]: + pin_list.append(j) + for pin in pin_list: + if not pin.net.is_null: + if pin.net.name == net_name: + pin_names.append(self.get_aedt_pin_name(pin)) + return pin_names + + def get_nets_from_pin_list(self, pins): + """Retrieve nets with one or more pins. + + Parameters + ---------- + PinList : list + List of pins. + + Returns + ------- + list + List of nets with one or more pins. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> edbapp.components.get_nets_from_pin_list(pins) + + """ + return list(set([pin.net.name for pin in pins])) + + def get_component_net_connection_info(self, refdes): + """Retrieve net connection information. + + Parameters + ---------- + refdes : + Reference designator for the net. + + Returns + ------- + dict + Dictionary of the net connection information for the reference designator. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> edbapp.components.get_component_net_connection_info(refdes) + + """ + data = {"refdes": [], "pin_name": [], "net_name": []} + for _, pin_obj in self.instances[refdes].pins.items(): + pin_name = pin_obj.name + if not pin_obj.net.is_null: + net_name = pin_obj.net.name + if pin_name: + data["refdes"].append(refdes) + data["pin_name"].append(pin_name) + data["net_name"].append(net_name) + return data + + def get_rats(self): + """Retrieve a list of dictionaries of the reference designator, pin names, and net names. + + Returns + ------- + list + List of dictionaries of the reference designator, pin names, + and net names. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> edbapp.components.get_rats() + + """ + df_list = [] + for refdes in self.instances.keys(): + df = self.get_component_net_connection_info(refdes) + df_list.append(df) + return df_list + + def get_through_resistor_list(self, threshold=1): + """Retrieve through resistors. + + Parameters + ---------- + threshold : int, optional + Threshold value. The default is ``1``. + + Returns + ------- + list + List of through resistors. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> edbapp.components.get_through_resistor_list() + + """ + through_comp_list = [] + for refdes, comp_obj in self.resistors.items(): + numpins = comp_obj.numpins + + if numpins == 2: + value = comp_obj.res_value + value = resistor_value_parser(value) + + if value <= threshold: + through_comp_list.append(refdes) + + return through_comp_list + + def short_component_pins(self, component_name, pins_to_short=None, width=1e-3): + """Short pins of component with a trace. + + Parameters + ---------- + component_name : str + Name of the component. + pins_to_short : list, optional + List of pins to short. If `None`, all pins will be shorted. + width : float, optional + Short Trace width. It will be used in trace computation algorithm + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> edbapp.components.short_component_pins("J4A2", ["G4", "9", "3"]) + + """ + component = self.instances[component_name] + pins = component.pins + pins_list = [] + + for pin_name, pin in pins.items(): + if pins_to_short: + if pin_name in pins_to_short: + pins_list.append(pin) + else: + pins_list.append(pin) + positions_to_short = [] + center = component.center + c = [center[0], center[1], 0] + delta_pins = [] + w = width + for pin in pins_list: + placement_layer = pin.placement_layer + positions_to_short.append(pin.position) + if placement_layer in self._pedb.padstacks.definitions[pin.padstack_def.name].pad_by_layer: + pad = self._pedb.padstacks.definitions[pin.padstack_def.name].pad_by_layer[placement_layer] + else: + layer = list(self._pedb.padstacks.definitions[pin.padstack_def.name].pad_by_layer.keys())[0] + pad = self._pedb.padstacks.definitions[pin.padstack_def.name].pad_by_layer[layer] + pars = pad.parameters_values + if pad.geometry_type < 6 and pars: + delta_pins.append(max(pars) + min(pars) / 2) + w = min(min(pars), w) + elif pars: + delta_pins.append(1.5 * pars[0]) + w = min(pars[0], w) + elif pad.polygon_data: # pragma: no cover + bbox = pad.polygon_data.bbox() + lower = [bbox[0].x.value, bbox[0].y.value] + upper = [bbox[1].x.value, bbox[1].y.value] + pars = [abs(lower[0] - upper[0]), abs(lower[1] - upper[1])] + delta_pins.append(max(pars) + min(pars) / 2) + w = min(min(pars), w) + else: + delta_pins.append(1.5 * width) + i = 0 + + while i < len(positions_to_short) - 1: + p0 = [] + p0.append([positions_to_short[i][0] - delta_pins[i], positions_to_short[i][1], 0]) + p0.append([positions_to_short[i][0] + delta_pins[i], positions_to_short[i][1], 0]) + p0.append([positions_to_short[i][0], positions_to_short[i][1] - delta_pins[i], 0]) + p0.append([positions_to_short[i][0], positions_to_short[i][1] + delta_pins[i], 0]) + p0.append([positions_to_short[i][0], positions_to_short[i][1], 0]) + l0 = [ + GeometryOperators.points_distance(p0[0], c), + GeometryOperators.points_distance(p0[1], c), + GeometryOperators.points_distance(p0[2], c), + GeometryOperators.points_distance(p0[3], c), + GeometryOperators.points_distance(p0[4], c), + ] + l0_min = l0.index(min(l0)) + p1 = [] + p1.append( + [ + positions_to_short[i + 1][0] - delta_pins[i + 1], + positions_to_short[i + 1][1], + 0, + ] + ) + p1.append( + [ + positions_to_short[i + 1][0] + delta_pins[i + 1], + positions_to_short[i + 1][1], + 0, + ] + ) + p1.append( + [ + positions_to_short[i + 1][0], + positions_to_short[i + 1][1] - delta_pins[i + 1], + 0, + ] + ) + p1.append( + [ + positions_to_short[i + 1][0], + positions_to_short[i + 1][1] + delta_pins[i + 1], + 0, + ] + ) + p1.append([positions_to_short[i + 1][0], positions_to_short[i + 1][1], 0]) + + l1 = [ + GeometryOperators.points_distance(p1[0], c), + GeometryOperators.points_distance(p1[1], c), + GeometryOperators.points_distance(p1[2], c), + GeometryOperators.points_distance(p1[3], c), + GeometryOperators.points_distance(p1[4], c), + ] + l1_min = l1.index(min(l1)) + + trace_points = [positions_to_short[i]] + + trace_points.append(p0[l0_min][:2]) + trace_points.append(c[:2]) + trace_points.append(p1[l1_min][:2]) + + trace_points.append(positions_to_short[i + 1]) + + self._pedb.modeler.create_trace( + trace_points, + layer_name=placement_layer, + net_name="short", + width=w, + start_cap_style="Flat", + end_cap_style="Flat", + ) + i += 1 + return True + + def create_pin_group(self, reference_designator, pin_numbers, group_name=None): + """Create pin group on the component. + + Parameters + ---------- + reference_designator : str + References designator of the component. + pin_numbers : int, str, list[str] or list[:class: `PadstackInstance]` + List of pins. + group_name : str, optional + Name of the pin group. + + Returns + ------- + PinGroup + """ + if not isinstance(pin_numbers, list): + pin_numbers = [pin_numbers] + pin_numbers = [str(p) for p in pin_numbers] + if group_name is None: + group_name = PinGroup.unique_name(self._active_layout, "") + comp = self.instances[reference_designator] + pins = [pin for pin_name, pin in comp.pins.items() if pin_name in pin_numbers] + if not pins: + pins = [pin for pin_name, pin in comp.pins.items() if pin.name in pin_numbers] + if not pins: + self._pedb.logger.error("No pin found to create pin group") + return False + pingroup = PinGroup.create(self._active_layout, group_name, pins) + + if pingroup.is_null: # pragma: no cover + self._logger.error(f"Failed to create pin group {group_name}.") + return False + else: + for pin in pins: + if not pin.net.is_null: + if pin.net.name: + pingroup.net = pin.net + return group_name + return False + + def create_pin_group_on_net(self, reference_designator, net_name, group_name=None): + """Create pin group on component by net name. + + Parameters + ---------- + reference_designator : str + References designator of the component. + net_name : str + Name of the net. + group_name : str, optional + Name of the pin group. The default value is ``None``. + + Returns + ------- + PinGroup + """ + pins = [ + pin.name for pin in list(self.instances[reference_designator].pins.values()) if pin.net_name == net_name + ] + return self.create_pin_group(reference_designator, pins, group_name) diff --git a/src/pyedb/grpc/database/control_file.py b/src/pyedb/grpc/database/control_file.py new file mode 100644 index 0000000000..7bffc083f2 --- /dev/null +++ b/src/pyedb/grpc/database/control_file.py @@ -0,0 +1,1277 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import copy +import os +import re +import subprocess +import sys + +from pyedb.edb_logger import pyedb_logger +from pyedb.generic.general_methods import ET, env_path, env_value, is_linux +from pyedb.misc.aedtlib_personalib_install import write_pretty_xml +from pyedb.misc.misc import list_installed_ansysem + + +def convert_technology_file(tech_file, edbversion=None, control_file=None): + """Convert a technology file to edb control file (xml). + + Parameters + ---------- + tech_file : str + Full path to technology file + edbversion : str, optional + Edb version to use. Default is `None` to use latest available version of Edb. + control_file : str, optional + Control file output file. Default is `None` to use same path and same name of `tech_file`. + + Returns + ------- + str + Control file full path if created. + """ + if is_linux: # pragma: no cover + if not edbversion: + edbversion = "20{}.{}".format(list_installed_ansysem()[0][-3:-1], list_installed_ansysem()[0][-1:]) + if env_value(edbversion) in os.environ: + base_path = env_path(edbversion) + sys.path.append(base_path) + else: + pyedb_logger.error("No Edb installation found. Check environment variables") + return False + os.environ["HELIC_ROOT"] = os.path.join(base_path, "helic") + if os.getenv("ANSYSLMD_LICENCE_FILE", None) is None: + lic = os.path.join(base_path, "..", "..", "shared_files", "licensing", "ansyslmd.ini") + if os.path.exists(lic): + with open(lic, "r") as fh: + lines = fh.read().splitlines() + for line in lines: + if line.startswith("SERVER="): + os.environ["ANSYSLMD_LICENSE_FILE"] = line.split("=")[1] + break + else: + pyedb_logger.error("ANSYSLMD_LICENSE_FILE is not defined.") + vlc_file_name = os.path.splitext(tech_file)[0] + if not control_file: + control_file = vlc_file_name + ".xml" + vlc_file = vlc_file_name + ".vlc.tech" + commands = [] + command = [ + os.path.join(base_path, "helic", "tools", "bin", "afet", "tech2afet"), + "-i", + tech_file, + "-o", + vlc_file, + "--backplane", + "False", + ] + commands.append(command) + command = [ + os.path.join(base_path, "helic", "tools", "raptorh", "bin", "make-edb"), + "--dielectric-simplification-method", + "1", + "-t", + vlc_file, + "-o", + vlc_file_name, + "--export-xml", + control_file, + ] + commands.append(command) + commands.append(["rm", "-r", vlc_file_name + ".aedb"]) + my_env = os.environ.copy() + for command in commands: + p = subprocess.Popen(command, env=my_env) + p.wait() + if os.path.exists(control_file): + pyedb_logger.info("Xml file created.") + return control_file + pyedb_logger.error("Technology files are supported only in Linux. Use control file instead.") + return False + + +class ControlProperty: + def __init__(self, property_name, value): + self.name = property_name + self.value = value + if isinstance(value, str): + self.type = 1 + elif isinstance(value, list): + self.type = 2 + else: + try: + float(value) + self.type = 0 + except TypeError: + pass + + def _write_xml(self, root): + try: + if self.type == 0: + content = ET.SubElement(root, self.name) + double = ET.SubElement(content, "Double") + double.text = str(self.value) + else: + pass + except: + pass + + +class ControlFileMaterial: + def __init__(self, name, properties): + self.name = name + self.properties = {} + for name, property in properties.items(): + self.properties[name] = ControlProperty(name, property) + + def _write_xml(self, root): + content = ET.SubElement(root, "Material") + content.set("Name", self.name) + for property_name, property in self.properties.items(): + property._write_xml(content) + + +class ControlFileDielectric: + def __init__(self, name, properties): + self.name = name + self.properties = {} + for name, prop in properties.items(): + self.properties[name] = prop + + def _write_xml(self, root): + content = ET.SubElement(root, "Layer") + for property_name, property in self.properties.items(): + if not property_name == "Index": + content.set(property_name, str(property)) + + +class ControlFileLayer: + def __init__(self, name, properties): + self.name = name + self.properties = {} + for name, prop in properties.items(): + self.properties[name] = prop + + def _write_xml(self, root): + content = ET.SubElement(root, "Layer") + content.set("Color", self.properties.get("Color", "#5c4300")) + if self.properties.get("Elevation"): + content.set("Elevation", self.properties["Elevation"]) + if self.properties.get("GDSDataType"): + content.set("GDSDataType", self.properties["GDSDataType"]) + if self.properties.get("GDSIIVia") or self.properties.get("GDSDataType"): + content.set("GDSIIVia", self.properties.get("GDSIIVia", "false")) + if self.properties.get("Material"): + content.set("Material", self.properties.get("Material", "air")) + content.set("Name", self.name) + if self.properties.get("StartLayer"): + content.set("StartLayer", self.properties["StartLayer"]) + if self.properties.get("StopLayer"): + content.set("StopLayer", self.properties["StopLayer"]) + if self.properties.get("TargetLayer"): + content.set("TargetLayer", self.properties["TargetLayer"]) + if self.properties.get("Thickness"): + content.set("Thickness", self.properties.get("Thickness", "0.001")) + if self.properties.get("Type"): + content.set("Type", self.properties.get("Type", "conductor")) + + +class ControlFileVia(ControlFileLayer): + def __init__(self, name, properties): + ControlFileLayer.__init__(self, name, properties) + self.create_via_group = False + self.check_containment = True + self.method = "proximity" + self.persistent = False + self.tolerance = "1um" + self.snap_via_groups = False + self.snap_method = "areaFactor" + self.remove_unconnected = True + self.snap_tolerance = 3 + + def _write_xml(self, root): + content = ET.SubElement(root, "Layer") + content.set("Color", self.properties.get("Color", "#5c4300")) + if self.properties.get("Elevation"): + content.set("Elevation", self.properties["Elevation"]) + if self.properties.get("GDSDataType"): + content.set("GDSDataType", self.properties["GDSDataType"]) + if self.properties.get("Material"): + content.set("Material", self.properties.get("Material", "air")) + content.set("Name", self.name) + content.set("StartLayer", self.properties.get("StartLayer", "")) + content.set("StopLayer", self.properties.get("StopLayer", "")) + if self.properties.get("TargetLayer"): + content.set("TargetLayer", self.properties["TargetLayer"]) + if self.properties.get("Thickness"): + content.set("Thickness", self.properties.get("Thickness", "0.001")) + if self.properties.get("Type"): + content.set("Type", self.properties.get("Type", "conductor")) + if self.create_via_group: + viagroup = ET.SubElement(content, "CreateViaGroups") + viagroup.set("CheckContainment", "true" if self.check_containment else "false") + viagroup.set("Method", self.method) + viagroup.set("Persistent", "true" if self.persistent else "false") + viagroup.set("Tolerance", self.tolerance) + if self.snap_via_groups: + snapgroup = ET.SubElement(content, "SnapViaGroups") + snapgroup.set("Method", self.snap_method) + snapgroup.set("RemoveUnconnected", "true" if self.remove_unconnected else "false") + snapgroup.set("Tolerance", str(self.snap_tolerance)) + + +class ControlFileStackup: + """Class that manages the Stackup info.""" + + def __init__(self, units="mm"): + self._materials = {} + self._layers = [] + self._dielectrics = [] + self._vias = [] + self.units = units + self.metal_layer_snapping_tolerance = None + self.dielectrics_base_elevation = 0 + + @property + def vias(self): + """Via list. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileVia` + + """ + return self._vias + + @property + def materials(self): + """Material list. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileMaterial` + + """ + return self._materials + + @property + def dielectrics(self): + """Dielectric layer list. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileLayer` + + """ + return self._dielectrics + + @property + def layers(self): + """Layer list. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileLayer` + + """ + return self._layers + + def add_material( + self, + material_name, + permittivity=1.0, + dielectric_loss_tg=0.0, + permeability=1.0, + conductivity=0.0, + properties=None, + ): + """Add a new material with specific properties. + + Parameters + ---------- + material_name : str + Material name. + permittivity : float, optional + Material permittivity. The default is ``1.0``. + dielectric_loss_tg : float, optional + Material tangent losses. The default is ``0.0``. + permeability : float, optional + Material permeability. The default is ``1.0``. + conductivity : float, optional + Material conductivity. The default is ``0.0``. + properties : dict, optional + Specific material properties. The default is ``None``. + Dictionary with key and material property value. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileMaterial` + """ + if isinstance(properties, dict): + self._materials[material_name] = ControlFileMaterial(material_name, properties) + return self._materials[material_name] + else: + properties = { + "Name": material_name, + "Permittivity": permittivity, + "Permeability": permeability, + "Conductivity": conductivity, + "DielectricLossTangent": dielectric_loss_tg, + } + self._materials[material_name] = ControlFileMaterial(material_name, properties) + return self._materials[material_name] + + def add_layer( + self, + layer_name, + elevation=0.0, + material="", + gds_type=0, + target_layer="", + thickness=0.0, + layer_type="conductor", + solve_inside=True, + properties=None, + ): + """Add a new layer. + + Parameters + ---------- + layer_name : str + Layer name. + elevation : float + Layer elevation. + material : str + Material for the layer. + gds_type : int + GDS type assigned on the layer. The value must be the same as in the GDS file otherwise geometries won't be + imported. + target_layer : str + Layer name assigned in EDB or HFSS 3D layout after import. + thickness : float + Layer thickness + layer_type : str + Define the layer type, default value for a layer is ``"conductor"`` + solve_inside : bool + When ``True`` solver will solve inside metal, and not id ``False``. Default value is ``True``. + properties : dict + Dictionary with key and property value. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileLayer` + """ + if isinstance(properties, dict): + self._layers.append(ControlFileLayer(layer_name, properties)) + return self._layers[-1] + else: + properties = { + "Name": layer_name, + "GDSDataType": str(gds_type), + "TargetLayer": target_layer, + "Type": layer_type, + "Material": material, + "Thickness": str(thickness), + "Elevation": str(elevation), + "SolveInside": str(solve_inside).lower(), + } + self._layers.append(ControlFileDielectric(layer_name, properties)) + return self._layers[-1] + + def add_dielectric( + self, + layer_name, + layer_index=None, + material="", + thickness=0.0, + properties=None, + base_layer=None, + add_on_top=True, + ): + """Add a new dielectric. + + Parameters + ---------- + layer_name : str + Layer name. + layer_index : int, optional + Dielectric layer index as they must be stacked. If not provided the layer index will be incremented. + material : str + Material name. + thickness : float + Layer thickness. + properties : dict + Dictionary with key and property value. + base_layer : str, optional + Layer name used for layer placement. Default value is ``None``. This option is used for inserting + dielectric layer between two existing ones. When no argument is provided the dielectric layer will be placed + on top of the stacked ones. + method : bool, Optional. + Provides the method to use when the argument ``base_layer`` is provided. When ``True`` the layer is added + on top on the base layer, when ``False`` it will be added below. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileDielectric` + """ + if isinstance(properties, dict): + self._dielectrics.append(ControlFileDielectric(layer_name, properties)) + return self._dielectrics[-1] + else: + if not layer_index and self.dielectrics and not base_layer: + layer_index = max([diel.properties["Index"] for diel in self.dielectrics]) + 1 + elif base_layer and self.dielectrics: + if base_layer in [diel.properties["Name"] for diel in self.dielectrics]: + base_layer_index = next( + diel.properties["Index"] for diel in self.dielectrics if diel.properties["Name"] == base_layer + ) + if add_on_top: + layer_index = base_layer_index + 1 + for diel_layer in self.dielectrics: + if diel_layer.properties["Index"] > base_layer_index: + diel_layer.properties["Index"] += 1 + else: + layer_index = base_layer_index + for diel_layer in self.dielectrics: + if diel_layer.properties["Index"] >= base_layer_index: + diel_layer.properties["Index"] += 1 + elif not layer_index: + layer_index = 0 + properties = {"Index": layer_index, "Material": material, "Name": layer_name, "Thickness": thickness} + self._dielectrics.append(ControlFileDielectric(layer_name, properties)) + return self._dielectrics[-1] + + def add_via( + self, + layer_name, + material="", + gds_type=0, + target_layer="", + start_layer="", + stop_layer="", + solve_inside=True, + via_group_method="proximity", + via_group_tol=1e-6, + via_group_persistent=True, + snap_via_group_method="distance", + snap_via_group_tol=10e-9, + properties=None, + ): + """Add a new via layer. + + Parameters + ---------- + layer_name : str + Layer name. + material : str + Define the material for this layer. + gds_type : int + Define the gds type. + target_layer : str + Target layer used after layout import in EDB and HFSS 3D layout. + start_layer : str + Define the start layer for the via + stop_layer : str + Define the stop layer for the via. + solve_inside : bool + When ``True`` solve inside this layer is anbled. Default value is ``True``. + via_group_method : str + Define the via group method, default value is ``"proximity"`` + via_group_tol : float + Define the via group tolerance. + via_group_persistent : bool + When ``True`` activated otherwise when ``False``is deactivated. Default value is ``True``. + snap_via_group_method : str + Define the via group method, default value is ``"distance"`` + snap_via_group_tol : float + Define the via group tolerance, default value is 10e-9. + properties : dict + Dictionary with key and property value. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileVia` + """ + if isinstance(properties, dict): + self._vias.append(ControlFileVia(layer_name, properties)) + return self._vias[-1] + else: + properties = { + "Name": layer_name, + "GDSDataType": str(gds_type), + "TargetLayer": target_layer, + "Material": material, + "StartLayer": start_layer, + "StopLayer": stop_layer, + "SolveInside": str(solve_inside).lower(), + "ViaGroupMethod": via_group_method, + "Persistent": via_group_persistent, + "ViaGroupTolerance": via_group_tol, + "SnapViaGroupMethod": snap_via_group_method, + "SnapViaGroupTolerance": snap_via_group_tol, + } + self._vias.append(ControlFileVia(layer_name, properties)) + return self._vias[-1] + + def _write_xml(self, root): + content = ET.SubElement(root, "Stackup") + content.set("schemaVersion", "1.0") + materials = ET.SubElement(content, "Materials") + for materialname, material in self.materials.items(): + material._write_xml(materials) + elayers = ET.SubElement(content, "ELayers") + elayers.set("LengthUnit", self.units) + if self.metal_layer_snapping_tolerance: + elayers.set("MetalLayerSnappingTolerance", str(self.metal_layer_snapping_tolerance)) + dielectrics = ET.SubElement(elayers, "Dielectrics") + dielectrics.set("BaseElevation", str(self.dielectrics_base_elevation)) + # sorting dielectric layers + self._dielectrics = list(sorted(list(self._dielectrics), key=lambda x: x.properties["Index"], reverse=False)) + for layer in self.dielectrics: + layer._write_xml(dielectrics) + layers = ET.SubElement(elayers, "Layers") + + for layer in self.layers: + layer._write_xml(layers) + vias = ET.SubElement(elayers, "Vias") + + for layer in self.vias: + layer._write_xml(vias) + + +class ControlFileImportOptions: + """Import Options.""" + + def __init__(self): + self.auto_close = False + self.convert_closed_wide_lines_to_polys = False + self.round_to = 0 + self.defeature_tolerance = 0.0 + self.flatten = True + self.enable_default_component_values = True + self.import_dummy_nets = False + self.gdsii_convert_polygon_to_circles = False + self.import_cross_hatch_shapes_as_lines = True + self.max_antipad_radius = 0.0 + self.extracta_use_pin_names = False + self.min_bondwire_width = 0.0 + self.antipad_repalce_radius = 0.0 + self.gdsii_scaling_factor = 0.0 + self.delte_empty_non_laminate_signal_layers = False + + def _write_xml(self, root): + content = ET.SubElement(root, "ImportOptions") + content.set("AutoClose", str(self.auto_close).lower()) + if self.round_to != 0: + content.set("RoundTo", str(self.round_to)) + if self.defeature_tolerance != 0.0: + content.set("DefeatureTolerance", str(self.defeature_tolerance)) + content.set("Flatten", str(self.flatten).lower()) + content.set("EnableDefaultComponentValues", str(self.enable_default_component_values).lower()) + content.set("ImportDummyNet", str(self.import_dummy_nets).lower()) + content.set("GDSIIConvertPolygonToCircles", str(self.convert_closed_wide_lines_to_polys).lower()) + content.set("ImportCrossHatchShapesAsLines", str(self.import_cross_hatch_shapes_as_lines).lower()) + content.set("ExtractaUsePinNames", str(self.extracta_use_pin_names).lower()) + if self.max_antipad_radius != 0.0: + content.set("MaxAntiPadRadius", str(self.max_antipad_radius)) + if self.antipad_repalce_radius != 0.0: + content.set("AntiPadReplaceRadius", str(self.antipad_repalce_radius)) + if self.min_bondwire_width != 0.0: + content.set("MinBondwireWidth", str(self.min_bondwire_width)) + if self.gdsii_scaling_factor != 0.0: + content.set("GDSIIScalingFactor", str(self.gdsii_scaling_factor)) + content.set("DeleteEmptyNonLaminateSignalLayers", str(self.delte_empty_non_laminate_signal_layers).lower()) + + +class ControlExtent: + """Extent options.""" + + def __init__( + self, + type="bbox", + dieltype="bbox", + diel_hactor=0.25, + airbox_hfactor=0.25, + airbox_vr_p=0.25, + airbox_vr_n=0.25, + useradiation=True, + honor_primitives=True, + truncate_at_gnd=True, + ): + self.type = type + self.dieltype = dieltype + self.diel_hactor = diel_hactor + self.airbox_hfactor = airbox_hfactor + self.airbox_vr_p = airbox_vr_p + self.airbox_vr_n = airbox_vr_n + self.useradiation = useradiation + self.honor_primitives = honor_primitives + self.truncate_at_gnd = truncate_at_gnd + + def _write_xml(self, root): + content = ET.SubElement(root, "Extents") + content.set("Type", self.type) + content.set("DielType", self.dieltype) + content.set("DielHorizFactor", str(self.diel_hactor)) + content.set("AirboxHorizFactor", str(self.airbox_hfactor)) + content.set("AirboxVertFactorPos", str(self.airbox_vr_p)) + content.set("AirboxVertFactorNeg", str(self.airbox_vr_n)) + content.set("UseRadiationBoundary", str(self.useradiation).lower()) + content.set("DielHonorPrimitives", str(self.honor_primitives).lower()) + content.set("AirboxTruncateAtGround", str(self.truncate_at_gnd).lower()) + + +class ControlCircuitPt: + """Circuit Port.""" + + def __init__(self, name, x1, y1, lay1, x2, y2, lay2, z0): + self.name = name + self.x1 = x1 + self.x2 = x2 + self.lay1 = lay1 + self.lay2 = lay2 + self.y1 = y1 + self.y2 = y2 + self.z0 = z0 + + def _write_xml(self, root): + content = ET.SubElement(root, "CircuitPortPt") + content.set("Name", self.name) + content.set("x1", self.x1) + content.set("y1", self.y1) + content.set("Layer1", self.lay1) + content.set("x2", self.x2) + content.set("y2", self.y2) + content.set("Layer2", self.lay2) + content.set("Z0", self.z0) + + +class ControlFileComponent: + """Components.""" + + def __init__(self): + self.refdes = "U1" + self.partname = "BGA" + self.parttype = "IC" + self.die_type = "None" + self.die_orientation = "Chip down" + self.solderball_shape = "None" + self.solder_diameter = "65um" + self.solder_height = "65um" + self.solder_material = "solder" + self.pins = [] + self.ports = [] + + def add_pin(self, name, x, y, layer): + self.pins.append({"Name": name, "x": x, "y": y, "Layer": layer}) + + def add_port(self, name, z0, pospin, refpin=None, pos_type="pin", ref_type="pin"): + args = {"Name": name, "Z0": z0} + if pos_type == "pin": + args["PosPin"] = pospin + elif pos_type == "pingroup": + args["PosPinGroup"] = pospin + if refpin: + if ref_type == "pin": + args["RefPin"] = refpin + elif ref_type == "pingroup": + args["RefPinGroup"] = refpin + elif ref_type == "net": + args["RefNet"] = refpin + self.ports.append(args) + + def _write_xml(self, root): + content = ET.SubElement(root, "GDS_COMPONENT") + for p in self.pins: + prop = ET.SubElement(content, "GDS_PIN") + for pname, value in p.items(): + prop.set(pname, value) + + prop = ET.SubElement(content, "Component") + prop.set("RefDes", self.refdes) + prop.set("PartName", self.partname) + prop.set("PartType", self.parttype) + prop2 = ET.SubElement(prop, "DieProperties") + prop2.set("Type", self.die_type) + prop2.set("Orientation", self.die_orientation) + prop2 = ET.SubElement(prop, "SolderballProperties") + prop2.set("Shape", self.solderball_shape) + prop2.set("Diameter", self.solder_diameter) + prop2.set("Height", self.solder_height) + prop2.set("Material", self.solder_material) + for p in self.ports: + prop = ET.SubElement(prop, "ComponentPort") + for pname, value in p.items(): + prop.set(pname, value) + + +class ControlFileComponents: + """Class for component management.""" + + def __init__(self): + self.units = "um" + self.components = [] + + def add_component(self, ref_des, partname, component_type, die_type="None", solderball_shape="None"): + """Create a new component. + + Parameters + ---------- + ref_des : str + Reference Designator name. + partname : str + Part name. + component_type : str + Component Type. Can be `"IC"`, `"IO"` or `"Other"`. + die_type : str, optional + Die Type. Can be `"None"`, `"Flip chip"` or `"Wire bond"`. + solderball_shape : str, optional + Solderball Type. Can be `"None"`, `"Cylinder"` or `"Spheroid"`. + + Returns + ------- + + """ + comp = ControlFileComponent() + comp.refdes = ref_des + comp.partname = partname + comp.parttype = component_type + comp.die_type = die_type + comp.solderball_shape = solderball_shape + self.components.append(comp) + return comp + + +class ControlFileBoundaries: + """Boundaries management.""" + + def __init__(self, units="um"): + self.ports = {} + self.extents = [] + self.circuit_models = {} + self.circuit_elements = {} + self.units = units + + def add_port(self, name, x1, y1, layer1, x2, y2, layer2, z0=50): + """Add a new port to the gds. + + Parameters + ---------- + name : str + Port name. + x1 : str + Pin 1 x position. + y1 : str + Pin 1 y position. + layer1 : str + Pin 1 layer. + x2 : str + Pin 2 x position. + y2 : str + Pin 2 y position. + layer2 : str + Pin 2 layer. + z0 : str + Characteristic impedance. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlCircuitPt` + """ + self.ports[name] = ControlCircuitPt(name, str(x1), str(y1), layer1, str(x2), str(y2), layer2, str(z0)) + return self.ports[name] + + def add_extent( + self, + type="bbox", + dieltype="bbox", + diel_hactor=0.25, + airbox_hfactor=0.25, + airbox_vr_p=0.25, + airbox_vr_n=0.25, + useradiation=True, + honor_primitives=True, + truncate_at_gnd=True, + ): + """Add a new extent. + + Parameters + ---------- + type + dieltype + diel_hactor + airbox_hfactor + airbox_vr_p + airbox_vr_n + useradiation + honor_primitives + truncate_at_gnd + + Returns + ------- + + """ + self.extents.append( + ControlExtent( + type=type, + dieltype=dieltype, + diel_hactor=diel_hactor, + airbox_hfactor=airbox_hfactor, + airbox_vr_p=airbox_vr_p, + airbox_vr_n=airbox_vr_n, + useradiation=useradiation, + honor_primitives=honor_primitives, + truncate_at_gnd=truncate_at_gnd, + ) + ) + return self.extents[-1] + + def _write_xml(self, root): + content = ET.SubElement(root, "Boundaries") + content.set("LengthUnit", self.units) + for p in self.circuit_models.values(): + p._write_xml(content) + for p in self.circuit_elements.values(): + p._write_xml(content) + for p in self.ports.values(): + p._write_xml(content) + for p in self.extents: + p._write_xml(content) + + +class ControlFileSweep: + def __init__(self, name, start, stop, step, sweep_type, step_type, use_q3d): + self.name = name + self.start = start + self.stop = stop + self.step = step + self.sweep_type = sweep_type + self.step_type = step_type + self.use_q3d = use_q3d + + def _write_xml(self, root): + sweep = ET.SubElement(root, "FreqSweep") + prop = ET.SubElement(sweep, "Name") + prop.text = self.name + prop = ET.SubElement(sweep, "UseQ3DForDC") + prop.text = str(self.use_q3d).lower() + prop = ET.SubElement(sweep, self.sweep_type) + prop2 = ET.SubElement(prop, self.step_type) + prop3 = ET.SubElement(prop2, "Start") + prop3.text = self.start + prop3 = ET.SubElement(prop2, "Stop") + prop3.text = self.stop + if self.step_type == "LinearStep": + prop3 = ET.SubElement(prop2, "Step") + prop3.text = str(self.step) + else: + prop3 = ET.SubElement(prop2, "Count") + prop3.text = str(self.step) + + +class ControlFileMeshOp: + def __init__(self, name, region, type, nets_layers): + self.name = name + self.region = name + self.type = type + self.nets_layers = nets_layers + self.num_max_elem = 1000 + self.restrict_elem = False + self.restrict_length = True + self.max_length = "20um" + self.skin_depth = "1um" + self.surf_tri_length = "1mm" + self.num_layers = 2 + self.region_solve_inside = False + + def _write_xml(self, root): + mop = ET.SubElement(root, "MeshOperation") + prop = ET.SubElement(mop, "Name") + prop.text = self.name + prop = ET.SubElement(mop, "Enabled") + prop.text = "true" + prop = ET.SubElement(mop, "Region") + prop.text = self.region + prop = ET.SubElement(mop, "Type") + prop.text = self.type + prop = ET.SubElement(mop, "NetsLayers") + for net, layer in self.nets_layers.items(): + prop2 = ET.SubElement(prop, "NetsLayer") + prop3 = ET.SubElement(prop2, "Net") + prop3.text = net + prop3 = ET.SubElement(prop2, "Layer") + prop3.text = layer + prop = ET.SubElement(mop, "RestrictElem") + prop.text = self.restrict_elem + prop = ET.SubElement(mop, "NumMaxElem") + prop.text = self.num_max_elem + if self.type == "MeshOperationLength": + prop = ET.SubElement(mop, "RestrictLength") + prop.text = self.restrict_length + prop = ET.SubElement(mop, "MaxLength") + prop.text = self.max_length + else: + prop = ET.SubElement(mop, "SkinDepth") + prop.text = self.skin_depth + prop = ET.SubElement(mop, "SurfTriLength") + prop.text = self.surf_tri_length + prop = ET.SubElement(mop, "NumLayers") + prop.text = self.num_layers + prop = ET.SubElement(mop, "RegionSolveInside") + prop.text = self.region_solve_inside + + +class ControlFileSetup: + """Setup Class.""" + + def __init__(self, name): + self.name = name + self.enabled = True + self.save_fields = False + self.save_rad_fields = False + self.frequency = "1GHz" + self.maxpasses = 10 + self.max_delta = 0.02 + self.union_polygons = True + self.small_voids_area = 0 + self.mode_type = "IC" + self.ic_model_resolution = "Auto" + self.order_basis = "FirstOrder" + self.solver_type = "Auto" + self.low_freq_accuracy = False + self.mesh_operations = [] + self.sweeps = [] + + def add_sweep(self, name, start, stop, step, sweep_type="Interpolating", step_type="LinearStep", use_q3d=True): + """Add a new sweep. + + Parameters + ---------- + name : str + Sweep name. + start : str + Frequency start. + stop : str + Frequency stop. + step : str + Frequency step or count. + sweep_type : str + Sweep type. It can be `"Discrete"` or `"Interpolating"`. + step_type : str + Sweep type. It can be `"LinearStep"`, `"DecadeCount"` or `"LinearCount"`. + use_q3d + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileSweep` + """ + self.sweeps.append(ControlFileSweep(name, start, stop, step, sweep_type, step_type, use_q3d)) + return self.sweeps[-1] + + def add_mesh_operation(self, name, region, type, nets_layers): + """Add mesh operations. + + Parameters + ---------- + name : str + Mesh name. + region : str + Region to apply mesh operation. + type : str + Mesh operation type. It can be `"MeshOperationLength"` or `"MeshOperationSkinDepth"`. + nets_layers : dict + Dictionary containing nets and layers on which apply mesh. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileMeshOp` + + """ + mop = ControlFileMeshOp(name, region, type, nets_layers) + self.mesh_operations.append(mop) + return mop + + def _write_xml(self, root): + setups = ET.SubElement(root, "HFSSSetup") + setups.set("schemaVersion", "1.0") + setups.set("Name", self.name) + setup = ET.SubElement(setups, "HFSSSimulationSettings") + prop = ET.SubElement(setup, "Enabled") + prop.text = str(self.enabled).lower() + prop = ET.SubElement(setup, "SaveFields") + prop.text = str(self.save_fields).lower() + prop = ET.SubElement(setup, "SaveRadFieldsOnly") + prop.text = str(self.save_rad_fields).lower() + prop = ET.SubElement(setup, "HFSSAdaptiveSettings") + prop = ET.SubElement(prop, "AdaptiveSettings") + prop = ET.SubElement(prop, "SingleFrequencyDataList") + prop = ET.SubElement(prop, "AdaptiveFrequencyData") + prop2 = ET.SubElement(prop, "AdaptiveFrequency") + prop2.text = self.frequency + prop2 = ET.SubElement(prop, "MaxPasses") + prop2.text = str(self.maxpasses) + prop2 = ET.SubElement(prop, "MaxDelta") + prop2.text = str(self.max_delta) + prop = ET.SubElement(setup, "HFSSDefeatureSettings") + prop2 = ET.SubElement(prop, "UnionPolygons") + prop2.text = str(self.union_polygons).lower() + + prop2 = ET.SubElement(prop, "SmallVoidArea") + prop2.text = str(self.small_voids_area) + prop2 = ET.SubElement(prop, "ModelType") + prop2.text = str(self.mode_type) + prop2 = ET.SubElement(prop, "ICModelResolutionType") + prop2.text = str(self.ic_model_resolution) + + prop = ET.SubElement(setup, "HFSSSolverSettings") + prop2 = ET.SubElement(prop, "OrderBasis") + prop2.text = str(self.order_basis) + prop2 = ET.SubElement(prop, "SolverType") + prop2.text = str(self.solver_type) + prop = ET.SubElement(setup, "HFSSMeshOperations") + for mesh in self.mesh_operations: + mesh._write_xml(prop) + prop = ET.SubElement(setups, "HFSSSweepDataList") + for sweep in self.sweeps: + sweep._write_xml(prop) + + +class ControlFileSetups: + """Setup manager class.""" + + def __init__(self): + self.setups = [] + + def add_setup(self, name, frequency): + """Add a new setup + + Parameters + ---------- + name : str + Setup name. + frequency : str + Setup Frequency. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileSetup` + """ + setup = ControlFileSetup(name) + setup.frequency = frequency + self.setups.append(setup) + return setup + + def _write_xml(self, root): + content = ET.SubElement(root, "SimulationSetups") + for setup in self.setups: + setup._write_xml(content) + + +class ControlFile: + """Control File Class. It helps the creation and modification of edb xml control files.""" + + def __init__(self, xml_input=None, tecnhology=None, layer_map=None): + self.stackup = ControlFileStackup() + if xml_input: + self.parse_xml(xml_input) + if tecnhology: + self.parse_technology(tecnhology) + if layer_map: + self.parse_layer_map(layer_map) + self.boundaries = ControlFileBoundaries() + self.remove_holes = False + self.remove_holes_area_minimum = 30 + self.remove_holes_units = "um" + self.setups = ControlFileSetups() + self.components = ControlFileComponents() + self.import_options = ControlFileImportOptions() + pass + + def parse_technology(self, tecnhology, edbversion=None): + """Parse technology files using Helic and convert it to xml file. + + Parameters + ---------- + layer_map : str + Full path to technology file. + + Returns + ------- + bool + """ + xml_temp = os.path.splitext(tecnhology)[0] + "_temp.xml" + xml_temp = convert_technology_file(tech_file=tecnhology, edbversion=edbversion, control_file=xml_temp) + if xml_temp: + return self.parse_xml(xml_temp) + + def parse_layer_map(self, layer_map): + """Parse layer map and adds info to the stackup info. + This operation must be performed after a tech file is imported. + + Parameters + ---------- + layer_map : str + Full path to `".map"` file. + + Returns + ------- + + """ + with open(layer_map, "r") as f: + lines = f.readlines() + for line in lines: + if not line.startswith("#") and re.search(r"\w+", line.strip()): + out = re.split(r"\s+", line.strip()) + layer_name = out[0] + layer_id = out[2] + layer_type = out[3] + for layer in self.stackup.layers[:]: + if layer.name == layer_name: + layer.properties["GDSDataType"] = layer_type + layer.name = layer_id + layer.properties["TargetLayer"] = layer_name + break + elif layer.properties.get("TargetLayer", None) == layer_name: + new_layer = ControlFileLayer(layer_id, copy.deepcopy(layer.properties)) + new_layer.properties["GDSDataType"] = layer_type + new_layer.name = layer_id + new_layer.properties["TargetLayer"] = layer_name + self.stackup.layers.append(new_layer) + break + for layer in self.stackup.vias[:]: + if layer.name == layer_name: + layer.properties["GDSDataType"] = layer_type + layer.name = layer_id + layer.properties["TargetLayer"] = layer_name + break + elif layer.properties.get("TargetLayer", None) == layer_name: + new_layer = ControlFileVia(layer_id, copy.deepcopy(layer.properties)) + new_layer.properties["GDSDataType"] = layer_type + new_layer.name = layer_id + new_layer.properties["TargetLayer"] = layer_name + self.stackup.vias.append(new_layer) + self.stackup.vias.append(new_layer) + break + return True + + def parse_xml(self, xml_input): + """Parse an xml and populate the class with materials and Stackup only. + + Parameters + ---------- + xml_input : str + Full path to xml. + + Returns + ------- + bool + """ + tree = ET.parse(xml_input) + root = tree.getroot() + for el in root: + if el.tag == "Stackup": + for st_el in el: + if st_el.tag == "Materials": + for mat in st_el: + mat_name = mat.attrib["Name"] + properties = {} + for prop in mat: + if prop[0].tag == "Double": + properties[prop.tag] = prop[0].text + self.stackup.add_material(mat_name, properties) + elif st_el.tag == "ELayers": + if st_el.attrib == "LengthUnits": + self.stackup.units = st_el.attrib + for layers_el in st_el: + if "BaseElevation" in layers_el.attrib: + self.stackup.dielectrics_base_elevation = layers_el.attrib["BaseElevation"] + for layer_el in layers_el: + properties = {} + layer_name = layer_el.attrib["Name"] + for propname, prop_val in layer_el.attrib.items(): + properties[propname] = prop_val + if layers_el.tag == "Dielectrics": + self.stackup.add_dielectric( + layer_name=layer_name, + material=properties["Material"], + thickness=properties["Thickness"], + ) + elif layers_el.tag == "Layers": + self.stackup.add_layer(layer_name=layer_name, properties=properties) + elif layers_el.tag == "Vias": + via = self.stackup.add_via(layer_name, properties=properties) + for i in layer_el: + if i.tag == "CreateViaGroups": + via.create_via_group = True + if "CheckContainment" in i.attrib: + via.check_containment = ( + True if i.attrib["CheckContainment"] == "true" else False + ) + if "Tolerance" in i.attrib: + via.tolerance = i.attrib["Tolerance"] + if "Method" in i.attrib: + via.method = i.attrib["Method"] + if "Persistent" in i.attrib: + via.persistent = True if i.attrib["Persistent"] == "true" else False + elif i.tag == "SnapViaGroups": + if "Method" in i.attrib: + via.snap_method = i.attrib["Method"] + if "Tolerance" in i.attrib: + via.snap_tolerance = i.attrib["Tolerance"] + if "RemoveUnconnected" in i.attrib: + via.remove_unconnected = ( + True if i.attrib["RemoveUnconnected"] == "true" else False + ) + return True + + def write_xml(self, xml_output): + """Write xml to output file + + Parameters + ---------- + xml_output : str + Path to the output xml file. + + Returns + ------- + bool + """ + control = ET.Element("{http://www.ansys.com/control}Control", attrib={"schemaVersion": "1.0"}) + self.stackup._write_xml(control) + if self.boundaries.ports or self.boundaries.extents: + self.boundaries._write_xml(control) + if self.remove_holes: + hole = ET.SubElement(control, "RemoveHoles") + hole.set("HoleAreaMinimum", str(self.remove_holes_area_minimum)) + hole.set("LengthUnit", self.remove_holes_units) + if self.setups.setups: + setups = ET.SubElement(control, "SimulationSetups") + for setup in self.setups.setups: + setup._write_xml(setups) + self.import_options._write_xml(control) + if self.components.components: + comps = ET.SubElement(control, "GDS_COMPONENTS") + comps.set("LengthUnit", self.components.units) + for comp in self.components.components: + comp._write_xml(comps) + write_pretty_xml(control, xml_output) + return True if os.path.exists(xml_output) else False diff --git a/src/pyedb/grpc/database/definition/__init__.py b/src/pyedb/grpc/database/definition/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/pyedb/grpc/database/definition/component_def.py b/src/pyedb/grpc/database/definition/component_def.py new file mode 100644 index 0000000000..248a8cb3dd --- /dev/null +++ b/src/pyedb/grpc/database/definition/component_def.py @@ -0,0 +1,180 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os + +from ansys.edb.core.definition.component_def import ComponentDef as GrpcComponentDef + +from pyedb.grpc.database.definition.component_pins import ComponentPin +from pyedb.grpc.database.hierarchy.component import Component + + +class ComponentDef(GrpcComponentDef): + """Manages EDB functionalities for component definitions. + + Parameters + ---------- + pedb : :class:`pyedb.edb` + Inherited AEDT object. + edb_object : object + Edb ComponentDef Object + """ + + def __init__(self, pedb, edb_object): + super().__init__(edb_object.msg) + self._pedb = pedb + + @property + def part_name(self): + """Retrieve component definition name.""" + return self.name + + @part_name.setter + def part_name(self, name): + self.name = name + + @property + def type(self): + """Retrieve the component definition type. + + Returns + ------- + str + """ + if self.components: + return list(self.components.values())[0].type + else: + return "" + + @type.setter + def type(self, value): + if value.lower() == "resistor": + for _, component in self.components.items(): + component.type = "resistor" + elif value.lower() == "inductor": + for _, component in self.components.items(): + component.type = "inductor" + elif value.lower() == "capacitor": + for _, component in self.components.items(): + component.type = "capacitor" + elif value.lower() == "ic": + for _, component in self.components.items(): + component.type = "ic" + elif value.lower() == "io": + for _, component in self.components.items(): + component.type = "io" + elif value.lower() == "other": + for _, component in self.components.items(): + component.type = "other" + else: + return + + @property + def components(self): + """Get the list of components belonging to this component definition. + + Returns + ------- + dict of :class:`EDBComponent` + """ + comp_list = [Component(self._pedb, l) for l in Component.find_by_def(self._pedb.active_layout, self.part_name)] + return {comp.refdes: comp for comp in comp_list} + + @property + def component_pins(self): + return [ComponentPin(self._pedb, pin) for pin in super().component_pins] + + def assign_rlc_model(self, res=None, ind=None, cap=None, is_parallel=False): + """Assign RLC to all components under this part name. + + Parameters + ---------- + res : int, float + Resistance. Default is ``None``. + ind : int, float + Inductance. Default is ``None``. + cap : int, float + Capacitance. Default is ``None``. + is_parallel : bool, optional + Whether it is parallel or series RLC component. + """ + for comp in list(self.components.values()): + res, ind, cap = res, ind, cap + comp.assign_rlc_model(res, ind, cap, is_parallel) + return True + + def assign_s_param_model(self, file_path, model_name=None, reference_net=None): + """Assign S-parameter to all components under this part name. + + Parameters + ---------- + file_path : str + File path of the S-parameter model. + name : str, optional + Name of the S-parameter model. + + Returns + ------- + + """ + for comp in list(self.components.values()): + comp.assign_s_param_model(file_path, model_name, reference_net) + return True + + def assign_spice_model(self, file_path, model_name=None): + """Assign Spice model to all components under this part name. + + Parameters + ---------- + file_path : str + File path of the Spice model. + name : str, optional + Name of the Spice model. + + Returns + ------- + + """ + for comp in list(self.components.values()): + comp.assign_spice_model(file_path, model_name) + return True + + @property + def reference_file(self): + return [model.reference_file for model in self.component_models] + + def add_n_port_model(self, fpath, name=None): + from ansys.edb.core.definition.component_model import ( + NPortComponentModel as GrpcNPortComponentModel, + ) + + if not name: + name = os.path.splitext(os.path.basename(fpath)[0]) + for model in self.component_models: + if model.model_name == name: + self._pedb.logger.error(f"Model {name} already defined for component definition {self.name}") + return False + model = [model for model in self.component_models if model.name == name] + if not model: + n_port_model = GrpcNPortComponentModel.create(name=name) + n_port_model.reference_file = fpath + self.add_component_model(n_port_model) diff --git a/src/pyedb/grpc/database/definition/component_model.py b/src/pyedb/grpc/database/definition/component_model.py new file mode 100644 index 0000000000..62009438b7 --- /dev/null +++ b/src/pyedb/grpc/database/definition/component_model.py @@ -0,0 +1,39 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.definition.component_model import ( + ComponentModel as GrpcComponentModel, +) + + +class ComponentModel(GrpcComponentModel): + """Manages component model class.""" + + def __init__(self): + super().__init__(self.msg) + + +class NPortComponentModel(GrpcComponentModel): + """Class for n-port component models.""" + + def __init__(self): + super().__init__(self.msg) diff --git a/src/pyedb/grpc/database/definition/component_pins.py b/src/pyedb/grpc/database/definition/component_pins.py new file mode 100644 index 0000000000..bc6c0d0353 --- /dev/null +++ b/src/pyedb/grpc/database/definition/component_pins.py @@ -0,0 +1,30 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.definition.component_pin import ComponentPin as GrpcComponentPin + + +class ComponentPin(GrpcComponentPin): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb diff --git a/src/pyedb/grpc/database/definition/materials.py b/src/pyedb/grpc/database/definition/materials.py new file mode 100644 index 0000000000..b8e8cc6ea2 --- /dev/null +++ b/src/pyedb/grpc/database/definition/materials.py @@ -0,0 +1,1034 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import # noreorder + +import difflib +import logging +import os +import re +from typing import Optional +import warnings + +from ansys.edb.core.definition.debye_model import DebyeModel as GrpcDebyeModel +from ansys.edb.core.definition.djordjecvic_sarkar_model import ( + DjordjecvicSarkarModel as GrpcDjordjecvicSarkarModel, +) +from ansys.edb.core.definition.material_def import ( + MaterialProperty as GrpcMaterialProperty, +) +from ansys.edb.core.definition.material_def import MaterialDef as GrpcMaterialDef +from ansys.edb.core.definition.multipole_debye_model import ( + MultipoleDebyeModel as GrpcMultipoleDebyeModel, +) +from ansys.edb.core.utility.value import Value as GrpcValue +from pydantic import BaseModel, confloat + +from pyedb import Edb +from pyedb.exceptions import MaterialModelException + +logger = logging.getLogger(__name__) + +# TODO: Once we are Python3.9+ change PositiveInt implementation like +# from annotated_types import Gt +# from typing_extensions import Annotated +# PositiveFloat = Annotated[float, Gt(0)] +try: + from annotated_types import Gt + from typing_extensions import Annotated + + PositiveFloat = Annotated[float, Gt(0)] +except: + PositiveFloat = confloat(gt=0) + +ATTRIBUTES = [ + "conductivity", + "dielectric_loss_tangent", + "magnetic_loss_tangent", + "mass_density", + "permittivity", + "permeability", + "poisson_ratio", + "specific_heat", + "thermal_conductivity", + "youngs_modulus", + "thermal_expansion_coefficient", +] +DC_ATTRIBUTES = [ + "dielectric_model_frequency", + "loss_tangent_at_frequency", + "permittivity_at_frequency", + "dc_conductivity", + "dc_permittivity", +] +PERMEABILITY_DEFAULT_VALUE = 1 + + +def get_line_float_value(line): + """Retrieve the float value expected in the line of an AMAT file. + + The associated string is expected to follow one of the following cases: + - simple('permittivity', 12.) + - permittivity='12'. + """ + try: + return float(re.split(",|=", line)[-1].strip("'\n)")) + except ValueError: + return None + + +class MaterialProperties(BaseModel): + """Store material properties.""" + + conductivity: Optional[PositiveFloat] = None + dielectric_loss_tangent: Optional[PositiveFloat] = None + magnetic_loss_tangent: Optional[PositiveFloat] = None + mass_density: Optional[PositiveFloat] = None + permittivity: Optional[PositiveFloat] = None + permeability: Optional[PositiveFloat] = None + poisson_ratio: Optional[PositiveFloat] = None + specific_heat: Optional[PositiveFloat] = None + thermal_conductivity: Optional[PositiveFloat] = None + youngs_modulus: Optional[PositiveFloat] = None + thermal_expansion_coefficient: Optional[PositiveFloat] = None + dc_conductivity: Optional[PositiveFloat] = None + dc_permittivity: Optional[PositiveFloat] = None + dielectric_model_frequency: Optional[PositiveFloat] = None + loss_tangent_at_frequency: Optional[PositiveFloat] = None + permittivity_at_frequency: Optional[PositiveFloat] = None + + +class Material(GrpcMaterialDef): + """Manage EDB methods for material property management.""" + + def __init__(self, edb: Edb, edb_material_def): + super().__init__(edb_material_def.msg) + self.__edb: Edb = edb + self.__name: str = edb_material_def.name + self.__material_def = edb_material_def + self.__dielectric_model = None + + @property + def name(self): + """Material name.""" + return self.__name + + @property + def dc_model(self): + return self.dielectric_material_model + + @property + def dielectric_material_model(self): + """Material dielectric model.""" + try: + if super().dielectric_material_model.type.name.lower() == "debye": + self.__dielectric_model = GrpcDebyeModel(super().dielectric_material_model) + elif super().dielectric_material_model.type.name.lower() == "multipole_debye": + self.__dielectric_model = GrpcMultipoleDebyeModel(super().dielectric_material_model) + elif super().dielectric_material_model.type.name.lower() == "djordjecvic_sarkar": + self.__dielectric_model = GrpcDjordjecvicSarkarModel(super().dielectric_material_model) + return self.__dielectric_model + except: + return None + + @property + def conductivity(self): + """Get material conductivity.""" + try: + value = self.get_property(GrpcMaterialProperty.CONDUCTIVITY).value + return value + except: + return None + + @conductivity.setter + def conductivity(self, value): + """Set material conductivity.""" + if self.dielectric_material_model: + self.__edb.logger.error( + f"Dielectric model defined on material {self.name}. Conductivity can not be changed" + f"Changing conductivity is only allowed when no dielectric model is assigned." + ) + else: + self.set_property(GrpcMaterialProperty.CONDUCTIVITY, GrpcValue(value)) + + @property + def dc_conductivity(self): + try: + return self.dielectric_material_model.dc_conductivity + except: + return + + @dc_conductivity.setter + def dc_conductivity(self, value): + if self.dielectric_material_model: + self.dielectric_material_model.dc_conductivity = float(value) + + @property + def dc_permittivity(self): + try: + return self.dielectric_material_model.dc_relative_permitivity + except: + return + + @dc_permittivity.setter + def dc_permittivity(self, value): + if self.dielectric_material_model: + self.dielectric_material_model.dc_relative_permitivity = float(value) + + @property + def loss_tangent_at_frequency(self): + try: + return self.dielectric_material_model.loss_tangent_at_frequency + except: + return + + @loss_tangent_at_frequency.setter + def loss_tangent_at_frequency(self, value): + if self.dielectric_material_model: + self.dielectric_material_model.loss_tangent_at_frequency = float(value) + + @property + def dielectric_model_frequency(self): + try: + return self.dielectric_material_model.frequency + except: + return + + @dielectric_model_frequency.setter + def dielectric_model_frequency(self, value): + if self.dielectric_material_model: + self.dielectric_material_model.frequency = float(value) + + @property + def permittivity_at_frequency(self): + try: + return self.dielectric_material_model.relative_permitivity_at_frequency + except: + return + + @permittivity_at_frequency.setter + def permittivity_at_frequency(self, value): + if self.dielectric_material_model: + self.dielectric_material_model.relative_permitivity_at_frequency = float(value) + + @property + def permittivity(self): + """Get material permittivity.""" + try: + value = self.get_property(GrpcMaterialProperty.PERMITTIVITY).value + return value + except: + return None + + @permittivity.setter + def permittivity(self, value): + """Set material permittivity.""" + self.set_property(GrpcMaterialProperty.PERMITTIVITY, GrpcValue(value)) + + @property + def permeability(self): + """Get material permeability.""" + try: + value = self.get_property(GrpcMaterialProperty.PERMEABILITY).value + return value + except: + return None + + @permeability.setter + def permeability(self, value): + """Set material permeability.""" + self.set_property(GrpcMaterialProperty.PERMEABILITY, GrpcValue(value)) + + @property + def loss_tangent(self): + """Get material loss tangent.""" + warnings.warn( + "This method is deprecated in versions >0.7.0 and will soon be removed. " + "Use property dielectric_loss_tangent instead.", + DeprecationWarning, + ) + return self.dielectric_loss_tangent + + @property + def dielectric_loss_tangent(self): + """Get material loss tangent.""" + try: + return self.get_property(GrpcMaterialProperty.DIELECTRIC_LOSS_TANGENT).value + except: + return None + + @loss_tangent.setter + def loss_tangent(self, value): + """Set material loss tangent.""" + warnings.warn( + "This method is deprecated in versions >0.7.0 and will soon be removed. " + "Use property dielectric_loss_tangent instead.", + DeprecationWarning, + ) + self.dielectric_loss_tangent(value) + + @dielectric_loss_tangent.setter + def dielectric_loss_tangent(self, value): + """Set material loss tangent.""" + self.set_property(GrpcMaterialProperty.DIELECTRIC_LOSS_TANGENT, GrpcValue(value)) + + @property + def magnetic_loss_tangent(self): + """Get material magnetic loss tangent.""" + try: + value = self.get_property(GrpcMaterialProperty.MAGNETIC_LOSS_TANGENT).value + return value + except: + return None + + @magnetic_loss_tangent.setter + def magnetic_loss_tangent(self, value): + """Set material magnetic loss tangent.""" + self.set_property(GrpcMaterialProperty.MAGNETIC_LOSS_TANGENT, GrpcValue(value)) + + @property + def thermal_conductivity(self): + """Get material thermal conductivity.""" + try: + value = self.get_property(GrpcMaterialProperty.THERMAL_CONDUCTIVITY).value + return value + except: + return None + + @thermal_conductivity.setter + def thermal_conductivity(self, value): + """Set material thermal conductivity.""" + self.set_property(GrpcMaterialProperty.THERMAL_CONDUCTIVITY, GrpcValue(value)) + + @property + def mass_density(self): + """Get material mass density.""" + try: + value = self.get_property(GrpcMaterialProperty.MASS_DENSITY).value + return value + except: + return None + + @mass_density.setter + def mass_density(self, value): + """Set material mass density.""" + self.set_property(GrpcMaterialProperty.MASS_DENSITY, GrpcValue(value)) + + @property + def youngs_modulus(self): + """Get material youngs modulus.""" + try: + value = self.get_property(GrpcMaterialProperty.YOUNGS_MODULUS).value + return value + except: + return None + + @youngs_modulus.setter + def youngs_modulus(self, value): + """Set material youngs modulus.""" + self.set_property(GrpcMaterialProperty.YOUNGS_MODULUS, GrpcValue(value)) + + @property + def specific_heat(self): + """Get material specific heat.""" + try: + return self.get_property(GrpcMaterialProperty.SPECIFIC_HEAT).value + except: + return None + + @specific_heat.setter + def specific_heat(self, value): + """Set material specific heat.""" + self.set_property(GrpcMaterialProperty.SPECIFIC_HEAT, GrpcValue(value)) + + @property + def poisson_ratio(self): + """Get material poisson ratio.""" + try: + return self.get_property(GrpcMaterialProperty.POISSONS_RATIO).value + except: + return None + + @poisson_ratio.setter + def poisson_ratio(self, value): + """Set material poisson ratio.""" + self.set_property(GrpcMaterialProperty.POISSONS_RATIO, GrpcValue(value)) + + @property + def thermal_expansion_coefficient(self): + """Get material thermal coefficient.""" + try: + return self.get_property(GrpcMaterialProperty.THERMAL_EXPANSION_COEFFICIENT).value + except: + return None + + @thermal_expansion_coefficient.setter + def thermal_expansion_coefficient(self, value): + """Set material thermal coefficient.""" + self.set_property(GrpcMaterialProperty.THERMAL_EXPANSION_COEFFICIENT, GrpcValue(value)) + + def set_debye_model(self): + super(Material, self.__class__).dielectric_material_model.__set__(self, GrpcDebyeModel.create()) + + def set_multipole_debye_model(self): + super(Material, self.__class__).dielectric_material_model.__set__(self, GrpcMultipoleDebyeModel.create()) + + def set_djordjecvic_sarkar_model(self): + super(Material, self.__class__).dielectric_material_model.__set__(self, GrpcDjordjecvicSarkarModel.create()) + + def to_dict(self): + """Convert material into dictionary.""" + properties = self.__load_all_properties() + + res = {"name": self.name} + res.update(properties.model_dump()) + return res + + def update(self, input_dict: dict): + if input_dict: + # Update attributes + for attribute in ATTRIBUTES: + if attribute in input_dict: + setattr(self, attribute, input_dict[attribute]) + if "loss_tangent" in input_dict: # pragma: no cover + setattr(self, "loss_tangent", input_dict["loss_tangent"]) + + # Update DS model + # NOTE: Contrary to before we don't test 'dielectric_model_frequency' only + if any(map(lambda attribute: input_dict.get(attribute, None) is not None, DC_ATTRIBUTES)): + if not self.__dielectric_model: + self.__dielectric_model = GrpcDjordjecvicSarkarModel.create() + for attribute in DC_ATTRIBUTES: + if attribute in input_dict: + if attribute == "use_dc_relative_conductivity" and input_dict[attribute] is not None: + self.__dielectric_model.use_dc_relative_conductivity = True + setattr(self, attribute, input_dict[attribute]) + self.__material_def.dielectric_material_model = ( + self.__dielectric_model + ) # Check material is properly assigned + # Unset DS model if it is already assigned to the material in the database + elif self.__dielectric_model: + self.__material_def.dielectric_material_model = None + + def __load_all_properties(self): + """Load all properties of the material.""" + res = MaterialProperties() + for property in res.model_dump().keys(): + value = getattr(self, property) + setattr(res, property, value) + return res + + +class Materials(object): + """Manages EDB methods for material management accessible from `Edb.materials` property.""" + + def __init__(self, edb: Edb): + self.__edb = edb + self.__syslib = os.path.join(self.__edb.base_path, "syslib") + + def __contains__(self, item): + if isinstance(item, Material): + return item.name in self.materials + else: + return item in self.materials + + def __getitem__(self, item): + return self.materials[item] + + @property + def syslib(self): + """Get the project sys library.""" + return self.__syslib + + @property + def materials(self): + """Get materials.""" + materials = { + material_def.name: Material(self.__edb, material_def) for material_def in self.__edb.active_db.material_defs + } + return materials + + def add_material(self, name: str, **kwargs): + """Add a new material. + + Parameters + ---------- + name : str + Material name. + + Returns + ------- + :class:`pyedb.grpc.edb_core.materials.Material` + """ + curr_materials = self.materials + if name in curr_materials: + raise ValueError(f"Material {name} already exists in material library.") + elif name.lower() in (material.lower() for material in curr_materials): + m = {material.lower(): material for material in curr_materials}[name.lower()] + raise ValueError(f"Material names are case-insensitive and '{name}' already exists as '{m}'.") + + material_def = GrpcMaterialDef.create(self.__edb.active_db, name) + material = Material(self.__edb, material_def) + # Apply default values to the material + if "permeability" not in kwargs: + kwargs["permeability"] = PERMEABILITY_DEFAULT_VALUE + attributes_input_dict = {key: val for (key, val) in kwargs.items() if key in ATTRIBUTES + DC_ATTRIBUTES} + if "loss_tangent" in kwargs: # pragma: no cover + warnings.warn( + "This key is deprecated in versions >0.7.0 and will soon be removed. " + "Use key dielectric_loss_tangent instead.", + DeprecationWarning, + ) + attributes_input_dict["dielectric_loss_tangent"] = kwargs["loss_tangent"] + if attributes_input_dict: + material.update(attributes_input_dict) + + return material + + def add_conductor_material(self, name, conductivity, **kwargs): + """Add a new conductor material. + + Parameters + ---------- + name : str + Name of the new material. + conductivity : str, float, int + Conductivity of the new material. + + Returns + ------- + :class:`pyedb.grpc.edb_core.materials.Material` + + """ + extended_kwargs = {key: value for (key, value) in kwargs.items()} + extended_kwargs["conductivity"] = conductivity + material = self.add_material(name, **extended_kwargs) + + return material + + def add_dielectric_material(self, name, permittivity, dielectric_loss_tangent, **kwargs): + """Add a new dielectric material in library. + + Parameters + ---------- + name : str + Name of the new material. + permittivity : str, float, int + Permittivity of the new material. + dielectric_loss_tangent : str, float, int + Dielectric loss tangent of the new material. + + Returns + ------- + :class:`pyedb.dotnet.database.materials.Material` + """ + extended_kwargs = {key: value for (key, value) in kwargs.items()} + extended_kwargs["permittivity"] = permittivity + extended_kwargs["dielectric_loss_tangent"] = dielectric_loss_tangent + material = self.add_material(name, **extended_kwargs) + + return material + + def add_djordjevicsarkar_dielectric( + self, + name, + permittivity_at_frequency, + loss_tangent_at_frequency, + dielectric_model_frequency, + dc_conductivity=None, + dc_permittivity=None, + **kwargs, + ): + """Add a dielectric using the Djordjevic-Sarkar model. + + Parameters + ---------- + name : str + Name of the dielectric. + permittivity_at_frequency : str, float, int + Relative permittivity of the dielectric. + loss_tangent_at_frequency : str, float, int + Loss tangent for the material. + dielectric_model_frequency : str, float, int + Test frequency in GHz for the dielectric. + + Returns + ------- + :class:`pyedb.dotnet.database.materials.Material` + """ + curr_materials = self.materials + if name in curr_materials: + raise ValueError(f"Material {name} already exists in material library.") + elif name.lower() in (material.lower() for material in curr_materials): + raise ValueError(f"Material names are case-insensitive and {name.lower()} already exists.") + + material_model = GrpcDjordjecvicSarkarModel.create() + material_model.relative_permitivity_at_frequency = permittivity_at_frequency + material_model.loss_tangent_at_frequency = loss_tangent_at_frequency + material_model.frequency = dielectric_model_frequency + if dc_conductivity is not None: + material_model.dc_conductivity = dc_conductivity + material_model.use_dc_relative_conductivity = True + if dc_permittivity is not None: + material_model.dc_relative_permitivity = dc_permittivity + try: + material = self.__add_dielectric_material_model(name, material_model) + for key, value in kwargs.items(): + setattr(material, key, value) + if "loss_tangent" in kwargs: # pragma: no cover + warnings.warn( + "This key is deprecated in versions >0.7.0 and will soon be removed. " + "Use key dielectric_loss_tangent instead.", + DeprecationWarning, + ) + setattr(material, "dielectric_loss_tangent", kwargs["loss_tangent"]) + return material + except MaterialModelException: + raise ValueError("Use realistic values to define DS model.") + + def add_debye_material( + self, + name, + permittivity_low, + permittivity_high, + loss_tangent_low, + loss_tangent_high, + lower_freqency, + higher_frequency, + **kwargs, + ): + """Add a dielectric with the Debye model. + + Parameters + ---------- + name : str + Name of the dielectric. + permittivity_low : float, int + Relative permittivity of the dielectric at the frequency specified + for ``lower_frequency``. + permittivity_high : float, int + Relative permittivity of the dielectric at the frequency specified + for ``higher_frequency``. + loss_tangent_low : float, int + Loss tangent for the material at the frequency specified + for ``lower_frequency``. + loss_tangent_high : float, int + Loss tangent for the material at the frequency specified + for ``higher_frequency``. + lower_freqency : str, float, int + Value for the lower frequency. + higher_frequency : str, float, int + Value for the higher frequency. + + Returns + ------- + :class:`pyedb.dotnet.database.materials.Material` + """ + curr_materials = self.materials + if name in curr_materials: + raise ValueError(f"Material {name} already exists in material library.") + elif name.lower() in (material.lower() for material in curr_materials): + raise ValueError(f"Material names are case-insensitive and {name.lower()} already exists.") + material_model = GrpcDebyeModel.create() + material_model.frequency_range = (lower_freqency, higher_frequency) + material_model.loss_tangent_at_high_low_frequency = (loss_tangent_low, loss_tangent_high) + material_model.relative_permitivity_at_high_low_frequency = (permittivity_low, permittivity_high) + try: + material = self.__add_dielectric_material_model(name, material_model) + for key, value in kwargs.items(): + setattr(material, key, value) + if "loss_tangent" in kwargs: # pragma: no cover + warnings.warn( + "This key is deprecated in versions >0.7.0 and will soon be removed. " + "Use key dielectric_loss_tangent instead.", + DeprecationWarning, + ) + setattr(material, "dielectric_loss_tangent", kwargs["loss_tangent"]) + return material + except MaterialModelException: + raise ValueError("Use realistic values to define Debye model.") + + def add_multipole_debye_material( + self, + name, + frequencies, + permittivities, + loss_tangents, + **kwargs, + ): + """Add a dielectric with the Multipole Debye model. + + Parameters + ---------- + name : str + Name of the dielectric. + frequencies : list + Frequencies in GHz. + permittivities : list + Relative permittivities at each frequency. + loss_tangents : list + Loss tangents at each frequency. + + Returns + ------- + :class:`pyedb.dotnet.database.materials.Material` + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb() + >>> freq = [0, 2, 3, 4, 5, 6] + >>> rel_perm = [1e9, 1.1e9, 1.2e9, 1.3e9, 1.5e9, 1.6e9] + >>> loss_tan = [0.025, 0.026, 0.027, 0.028, 0.029, 0.030] + >>> diel = edb.materials.add_multipole_debye_material("My_MP_Debye", freq, rel_perm, loss_tan) + """ + curr_materials = self.materials + if name in curr_materials: + raise ValueError(f"Material {name} already exists in material library.") + elif name.lower() in (material.lower() for material in curr_materials): + raise ValueError(f"Material names are case-insensitive and {name.lower()} already exists.") + + frequencies = [float(i) for i in frequencies] + permittivities = [float(i) for i in permittivities] + loss_tangents = [float(i) for i in loss_tangents] + material_model = GrpcMultipoleDebyeModel.create() + material_model.set_parameters(frequencies, permittivities, loss_tangents) + try: + material = self.__add_dielectric_material_model(name, material_model) + for key, value in kwargs.items(): + setattr(material, key, value) + if "loss_tangent" in kwargs: # pragma: no cover + warnings.warn( + "This key is deprecated in versions >0.7.0 and will soon be removed. " + "Use key dielectric_loss_tangent instead.", + DeprecationWarning, + ) + setattr(material, "dielectric_loss_tangent", kwargs["loss_tangent"]) + return material + except MaterialModelException: + raise ValueError("Use realistic values to define Multipole Debye model.") + + def __add_dielectric_material_model(self, name, material_model): + """Add a dielectric material model. + + Parameters + ---------- + name : str + Name of the dielectric. + material_model : Any + Dielectric material model. + """ + if GrpcMaterialDef.find_by_name(self.__edb.active_db, name).is_null: + if name.lower() in (material.lower() for material in self.materials): + raise ValueError(f"Material names are case-insensitive and {name.lower()} already exists.") + GrpcMaterialDef.create(self.__edb.active_db, name) + + material_def = GrpcMaterialDef.find_by_name(self.__edb.active_db, name) + material_def.dielectric_material_model = material_model + material = Material(self.__edb, material_def) + return material + + def duplicate(self, material_name, new_material_name): + """Duplicate a material from the database. + + Parameters + ---------- + material_name : str + Name of the existing material. + new_material_name : str + Name of the new duplicated material. + + Returns + ------- + :class:`pyedb.dotnet.database.materials.Material` + """ + curr_materials = self.materials + if new_material_name in curr_materials: + raise ValueError(f"Material {new_material_name} already exists in material library.") + elif new_material_name.lower() in (material.lower() for material in curr_materials): + raise ValueError(f"Material names are case-insensitive and {new_material_name.lower()} already exists.") + + material = self.materials[material_name] + material_def = GrpcMaterialDef.create(self.__edb.active_db, new_material_name) + material_dict = material.to_dict() + new_material = Material(self.__edb, material_def) + new_material.update(material_dict) + return new_material + + def delete_material(self, material_name): + """ + + .deprecated: pyedb 0.32.0 use `delete` instead. + + Parameters + ---------- + material_name : str + Name of the material to delete. + + """ + warnings.warn( + "`delete_material` is deprecated use `delete` instead.", + DeprecationWarning, + ) + self.delete(material_name) + + def delete(self, material_name): + """Remove a material from the database.""" + material_def = GrpcMaterialDef.find_by_name(self.__edb.active_db, material_name) + if material_def.is_null: + raise ValueError(f"Cannot find material {material_name}.") + material_def.delete() + + def update_material(self, material_name, input_dict): + """Update material attributes.""" + if material_name not in self.materials: + raise ValueError(f"Material {material_name} does not exist in material library.") + + material = self[material_name] + attributes_input_dict = {key: val for (key, val) in input_dict.items() if key in ATTRIBUTES + DC_ATTRIBUTES} + if "loss_tangent" in input_dict: # pragma: no cover + warnings.warn( + "This key is deprecated in versions >0.7.0 and will soon be removed. " + "Use key dielectric_loss_tangent instead.", + DeprecationWarning, + ) + attributes_input_dict["dielectric_loss_tangent"] = input_dict["loss_tangent"] + if attributes_input_dict: + material.update(attributes_input_dict) + return material + + def load_material(self, material: dict): + """Load material.""" + if material: + material_name = material["name"] + material_conductivity = material.get("conductivity", None) + if material_conductivity and material_conductivity > 1e4: + self.add_conductor_material(material_name, material_conductivity) + else: + material_permittivity = material["permittivity"] + if "loss_tangent" in material: # pragma: no cover + warnings.warn( + "This key is deprecated in versions >0.7.0 and will soon be removed. " + "Use key dielectric_loss_tangent instead.", + DeprecationWarning, + ) + material_dlt = material["loss_tangent"] + else: + material_dlt = material["dielectric_loss_tangent"] + self.add_dielectric_material(material_name, material_permittivity, material_dlt) + + def material_property_to_id(self, property_name): + """Convert a material property name to a material property ID. + + Parameters + ---------- + property_name : str + Name of the material property. + + Returns + ------- + Any + """ + # material_property_id = GrpcMaterialProperty.CONDUCTIVITY self.__edb_definition.MaterialPropertyId + property_name_to_id = { + "Permittivity": GrpcMaterialProperty.PERMITTIVITY, + "Permeability": GrpcMaterialProperty.PERMEABILITY, + "Conductivity": GrpcMaterialProperty.CONDUCTIVITY, + "DielectricLossTangent": GrpcMaterialProperty.DIELECTRIC_LOSS_TANGENT, + "MagneticLossTangent": GrpcMaterialProperty.MAGNETIC_LOSS_TANGENT, + "ThermalConductivity": GrpcMaterialProperty.THERMAL_CONDUCTIVITY, + "MassDensity": GrpcMaterialProperty.MASS_DENSITY, + "SpecificHeat": GrpcMaterialProperty.SPECIFIC_HEAT, + "YoungsModulus": GrpcMaterialProperty.YOUNGS_MODULUS, + "PoissonsRatio": GrpcMaterialProperty.POISSONS_RATIO, + "ThermalExpansionCoefficient": GrpcMaterialProperty.THERMAL_EXPANSION_COEFFICIENT, + "InvalidProperty": GrpcMaterialProperty.INVALID_PROPERTY, + } + + if property_name == "loss_tangent": + warnings.warn( + "This key is deprecated in versions >0.7.0 and will soon be removed. " + "Use key dielectric_loss_tangent instead.", + DeprecationWarning, + ) + property_name = "dielectric_loss_tangent" + match = difflib.get_close_matches(property_name, property_name_to_id, 1, 0.7) + if match: + return property_name_to_id[match[0]] + else: + return property_name_to_id["InvalidProperty"] + + def load_amat(self, amat_file): + """Load materials from an AMAT file. + + Parameters + ---------- + amat_file : str + Full path to the AMAT file to read and add to the Edb. + + Returns + ------- + bool + """ + if not os.path.exists(amat_file): + raise FileNotFoundError(f"File path {amat_file} does not exist.") + materials_dict = self.read_materials(amat_file) + for material_name, material_properties in materials_dict.items(): + if not material_name in self: + if "tangent_delta" in material_properties: + material_properties["dielectric_loss_tangent"] = material_properties["tangent_delta"] + del material_properties["tangent_delta"] + elif "loss_tangent" in material_properties: # pragma: no cover + warnings.warn( + "This key is deprecated in versions >0.7.0 and will soon be removed. " + "Use key dielectric_loss_tangent instead.", + DeprecationWarning, + ) + material_properties["dielectric_loss_tangent"] = material_properties["loss_tangent"] + del material_properties["loss_tangent"] + self.add_material(material_name, **material_properties) + else: + self.__edb.logger.warning(f"Material {material_name} already exist and was not loaded from AMAT file.") + return True + + def iterate_materials_in_amat(self, amat_file=None): + """Iterate over material description in an AMAT file. + + Parameters + ---------- + amat_file : str + Full path to the AMAT file to read. + + Yields + ------ + dict + """ + if amat_file is None: + amat_file = os.path.join(self.__edb.base_path, "syslib", "Materials.amat") + + begin_regex = re.compile(r"^\$begin '(.+)'") + end_regex = re.compile(r"^\$end '(.+)'") + material_properties = ATTRIBUTES.copy() + # Remove cases manually handled + material_properties.remove("conductivity") + + with open(amat_file, "r") as amat_fh: + in_material_def = False + material_description = {} + for line in amat_fh: + if in_material_def: + # Yield material definition + if end_regex.search(line): + in_material_def = False + yield material_description + material_description = {} + # Extend material definition if possible + else: + for material_property in material_properties: + if material_property in line: + value = get_line_float_value(line) + if value is not None: + material_description[material_property] = value + break + # Extra case to cover bug in syslib AMAT file (see #364) + if "thermal_expansion_coeffcient" in line: + value = get_line_float_value(line) + if value is not None: + material_description["thermal_expansion_coefficient"] = value + # Extra case to avoid confusion ("conductivity" is included in "thermal_conductivity") + if "conductivity" in line and "thermal_conductivity" not in line: + value = get_line_float_value(line) + if value is not None: + material_description["conductivity"] = value + # Extra case to avoid confusion ("conductivity" is included in "thermal_conductivity") + if ( + "loss_tangent" in line + and "dielectric_loss_tangent" not in line + and "magnetic_loss_tangent" not in line + ): + warnings.warn( + "This key is deprecated in versions >0.7.0 and will soon be removed. " + "Use key dielectric_loss_tangent instead.", + DeprecationWarning, + ) + value = get_line_float_value(line) + if value is not None: + material_description["dielectric_loss_tangent"] = value + # Check if we reach the beginning of a material description + else: + match = begin_regex.search(line) + if match: + material_name = match.group(1) + # Skip unwanted data + if material_name in ("$index$", "$base_index$"): + continue + material_description["name"] = match.group(1) + in_material_def = True + + def read_materials(self, amat_file): + """Read materials from an AMAT file. + + Parameters + ---------- + amat_file : str + Full path to the AMAT file to read. + + Returns + ------- + dict + {material name: dict of material properties}. + """ + res = {} + for material in self.iterate_materials_in_amat(amat_file): + material_name = material["name"] + res[material_name] = {} + for material_property, value in material.items(): + if material_property != "name": + res[material_name][material_property] = value + + return res + + def read_syslib_material(self, material_name): + """Read a specific material from syslib AMAT file. + + Parameters + ---------- + material_name : str + Name of the material. + + Returns + ------- + dict + {material name: dict of material properties}. + """ + res = {} + amat_file = os.path.join(self.__edb.base_path, "syslib", "Materials.amat") + for material in self.iterate_materials_in_amat(amat_file): + iter_material_name = material["name"] + if iter_material_name == material_name or iter_material_name.lower() == material_name.lower(): + for material_property, value in material.items(): + if material_property != "name": + res[material_property] = value + return res + + self.__edb.logger.error(f"Material {material_name} does not exist in syslib AMAT file.") + return res diff --git a/src/pyedb/grpc/database/definition/n_port_component_model.py b/src/pyedb/grpc/database/definition/n_port_component_model.py new file mode 100644 index 0000000000..154a66f369 --- /dev/null +++ b/src/pyedb/grpc/database/definition/n_port_component_model.py @@ -0,0 +1,32 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.definition.component_model import ( + NPortComponentModel as GrpcNPortComponentModel, +) + + +class NPortComponentModel(GrpcNPortComponentModel): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb diff --git a/src/pyedb/grpc/database/definition/package_def.py b/src/pyedb/grpc/database/definition/package_def.py new file mode 100644 index 0000000000..5e88cd8f44 --- /dev/null +++ b/src/pyedb/grpc/database/definition/package_def.py @@ -0,0 +1,183 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.definition.package_def import PackageDef as GrpcPackageDef +from ansys.edb.core.geometry.polygon_data import PolygonData as GrpcPolygonData +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.edb_logger import pyedb_logger +from pyedb.grpc.database.utility.heat_sink import HeatSink + + +class PackageDef(GrpcPackageDef): + """Manages EDB functionalities for package definitions. + + Parameters + ---------- + pedb : :class:`pyedb.edb` + Edb object. + edb_object : object + Edb PackageDef Object + component_part_name : str, optional + Part name of the component. + extent_bounding_box : list, optional + Bounding box defines the shape of the package. For example, [[0, 0], ["2mm", "2mm"]]. + + """ + + def __init__(self, pedb, edb_object=None, name=None, component_part_name=None, extent_bounding_box=None): + super(GrpcPackageDef, self).__init__(edb_object.msg) + self._pedb = pedb + self._edb_object = edb_object + self._heat_sink = None + if self._edb_object is None and name is not None: + self._edb_object = self.__create_from_name(name, component_part_name, extent_bounding_box) + + def __create_from_name(self, name, component_part_name=None, extent_bounding_box=None): + """Create a package definition. + + Parameters + ---------- + name: str + Name of the package definition. + + Returns + ------- + edb_object: object + EDB PackageDef Object + """ + edb_object = GrpcPackageDef.create(self._pedb.active_db, name) + if component_part_name: + x_pt1, y_pt1, x_pt2, y_pt2 = list( + self._pedb.components.definitions[component_part_name].components.values() + )[0].bounding_box + x_mid = (x_pt1 + x_pt2) / 2 + y_mid = (y_pt1 + y_pt2) / 2 + bbox = [[y_pt1 - y_mid, x_pt1 - x_mid], [y_pt2 - y_mid, x_pt2 - x_mid]] + else: + bbox = extent_bounding_box + if bbox is None: + pyedb_logger.warning( + "Package creation uses bounding box but it cannot be inferred. " + "Please set argument 'component_part_name' or 'extent_bounding_box'." + ) + polygon_data = GrpcPolygonData(points=bbox) + + self.exterior_boundary = polygon_data + return edb_object + + @property + def exterior_boundary(self): + """Get the exterior boundary of a package definition.""" + return GrpcPolygonData(super().exterior_boundary.points) + + @exterior_boundary.setter + def exterior_boundary(self, value): + super(PackageDef, self.__class__).exterior_boundary.__set__(self, value) + + @property + def maximum_power(self): + """Maximum power of the package.""" + return super().maximum_power.value + + @maximum_power.setter + def maximum_power(self, value): + super(PackageDef, self.__class__).maximum_power.__set__(self, GrpcValue(value)) + + @property + def therm_cond(self): + """Thermal conductivity of the package.""" + return super().thermal_conductivity.value + + @therm_cond.setter + def therm_cond(self, value): + self.therm_cond = GrpcValue(value) + super(PackageDef, self.__class__).thermal_conductivity.__set__(self, GrpcValue(value)) + + @property + def theta_jb(self): + """Theta Junction-to-Board of the package.""" + return super().theta_jb.value + + @theta_jb.setter + def theta_jb(self, value): + super(PackageDef, self.__class__).theta_jb.__set__(self, GrpcValue(value)) + + @property + def theta_jc(self): + """Theta Junction-to-Case of the package.""" + return super().theta_jc.value + + @theta_jc.setter + def theta_jc(self, value): + super(PackageDef, self.__class__).theta_jc.__set__(self, GrpcValue(value)) + + @property + def height(self): + """Height of the package.""" + return super().height.value + + @height.setter + def height(self, value): + super(PackageDef, self.__class__).height.__set__(self, GrpcValue(value)) + + @property + def heat_sink(self): + return HeatSink(self._pedb, super().heat_sink) + + def set_heatsink(self, fin_base_height, fin_height, fin_orientation, fin_spacing, fin_thickness): + """Set Heat sink. + Parameters + ---------- + fin_base_height : str, float + Fin base height. + fin_height : str, float + Fin height. + fin_orientation : str + Fin orientation. Supported values, `x_oriented`, `y_oriented`. + fin_spacing : str, float + Fin spacing. + fin_thickness : str, float + Fin thickness. + """ + from ansys.edb.core.utility.heat_sink import ( + HeatSinkFinOrientation as GrpcHeatSinkFinOrientation, + ) + from ansys.edb.core.utility.heat_sink import HeatSink as GrpcHeatSink + + if fin_orientation == "x_oriented": + fin_orientation = GrpcHeatSinkFinOrientation.X_ORIENTED + elif fin_orientation == "y_oriented": + fin_orientation = GrpcHeatSinkFinOrientation.Y_ORIENTED + else: + fin_orientation = GrpcHeatSinkFinOrientation.OTHER_ORIENTED + super(PackageDef, self.__class__).heat_sink.__set__( + self, + GrpcHeatSink( + GrpcValue(fin_thickness), + GrpcValue(fin_spacing), + GrpcValue(fin_base_height), + GrpcValue(fin_height), + fin_orientation, + ), + ) + return self.heat_sink diff --git a/src/pyedb/grpc/database/definition/padstack_def.py b/src/pyedb/grpc/database/definition/padstack_def.py new file mode 100644 index 0000000000..17546a5167 --- /dev/null +++ b/src/pyedb/grpc/database/definition/padstack_def.py @@ -0,0 +1,790 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import math + +from ansys.edb.core.definition.padstack_def import PadstackDef as GrpcPadstackDef +from ansys.edb.core.definition.padstack_def_data import ( + PadGeometryType as GrpcPadGeometryType, +) +from ansys.edb.core.definition.padstack_def_data import ( + PadstackHoleRange as GrpcPadstackHoleRange, +) +from ansys.edb.core.definition.padstack_def_data import PadType as GrpcPadType +import ansys.edb.core.geometry.polygon_data +from ansys.edb.core.geometry.polygon_data import PolygonData as GrpcPolygonData +from ansys.edb.core.hierarchy.structure3d import MeshClosure as GrpcMeshClosure +from ansys.edb.core.hierarchy.structure3d import Structure3D as GrpcStructure3D +from ansys.edb.core.primitive.primitive import Circle as GrpcCircle +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.generic.general_methods import generate_unique_name + + +class EDBPadProperties: + """Manages EDB functionalities for pad properties. + + Parameters + ---------- + edb_padstack : + + layer_name : str + Name of the layer. + pad_type : + Type of the pad. + pedbpadstack : str + Inherited AEDT object. + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb(myedb, edbversion="2021.2") + >>> edb_pad_properties = edb.padstacks.definitions["MyPad"].pad_by_layer["TOP"] + """ + + def __init__(self, edb_padstack, layer_name, pad_type, p_edb_padstack): + self._edb_object = edb_padstack + self._pedbpadstack = p_edb_padstack + self.layer_name = layer_name + self.pad_type = pad_type + self._edb_padstack = self._edb_object + + @property + def _padstack_methods(self): + return self._pedbpadstack._padstack_methods + + @property + def _stackup_layers(self): + return self._pedbpadstack._stackup_layers + + @property + def _edb(self): + return self._pedbpadstack._edb + + @property + def _pad_parameter_value(self): + p_val = self._edb_padstack.get_pad_parameters(self.layer_name, GrpcPadType.REGULAR_PAD) + if isinstance(p_val[0], ansys.edb.core.geometry.polygon_data.PolygonData): + p_val = [GrpcPadGeometryType.PADGEOMTYPE_POLYGON] + [i for i in p_val] + return p_val + + @property + def geometry_type(self): + """Geometry type. + + Returns + ------- + int + Type of the geometry. + """ + return self._pad_parameter_value[0].value + + @property + def _edb_geometry_type(self): + return self._pad_parameter_value[0] + + @property + def shape(self): + """Get the shape of the pad.""" + return self._pad_parameter_value[0].name.split("_")[-1].lower() + + @property + def parameters_values(self): + """Parameters. + + Returns + ------- + list + List of parameters. + """ + try: + return [i.value for i in self._pad_parameter_value[1]] + except TypeError: + return [] + + @property + def parameters_values_string(self): + """Parameters value in string format.""" + try: + return [str(i) for i in self._pad_parameter_value[1]] + except TypeError: + return [] + + @property + def polygon_data(self): + """Parameters. + + Returns + ------- + list + List of parameters. + """ + p = self._pad_parameter_value[1] + return p if isinstance(p, ansys.edb.core.geometry.polygon_data.PolygonData) else None + + @property + def offset_x(self): + """Offset for the X axis. + + Returns + ------- + str + Offset for the X axis. + """ + return self._pad_parameter_value[2].value + + @property + def offset_y(self): + """Offset for the Y axis. + + Returns + ------- + str + Offset for the Y axis. + """ + + return self._pad_parameter_value[3].value + + @offset_x.setter + def offset_x(self, value): + self._update_pad_parameters_parameters(offsetx=value) + + @offset_y.setter + def offset_y(self, value): + self._update_pad_parameters_parameters(offsety=value) + + @property + def rotation(self): + """Rotation. + + Returns + ------- + str + Value for the rotation. + """ + + return self._pad_parameter_value[4].value + + @rotation.setter + def rotation(self, value): + self._update_pad_parameters_parameters(rotation=value) + + @rotation.setter + def rotation(self, value): + self._update_pad_parameters_parameters(rotation=value) + + @parameters_values.setter + def parameters_values(self, value): + if isinstance(value, (float, str)): + value = [value] + self._update_pad_parameters_parameters(params=value) + + def _update_pad_parameters_parameters( + self, + layer_name=None, + pad_type=None, + geom_type=None, + params=None, + offsetx=None, + offsety=None, + rotation=None, + ): + if layer_name is None: + layer_name = self.layer_name + if pad_type is None: + pad_type = GrpcPadType.REGULAR_PAD + if geom_type is None: + geom_type = self.geometry_type + for k in GrpcPadGeometryType: + if k.value == geom_type: + geom_type = k + if params is None: + params = self._pad_parameter_value[1] + elif isinstance(params, list): + offsetx = [GrpcValue(i, self._pedbpadstack._pedb.db) for i in params] + if rotation is None: + rotation = self._pad_parameter_value[4] + elif isinstance(rotation, (str, float, int)): + rotation = GrpcValue(rotation, self._pedbpadstack._pedb.db) + if offsetx is None: + offsetx = self._pad_parameter_value[2] + elif isinstance(offsetx, (str, float, int)): + offsetx = GrpcValue(offsetx, self._pedbpadstack._pedb.db) + if offsety is None: + offsety = self._pad_parameter_value[3] + elif isinstance(offsety, (str, float, int)): + offsety = GrpcValue(offsety, self._pedbpadstack._pedb.db) + self._edb_padstack.set_pad_parameters( + layer=layer_name, + pad_type=pad_type, + type_geom=geom_type, + offset_x=GrpcValue(offsetx, self._pedbpadstack._pedb.db), + offset_y=GrpcValue(offsety, self._pedbpadstack._pedb.db), + rotation=GrpcValue(rotation, self._pedbpadstack._pedb.db), + sizes=[GrpcValue(i, self._pedbpadstack._pedb.db) for i in params], + ) + + +class PadstackDef(GrpcPadstackDef): + """Manages EDB functionalities for a padstack. + + Parameters + ---------- + edb_padstack : + + ppadstack : str + Inherited AEDT object. + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb(myedb, edbversion="2021.2") + >>> edb_padstack = edb.padstacks.definitions["MyPad"] + """ + + def __init__(self, pedb, edb_object): + super().__init__(edb_object.msg) + self._pedb = pedb + self._pad_by_layer = {} + self._antipad_by_layer = {} + self._thermalpad_by_layer = {} + self._bounding_box = [] + + @property + def instances(self): + """Definitions Instances.""" + return [i for i in list(self._pedb.padstacks.instances.values()) if i.padstack_def.name == self.name] + + @property + def layers(self): + """Layers. + + Returns + ------- + list + List of layers. + """ + return self.data.layer_names + + @property + def start_layer(self): + """Starting layer. + + Returns + ------- + str + Name of the starting layer. + """ + return self.layers[0] + + @property + def stop_layer(self): + """Stopping layer. + + Returns + ------- + str + Name of the stopping layer. + """ + return self.layers[-1] + + @property + def hole_diameter(self): + """Hole diameter.""" + try: + hole_parameter = self.data.get_hole_parameters() + if hole_parameter[0].name.lower() == "padgeomtype_circle": + return round(hole_parameter[1][0].value, 6) + except: + return 0.0 + + @hole_diameter.setter + def hole_diameter(self, value): + hole_parameter = self.data.get_hole_parameters() + if not isinstance(value, list): + value = [GrpcValue(value)] + else: + value = [GrpcValue(p) for p in value] + hole_size = value + geometry_type = hole_parameter[0] + hole_offset_x = hole_parameter[2] + hole_offset_y = hole_parameter[3] + if not isinstance(geometry_type, GrpcPolygonData): + hole_rotation = hole_parameter[4] + self.data.set_hole_parameters( + offset_x=hole_offset_x, + offset_y=hole_offset_y, + rotation=hole_rotation, + type_geom=geometry_type, + sizes=hole_size, + ) + + @property + def hole_type(self): + return self.data.get_hole_parameters()[0].value + + @property + def edb_hole_type(self): + return self.data.get_hole_parameters()[0] + + @property + def hole_offset_x(self): + """Hole offset for the X axis. + + Returns + ------- + str + Hole offset value for the X axis. + """ + try: + return round(self.data.get_hole_parameters()[2].value, 6) + except: + return 0.0 + + @hole_offset_x.setter + def hole_offset_x(self, value): + hole_parameter = list(self.data.get_hole_parameters()) + hole_parameter[2] = GrpcValue(value, self._pedb.db) + self.data.set_hole_parameters( + offset_x=hole_parameter[2], + offset_y=hole_parameter[3], + rotation=hole_parameter[4], + type_geom=hole_parameter[0], + sizes=hole_parameter[1], + ) + + @property + def hole_offset_y(self): + """Hole offset for the Y axis. + + Returns + ------- + str + Hole offset value for the Y axis. + """ + try: + return round(self.data.get_hole_parameters()[3].value, 6) + except: + return 0.0 + + @hole_offset_y.setter + def hole_offset_y(self, value): + hole_parameter = list(self.data.get_hole_parameters()) + hole_parameter[3] = GrpcValue(value, self._pedb.db) + self.data.set_hole_parameters( + offset_x=hole_parameter[2], + offset_y=hole_parameter[3], + rotation=hole_parameter[4], + type_geom=hole_parameter[0], + sizes=hole_parameter[1], + ) + + @property + def hole_rotation(self): + """Hole rotation. + + Returns + ------- + str + Value for the hole rotation. + """ + try: + return round(self.data.get_hole_parameters()[4].value, 6) + except: + return 0.0 + + @hole_rotation.setter + def hole_rotation(self, value): + hole_parameter = list(self.data.get_hole_parameters()) + hole_parameter[4] = GrpcValue(value, self._pedb.db) + self.data.set_hole_parameters( + offset_x=hole_parameter[2], + offset_y=hole_parameter[3], + rotation=hole_parameter[4], + type_geom=hole_parameter[0], + sizes=hole_parameter[1], + ) + + @property + def pad_by_layer(self): + if not self._pad_by_layer: + for layer in self.layers: + try: + self._pad_by_layer[layer] = EDBPadProperties(self.data, layer, GrpcPadType.REGULAR_PAD, self) + except: + self._pad_by_layer[layer] = None + return self._pad_by_layer + + @property + def antipad_by_layer(self): + if not self._antipad_by_layer: + for layer in self.layers: + try: + self._pad_by_layer[layer] = EDBPadProperties(self.data, layer, GrpcPadType.ANTI_PAD, self) + except: + self._antipad_by_layer[layer] = None + return self._antipad_by_layer + + @property + def thermalpad_by_layer(self): + if not self._thermalpad_by_layer: + for layer in self.layers: + try: + self._pad_by_layer[layer] = EDBPadProperties(self.data, layer, GrpcPadType.THERMAL_PAD, self) + except: + self._thermalpad_by_layer[layer] = None + return self._thermalpad_by_layer + + @property + def hole_plating_ratio(self): + """Hole plating ratio. + + Returns + ------- + float + Percentage for the hole plating. + """ + return round(self.data.plating_percentage.value, 6) + + @hole_plating_ratio.setter + def hole_plating_ratio(self, ratio): + self.data.plating_percentage = GrpcValue(ratio) + + @property + def hole_plating_thickness(self): + """Hole plating thickness. + + Returns + ------- + float + Thickness of the hole plating if present. + """ + try: + if len(self.data.get_hole_parameters()) > 0: + return round((self.hole_diameter * self.hole_plating_ratio / 100) / 2, 6) + else: + return 0.0 + except: + return 0.0 + + @hole_plating_thickness.setter + def hole_plating_thickness(self, value): + """Hole plating thickness. + + Returns + ------- + float + Thickness of the hole plating if present. + """ + hr = 200 * GrpcValue(value).value / self.hole_diameter + self.hole_plating_ratio = hr + + @property + def hole_finished_size(self): + """Finished hole size. + + Returns + ------- + float + Finished size of the hole (Total Size + PlatingThickess*2). + """ + try: + if len(self.data.get_hole_parameters()) > 0: + return round(self.hole_diameter - (self.hole_plating_thickness * 2), 6) + else: + return 0.0 + except: + return 0.0 + + @property + def hole_range(self): + """Get hole range value from padstack definition. + + Returns + ------- + str + Possible returned values are ``"through"``, ``"begin_on_upper_pad"``, + ``"end_on_lower_pad"``, ``"upper_pad_to_lower_pad"``, and ``"undefined"``. + """ + return self.data.hole_range.name.lower() + + @hole_range.setter + def hole_range(self, value): + if isinstance(value, str): + if value == "through": + self.data.hole_range = GrpcPadstackHoleRange.THROUGH + elif value == "begin_on_upper_pad": + self.data.hole_range = GrpcPadstackHoleRange.BEGIN_ON_UPPER_PAD + elif value == "end_on_lower_pad": + self.data.hole_range = GrpcPadstackHoleRange.END_ON_LOWER_PAD + elif value == "upper_pad_to_lower_pad": + self.data.hole_range = GrpcPadstackHoleRange.UPPER_PAD_TO_LOWER_PAD + else: # pragma no cover + self.data.hole_range = GrpcPadstackHoleRange.UNKNOWN_RANGE + + @property + def material(self): + """Return hole material name.""" + return self.data.material.value + + @material.setter + def material(self, value): + self.data.material.value = value + + def convert_to_3d_microvias(self, convert_only_signal_vias=True, hole_wall_angle=15, delete_padstack_def=True): + """Convert actual padstack instance to microvias 3D Objects with a given aspect ratio. + + Parameters + ---------- + convert_only_signal_vias : bool, optional + Either to convert only vias belonging to signal nets or all vias. Defaults is ``True``. + hole_wall_angle : float, optional + Angle of laser penetration in degrees. The angle defines the lowest hole diameter with this formula: + HoleDiameter -2*tan(laser_angle* Hole depth). Hole depth is the height of the via (dielectric thickness). + The default is ``15``. + The lowest hole is ``0.75*HoleDepth/HoleDiam``. + delete_padstack_def : bool, optional + Whether to delete the padstack definition. The default is ``True``. + If ``False``, the padstack definition is not deleted and the hole size is set to zero. + + Returns + ------- + ``True`` when successful, ``False`` when failed. + """ + + if isinstance(self.data.get_hole_parameters()[0], GrpcPolygonData): + self._pedb.logger.error("Microvias cannot be applied on vias using hole shape polygon") + return False + + if self.start_layer == self.stop_layer: + self._pedb.logger.error("Microvias cannot be applied when Start and Stop Layers are the same.") + layout = self._pedb.active_layout + layers = self._pedb.stackup.signal_layers + layer_names = [i for i in list(layers.keys())] + if convert_only_signal_vias: + signal_nets = [i for i in list(self._pedb._pedb.nets.signal_nets.keys())] + topl, topz, bottoml, bottomz = self._pedb.stackup.limits(True) + if self.start_layer in layers: + start_elevation = layers[self.start_layer].lower_elevation + else: + start_elevation = layers[self.instances[0].start_layer].lower_elevation + if self.stop_layer in layers: + stop_elevation = layers[self.stop_layer].upper_elevation + else: + stop_elevation = layers[self.instances[0].stop_layer].upper_elevation + + diel_thick = abs(start_elevation - stop_elevation) + if self.hole_diameter: + rad1 = self.hole_diameter / 2 - math.tan(hole_wall_angle * diel_thick * math.pi / 180) + rad2 = self.hole_diameter / 2 + else: + rad1 = 0.0 + rad2 = 0.0 + + if start_elevation < (topz + bottomz) / 2: + rad1, rad2 = rad2, rad1 + i = 0 + for via in self.instances: + if convert_only_signal_vias and via.net_name in signal_nets or not convert_only_signal_vias: + pos = via.position + started = False + if len(self.pad_by_layer[self.start_layer].parameters_values) == 0: + self._pedb.modeler.create_polygon( + self.pad_by_layer[self.start_layer].polygon_data, + layer_name=self.start_layer, + net_name=via.net_name, + ) + else: + GrpcCircle.create( + layout, + self.start_layer, + via.net, + GrpcValue(pos[0]), + GrpcValue(pos[1]), + GrpcValue(self.pad_by_layer[self.start_layer].parameters_values[0] / 2), + ) + if len(self.pad_by_layer[self.stop_layer].parameters_values) == 0: + self._pedb.modeler.create_polygon( + self.pad_by_layer[self.stop_layer].polygon_data, + layer_name=self.stop_layer, + net_name=via.net_name, + ) + else: + GrpcCircle.create( + layout, + self.stop_layer, + via.net, + GrpcValue(pos[0]), + GrpcValue(pos[1]), + GrpcValue(self.pad_by_layer[self.stop_layer].parameters_values[0] / 2), + ) + for layer_name in layer_names: + stop = "" + if layer_name == via.start_layer or started: + start = layer_name + stop = layer_names[layer_names.index(layer_name) + 1] + cloned_circle = GrpcCircle.create( + layout, + start, + via.net, + GrpcValue(pos[0]), + GrpcValue(pos[1]), + GrpcValue(rad1), + ) + cloned_circle2 = GrpcCircle.create( + layout, + stop, + via.net, + GrpcValue(pos[0]), + GrpcValue(pos[1]), + GrpcValue(rad2), + ) + s3d = GrpcStructure3D.create( + layout, generate_unique_name("via3d_" + via.aedt_name.replace("via_", ""), n=3) + ) + s3d.add_member(cloned_circle) + s3d.add_member(cloned_circle2) + if not self.data.material.value: + self._pedb.logger.warning( + f"Padstack definution {self.name} has no material defined." f"Defaulting to copper" + ) + self.data.material = "copper" + s3d.set_material(self.data.material.value) + s3d.mesh_closure = GrpcMeshClosure.ENDS_CLOSED + started = True + i += 1 + if stop == via.stop_layer: + break + if delete_padstack_def: # pragma no cover + via.delete() + else: # pragma no cover + self.hole_diameter = 0.0 + self._pedb.logger.info("Padstack definition kept, hole size set to 0.") + + self._pedb.logger.info(f"{i} Converted successfully to 3D Objects.") + return True + + def split_to_microvias(self): + """Convert actual padstack definition to multiple microvias definitions. + + Returns + ------- + List of .:class:`pyedb.dotnet.database.padstackEDBPadstack` + """ + from pyedb.grpc.database.primitive.padstack_instances import PadstackInstance + + if self.start_layer == self.stop_layer: + self._pedb.logger.error("Microvias cannot be applied when Start and Stop Layers are the same.") + layout = self._pedb.active_layout + layers = self._pedb.stackup.signal_layers + layer_names = [i for i in list(layers.keys())] + if abs(layer_names.index(self.start_layer) - layer_names.index(self.stop_layer)) < 2: + self._pedb.logger.error( + "Conversion can be applied only if padstack definition is composed by more than 2 layers." + ) + return False + started = False + new_instances = [] + for layer_name in layer_names: + stop = "" + if layer_name == self.start_layer or started: + start = layer_name + stop = layer_names[layer_names.index(layer_name) + 1] + new_padstack_name = f"MV_{self.name}_{start}_{stop}" + included = [start, stop] + new_padstack_definition = GrpcPadstackDef.create(self._pedb.db, new_padstack_name) + new_padstack_definition.data.add_layers(included) + for layer in included: + pl = self.pad_by_layer[layer] + new_padstack_definition.data.set_pad_parameters( + layer=layer, + pad_type=GrpcPadType.REGULAR_PAD, + offset_x=GrpcValue(pl.offset_x, self._pedb.db), + offset_y=GrpcValue(pl.offset_y, self._pedb.db), + rotation=GrpcValue(pl.rotation, self._pedb.db), + type_geom=pl._edb_geometry_type, + sizes=pl.parameters_values, + ) + antipads = self.antipad_by_layer + if layer in antipads: + pl = antipads[layer] + new_padstack_definition.data.set_pad_parameters( + layer=layer, + pad_type=GrpcPadType.ANTI_PAD, + offset_x=GrpcValue(pl.offset_x, self._pedb.db), + offset_y=GrpcValue(pl.offset_y, self._pedb.db), + rotation=GrpcValue(pl.rotation, self._pedb.db), + type_geom=pl._edb_geometry_type, + sizes=pl.parameters_values, + ) + thermal_pads = self.thermalpad_by_layer + if layer in thermal_pads: + pl = thermal_pads[layer] + new_padstack_definition.data.set_pad_parameters( + layer=layer, + pad_type=GrpcPadType.THERMAL_PAD, + offset_x=GrpcValue(pl.offset_x, self._pedb.db), + offset_y=GrpcValue(pl.offset_y, self._pedb.db), + rotation=GrpcValue(pl.rotation, self._pedb.db), + type_geom=pl._edb_geometry_type, + sizes=pl.parameters_values, + ) + new_padstack_definition.data.set_hole_parameters( + offset_x=GrpcValue(self.hole_offset_x, self._pedb.db), + offset_y=GrpcValue(self.hole_offset_y, self._pedb.db), + rotation=GrpcValue(self.hole_rotation, self._pedb.db), + type_geom=self.edb_hole_type, + sizes=[self.hole_diameter], + ) + new_padstack_definition.data.material = self.material + new_padstack_definition.data.plating_percentage = GrpcValue(self.hole_plating_ratio, self._pedb.db) + new_instances.append(PadstackDef(self._pedb, new_padstack_definition)) + started = True + if self.stop_layer == stop: + break + i = 0 + for via in self.instances: + for instance in new_instances: + from_layer = self.data.layer_names[0] + to_layer = self.data.layer_names[-1] + from_layer = next(l for layer_name, l in self._pedb.stackup.layers.items() if l.name == from_layer) + to_layer = next(l for layer_name, l in self._pedb.stackup.layers.items() if l.name == to_layer) + padstack_instance = PadstackInstance.create( + layout=layout, + net=via.net, + name=generate_unique_name(instance.name), + padstack_def=instance, + position_x=via.position[0], + position_y=via.position[1], + rotation=0.0, + top_layer=from_layer, + bottom_layer=to_layer, + solder_ball_layer=None, + layer_map=None, + ) + padstack_instance.is_layout_pin = via.is_pin + i += 1 + via.delete() + self._pedb.logger.info("Created {} new microvias.".format(i)) + return new_instances + + # TODO check if update layer name is needed. diff --git a/src/pyedb/grpc/database/definitions.py b/src/pyedb/grpc/database/definitions.py new file mode 100644 index 0000000000..536313cb22 --- /dev/null +++ b/src/pyedb/grpc/database/definitions.py @@ -0,0 +1,70 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.geometry.polygon_data import PolygonData as GrpcPolygonData + +from pyedb.grpc.database.definition.component_def import ComponentDef +from pyedb.grpc.database.definition.package_def import PackageDef + + +class Definitions: + def __init__(self, pedb): + self._pedb = pedb + + @property + def component(self): + """Component definitions""" + return {l.name: ComponentDef(self._pedb, l) for l in self._pedb.active_db.component_defs} + + @property + def package(self): + """Package definitions.""" + return {l.name: PackageDef(self._pedb, l) for l in self._pedb.active_db.package_defs} + + def add_package_def(self, name, component_part_name=None, boundary_points=None): + """Add a package definition. + + Parameters + ---------- + name: str + Name of the package definition. + component_part_name : str, optional + Part name of the component. + boundary_points : list, optional + Boundary points which define the shape of the package. + + Returns + ------- + + """ + if not name in self.package: + package_def = PackageDef.create(self._pedb.active_db, name=name) + if component_part_name in self.component: + definition = self.component[component_part_name] + if not boundary_points and not definition.is_null: + package_def.exterior_boundary = GrpcPolygonData( + points=list(definition.components.values())[0].bounding_box + ) + if boundary_points: + package_def.exterior_boundary = GrpcPolygonData(points=boundary_points) + return PackageDef(self._pedb, package_def) + return False diff --git a/src/pyedb/grpc/database/general.py b/src/pyedb/grpc/database/general.py new file mode 100644 index 0000000000..c040cf6abc --- /dev/null +++ b/src/pyedb/grpc/database/general.py @@ -0,0 +1,43 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains EDB general methods and related methods. + +""" + +from __future__ import absolute_import # noreorder + +import logging +import re + +logger = logging.getLogger(__name__) + + +def pascal_to_snake(s): + # Convert PascalCase to snake_case + return re.sub(r"(?>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> pins =edbapp.components.get_pin_from_component("U2A5") + >>> edbapp.hfss.create_voltage_source_on_pin(pins[0], pins[1],50,"source_name") + """ + warnings.warn( + "`create_voltage_source_on_pin` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_voltage_source_on_pin` instead.", + DeprecationWarning, + ) + return self._pedb.excitations.create_voltage_source_on_pin( + pos_pin, neg_pin, voltage_value, phase_value, source_name + ) + + def create_current_source_on_pin(self, pos_pin, neg_pin, current_value=0.1, phase_value=0, source_name=""): + """Create a current source. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_current_source_on_pin` instead. + + Parameters + ---------- + pos_pin : Object + Positive Pin. + neg_pin : Object + Negative Pin. + current_value : float, optional + Value for the current. The default is ``0.1``. + phase_value : optional + Value for the phase. The default is ``0``. + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + Source Name. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> pins =edbapp.components.get_pin_from_component("U2A5") + >>> edbapp.hfss.create_current_source_on_pin(pins[0], pins[1],50,"source_name") + """ + warnings.warn( + "`create_current_source_on_pin` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_current_source_on_pin` instead.", + DeprecationWarning, + ) + return self._pedb.excitations.create_current_source_on_pin( + pos_pin, neg_pin, current_value, phase_value, source_name + ) + + def create_resistor_on_pin(self, pos_pin, neg_pin, rvalue=1, resistor_name=""): + """Create a Resistor boundary between two given pins. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_resistor_on_pin` instead. + + Parameters + ---------- + pos_pin : Object + Positive Pin. + neg_pin : Object + Negative Pin. + rvalue : float, optional + Resistance value. The default is ``1``. + resistor_name : str, optional + Name of the resistor. The default is ``""``. + + Returns + ------- + str + Name of the Resistor. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> pins =edbapp.components.get_pin_from_component("U2A5") + >>> edbapp.hfss.create_resistor_on_pin(pins[0], pins[1],50,"res_name") + """ + warnings.warn( + "`create_resistor_on_pin` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_resistor_on_pin` instead.", + DeprecationWarning, + ) + return self._pedb.excitations.create_resistor_on_pin(pos_pin, neg_pin, rvalue, resistor_name) + + def create_circuit_port_on_net( + self, + positive_component_name, + positive_net_name, + negative_component_name=None, + negative_net_name="GND", + impedance_value=50, + port_name="", + ): + """Create a circuit port on a NET. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_circuit_port_on_net` instead. + + It groups all pins belonging to the specified net and then applies the port on PinGroups. + + Parameters + ---------- + positive_component_name : str + Name of the positive component. + positive_net_name : str + Name of the positive net. + negative_component_name : str, optional + Name of the negative component. The default is ``None``, in which case the name of + the positive net is assigned. + negative_net_name : str, optional + Name of the negative net name. The default is ``"GND"``. + impedance_value : float, optional + Port impedance value. The default is ``50``. + port_name : str, optional + Name of the port. The default is ``""``. + + Returns + ------- + str + The name of the port. + """ + + warnings.warn( + "`create_circuit_port_on_net` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_circuit_port_on_net` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_circuit_port_on_net( + positive_component_name, + positive_net_name, + negative_component_name, + negative_net_name, + impedance_value, + port_name, + ) + + def create_voltage_source_on_net( + self, + positive_component_name, + positive_net_name, + negative_component_name=None, + negative_net_name="GND", + voltage_value=3.3, + phase_value=0, + source_name="", + ): + """Create a voltage source. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_voltage_source_on_net` instead. + + Parameters + ---------- + positive_component_name : str + Name of the positive component. + positive_net_name : str + Name of the positive net. + negative_component_name : str, optional + Name of the negative component. The default is ``None``, in which case the name of + the positive net is assigned. + negative_net_name : str, optional + Name of the negative net. The default is ``"GND"``. + voltage_value : float, optional + Value for the voltage. The default is ``3.3``. + phase_value : optional + Value for the phase. The default is ``0``. + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + Source Name. + """ + + warnings.warn( + "`create_voltage_source_on_net` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_voltage_source_on_net` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_voltage_source_on_net( + positive_component_name, + positive_net_name, + negative_component_name, + negative_net_name, + voltage_value, + phase_value, + source_name, + ) + + def create_current_source_on_net( + self, + positive_component_name, + positive_net_name, + negative_component_name=None, + negative_net_name="GND", + current_value=0.1, + phase_value=0, + source_name="", + ): + """Create a current source. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_current_source_on_net` instead. + + Parameters + ---------- + positive_component_name : str + Name of the positive component. + positive_net_name : str + Name of the positive net. + negative_component_name : str, optional + Name of the negative component. The default is ``None``, in which case the name of + the positive net is assigned. + negative_net_name : str, optional + Name of the negative net. The default is ``"GND"``. + current_value : float, optional + Value for the current. The default is ``0.1``. + phase_value : optional + Value for the phase. The default is ``0``. + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + Source Name. + """ + + warnings.warn( + "`create_current_source_on_net` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_current_source_on_net` instead.", + DeprecationWarning, + ) + return self._pedb.excitations.create_current_source_on_net( + positive_component_name, + positive_net_name, + negative_component_name, + negative_net_name, + current_value, + phase_value, + source_name, + ) + + def create_coax_port_on_component(self, ref_des_list, net_list, delete_existing_terminal=False): + """Create a coaxial port on a component or component list on a net or net list. + The name of the new coaxial port is automatically assigned. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_coax_port_on_component` instead. + + Parameters + ---------- + ref_des_list : list, str + List of one or more reference designators. + + net_list : list, str + List of one or more nets. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + warnings.warn( + "`create_coax_port_on_component` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_coax_port_on_component` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_coax_port_on_component( + ref_des_list, net_list, delete_existing_terminal + ) + + def create_differential_wave_port( + self, + positive_primitive_id, + positive_points_on_edge, + negative_primitive_id, + negative_points_on_edge, + port_name=None, + horizontal_extent_factor=5, + vertical_extent_factor=3, + pec_launch_width="0.01mm", + ): + """Create a differential wave port. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_differential_wave_port` instead. + + Parameters + ---------- + positive_primitive_id : int, EDBPrimitives + Primitive ID of the positive terminal. + positive_points_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be close to the target edge but not on the two + ends of the edge. + negative_primitive_id : int, EDBPrimitives + Primitive ID of the negative terminal. + negative_points_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be close to the target edge but not on the two + ends of the edge. + port_name : str, optional + Name of the port. The default is ``None``. + horizontal_extent_factor : int, float, optional + Horizontal extent factor. The default value is ``5``. + vertical_extent_factor : int, float, optional + Vertical extent factor. The default value is ``3``. + pec_launch_width : str, optional + Launch Width of PEC. The default value is ``"0.01mm"``. + + Returns + ------- + tuple + The tuple contains: (port_name, pyedb.dotnet.database.edb_data.sources.ExcitationDifferential). + + """ + warnings.warn( + "`create_differential_wave_port` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_differential_wave_port` instead.", + DeprecationWarning, + ) + return self._pedb.excitations.create_differential_wave_port( + positive_primitive_id, + positive_points_on_edge, + negative_primitive_id, + negative_points_on_edge, + port_name, + horizontal_extent_factor, + vertical_extent_factor, + pec_launch_width, + ) + + def create_bundle_wave_port( + self, + primitives_id, + points_on_edge, + port_name=None, + horizontal_extent_factor=5, + vertical_extent_factor=3, + pec_launch_width="0.01mm", + ): + """Create a bundle wave port. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_bundle_wave_port` instead. + + Parameters + ---------- + primitives_id : list + Primitive ID of the positive terminal. + points_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be close to the target edge but not on the two + ends of the edge. + port_name : str, optional + Name of the port. The default is ``None``. + horizontal_extent_factor : int, float, optional + Horizontal extent factor. The default value is ``5``. + vertical_extent_factor : int, float, optional + Vertical extent factor. The default value is ``3``. + pec_launch_width : str, optional + Launch Width of PEC. The default value is ``"0.01mm"``. + + Returns + ------- + tuple + The tuple contains: (port_name, pyedb.egacy.database.edb_data.sources.ExcitationDifferential). + """ + warnings.warn( + "`create_bundle_wave_port` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_bundle_wave_port` instead.", + DeprecationWarning, + ) + self._pedb.excitations.create_bundle_wave_port( + primitives_id, points_on_edge, port_name, horizontal_extent_factor, vertical_extent_factor, pec_launch_width + ) + + def create_hfss_ports_on_padstack(self, pinpos, portname=None): + """Create an HFSS port on a padstack. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_hfss_ports_on_padstack` instead. + + Parameters + ---------- + pinpos : + Position of the pin. + + portname : str, optional + Name of the port. The default is ``None``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + warnings.warn( + "`create_hfss_ports_on_padstack` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_hfss_ports_on_padstack` instead.", + DeprecationWarning, + ) + return self._pedb.excitations.create_hfss_ports_on_padstack(pinpos, portname) + + def create_edge_port_on_polygon( + self, + polygon=None, + reference_polygon=None, + terminal_point=None, + reference_point=None, + reference_layer=None, + port_name=None, + port_impedance=50.0, + force_circuit_port=False, + ): + """Create lumped port between two edges from two different polygons. Can also create a vertical port when + the reference layer name is only provided. When a port is created between two edge from two polygons which don't + belong to the same layer, a circuit port will be automatically created instead of lumped. To enforce the circuit + port instead of lumped,use the boolean force_circuit_port. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_edge_port_on_polygon` instead. + + + Parameters + ---------- + polygon : The EDB polygon object used to assign the port. + Edb.Cell.Primitive.Polygon object. + + reference_polygon : The EDB polygon object used to define the port reference. + Edb.Cell.Primitive.Polygon object. + + terminal_point : The coordinate of the point to define the edge terminal of the port. This point must be + located on the edge of the polygon where the port has to be placed. For instance taking the middle point + of an edge is a good practice but any point of the edge should be valid. Taking a corner might cause unwanted + port location. + list[float, float] with values provided in meter. + + reference_point : same as terminal_point but used for defining the reference location on the edge. + list[float, float] with values provided in meter. + + reference_layer : Name used to define port reference for vertical ports. + str the layer name. + + port_name : Name of the port. + str. + + port_impedance : port impedance value. Default value is 50 Ohms. + float, impedance value. + + force_circuit_port ; used to force circuit port creation instead of lumped. Works for vertical and coplanar + ports. + """ + + warnings.warn( + "`create_edge_port_on_polygon` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_edge_port_on_polygon` instead.", + DeprecationWarning, + ) + return self._pedb.excitations.create_edge_port_on_polygon( + polygon, + reference_polygon, + terminal_point, + reference_point, + reference_layer, + port_name, + port_impedance, + force_circuit_port, + ) + + def create_wave_port( + self, + prim_id, + point_on_edge, + port_name=None, + impedance=50, + horizontal_extent_factor=5, + vertical_extent_factor=3, + pec_launch_width="0.01mm", + ): + """Create a wave port. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_wave_port` instead. + + Parameters + ---------- + prim_id : int, Primitive + Primitive ID. + point_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be on the target edge but not on the two + ends of the edge. + port_name : str, optional + Name of the port. The default is ``None``. + impedance : int, float, optional + Impedance of the port. The default value is ``50``. + horizontal_extent_factor : int, float, optional + Horizontal extent factor. The default value is ``5``. + vertical_extent_factor : int, float, optional + Vertical extent factor. The default value is ``3``. + pec_launch_width : str, optional + Launch Width of PEC. The default value is ``"0.01mm"``. + + Returns + ------- + tuple + The tuple contains: (Port name, pyedb.dotnet.database.edb_data.sources.Excitation). + + """ + warnings.warn( + "`create_source_on_component` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_source_on_component` instead.", + DeprecationWarning, + ) + self._pedb.source_excitation.create_wave_port( + prim_id, + point_on_edge, + port_name, + impedance, + horizontal_extent_factor, + vertical_extent_factor, + pec_launch_width, + ) + + def create_edge_port_vertical( + self, + prim_id, + point_on_edge, + port_name=None, + impedance=50, + reference_layer=None, + hfss_type="Gap", + horizontal_extent_factor=5, + vertical_extent_factor=3, + pec_launch_width="0.01mm", + ): + """Create a vertical edge port. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_edge_port_vertical` instead. + + Parameters + ---------- + prim_id : int + Primitive ID. + point_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be on the target edge but not on the two + ends of the edge. + port_name : str, optional + Name of the port. The default is ``None``. + impedance : int, float, optional + Impedance of the port. The default value is ``50``. + reference_layer : str, optional + Reference layer of the port. The default is ``None``. + hfss_type : str, optional + Type of the port. The default value is ``"Gap"``. Options are ``"Gap"``, ``"Wave"``. + horizontal_extent_factor : int, float, optional + Horizontal extent factor. The default value is ``5``. + vertical_extent_factor : int, float, optional + Vertical extent factor. The default value is ``3``. + radial_extent_factor : int, float, optional + Radial extent factor. The default value is ``0``. + pec_launch_width : str, optional + Launch Width of PEC. The default value is ``"0.01mm"``. + + Returns + ------- + str + Port name. + """ + warnings.warn( + "`create_edge_port_vertical` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_edge_port_vertical` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_edge_port_vertical( + prim_id, + point_on_edge, + port_name, + impedance, + reference_layer, + hfss_type, + horizontal_extent_factor, + vertical_extent_factor, + pec_launch_width, + ) + + def create_edge_port_horizontal( + self, + prim_id, + point_on_edge, + ref_prim_id=None, + point_on_ref_edge=None, + port_name=None, + impedance=50, + layer_alignment="Upper", + ): + """Create a horizontal edge port. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_edge_port_horizontal` instead. + + Parameters + ---------- + prim_id : int + Primitive ID. + point_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be on the target edge but not on the two + ends of the edge. + ref_prim_id : int, optional + Reference primitive ID. The default is ``None``. + point_on_ref_edge : list, optional + Coordinate of the point to define the reference edge + terminal. The point must be on the target edge but not + on the two ends of the edge. The default is ``None``. + port_name : str, optional + Name of the port. The default is ``None``. + impedance : int, float, optional + Impedance of the port. The default value is ``50``. + layer_alignment : str, optional + Layer alignment. The default value is ``Upper``. Options are ``"Upper"``, ``"Lower"``. + + Returns + ------- + str + Name of the port. + """ + warnings.warn( + "`create_edge_port_horizontal` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_edge_port_horizontal` instead.", + DeprecationWarning, + ) + return self._pedb.excitations.create_edge_port_horizontal( + prim_id, point_on_edge, ref_prim_id, point_on_ref_edge, port_name, impedance, layer_alignment + ) + + def create_lumped_port_on_net(self, nets, reference_layer, return_points_only, digit_resolution, at_bounding_box): + """Create an edge port on nets. This command looks for traces and polygons on the + nets and tries to assign vertical lumped port. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_lumped_port_on_net` instead. + + Parameters + ---------- + nets : list, optional + List of nets, str or Edb net. + + reference_layer : str, Edb layer. + Name or Edb layer object. + + return_points_only : bool, optional + Use this boolean when you want to return only the points from the edges and not creating ports. Default + value is ``False``. + + digit_resolution : int, optional + The number of digits carried for the edge location accuracy. The default value is ``6``. + + at_bounding_box : bool + When ``True`` will keep the edges from traces at the layout bounding box location. This is recommended when + a cutout has been performed before and lumped ports have to be created on ending traces. Default value is + ``True``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + warnings.warn( + "`create_lumped_port_on_net` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_lumped_port_on_net` instead.", + DeprecationWarning, + ) + return self._pedb.excitations.create_lumped_port_on_net( + nets, reference_layer, return_points_only, digit_resolution, at_bounding_box + ) + + def create_vertical_circuit_port_on_clipped_traces(self, nets=None, reference_net=None, user_defined_extent=None): + """Create an edge port on clipped signal traces. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_vertical_circuit_port_on_clipped_traces` instead. + + Parameters + ---------- + nets : list, optional + String of one net or EDB net or a list of multiple nets or EDB nets. + + reference_net : str, Edb net. + Name or EDB reference net. + + user_defined_extent : [x, y], EDB PolygonData + Use this point list or PolygonData object to check if ports are at this polygon border. + + Returns + ------- + [[str]] + Nested list of str, with net name as first value, X value for point at border, Y value for point at border, + and terminal name. + """ + warnings.warn( + "`create_source_on_component` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_source_on_component` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_vertical_circuit_port_on_clipped_traces( + nets, reference_net, user_defined_extent + ) + + def get_layout_bounding_box(self, layout=None, digit_resolution=6): + """Evaluate the layout bounding box. + + Parameters + ---------- + layout : + Edb layout. + + digit_resolution : int, optional + Digit Resolution. The default value is ``6``. + + Returns + ------- + list + [lower left corner X, lower left corner, upper right corner X, upper right corner Y]. + """ + if not layout: + layout = self._active_layout + layout_obj_instances = layout.layout_instance.query_layout_obj_instances() + tuple_list = [] + for lobj in layout_obj_instances: + lobj_bbox = lobj.get_bbox() + tuple_list.append(lobj_bbox) + _bbox = GrpcPolygonData.bbox_of_polygons(tuple_list) + layout_bbox = [ + round(_bbox[0].x.value, digit_resolution), + round(_bbox[0].y.value, digit_resolution), + round(_bbox[1].x.value, digit_resolution), + round(_bbox[1].y.value, digit_resolution), + ] + return layout_bbox + + def configure_hfss_extents(self, simulation_setup=None): + """Configure the HFSS extent box. + + . deprecated:: pyedb 0.28.0 + Use :func:`self._pedb.utility.simulation_configuration.ProcessSimulationConfiguration.configure_hfss_extents` + instead. + + Parameters + ---------- + simulation_setup : + Edb_DATA.SimulationConfiguration object + + Returns + ------- + bool + True when succeeded, False when failed. + """ + warnings.warn( + "`configure_hfss_extents` is deprecated and is now located here " + "`pyedb.grpc.core.utility.simulation_configuration.ProcessSimulationConfiguration.configure_hfss_extents`" + "instead.", + DeprecationWarning, + ) + return self._pedb.utility.simulation_configuration.ProcessSimulationConfiguration.configure_hfss_extents( + simulation_setup + ) + + def configure_hfss_analysis_setup(self, simulation_setup=None): + """ + Configure HFSS analysis setup. + + . deprecated:: pyedb 0.28.0 + Use :func: + `pyedb.grpc.core.utility.simulation_configuration.ProcessSimulationConfiguration.configure_hfss_analysis_setup` + instead. + + + Parameters + ---------- + simulation_setup : + Edb_DATA.SimulationConfiguration object + + Returns + ------- + bool + True when succeeded, False when failed. + """ + warnings.warn( + "`configure_hfss_analysis_setup` is deprecated and is now located here " + "`pyedb.grpc.core.utility.simulation_configuration.ProcessSimulationConfiguration." + "configure_hfss_analysis_setup` instead.", + DeprecationWarning, + ) + self._pedb.utility.simulation_configuration.ProcessSimulationConfiguration.configure_hfss_analysis_setup( + simulation_setup + ) + + def _setup_decade_count_sweep(self, sweep, start_freq="1", stop_freq="1MHz", decade_count="10"): + start_f = GeometryOperators.parse_dim_arg(start_freq) + if start_f == 0.0: + start_f = 10 + self._logger.warning("Decade Count sweep does not support DC value, defaulting starting frequency to 10Hz") + + stop_f = GeometryOperators.parse_dim_arg(stop_freq) + decade_cnt = GeometryOperators.parse_dim_arg(decade_count) + freq = start_f + sweep.Frequencies.Add(str(freq)) + + while freq < stop_f: + freq = freq * math.pow(10, 1.0 / decade_cnt) + sweep.Frequencies.Add(str(freq)) + + def trim_component_reference_size(self, simulation_setup=None, trim_to_terminals=False): + """Trim the common component reference to the minimally acceptable size. + + . deprecated:: pyedb 0.28.0 + Use :func: + `pyedb.grpc.core.utility.simulation_configuration.ProcessSimulationConfiguration.trim_component_reference_size` + instead. + + Parameters + ---------- + simulation_setup : + Edb_DATA.SimulationConfiguration object + + trim_to_terminals : + bool. + True, reduce the reference to a box covering only the active terminals (i.e. those with + ports). + False, reduce the reference to the minimal size needed to cover all pins + + Returns + ------- + bool + True when succeeded, False when failed. + """ + + warnings.warn( + "`trim_component_reference_size` is deprecated and is now located here " + "`pyedb.grpc.core.utility.simulation_configuration.ProcessSimulationConfiguration." + "trim_component_reference_size` instead.", + DeprecationWarning, + ) + self._pedb.utility.simulation_configuration.ProcessSimulationConfiguration.trim_component_reference_size( + simulation_setup + ) + + def set_coax_port_attributes(self, simulation_setup=None): + """Set coaxial port attribute with forcing default impedance to 50 Ohms and adjusting the coaxial extent radius. + + . deprecated:: pyedb 0.28.0 + Use :func: + `pyedb.grpc.core.utility.simulation_configuration.ProcessSimulationConfiguration.set_coax_port_attributes` + instead. + + Parameters + ---------- + simulation_setup : + Edb_DATA.SimulationConfiguration object. + + Returns + ------- + bool + True when succeeded, False when failed. + """ + warnings.warn( + "`set_coax_port_attributes` is deprecated and is now located here " + "`pyedb.grpc.core.utility.simulation_configuration.ProcessSimulationConfiguration." + "set_coax_port_attributes` instead.", + DeprecationWarning, + ) + self._pedb.utility.simulation_configuration.ProcessSimulationConfiguration.set_coax_port_attributes( + simulation_setup + ) + + def _get_terminals_bbox(self, comp, l_inst, terminals_only): + terms_loi = [] + if terminals_only: + term_list = [] + for pin in comp.pins: + padstack_instance_term = pin.get_padstack_instance_terminal() + if not padstack_instance_term.is_null: + term_list.append(padstack_instance_term) + for tt in term_list: + term_param = tt.get_parameters() + if term_param: + loi = l_inst.get_layout_obj_instance(term_param[0], None) + terms_loi.append(loi) + else: + pin_list = comp.pins + for pi in pin_list: + loi = l_inst.get_layout_obj_instance(pi, None) + terms_loi.append(loi) + + if len(terms_loi) == 0: + return None + + terms_bbox = [] + for loi in terms_loi: + # Need to account for the coax port dimension + bb = loi.GetBBox() + ll = [bb[0].x.value, bb[0].y.value] + ur = [bb[1].x.value, bb[1].y.value] + # dim = 0.26 * max(abs(UR[0]-LL[0]), abs(UR[1]-LL[1])) # 0.25 corresponds to the default 0.5 + # Radial Extent Factor, so set slightly larger to avoid validation errors + dim = 0.30 * max(abs(ur[0] - ll[0]), abs(ur[1] - ll[1])) # 0.25 corresponds to the default 0.5 + terms_bbox.append(GrpcPolygonData([ll[0] - dim, ll[1] - dim, ur[0] + dim, ur[1] + dim])) + return GrpcPolygonData.bbox_of_polygons(terms_bbox) + + def get_ports_number(self): + """Return the total number of excitation ports in a layout. + + Parameters + ---------- + None + + Returns + ------- + int + Number of ports. + + """ + warnings.warn( + "`get_ports_number` is deprecated and is now located here " + "`pyedb.grpc.core.excitation.get_ports_number` instead.", + DeprecationWarning, + ) + self._pedb.excitations.get_ports_number() + + def layout_defeaturing(self, simulation_setup=None): + """Defeature the layout by reducing the number of points for polygons based on surface deviation criteria. + + . deprecated:: pyedb 0.28.0 + Use :func: + `pyedb.grpc.core.utility.simulation_configuration.ProcessSimulationConfiguration.layout_defeaturing` + instead. + + Parameters + ---------- + simulation_setup : Edb_DATA.SimulationConfiguration object + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + warnings.warn( + "`layout_defeaturing` is deprecated and is now located here " + "`pyedb.grpc.core.utility.simulation_configuration.ProcessSimulationConfiguration." + "layout_defeaturing` instead.", + DeprecationWarning, + ) + self._pedb.utility.simulation_configuration.ProcessSimulationConfiguration.layout_defeaturing(simulation_setup) + + def create_rlc_boundary_on_pins(self, positive_pin=None, negative_pin=None, rvalue=0.0, lvalue=0.0, cvalue=0.0): + """Create hfss rlc boundary on pins. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_rlc_boundary_on_pins` instead. + + Parameters + ---------- + positive_pin : Positive pin. + Edb.Cell.Primitive.PadstackInstance + + negative_pin : Negative pin. + Edb.Cell.Primitive.PadstackInstance + + rvalue : Resistance value + + lvalue : Inductance value + + cvalue . Capacitance value. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + warnings.warn( + "`create_rlc_boundary_on_pins` is deprecated and is now located here " + "`pyedb.grpc.core.create_rlc_boundary_on_pins.get_ports_number` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_rlc_boundary_on_pins( + positive_pin, negative_pin, rvalue, lvalue, cvalue + ) + + def add_setup( + self, + name=None, + distribution="linear", + start_freq=0, + stop_freq=20e9, + step_freq=1e6, + discrete_sweep=False, + ): + """Add a HFSS analysis to EDB. + + Parameters + ---------- + name : str, optional + Setup name. + Sweep type. `"interpolating"` or `"discrete"`. + distribution : str, optional + Type of the sweep. The default is `"linear"`. Options are: + - `"linear"` + - `"linear_count"` + - `"decade_count"` + - `"octave_count"` + - `"exponential"` + start_freq : str, float, optional + Starting frequency. The default is ``0``. + stop_freq : str, float, optional + Stopping frequency. The default is ``20e9``. + step_freq : str, float, int, optional + Frequency step. The default is ``1e6``. or used for `"decade_count"`, "linear_count"`, "octave_count"` + distribution. Must be integer in that case. + discrete_sweep : bool, optional + Whether the sweep is discrete. The default is ``False``. + + Returns + ------- + :class:`HfssSimulationSetup` + Setup object class. + """ + from ansys.edb.core.simulation_setup.hfss_simulation_setup import ( + HfssSimulationSetup as GrpcHfssSimulationSetup, + ) + from ansys.edb.core.simulation_setup.simulation_setup import ( + SweepData as GrpcSweepData, + ) + + if not name: + name = generate_unique_name("HFSS_pyedb") + if name in self._pedb.setups: + self._pedb.logger.error(f"HFSS setup {name} already defined.") + return False + setup = GrpcHfssSimulationSetup.create(self._pedb.active_cell, name) + start_freq = self._pedb.number_with_units(start_freq, "Hz") + stop_freq = self._pedb.number_with_units(stop_freq, "Hz") + if distribution.lower() == "linear": + distribution = "LIN" + elif distribution.lower() == "linear_count": + distribution = "LINC" + elif distribution.lower() == "exponential": + distribution = "ESTP" + elif distribution.lower() == "decade_count": + distribution = "DEC" + elif distribution.lower() == "octave_count": + distribution = "OCT" + else: + distribution = "LIN" + sweep_name = f"sweep_{len(setup.sweep_data) + 1}" + sweep_data = [ + GrpcSweepData( + name=sweep_name, distribution=distribution, start_f=start_freq, end_f=stop_freq, step=step_freq + ) + ] + if discrete_sweep: + sweep_data[0].type = sweep_data[0].type.DISCRETE_SWEEP + for sweep in setup.sweep_data: + sweep_data.append(sweep) + # TODO check bug #441 status + # setup.sweep_data = sweep_data + return HfssSimulationSetup(self._pedb, setup) diff --git a/src/pyedb/grpc/database/hierarchy/__init__.py b/src/pyedb/grpc/database/hierarchy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/pyedb/grpc/database/hierarchy/component.py b/src/pyedb/grpc/database/hierarchy/component.py new file mode 100644 index 0000000000..16042a0fac --- /dev/null +++ b/src/pyedb/grpc/database/hierarchy/component.py @@ -0,0 +1,1125 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import logging +import re +from typing import Optional +import warnings + +from ansys.edb.core.definition.component_model import ( + NPortComponentModel as GrpcNPortComponentModel, +) +from ansys.edb.core.definition.die_property import DieOrientation as GrpcDieOrientation +from ansys.edb.core.definition.die_property import DieType as GrpcDieType +from ansys.edb.core.definition.solder_ball_property import SolderballShape +from ansys.edb.core.geometry.polygon_data import PolygonData as GrpcPolygonData +from ansys.edb.core.hierarchy.component_group import ( + ComponentGroup as GrpcComponentGroup, +) +from ansys.edb.core.hierarchy.component_group import ComponentType as GrpcComponentType +from ansys.edb.core.hierarchy.netlist_model import NetlistModel as GrpcNetlistModel +from ansys.edb.core.hierarchy.pin_pair_model import PinPairModel as GrpcPinPairModel +from ansys.edb.core.hierarchy.sparameter_model import ( + SParameterModel as GrpcSParameterModel, +) +from ansys.edb.core.primitive.primitive import PadstackInstance as GrpcPadstackInstance +from ansys.edb.core.terminal.terminals import ( + PadstackInstanceTerminal as GrpcPadstackInstanceTerminal, +) +from ansys.edb.core.utility.rlc import Rlc as GrpcRlc +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.grpc.database.hierarchy.pin_pair_model import PinPairModel +from pyedb.grpc.database.hierarchy.spice_model import SpiceModel +from pyedb.grpc.database.layers.stackup_layer import StackupLayer +from pyedb.grpc.database.primitive.padstack_instances import PadstackInstance +from pyedb.grpc.database.terminal.padstack_instance_terminal import ( + PadstackInstanceTerminal, +) + +try: + import numpy as np +except ImportError: + warnings.warn( + "The NumPy module is required to run some functionalities of EDB.\n" + "Install with \n\npip install numpy\n\nRequires CPython." + ) +from pyedb.generic.general_methods import get_filename_without_extension + + +class Component(GrpcComponentGroup): + """Manages EDB functionalities for components. + + Parameters + ---------- + parent : :class:`pyedb.dotnet.database.components.Components` + Components object. + component : object + Edb Component Object + + """ + + def __init__(self, pedb, edb_object): + super().__init__(edb_object.msg) + self._pedb = pedb + self._layout_instance = None + self._comp_instance = None + self._logger = pedb.logger + self._package_def = None + + @property + def group_type(self): + return str(self.type).split(".")[-1].lower() + + @property + def layout_instance(self): + """EDB layout instance object.""" + return self._pedb.layout_instance + + @property + def component_instance(self): + """Edb component instance.""" + if self._comp_instance is None: + self._comp_instance = self.layout_instance.get_layout_obj_instance_in_context(self, None) + return self._comp_instance + + @property + def is_enabled(self): + return self.enabled + + @is_enabled.setter + def is_enabled(self, value): + self.enabled = value + + @property + def ic_die_properties(self): + if self.type == "ic": + return ICDieProperty(self) + else: + return None + + @property + def _active_layout(self): # pragma: no cover + return self._pedb.active_layout + + @property + def _edb_model(self): # pragma: no cover + comp_prop = self.component_property + return comp_prop.model + + @property # pragma: no cover + def _pin_pairs(self): + edb_model = self._edb_model + return edb_model.pin_pairs() + + @property + def _rlc(self): + if self.model_type == "SPICEModel": + if len(self.pins) == 2: + self._pedb.logger.warning(f"Spice model defined on component {self.name}, replacing model by ") + rlc = GrpcRlc() + pins = list(self.pins.keys()) + pin_pair = (pins[0], pins[1]) + rlc_model = PinPairModel(self._pedb, GrpcPinPairModel.create()) + rlc_model.set_rlc(pin_pair, rlc) + component_property = self.component_property + component_property.model = rlc_model + self.component_property = component_property + return [self._edb_model.rlc(pin_pair) for pin_pair in self._edb_model.pin_pairs()] + + @property + def model(self): + """Component model.""" + return self.component_property.model + + @model.setter + def model(self, value): + if not isinstance(value, PinPairModel): + self._pedb.logger.error("Invalid input. Set model failed.") + + comp_prop = self.component_property + comp_prop.model = value + self.component_property = comp_prop + + @property + def package_def(self): + """Package definition.""" + return self.component_property.package_def + + @package_def.setter + def package_def(self, value): + from pyedb.grpc.database.definition.package_def import PackageDef + + if value not in [package.name for package in self._pedb.package_defs]: + from ansys.edb.core.definition.package_def import ( + PackageDef as GrpcPackageDef, + ) + + self._package_def = GrpcPackageDef.create(self._pedb.db, name=value) + self._package_def.exterior_boundary = GrpcPolygonData(points=self.bounding_box) + comp_prop = self.component_property + comp_prop.package_def = self._package_def + self.component_property = comp_prop + elif isinstance(value, str): + package = next(package for package in self._pedb.package_defs if package.name == value) + comp_prop = self.component_property + comp_prop.package_def = package + self.component_property = comp_prop + + elif isinstance(value, PackageDef): + comp_prop = self.component_property + comp_prop.package_def = value + self.component_property = comp_prop + + @property + def is_mcad(self): + return super().is_mcad.value + + @is_mcad.setter + def is_mcad(self, value): + if isinstance(value, bool): + super(Component, self.__class__).is_mcad.__set__(self, GrpcValue(value)) + + @property + def is_mcad_3d_comp(self): + return super().is_mcad_3d_comp.value + + @is_mcad_3d_comp.setter + def is_mcad_3d_comp(self, value): + if isinstance(value, bool): + super(Component, self.__class__).is_mcad_3d_comp.__set__(self, GrpcValue(value)) + + @property + def is_mcad_hfss(self): + return super().is_mcad_hfss.value + + @is_mcad_hfss.setter + def is_mcad_hfss(self, value): + if isinstance(value, bool): + super(Component, self.__class__).is_mcad_hfss.__set__(self, GrpcValue(value)) + + @property + def is_mcad_stride(self): + return super().is_mcad_stride.value + + @is_mcad_stride.setter + def is_mcad_stride(self, value): + if isinstance(value, bool): + super(Component, self.__class__).is_mcad_stride.__set__(self, GrpcValue(value)) + + def create_package_def(self, name="", component_part_name=None): + """Create a package definition and assign it to the component. + + Parameters + ---------- + name: str, optional + Name of the package definition + component_part_name : str, optional + Part name of the component. + + Returns + ------- + bool + ``True`` if succeeded, ``False`` otherwise. + """ + if not name: + name = f"{self.refdes}_{self.part_name}" + if name not in [package.name for package in self._pedb.package_defs]: + self.package_def = name + return True + else: + logging.error(f"Package definition {name} already exists") + return False + + @property + def enabled(self): + """Get or Set the component to active mode.""" + if self.type.lower() in ["resistor", "capacitor", "inductor"]: + return self.component_property.enabled + else: + return + + @enabled.setter + def enabled(self, value): + cmp_prop = self.component_property + cmp_prop.enabled = value + self.component_property = cmp_prop + + @property + def spice_model(self): + """Get assigned Spice model properties.""" + if not self.model_type == "SPICEModel": + return None + else: + return SpiceModel(self._edb_model) + + @property + def s_param_model(self): + """Get assigned S-parameter model properties.""" + if not self.model_type == "SParameterModel": + return None + else: + return GrpcSParameterModel(self._edb_model) + + @property + def netlist_model(self): + """Get assigned netlist model properties.""" + if not self.model_type == "NetlistModel": + return None + else: + return GrpcNetlistModel(self._edb_model) + + @property + def solder_ball_height(self): + """Solder ball height if available.""" + if not self.component_property.solder_ball_property.is_null: + return self.component_property.solder_ball_property.height.value + return None + + @solder_ball_height.setter + def solder_ball_height(self, value): + if not self.component_property.solder_ball_property.is_null: + cmp_property = self.component_property + solder_ball_prop = cmp_property.solder_ball_property + solder_ball_prop.height = round(GrpcValue(value).value, 9) + cmp_property.solder_ball_property = solder_ball_prop + self.component_property = cmp_property + + @property + def solder_ball_shape(self): + """Solder ball shape.""" + if not self.component_property.solder_ball_property.is_null: + shape = self.component_property.solder_ball_property.shape + if shape == SolderballShape.NO_SOLDERBALL: + return "none" + elif shape == SolderballShape.SOLDERBALL_CYLINDER: + return "cylinder" + elif shape == SolderballShape.SOLDERBALL_SPHEROID: + return "spheroid" + + @solder_ball_shape.setter + def solder_ball_shape(self, value): + if not self.component_property.solder_ball_property.is_null: + shape = None + if isinstance(value, str): + if value.lower() == "cylinder": + shape = SolderballShape.SOLDERBALL_CYLINDER + elif value.lower() == "none": + shape = SolderballShape.NO_SOLDERBALL + elif value.lower() == "spheroid": + shape = SolderballShape.SOLDERBALL_SPHEROID + if shape: + cmp_property = self.component_property + solder_ball_prop = cmp_property.solder_ball_property + solder_ball_prop.shape = shape + cmp_property.solder_ball_property = solder_ball_prop + self.component_property = cmp_property + + @property + def solder_ball_diameter(self): + """Solder ball diameter.""" + if not self.component_property.solder_ball_property.is_null: + diameter, mid_diameter = self.component_property.solder_ball_property.get_diameter() + return diameter.value, mid_diameter.value + + @solder_ball_diameter.setter + def solder_ball_diameter(self, value): + if not self.component_property.solder_ball_property.is_null: + diameter = None + mid_diameter = diameter + if isinstance(value, tuple) or isinstance(value, list): + if len(value) == 2: + diameter = GrpcValue(value[0]) + mid_diameter = GrpcValue(value[1]) + elif len(value) == 1: + diameter = GrpcValue(value[0]) + mid_diameter = GrpcValue(value[0]) + if isinstance(value, str) or isinstance(value, float): + diameter = GrpcValue(value) + mid_diameter = GrpcValue(value) + cmp_property = self.component_property + solder_ball_prop = cmp_property.solder_ball_property + solder_ball_prop.set_diameter(diameter, mid_diameter) + cmp_property.solder_ball_property = solder_ball_prop + self.component_property = cmp_property + + @property + def solder_ball_placement(self): + """Solder ball placement if available..""" + if not self.component_property.solder_ball_property.is_null: + solder_placement = self.component_property.solder_ball_property.placement + return solder_placement.value + + @property + def refdes(self): + """Reference Designator Name. + + Returns + ------- + str + Reference Designator Name. + """ + return self.name + + @refdes.setter + def refdes(self, name): + self.name = name + + @property + def model_type(self): + """Retrieve assigned model type.""" + _model_type = str(self._edb_model).split(".")[-1] + if _model_type == "PinPairModel": + return "RLC" + elif "SParameterModel" in _model_type: + return "SParameterModel" + elif "SPICEModel" in _model_type: + return "SPICEModel" + else: + return _model_type + + @property + def rlc_values(self): + """Get component rlc values.""" + if not len(self._rlc): + return [None, None, None] + elif len(self._rlc) == 1: + return [self._rlc[0].r.value, self._rlc[0].l.value, self._rlc[0].c.value] + else: + return [[rlc.r.value, rlc.l.value, rlc.c.value] for rlc in self._rlc] + + @rlc_values.setter + def rlc_values(self, value): + comp_property = self.component_property + if not isinstance(value, list) or isinstance(value, tuple): + self._logger.error("RLC values must be provided as `List` or `Tuple` in this order.") + return + if not len(value) == 3: + self._logger.error("RLC values must be provided as `List` or `Tuple` in this order.") + return + _rlc = [] + for rlc in self._rlc: + if value[0]: + rlc.r = GrpcValue(value[0]) + rlc.r_enabled = True + else: + rlc.r_enabled = False + if value[1]: + rlc.l = GrpcValue(value[1]) + rlc.l_enabled = True + else: + rlc.l_enabled = False + if value[2]: + rlc.c = GrpcValue(value[2]) + rlc.c_enabled = True + else: + rlc.c_enabled = False + _rlc.append(rlc) + for ind in range(len(self._rlc)): + self._edb_model.set_rlc(self._pin_pairs[ind], self._rlc[ind]) + comp_property.model = self._edb_model + self.component_property = comp_property + + @property + def value(self): + """Retrieve discrete component value. + + Returns + ------- + str + Value. ``None`` if not an RLC Type. + """ + _values = {"resistor": self.rlc_values[0], "inductor": self.rlc_values[1], "capacitor": self.rlc_values[2]} + if self.type in _values: + return _values[self.type] + else: + return 0.0 + + @value.setter + def value(self, value): + if self.type == "resistor": + self.res_value = value + elif self.type == "inductor": + self.ind_value = value + elif self.type == "capacitor": + self.cap_value = value + + @property + def res_value(self): + """Resistance value. + + Returns + ------- + str + Resistance value or ``None`` if not an RLC type. + """ + cmp_type = self.component_type + if 0 < cmp_type.value < 4: + result = [rlc.r.value for rlc in self._rlc] + if len(result) == 1: + return result[0] + else: + return result + return None + + @res_value.setter + def res_value(self, value): # pragma no cover + _rlc = [] + model = PinPairModel(self._pedb, GrpcPinPairModel.create()) + for rlc in self._rlc: + rlc.r_enabled = True + rlc.r = GrpcValue(value) + _rlc.append(rlc) + for ind in range(len(self._pin_pairs)): + model.set_rlc(self._pin_pairs[ind], _rlc[ind]) + comp_prop = self.component_property + comp_prop.model = model + self.component_property = comp_prop + + @property + def cap_value(self): + """Capacitance Value. + + Returns + ------- + str + Capacitance Value. ``None`` if not an RLC Type. + """ + cmp_type = self.component_type + if 0 < cmp_type.value < 4: + result = [rlc.c.value for rlc in self._rlc] + if len(result) == 1: + return result[0] + else: + return result + return None + + @cap_value.setter + def cap_value(self, value): # pragma no cover + if value: + _rlc = [] + model = PinPairModel(self._pedb, GrpcPinPairModel.create()) + for rlc in self._rlc: + rlc.c_enabled = True + rlc.c = GrpcValue(value) + _rlc.append(rlc) + for ind in range(len(self._pin_pairs)): + model.set_rlc(self._pin_pairs[ind], _rlc[ind]) + comp_prop = self.component_property + comp_prop.model = model + self.component_property = comp_prop + + @property + def ind_value(self): + """Inductance Value. + + Returns + ------- + str + Inductance Value. ``None`` if not an RLC Type. + """ + cmp_type = self.component_type + if 0 < cmp_type.value < 4: + result = [rlc.l.value for rlc in self._rlc] + if len(result) == 1: + return result[0] + else: + return result + return None + + @ind_value.setter + def ind_value(self, value): # pragma no cover + if value: + _rlc = [] + model = PinPairModel(self._pedb, GrpcPinPairModel.create()) + for rlc in self._rlc: + rlc.l_enabled = True + rlc.l = GrpcValue(value) + _rlc.append(rlc) + for ind in range(len(self._pin_pairs)): + model.set_rlc(self._pin_pairs[ind], _rlc[ind]) + comp_prop = self.component_property + comp_prop.model = model + self.component_property = comp_prop + + @property + def is_parallel_rlc(self): + """Define if model is Parallel or Series. + + Returns + ------- + bool + `True´ if it is a parallel rlc model. + `False` for series RLC. + `None` if not an RLC Type. + """ + cmp_type = self.component_type + if 0 < cmp_type.value < 4: + return self._rlc[0].is_parallel + return None + + @is_parallel_rlc.setter + def is_parallel_rlc(self, value): # pragma no cover + if not len(self._pin_pairs): + logging.warning(self.refdes, " has no pin pair.") + else: + if isinstance(value, bool): + for rlc in self._rlc: + rlc.is_parallel = value + comp_property = self.component_property + comp_property.set_rcl(rlc) + self.component_property = comp_property + + @property + def center(self): + """Compute the component center. + + Returns + ------- + list + """ + return self.location + + @property + def location(self): + return [pt.value for pt in super().location] + + @location.setter + def location(self, value): + if isinstance(value, list): + _location = [GrpcValue(val) for val in value] + super(Component, self.__class__).location.__set__(self, _location) + + @property + def bounding_box(self): + """Component's bounding box. + + Returns + ------- + List[float] + List of coordinates for the component's bounding box, with the list of + coordinates in this order: [X lower left corner, Y lower left corner, + X upper right corner, Y upper right corner]. + """ + bbox = self.component_instance.get_bbox().points + pt1 = bbox[0] + pt2 = bbox[2] + return [pt1.x.value, pt1.y.value, pt2.x.value, pt2.y.value] + + @property + def rotation(self): + """Compute the component rotation in radian. + + Returns + ------- + float + """ + return self.transform.rotation.value + + @property + def pinlist(self): + """Pins of the component. + + Returns + ------- + list + List of Pins of Component. + """ + return self.pins + + @property + def nets(self): + """Nets of Component. + + Returns + ------- + list[str] + List of net name from component. + """ + nets = [] + for pin in list(self.pins.values()): + if not pin.net.is_null: + nets.append(pin.net.name) + return list(set(nets)) + + @property + def pins(self): + """EDBPadstackInstance of Component. + + Returns + ------- + dic[str, :class:`dotnet.database.edb_data.definitions.EDBPadstackInstance`] + Dictionary of EDBPadstackInstance Components. + """ + _pins = {} + for connectable in self.members: + if isinstance(connectable, GrpcPadstackInstanceTerminal): + _pins[connectable.name] = PadstackInstanceTerminal(self._pedb, connectable) + if isinstance(connectable, GrpcPadstackInstance): + _pins[connectable.name] = PadstackInstance(self._pedb, connectable) + return _pins + + @property + def type(self): + """Component type. + + Returns + ------- + str + Component type. + """ + return self.component_type.name.lower() + + @type.setter + def type(self, new_type): + """Set component type + + Parameters + ---------- + new_type : str + Type of the component. Options are ``"Resistor"``, ``"Inductor"``, ``"Capacitor"``, + ``"IC"``, ``"IO"`` and ``"Other"``. + """ + new_type = new_type.lower() + if new_type == "resistor": + self.component_type = GrpcComponentType.RESISTOR + elif new_type == "inductor": + self.component_type = GrpcComponentType.INDUCTOR + elif new_type == "capacitor": + self.component_type = GrpcComponentType.CAPACITOR + elif new_type == "ic": + self.component_type = GrpcComponentType.IC + elif new_type == "io": + self.component_type = GrpcComponentType.IO + elif new_type == "other": + self.component_type = GrpcComponentType.OTHER + else: + return + + @property + def numpins(self): + """Number of Pins of Component. + + Returns + ------- + int + Number of Pins of Component. + """ + return self.num_pins + + @property + def partname(self): # pragma: no cover + """Component part name. + + Returns + ------- + str + Component Part Name. + """ + return self.part_name + + @partname.setter + def partname(self, name): # pragma: no cover + """Set component part name.""" + self.part_name = name + + @property + def part_name(self): + """Component part name. + + Returns + ------- + str + Component part name. + """ + return self.component_def.name + + @part_name.setter + def part_name(self, name): # pragma: no cover + """Set component part name.""" + self.component_def.name = name + + @property + def placement_layer(self): + """Placement layern name. + + Returns + ------- + str + Name of the placement layer. + """ + return super().placement_layer.name + + @property + def layer(self): + """Placement layern object. + + Returns + ------- + :class:`pyedb.grpc.database.layers.stackup_layer.StackupLayer` + Placement layer. + """ + return StackupLayer(self._pedb, super().placement_layer) + + @property + def is_top_mounted(self): + """Check if a component is mounted on top or bottom of the layout. + + Returns + ------- + bool + ``True`` component is mounted on top, ``False`` on down. + """ + signal_layers = [lay.name for lay in list(self._pedb.stackup.signal_layers.values())] + if self.placement_layer in signal_layers[: int(len(signal_layers) / 2)]: + return True + return False + + @property + def lower_elevation(self): + """Lower elevation of the placement layer. + + Returns + ------- + float + Lower elevation of the placement layer. + """ + return self.layer.lower_elevation + + @property + def upper_elevation(self): + """Upper elevation of the placement layer. + + Returns + ------- + float + Upper elevation of the placement layer. + + """ + return self.layer.upper_elevation + + @property + def top_bottom_association(self): + """Top/bottom association of the placement layer. + + Returns + ------- + int + Top/bottom association of the placement layer, where: + + * 0 - Top associated + * 1 - No association + * 2 - Bottom associated + * 4 - Number of top/bottom associations. + * -1 - Undefined + """ + return self.layer.top_bottom_association.value + + def _set_model(self, model): # pragma: no cover + comp_prop = self.component_property + comp_prop.model = model + self.component_property = comp_prop + return model + + def assign_spice_model( + self, + file_path: str, + name: Optional[str] = None, + sub_circuit_name: Optional[str] = None, + terminal_pairs: Optional[list] = None, + ): + """Assign Spice model to this component. + + Parameters + ---------- + file_path : str + File path of the Spice model. + name : str, optional + Name of the Spice model. + + Returns + ------- + + """ + + # + # model = self._edb.cell.hierarchy._hierarchy.SPICEModel() + # model.SetModelPath(file_path) + # model.SetModelName(name) + # if sub_circuit_name: + # model.SetSubCkt(sub_circuit_name) + # + # if terminal_pairs: + # terminal_pairs = terminal_pairs if isinstance(terminal_pairs[0], list) else [terminal_pairs] + # for pair in terminal_pairs: + # pname, pnumber = pair + # if pname not in pin_names_sp: # pragma: no cover + # raise ValueError(f"Pin name {pname} doesn't exist in {file_path}.") + # model.AddTerminalPinPair(pname, str(pnumber)) + # else: + # for idx, pname in enumerate(pin_names_sp): + # model.AddTerminalPinPair(pname, str(idx + 1)) + # + # return self._set_model(model) + + if not name: + name = get_filename_without_extension(file_path) + + with open(file_path, "r") as f: + for line in f: + if "subckt" in line.lower(): + pin_names_sp = [i.strip() for i in re.split(" |\t", line) if i] + pin_names_sp.remove(pin_names_sp[0]) + pin_names_sp.remove(pin_names_sp[0]) + break + if not len(pin_names_sp) == self.numpins: # pragma: no cover + raise ValueError(f"Pin counts doesn't match component {self.name}.") + + model = SpiceModel(file_path=file_path, name=name, sub_circuit=name) + if sub_circuit_name: + model.sub_circuit = sub_circuit_name + + if terminal_pairs: + terminal_pairs = terminal_pairs if isinstance(terminal_pairs[0], list) else [terminal_pairs] + for pair in terminal_pairs: + pname, pnumber = pair + if pname not in pin_names_sp: # pragma: no cover + raise ValueError(f"Pin name {pname} doesn't exist in {file_path}.") + model.add_terminal(str(pnumber), pname) + else: + for idx, pname in enumerate(pin_names_sp): + model.add_terminal(pname, str(idx + 1)) + self._set_model(model) + if not model.is_null: + return model + else: + return False + + def assign_s_param_model(self, file_path, name=None, reference_net=None): + """Assign S-parameter to this component. + + Parameters + ---------- + file_path : str + File path of the S-parameter model. + name : str, optional + Name of the S-parameter model. + + Returns + ------- + SParameterModel object. + + """ + + if not name: + name = get_filename_without_extension(file_path) + for model in self.component_def.component_models: + if model.model_name == name: + self._pedb.logger.error(f"Model {name} already defined for component {self.refdes}") + return False + if not reference_net: + self._pedb.logger.warning( + f"No reference net provided for S parameter file {file_path}, net `GND` is " f"assigned by default" + ) + reference_net = "GND" + n_port_model = GrpcNPortComponentModel.find_by_name(self.component_def, name) + if n_port_model.is_null: + n_port_model = GrpcNPortComponentModel.create(name=name) + n_port_model.reference_file = file_path + self.component_def.add_component_model(n_port_model) + + model = GrpcSParameterModel.create(name=name, ref_net=reference_net) + return self._set_model(model) + + def use_s_parameter_model(self, name, reference_net=None): + """Use S-parameter model on the component. + + Parameters + ---------- + name: str + Name of the S-parameter model. + reference_net: str, optional + Reference net of the model. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + >>> edbapp = Edb() + >>>comp_def = edbapp.definitions.components["CAPC3216X180X55ML20T25"] + >>>comp_def.add_n_port_model("c:GRM32_DC0V_25degC_series.s2p", "GRM32_DC0V_25degC_series") + >>>edbapp.components["C200"].use_s_parameter_model("GRM32_DC0V_25degC_series") + """ + from ansys.edb.core.definition.component_model import ( + ComponentModel as GrpcComponentModel, + ) + + model = GrpcComponentModel.find_by_name(self.component_def, name) + if not model.is_null: + s_param_model = GrpcSParameterModel.create(name=name, ref_net="GND") + if reference_net: + s_param_model.reference_net = reference_net + return self._set_model(s_param_model) + return False + + def assign_rlc_model(self, res=None, ind=None, cap=None, is_parallel=False): + """Assign RLC to this component. + + Parameters + ---------- + res : int, float + Resistance. Default is ``None``. + ind : int, float + Inductance. Default is ``None``. + cap : int, float + Capacitance. Default is ``None``. + is_parallel : bool, optional + Whether it is a parallel or series RLC component. The default is ``False``. + """ + if res is None and ind is None and cap is None: + self._pedb.logger.error("At least one value has to be provided.") + return False + r_enabled = True if res else False + l_enabled = True if ind else False + c_enabled = True if cap else False + res = 0 if res is None else res + ind = 0 if ind is None else ind + cap = 0 if cap is None else cap + res, ind, cap = GrpcValue(res), GrpcValue(ind), GrpcValue(cap) + model = PinPairModel(self._pedb, self._edb_model) + pin_names = list(self.pins.keys()) + for idx, i in enumerate(np.arange(len(pin_names) // 2)): + # pin_pair = GrpcPinPair(pin_names[idx], pin_names[idx + 1]) + rlc = GrpcRlc( + r=res, + r_enabled=r_enabled, + l=ind, + l_enabled=l_enabled, + c=cap, + c_enabled=c_enabled, + is_parallel=is_parallel, + ) + model.set_rlc(("1", "2"), rlc) + return self._set_model(model) + + def create_clearance_on_component(self, extra_soldermask_clearance=1e-4): + """Create a Clearance on Soldermask layer by drawing a rectangle. + + Parameters + ---------- + extra_soldermask_clearance : float, optional + Extra Soldermask value in meter to be applied on component bounding box. + + Returns + ------- + bool + """ + bounding_box = self.bounding_box + opening = [bounding_box[0] - extra_soldermask_clearance] + opening.append(bounding_box[1] - extra_soldermask_clearance) + opening.append(bounding_box[2] + extra_soldermask_clearance) + opening.append(bounding_box[3] + extra_soldermask_clearance) + + comp_layer = self.layer + layer_names = list(self._pedb.stackup.layers.keys()) + layer_index = layer_names.index(comp_layer.name) + if comp_layer in [layer_names[0] + layer_names[-1]]: + return False + elif layer_index < len(layer_names) / 2: + soldermask_layer = layer_names[layer_index - 1] + else: + soldermask_layer = layer_names[layer_index + 1] + + if not self._pedb.modeler.get_primitives(layer_name=soldermask_layer): + all_nets = list(self._pedb.nets.nets.values()) + poly = self._pedb._create_conformal(all_nets, 0, 1e-12, False, 0) + self._pedb.modeler.create_polygon(poly, soldermask_layer, [], "") + + void = self._pedb.modeler.create_rectangle( + soldermask_layer, + "{}_opening".format(self.refdes), + lower_left_point=opening[:2], + upper_right_point=opening[2:], + ) + void.is_negative = True + return True + + +class ICDieProperty: + def __init__(self, component): + self._component = component + self._die_property = self._component.component_property.die_property + + @property + def die_orientation(self): + return self._die_property.die_orientation.name.lower() + + @die_orientation.setter + def die_orientation(self, value): + component_property = self._component.component_property + die_property = component_property.die_property + if value.lower() == "chip_up": + die_property.die_orientation = GrpcDieOrientation.CHIP_UP + elif value.lower() == "chip_down": + die_property.die_orientation = GrpcDieOrientation.CHIP_DOWN + else: + return + component_property.die_property = die_property + self._component.component_property = component_property + + @property + def die_type(self): + return self._die_property.die_type.name.lower() + + @die_type.setter + def die_type(self, value): + component_property = self._component.component_property + die_property = component_property.die_property + if value.lower() == "none": + die_property.die_type = GrpcDieType.NONE + elif value.lower() == "flipchip": + die_property.die_type = GrpcDieType.FLIPCHIP + elif value.lower() == "wirebond": + die_property.die_type = GrpcDieType.WIREBOND + else: + return + component_property.die_property = die_property + self._component.component_property = component_property + + @property + def height(self): + return self._die_property.height.value + + @height.setter + def height(self, value): + component_property = self._component.component_property + die_property = component_property.die_property + die_property.height = GrpcValue(value) + component_property.die_property = die_property + self._component.component_property = component_property + + @property + def is_null(self): + return self._die_property.is_null diff --git a/src/pyedb/grpc/database/hierarchy/model.py b/src/pyedb/grpc/database/hierarchy/model.py new file mode 100644 index 0000000000..abd944b1f7 --- /dev/null +++ b/src/pyedb/grpc/database/hierarchy/model.py @@ -0,0 +1,31 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.hierarchy.model import Model as GrpcModel + + +class Model(GrpcModel): + """Manages model class.""" + + def __init__(self, pedb): + super().__init__(self.msg) + self._pedb = pedb diff --git a/src/pyedb/grpc/database/hierarchy/netlist_model.py b/src/pyedb/grpc/database/hierarchy/netlist_model.py new file mode 100644 index 0000000000..bcc367ebcf --- /dev/null +++ b/src/pyedb/grpc/database/hierarchy/netlist_model.py @@ -0,0 +1,28 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.hierarchy.netlist_model import NetlistModel as GrpcNetlistModel + + +class NetlistModel(GrpcNetlistModel): # pragma: no cover + def __init__(self): + super().__init__(self.msg) diff --git a/src/pyedb/grpc/database/hierarchy/pin_pair_model.py b/src/pyedb/grpc/database/hierarchy/pin_pair_model.py new file mode 100644 index 0000000000..e41cca7352 --- /dev/null +++ b/src/pyedb/grpc/database/hierarchy/pin_pair_model.py @@ -0,0 +1,80 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +# from ansys.edb.core.hierarchy.pin_pair_model import PinPairModel +from ansys.edb.core.hierarchy.pin_pair_model import PinPairModel as GrpcPinPairModel +from ansys.edb.core.utility.value import Value as GrpcValue + + +class PinPairModel(GrpcPinPairModel): # pragma: no cover + def __init__(self, pedb, edb_object): + self._pedb_comp = pedb + super().__init__(edb_object.msg) + + @property + def rlc(self): + return super().rlc(self.pin_pairs()[0]) + + @property + def rlc_enable(self): + return [self.rlc.r_enabled, self.rlc.l_enabled, self.rlc.c_enabled] + + @rlc_enable.setter + def rlc_enable(self, value): + self.rlc.r_enabled = GrpcValue(value[0]) + self.rlc.l_enabled = GrpcValue(value[1]) + self.rlc.c_enabled = GrpcValue(value[2]) + + @property + def resistance(self): + return self.rlc.r.value # pragma: no cover + + @resistance.setter + def resistance(self, value): + self.rlc.r = GrpcValue(value) + + @property + def inductance(self): + return self.rlc.l.value # pragma: no cover + + @inductance.setter + def inductance(self, value): + self.rlc.l = GrpcValue(value) + + @property + def capacitance(self): + return self.rlc.c.value # pragma: no cover + + @capacitance.setter + def capacitance(self, value): + self.rlc.c = GrpcValue(value) + + @property + def rlc_values(self): # pragma: no cover + return [self.rlc.r.value, self.rlc.l.value, self.rlc.c.value] + + @rlc_values.setter + def rlc_values(self, values): # pragma: no cover + self.rlc.r = GrpcValue(values[0]) + self.rlc.l = GrpcValue(values[1]) + self.rlc.c = GrpcValue(values[2]) diff --git a/src/pyedb/grpc/database/hierarchy/pingroup.py b/src/pyedb/grpc/database/hierarchy/pingroup.py new file mode 100644 index 0000000000..849c8f1448 --- /dev/null +++ b/src/pyedb/grpc/database/hierarchy/pingroup.py @@ -0,0 +1,141 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.hierarchy.pin_group import PinGroup as GrpcPinGroup +from ansys.edb.core.terminal.terminals import BoundaryType as GrpcBoundaryType +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.generic.general_methods import generate_unique_name +from pyedb.grpc.database.hierarchy.component import Component +from pyedb.grpc.database.nets.net import Net +from pyedb.grpc.database.primitive.padstack_instances import PadstackInstance +from pyedb.grpc.database.terminal.pingroup_terminal import PinGroupTerminal + + +class PinGroup(GrpcPinGroup): + """Manages pin groups.""" + + def __init__(self, pedb, edb_pin_group=None): + if edb_pin_group: + super().__init__(edb_pin_group.msg) + self._pedb = pedb + + @property + def _active_layout(self): + return self._pedb.active_layout + + @property + def component(self): + """Component.""" + return Component(self._pedb, super().component) + + @component.setter + def component(self, value): + if isinstance(value, Component): + super(PinGroup, self.__class__).component.__set__(self, value) + + @property + def pins(self): + """Gets the pins belong to this pin group.""" + return [PadstackInstance(self._pedb, i) for i in super().pins] + + @property + def net(self): + """Net.""" + return Net(self._pedb, super().net) + + @net.setter + def net(self, value): + if isinstance(value, Net): + super(PinGroup, self.__class__).net.__set__(self, value) + + @property + def net_name(self): + return self.net.name + + # @property + # def terminal(self): + # """Terminal.""" + # term = PinGroupTerminal(self._pedb, self.get_pin_group_terminal()) # TODO check method is missing + # return term if not term.is_null else None + + def create_terminal(self, name=None): + """Create a terminal. + + Parameters + ---------- + name : str, optional + Name of the terminal. + """ + if not name: + name = generate_unique_name(self.name) + term = PinGroupTerminal.create( + layout=self._active_layout, name=name, pin_group=self, net=self.net, is_ref=False + ) + return PinGroupTerminal(self._pedb, term) + + def _json_format(self): + dict_out = {"component": self.component, "name": self.name, "net": self.net, "node_type": self.node_type} + return dict_out + + def create_current_source_terminal(self, magnitude=1, phase=0, impedance=1e6): + terminal = self.create_terminal() + terminal.boundary_type = GrpcBoundaryType.CURRENT_SOURCE + terminal.source_amplitude = GrpcValue(magnitude) + terminal.source_phase = GrpcValue(phase) + terminal.impedance = GrpcValue(impedance) + return terminal + + def create_voltage_source_terminal(self, magnitude=1, phase=0, impedance=0.001): + terminal = self.create_terminal() + terminal.boundary_type = GrpcBoundaryType.VOLTAGE_SOURCE + terminal.source_amplitude = GrpcValue(magnitude) + terminal.source_phase = GrpcValue(phase) + terminal.impedance = GrpcValue(impedance) + return terminal + + def create_voltage_probe_terminal(self, impedance=1000000): + terminal = self.create_terminal() + terminal.boundary_type = GrpcBoundaryType.VOLTAGE_PROBE + terminal.impedance = GrpcValue(impedance) + return terminal + + def create_port_terminal(self, impedance=50): + terminal = self.create_terminal() + terminal.boundary_type = GrpcBoundaryType.PORT + terminal.impedance = GrpcValue(impedance) + terminal.is_circuit_port = True + return terminal + + # def delete(self): + # """Delete active pin group. + # + # Returns + # ------- + # bool + # + # """ + # terminal = self.get_pin_group_terminal() # TODO check method exists in grpc + # self.delete() + # terminal.delete() + # return True diff --git a/src/pyedb/grpc/database/hierarchy/s_parameter_model.py b/src/pyedb/grpc/database/hierarchy/s_parameter_model.py new file mode 100644 index 0000000000..822ca734eb --- /dev/null +++ b/src/pyedb/grpc/database/hierarchy/s_parameter_model.py @@ -0,0 +1,31 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.hierarchy.sparameter_model import ( + SParameterModel as GrpcSParameterModel, +) + + +class SparamModel(GrpcSParameterModel): # pragma: no cover + def __init__(self, edb_model): + super().__init__(self.msg) + self._edb_model = edb_model diff --git a/src/pyedb/grpc/database/hierarchy/spice_model.py b/src/pyedb/grpc/database/hierarchy/spice_model.py new file mode 100644 index 0000000000..7b008c8d07 --- /dev/null +++ b/src/pyedb/grpc/database/hierarchy/spice_model.py @@ -0,0 +1,38 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.hierarchy.spice_model import SPICEModel as GrpcSpiceModel + + +class SpiceModel(GrpcSpiceModel): # pragma: no cover + def __init__(self, edb_object=None, name=None, file_path=None, sub_circuit=None): + if edb_object: + super().__init__(edb_object) + elif name and file_path: + if not sub_circuit: + sub_circuit = name + edb_object = GrpcSpiceModel.create(name=name, path=file_path, sub_circuit=sub_circuit) + super().__init__(edb_object.msg) + + @property + def name(self): + return self.model_name diff --git a/src/pyedb/grpc/database/layers/__init__.py b/src/pyedb/grpc/database/layers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/pyedb/grpc/database/layers/layer.py b/src/pyedb/grpc/database/layers/layer.py new file mode 100644 index 0000000000..b5b060f92c --- /dev/null +++ b/src/pyedb/grpc/database/layers/layer.py @@ -0,0 +1,57 @@ +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import + +from ansys.edb.core.layer.layer import Layer as GrpcLayer +from ansys.edb.core.layer.layer import LayerType as GrpcLayerType + + +class Layer(GrpcLayer): + """Manages Edb Layers. Replaces EDBLayer.""" + + def __init__(self, pedb, edb_object=None, name="", layer_type="undefined", **kwargs): + super().__init__(edb_object.msg) + self._pedb = pedb + self._name = name + self._color = () + self._type = "" + if edb_object: + self._cloned_layer = self.clone() + else: + layer_type_mapping = { + "conducting_layer": GrpcLayerType.CONDUCTING_LAYER, + "air_lines_layer": GrpcLayerType.AIRLINES_LAYER, + "errors_layer": GrpcLayerType.ERRORS_LAYER, + "symbol_layer": GrpcLayerType.SYMBOL_LAYER, + "measure_layer": GrpcLayerType.MEASURE_LAYER, + "assembly_layer": GrpcLayerType.ASSEMBLY_LAYER, + "silkscreen_layer": GrpcLayerType.SILKSCREEN_LAYER, + "solder_mask_layer": GrpcLayerType.SOLDER_MASK_LAYER, + "solder_paste_layer": GrpcLayerType.SOLDER_PASTE_LAYER, + "glue_layer": GrpcLayerType.GLUE_LAYER, + "wirebond_layer": GrpcLayerType.WIREBOND_LAYER, + "user_layer": GrpcLayerType.USER_LAYER, + "siwave_hfss_solver_regions": GrpcLayerType.SIWAVE_HFSS_SOLVER_REGIONS, + "postprocessing_layer": GrpcLayerType.POST_PROCESSING_LAYER, + "outline_layer": GrpcLayerType.OUTLINE_LAYER, + "layer_types_count": GrpcLayerType.LAYER_TYPES_COUNT, + "undefined_layer_type": GrpcLayerType.UNDEFINED_LAYER_TYPE, + } + if layer_type in layer_type_mapping: + self.create(name=name, lyr_type=layer_type_mapping[layer_type]) + self.update(**kwargs) + + def update(self, **kwargs): + for k, v in kwargs.items(): + if k in dir(self): + self.__setattr__(k, v) + else: + self._pedb.logger.error(f"{k} is not a valid layer attribute") + + @property + def _layer_name_mapping_reversed(self): + return {j: i for i, j in self._layer_name_mapping.items()} diff --git a/src/pyedb/grpc/database/layers/stackup_layer.py b/src/pyedb/grpc/database/layers/stackup_layer.py new file mode 100644 index 0000000000..b01f7b8ddc --- /dev/null +++ b/src/pyedb/grpc/database/layers/stackup_layer.py @@ -0,0 +1,345 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import + +from ansys.edb.core.layer.layer import LayerType as GrpcLayerType +from ansys.edb.core.layer.stackup_layer import RoughnessRegion as GrpcRoughnessRegion +from ansys.edb.core.layer.stackup_layer import StackupLayer as GrpcStackupLayer +from ansys.edb.core.utility.value import Value as GrpcValue + + +class StackupLayer(GrpcStackupLayer): + def __init__(self, pedb, edb_object=None): + super().__init__(edb_object.msg) + self._pedb = pedb + self._edb_object = edb_object + + @property + def _stackup_layer_mapping(self): + return { + "conducting_layer": GrpcLayerType.CONDUCTING_LAYER, + "silkscreen_layer": GrpcLayerType.SILKSCREEN_LAYER, + "solder_mask_layer": GrpcLayerType.SOLDER_MASK_LAYER, + "solder_paste_layer": GrpcLayerType.SOLDER_PASTE_LAYER, + "glue_layer": GrpcLayerType.GLUE_LAYER, + "wirebond_layer": GrpcLayerType.WIREBOND_LAYER, + "user_layer": GrpcLayerType.USER_LAYER, + "siwave_hfss_solver_regions": GrpcLayerType.SIWAVE_HFSS_SOLVER_REGIONS, + } + + @property + def type(self): + """Retrieve type of the layer.""" + return super().type.name.lower() + + @type.setter + def type(self, value): + if value in self._stackup_layer_mapping: + super(StackupLayer, self.__class__).type.__set__(self, self._stackup_layer_mapping[value]) + + def _create(self, layer_type): + if layer_type in self._stackup_layer_mapping: + layer_type = self._stackup_layer_mapping[layer_type] + self._edb_object = GrpcStackupLayer.create( + self._name, + layer_type, + GrpcValue(0), + GrpcValue(0), + "copper", + ) + + @property + def lower_elevation(self): + """Lower elevation. + + Returns + ------- + float + Lower elevation. + """ + return round(super().lower_elevation.value, 9) + + @lower_elevation.setter + def lower_elevation(self, value): + if self._pedb.stackup.mode == "overlapping": + super(StackupLayer, self.__class__).lower_elevation.__set__(self, GrpcValue(value)) + + @property + def fill_material(self): + """The layer's fill material.""" + if self.is_stackup_layer: + return self.get_fill_material() + + @fill_material.setter + def fill_material(self, value): + if self.is_stackup_layer: + self.set_fill_material(value) + + @property + def upper_elevation(self): + """Upper elevation. + + Returns + ------- + float + Upper elevation. + """ + return round(super().upper_elevation.value, 9) + + @property + def is_negative(self): + """Determine whether this layer is a negative layer. + + Returns + ------- + bool + True if this layer is a negative layer, False otherwise. + """ + return self.negative + + @is_negative.setter + def is_negative(self, value): + self.negative = value + + @property + def material(self): + """Get/Set the material loss_tangent. + + Returns + ------- + float + """ + return self.get_material() + + @material.setter + def material(self, name): + self.set_material(name) + + @property + def conductivity(self): + """Get the material conductivity. + + Returns + ------- + float + """ + if self.material in self._pedb.materials.materials: + return self._pedb.materials[self.material].conductivity + return None + + @property + def permittivity(self): + """Get the material permittivity. + + Returns + ------- + float + """ + if self.material in self._pedb.materials.materials: + return self._pedb.materials[self.material].permittivity + return None + + @property + def loss_tangent(self): + """Get the material loss_tangent. + + Returns + ------- + float + """ + if self.material in self._pedb.materials.materials: + return self._pedb.materials[self.material].loss_tangent + return None + + @property + def dielectric_fill(self): + """Retrieve material name of the layer dielectric fill.""" + if self.type == "signal": + return self.get_fill_material() + else: + return + + @dielectric_fill.setter + def dielectric_fill(self, name): + if self.type == "signal": + self.set_fill_material(name) + else: + pass + + @property + def thickness(self): + """Retrieve thickness of the layer. + + Returns + ------- + float + """ + return round(super().thickness.value, 9) + + @thickness.setter + def thickness(self, value): + super(StackupLayer, self.__class__).thickness.__set__(self, GrpcValue(value)) + + @property + def etch_factor(self): + """Retrieve etch factor of this layer. + + Returns + ------- + float + """ + return super().etch_factor.value + + @etch_factor.setter + def etch_factor(self, value): + if not value: + self.etch_factor_enabled = False + else: + self.etch_factor_enabled = True + super(StackupLayer, self.__class__).etch_factor.__set__(self, GrpcValue(value)) + + @property + def top_hallhuray_nodule_radius(self): + """Retrieve huray model nodule radius on top of the conductor.""" + top_roughness_model = self.get_roughness_model(GrpcRoughnessRegion.TOP) + if top_roughness_model: + return top_roughness_model.nodule_radius.value + else: + return None + + @top_hallhuray_nodule_radius.setter + def top_hallhuray_nodule_radius(self, value): + top_roughness_model = self.get_roughness_model(GrpcRoughnessRegion.TOP) + top_roughness_model.nodule_radius = GrpcValue(value) + + @property + def top_hallhuray_surface_ratio(self): + """Retrieve huray model surface ratio on top of the conductor.""" + top_roughness_model = self.get_roughness_model(GrpcRoughnessRegion.TOP) + if top_roughness_model: + return top_roughness_model.surface_ratio.value + else: + return None + + @top_hallhuray_surface_ratio.setter + def top_hallhuray_surface_ratio(self, value): + top_roughness_model = self.get_roughness_model(GrpcRoughnessRegion.TOP) + top_roughness_model.surface_roughness = GrpcValue(value) + + @property + def bottom_hallhuray_nodule_radius(self): + """Retrieve huray model nodule radius on bottom of the conductor.""" + bottom_roughness_model = self.get_roughness_model(GrpcRoughnessRegion.BOTTOM) + if bottom_roughness_model: + return bottom_roughness_model.nodule_radius.value + return None + + @bottom_hallhuray_nodule_radius.setter + def bottom_hallhuray_nodule_radius(self, value): + top_roughness_model = self.get_roughness_model(GrpcRoughnessRegion.BOTTOM) + top_roughness_model.nodule_radius = GrpcValue(value) + + @property + def bottom_hallhuray_surface_ratio(self): + """Retrieve huray model surface ratio on bottom of the conductor.""" + bottom_roughness_model = self.get_roughness_model(GrpcRoughnessRegion.BOTTOM) + if bottom_roughness_model: + return bottom_roughness_model.surface_ratio.value + return None + + @bottom_hallhuray_surface_ratio.setter + def bottom_hallhuray_surface_ratio(self, value): + top_roughness_model = self.get_roughness_model(GrpcRoughnessRegion.BOTTOM) + top_roughness_model.surface_ratio = GrpcValue(value) + + @property + def side_hallhuray_nodule_radius(self): + """Retrieve huray model nodule radius on sides of the conductor.""" + side_roughness_model = self.get_roughness_model(GrpcRoughnessRegion.SIDE) + if side_roughness_model: + return side_roughness_model.nodule_radius.value + return None + + @side_hallhuray_nodule_radius.setter + def side_hallhuray_nodule_radius(self, value): + top_roughness_model = self.get_roughness_model(GrpcRoughnessRegion.SIDE) + top_roughness_model.nodule_radius = GrpcValue(value) + + @property + def side_hallhuray_surface_ratio(self): + """Retrieve huray model surface ratio on sides of the conductor.""" + side_roughness_model = self.get_roughness_model(GrpcRoughnessRegion.SIDE) + if side_roughness_model: + return side_roughness_model.surface_ratio.value + else: + return None + + @side_hallhuray_surface_ratio.setter + def side_hallhuray_surface_ratio(self, value): + top_roughness_model = self.get_roughness_model(GrpcRoughnessRegion.SIDE) + top_roughness_model.surface_ratio = GrpcValue(value) + + def assign_roughness_model( + self, + model_type="huray", + huray_radius="0.5um", + huray_surface_ratio="2.9", + groisse_roughness="1um", + apply_on_surface="all", + ): + """Assign roughness model on this layer. + + Parameters + ---------- + model_type : str, optional + Type of roughness model. The default is ``"huray"``. Options are ``"huray"``, ``"groisse"``. + huray_radius : str, float, optional + Radius of huray model. The default is ``"0.5um"``. + huray_surface_ratio : str, float, optional. + Surface ratio of huray model. The default is ``"2.9"``. + groisse_roughness : str, float, optional + Roughness of groisse model. The default is ``"1um"``. + apply_on_surface : str, optional. + Where to assign roughness model. The default is ``"all"``. Options are ``"top"``, ``"bottom"``, + ``"side"``. + + Returns + ------- + + """ + regions = [] + if apply_on_surface == "all": + regions = [GrpcRoughnessRegion.TOP, GrpcRoughnessRegion.BOTTOM, GrpcRoughnessRegion.SIDE] + elif apply_on_surface == "top": + regions = [GrpcRoughnessRegion.TOP] + elif apply_on_surface == "bottom": + regions = [GrpcRoughnessRegion.BOTTOM] + elif apply_on_surface == "side": + regions = [GrpcRoughnessRegion.BOTTOM] + self.roughness_enabled = True + for r in regions: + if model_type == "huray": + model = (GrpcValue(huray_radius), GrpcValue(huray_surface_ratio)) + else: + model = GrpcValue(groisse_roughness) + self.set_roughness_model(model, r) diff --git a/src/pyedb/grpc/database/layout/__init__.py b/src/pyedb/grpc/database/layout/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/pyedb/grpc/database/layout/cell.py b/src/pyedb/grpc/database/layout/cell.py new file mode 100644 index 0000000000..39ca5e5c12 --- /dev/null +++ b/src/pyedb/grpc/database/layout/cell.py @@ -0,0 +1,28 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.layout.cell import Cell as GrpcCell + + +class Cell(GrpcCell): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) diff --git a/src/pyedb/grpc/database/layout/layout.py b/src/pyedb/grpc/database/layout/layout.py new file mode 100644 index 0000000000..69ad82d921 --- /dev/null +++ b/src/pyedb/grpc/database/layout/layout.py @@ -0,0 +1,144 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains these classes: `EdbLayout` and `Shape`. +""" +from typing import Union + +from ansys.edb.core.layout.layout import Layout as GrpcLayout + +from pyedb.grpc.database.hierarchy.component import Component +from pyedb.grpc.database.hierarchy.pingroup import PinGroup +from pyedb.grpc.database.layout.voltage_regulator import VoltageRegulator +from pyedb.grpc.database.nets.differential_pair import DifferentialPair +from pyedb.grpc.database.nets.extended_net import ExtendedNet +from pyedb.grpc.database.nets.net import Net +from pyedb.grpc.database.nets.net_class import NetClass +from pyedb.grpc.database.primitive.padstack_instances import PadstackInstance +from pyedb.grpc.database.terminal.bundle_terminal import BundleTerminal +from pyedb.grpc.database.terminal.edge_terminal import EdgeTerminal +from pyedb.grpc.database.terminal.padstack_instance_terminal import ( + PadstackInstanceTerminal, +) +from pyedb.grpc.database.terminal.pingroup_terminal import PinGroupTerminal +from pyedb.grpc.database.terminal.point_terminal import PointTerminal + + +class Layout(GrpcLayout): + def __init__(self, pedb): + super().__init__(pedb.active_cell._Cell__stub.GetLayout(pedb.active_cell.msg)) + self._pedb = pedb + + @property + def cell(self): + """:class:`Cell `: Owning cell for this layout. + + Read-Only. + """ + return self._pedb._active_cell + + @property + def terminals(self): + """Get terminals belonging to active layout. + + Returns + ------- + Terminal dictionary : Dict[str, pyedb.dotnet.database.edb_data.terminals.Terminal] + """ + temp = [] + for i in self._pedb.active_cell.layout.terminals: + if i.type.name.lower() == "pin_group": + temp.append(PinGroupTerminal(self._pedb, i)) + elif i.type.name.lower() == "padstack_inst": + temp.append(PadstackInstanceTerminal(self._pedb, i)) + elif i.type.name.lower() == "edge": + temp.append(EdgeTerminal(self._pedb, i)) + elif i.type.name.lower() == "bundle": + temp.append(BundleTerminal(self._pedb, i)) + elif i.type.name.lower() == "point": + temp.append(PointTerminal(self._pedb, i)) + return temp + + @property + def nets(self): + """Nets. + + Returns + ------- + """ + return [Net(self._pedb, net) for net in super().nets] + + @property + def bondwires(self): + """Bondwires. + + Returns + ------- + list : + List of bondwires. + """ + return [i for i in self.primitives if i.primitive_type == "bondwire"] + + @property + def groups(self): + return [Component(self._pedb, g) for g in self._pedb.active_cell.layout.groups] + + @property + def pin_groups(self): + return [PinGroup(self._pedb, i) for i in self._pedb.active_cell.layout.pin_groups] + + @property + def net_classes(self): + return [NetClass(self._pedb, i) for i in self._pedb.active_cell.layout.net_classes] + + @property + def extended_nets(self): + return [ExtendedNet(self._pedb, i) for i in self._pedb.active_cell.layout.extended_nets] + + @property + def differential_pairs(self): + return [DifferentialPair(self._pedb, i) for i in self._pedb.active_cell.layout.differential_pairs] + + @property + def padstack_instances(self): + """Get all padstack instances in a list.""" + return [PadstackInstance(self._pedb, i) for i in self._pedb.active_cell.layout.padstack_instances] + + # + @property + def voltage_regulators(self): + return [VoltageRegulator(self._pedb, i) for i in self._pedb.active_cell.layout.voltage_regulators] + + def find_primitive(self, layer_name: Union[str, list]) -> list: + """Find a primitive objects by layer name. + + Parameters + ---------- + layer_name : str, list + Name of the layer. + Returns + ------- + list + """ + layer_name = layer_name if isinstance(layer_name, list) else [layer_name] + return [i for i in self.primitives if i.layer.name in layer_name] diff --git a/src/pyedb/grpc/database/layout/voltage_regulator.py b/src/pyedb/grpc/database/layout/voltage_regulator.py new file mode 100644 index 0000000000..59f5ad5c9f --- /dev/null +++ b/src/pyedb/grpc/database/layout/voltage_regulator.py @@ -0,0 +1,113 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.layout.voltage_regulator import ( + VoltageRegulator as GrpcVoltageRegulator, +) +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance + + +class VoltageRegulator(GrpcVoltageRegulator): + """Class managing EDB voltage regulator.""" + + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb + + @property + def component(self): + """Retrieve voltage regulator component""" + if not self.component.is_null: + ref_des = self.component.name + if not ref_des: + return False + return self._pedb.components.instances[ref_des] + return False + + @component.setter + def component(self, value): + if not isinstance(value, str): + self._pedb.logger.error("refdes name must be provided to set vrm component") + return + if value not in self._pedb.components.instances: + self._pedb.logger.error(f"component {value} not found in layout") + return + self.group = self._pedb.components.instances[value] + + @property + def load_regulator_current(self): + """Retrieve load regulator current value""" + return self.load_regulator_current.value + + @load_regulator_current.setter + def load_regulator_current(self, value): + self.load_regulation_percent = GrpcValue(value) + + @property + def load_regulation_percent(self): + """Retrieve load regulation percent value.""" + return self.load_regulation_percent.value + + @load_regulation_percent.setter + def load_regulation_percent(self, value): + self.load_regulation_percent = GrpcValue(value) + + @property + def negative_remote_sense_pin(self): + """Retrieve negative remote sense pin.""" + return self._pedb.padstacks.instances[self.negative_remote_sense_pin.id] + + @negative_remote_sense_pin.setter + def negative_remote_sense_pin(self, value): + if isinstance(value, int): + if value in self._pedb.padsatcks.instances: + self.neg_remote_sense_pin = self._pedb.padsatcks.instances[value] + elif isinstance(value, EDBPadstackInstance): + self.neg_remote_sense_pin = value + + @property + def positive_remote_sense_pin(self): + """Retrieve positive remote sense pin.""" + return self._pedb.padstacks.instances[self.pos_remote_sense_pin.id] + + @positive_remote_sense_pin.setter + def positive_remote_sense_pin(self, value): + if isinstance(value, int): + if value in self._pedb.padsatcks.instances: + self.positive_remote_sense_pin = self._pedb.padsatcks.instances[value] + if not self.component: + self.component = self._pedb.padsatcks.instances[value].component.name + elif isinstance(value, EDBPadstackInstance): + self.positive_remote_sense_pin = value + if not self.component: + self.component = value.component.name + + @property + def voltage(self): + """Retrieve voltage value.""" + return self.voltage.value + + @voltage.setter + def voltage(self, value): + self.voltage = GrpcValue(value) diff --git a/src/pyedb/grpc/database/layout_validation.py b/src/pyedb/grpc/database/layout_validation.py new file mode 100644 index 0000000000..b5ec883b28 --- /dev/null +++ b/src/pyedb/grpc/database/layout_validation.py @@ -0,0 +1,319 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import re + +# from pyedb.generic.general_methods import generate_unique_name +# from pyedb.grpc.database.primitive.padstack_instances import PadstackInstance +# from pyedb.grpc.database.primitive.primitive import Primitive + + +class LayoutValidation: + """Manages all layout validation capabilities""" + + def __init__(self, pedb): + self._pedb = pedb + self._layout_instance = self._pedb.layout_instance + + def dc_shorts(self, net_list=None, fix=False): + """Find DC shorts on layout. + + Parameters + ---------- + net_list : str or list[str], optional + List of nets. + fix : bool, optional + If `True`, rename all the nets. (default) + If `False`, only report dc shorts. + + Returns + ------- + List[List[str, str]] + [[net name, net name]]. + + Examples + -------- + + >>> edb = Edb("edb_file") + >>> dc_shorts = edb.layout_validation.dc_shorts() + + """ + if not net_list: + net_list = list(self._pedb.nets.nets.keys()) + elif isinstance(net_list, str): + net_list = [net_list] + _objects_list = {} + _padstacks_list = {} + for prim in self._pedb.modeler.primitives: + n_name = prim.net_name + if n_name in _objects_list: + _objects_list[n_name].append(prim) + else: + _objects_list[n_name] = [prim] + for pad in list(self._pedb.padstacks.instances.values()): + n_name = pad.net_name + if n_name in _padstacks_list: + _padstacks_list[n_name].append(pad) + else: + _padstacks_list[n_name] = [pad] + dc_shorts = [] + all_shorted_nets = [] + for net in net_list: + if net in all_shorted_nets: + continue + objs = [] + for i in _objects_list.get(net, []): + objs.append(i) + for i in _padstacks_list.get(net, []): + objs.append(i) + if not len(objs): + self._pedb.nets[net].delete() + continue + + connected_objs = objs[0].get_connected_objects() + connected_objs.append(objs[0]) + net_dc_shorts = [obj for obj in connected_objs] + all_shorted_nets.append(net) + if net_dc_shorts: + dc_nets = list(set([obj.net.name for obj in net_dc_shorts])) + dc_nets = [i for i in dc_nets if i != net] + for dc in dc_nets: + if dc: + dc_shorts.append([net, dc]) + all_shorted_nets.append(dc) + if fix: + temp = [] + for i in net_dc_shorts: + temp.append(i.net.name) + temp_key = set(temp) + temp_count = {temp.count(i): i for i in temp_key} + temp_count = dict(sorted(temp_count.items())) + while True: + temp_name = list(temp_count.values()).pop() + if not temp_name.lower().startswith("unnamed"): + break + elif temp_name.lower(): + break + elif len(temp) == 0: + break + rename_shorts = [i for i in net_dc_shorts if i.net.name != temp_name] + for i in rename_shorts: + i.net = self._pedb.nets.nets[temp_name] + return dc_shorts + + # def disjoint_nets( + # self, + # net_list=None, + # keep_only_main_net=False, + # clean_disjoints_less_than=0.0, + # order_by_area=False, + # keep_disjoint_pins=False, + # ): + # """Find and fix disjoint nets from a given netlist. + # + # Parameters + # ---------- + # net_list : str, list, optional + # List of nets on which check disjoints. If `None` is provided then the algorithm will loop on all nets. + # keep_only_main_net : bool, optional + # Remove all secondary nets other than principal one (the one with more objects in it). Default is `False`. + # clean_disjoints_less_than : bool, optional + # Clean all disjoint nets with area less than specified area in square meters. Default is `0.0` to disable it. + # order_by_area : bool, optional + # Whether if the naming order has to be by number of objects (fastest) or area (slowest but more accurate). + # Default is ``False``. + # keep_disjoint_pins : bool, optional + # Whether if delete disjoints pins not connected to any other primitive or not. Default is ``False``. + # + # Returns + # ------- + # List + # New nets created. + # + # Examples + # -------- + # + # >>> renamed_nets = edb.layout_validation.disjoint_nets(["GND","Net2"]) + # """ + # from ansys.edb.core.geometry.point_data import PointData as GrpcPointData + # timer_start = self._pedb.logger.reset_timer() + # + # if not net_list: + # net_list = list(self._pedb.nets.keys()) + # elif isinstance(net_list, str): + # net_list = [net_list] + # _objects_list = {} + # _padstacks_list = {} + # for prim in self._pedb.modeler.primitives: + # if not prim.net.is_null: + # n_name = prim.net.name + # if n_name in _objects_list: + # _objects_list[n_name].append(prim) + # else: + # _objects_list[n_name] = [prim] + # for pad in list(self._pedb.padstacks.instances.values()): + # if not pad.net.is_null: + # n_name = pad.net_name + # if n_name in _padstacks_list: + # _padstacks_list[n_name].append(pad) + # else: + # _padstacks_list[n_name] = [pad] + # new_nets = [] + # disjoints_objects = [] + # self._pedb.logger.reset_timer() + # for net in net_list: + # net_groups = [] + # obj_dict = {} + # for i in _objects_list.get(net, []): + # obj_dict[i.id] = i + # for i in _padstacks_list.get(net, []): + # obj_dict[i.id] = i + # objs = list(obj_dict.values()) + # l = len(objs) + # while l > 0: + # l1 = self._layout_instance.get_connected_objects(objs[0].layout_object_instance, False) + # l1.append(objs[0].id) + # repetition = False + # for net_list in net_groups: + # if set(l1).intersection(net_list): + # net_groups.append([i for i in l1 if i not in net_list]) + # repetition = True + # if not repetition: + # net_groups.append(l1) + # objs = [i for i in objs if i.id not in l1] + # l = len(objs) + # if len(net_groups) > 1: + # + # def area_calc(elem): + # sum = 0 + # for el in elem: + # try: + # if el.layout_obj.obj_type.value == 0: + # if not el.is_void: + # sum += el.area() + # except: + # pass + # return sum + # + # if order_by_area: + # areas = [area_calc(i) for i in net_groups] + # sorted_list = [x for _, x in sorted(zip(areas, net_groups), reverse=True)] + # else: + # sorted_list = sorted(net_groups, key=len, reverse=True) + # for disjoints in sorted_list[1:]: + # if keep_only_main_net: + # for geo in disjoints: + # try: + # obj_dict[geo].delete() + # except KeyError: + # pass + # elif len(disjoints) == 1 and ( + # clean_disjoints_less_than + # and "area" in dir(obj_dict[disjoints[0]]) + # and obj_dict[disjoints[0]].area() < clean_disjoints_less_than + # ): + # try: + # obj_dict[disjoints[0]].delete() + # except KeyError: + # pass + # elif ( + # len(disjoints) == 1 + # and not keep_disjoint_pins + # and isinstance(obj_dict[disjoints[0]], PadstackInstance) + # ): + # try: + # obj_dict[disjoints[0]].delete() + # except KeyError: + # pass + # + # else: + # new_net_name = generate_unique_name(net, n=6) + # net_obj = self._pedb.nets.find_or_create_net(new_net_name) + # if net_obj: + # new_nets.append(net_obj.name) + # for geo in disjoints: + # try: + # obj_dict[geo].net_name = net_obj.name + # except KeyError: + # pass + # disjoints_objects.extend(disjoints) + # self._pedb._logger.info("Found {} objects in {} new nets.".format(len(disjoints_objects), len(new_nets))) + # self._pedb._logger.info_timer("Disjoint Cleanup Completed.", timer_start) + # + # return new_nets + + def fix_self_intersections(self, net_list=None): + """Find and fix self intersections from a given netlist. + + Parameters + ---------- + net_list : str, list, optional + List of nets on which check disjoints. If `None` is provided then the algorithm will loop on all nets. + + Returns + ------- + bool + """ + if not net_list: + net_list = list(self._pedb.nets.keys()) + elif isinstance(net_list, str): + net_list = [net_list] + new_prims = [] + for prim in self._pedb.modeler.polygons: + if prim.net_name in net_list: + new_prims.extend(prim.fix_self_intersections()) + if new_prims: + self._pedb._logger.info("Self-intersections detected and removed.") + else: + self._pedb._logger.info("Self-intersection not found.") + return True + + def illegal_net_names(self, fix=False): + """Find and fix illegal net names.""" + pattern = r"[\(\)\\\/:;*?<>\'\"|`~$]" + + nets = self._pedb.nets.nets + + renamed_nets = [] + for net, val in nets.items(): + if re.findall(pattern, net): + renamed_nets.append(net) + if fix: + new_name = re.sub(pattern, "_", net) + val.name = new_name + + self._pedb._logger.info("Found {} illegal net names.".format(len(renamed_nets))) + return + + def illegal_rlc_values(self, fix=False): + """Find and fix RLC illegal values.""" + inductors = self._pedb.components.inductors + + temp = [] + for k, v in inductors.items(): + model = v.component_property.model + if not len(model.pin_pairs): # pragma: no cover + temp.append(k) + if fix: + v.rlc_values = [0, 1, 0] + self._pedb._logger.info(f"Found {len(temp)} inductors have no value.") + return diff --git a/src/pyedb/grpc/database/modeler.py b/src/pyedb/grpc/database/modeler.py new file mode 100644 index 0000000000..83be6986fe --- /dev/null +++ b/src/pyedb/grpc/database/modeler.py @@ -0,0 +1,1468 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains these classes: `EdbLayout` and `Shape`. +""" +import math + +from ansys.edb.core.geometry.arc_data import ArcData as GrpcArcData +from ansys.edb.core.geometry.point_data import PointData as GrpcPointData +from ansys.edb.core.geometry.polygon_data import ( + PolygonSenseType as GrpcPolygonSenseType, +) +from ansys.edb.core.geometry.polygon_data import PolygonData as GrpcPolygonData +from ansys.edb.core.hierarchy.pin_group import PinGroup as GrpcPinGroup +from ansys.edb.core.inner.exceptions import InvalidArgumentException +from ansys.edb.core.primitive.primitive import ( + RectangleRepresentationType as GrpcRectangleRepresentationType, +) +from ansys.edb.core.primitive.primitive import BondwireType as GrpcBondwireType +from ansys.edb.core.primitive.primitive import PathCornerType as GrpcPathCornerType +from ansys.edb.core.primitive.primitive import PathEndCapType as GrpcPathEndCapType +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.grpc.database.primitive.bondwire import Bondwire +from pyedb.grpc.database.primitive.circle import Circle +from pyedb.grpc.database.primitive.path import Path +from pyedb.grpc.database.primitive.polygon import Polygon +from pyedb.grpc.database.primitive.primitive import Primitive +from pyedb.grpc.database.primitive.rectangle import Rectangle +from pyedb.grpc.database.utility.layout_statistics import LayoutStatistics + + +class Modeler(object): + """Manages EDB methods for primitives management accessible from `Edb.modeler` property. + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", edbversion="2021.2") + >>> edb_layout = edbapp.modeler + """ + + def __getitem__(self, name): + """Get a layout instance from the Edb project. + + Parameters + ---------- + name : str, int + + Returns + ------- + :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent` + + """ + for i in self.primitives: + if ( + (isinstance(name, str) and i.aedt_name == name) + or (isinstance(name, str) and i.aedt_name == name.replace("__", "_")) + or (isinstance(name, int) and i.id == name) + ): + return i + self._pedb.logger.error("Primitive not found.") + return + + def __init__(self, p_edb): + self._pedb = p_edb + self._primitives = [] + + @property + def _edb(self): + return self._pedb + + @property + def _logger(self): + """Logger.""" + return self._pedb.logger + + @property + def _active_layout(self): + return self._pedb.active_layout + + @property + def _layout(self): + return self._pedb.layout + + @property + def _cell(self): + return self._pedb.active_cell + + @property + def db(self): + """Db object.""" + return self._pedb.active_db + + @property + def layers(self): + """Dictionary of layers. + + Returns + ------- + dict + Dictionary of layers. + """ + return self._pedb.stackup.layers + + def get_primitive(self, primitive_id): + """Retrieve primitive from give id. + + Parameters + ---------- + primitive_id : int + Primitive id. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` + List of primitives. + """ + for p in self._layout.primitives: + if p.id == primitive_id: + return self.__mapping_primitive_type(p) + for p in self._layout.primitives: + for v in p.voids: + if v.id == primitive_id: + return self.__mapping_primitive_type(v) + + def __mapping_primitive_type(self, primitive): + from ansys.edb.core.primitive.primitive import ( + PrimitiveType as GrpcPrimitiveType, + ) + + if primitive.primitive_type == GrpcPrimitiveType.POLYGON: + return Polygon(self._pedb, primitive) + elif primitive.primitive_type == GrpcPrimitiveType.PATH: + return Path(self._pedb, primitive) + elif primitive.primitive_type == GrpcPrimitiveType.RECTANGLE: + return Rectangle(self._pedb, primitive) + elif primitive.primitive_type == GrpcPrimitiveType.CIRCLE: + return Circle(self._pedb, primitive) + elif primitive.primitive_type == GrpcPrimitiveType.BONDWIRE: + return Bondwire(self._pedb, primitive) + else: + return False + + @property + def primitives(self): + """Primitives. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` + List of primitives. + """ + return [self.__mapping_primitive_type(prim) for prim in self._pedb.layout.primitives] + + @property + def polygons_by_layer(self): + """Primitives with layer names as keys. + + Returns + ------- + dict + Dictionary of primitives with layer names as keys. + """ + _primitives_by_layer = {} + for lay in self.layers: + _primitives_by_layer[lay] = self.get_polygons_by_layer(lay) + return _primitives_by_layer + + @property + def primitives_by_net(self): + """Primitives with net names as keys. + + Returns + ------- + dict + Dictionary of primitives with nat names as keys. + """ + _prim_by_net = {} + for net, net_obj in self._pedb.nets.nets.items(): + _prim_by_net[net] = [i for i in net_obj.primitives] + return _prim_by_net + + @property + def primitives_by_layer(self): + """Primitives with layer names as keys. + + Returns + ------- + dict + Dictionary of primitives with layer names as keys. + """ + _primitives_by_layer = {} + for lay in self.layers: + _primitives_by_layer[lay] = [] + for lay in self._pedb.stackup.non_stackup_layers: + _primitives_by_layer[lay] = [] + for i in self._layout.primitives: + try: + lay = i.layer.name + if lay in _primitives_by_layer: + _primitives_by_layer[lay].append(Primitive(self._pedb, i)) + except (InvalidArgumentException, AttributeError): + pass + return _primitives_by_layer + + @property + def rectangles(self): + """Rectangles. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` + List of rectangles. + + """ + return [Rectangle(self._pedb, i) for i in self.primitives if i.type == "rectangle"] + + @property + def circles(self): + """Circles. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` + List of circles. + + """ + return [Circle(self._pedb, i) for i in self.primitives if i.type == "circle"] + + @property + def paths(self): + """Paths. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` + List of paths. + """ + return [Path(self._pedb, i) for i in self.primitives if i.type == "path"] + + @property + def polygons(self): + """Polygons. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` + List of polygons. + """ + return [Polygon(self._pedb, i) for i in self.primitives if i.type == "polygon"] + + def get_polygons_by_layer(self, layer_name, net_list=None): + """Retrieve polygons by a layer. + + Parameters + ---------- + layer_name : str + Name of the layer. + net_list : list, optional + List of net names. + + Returns + ------- + list + List of primitive objects. + """ + objinst = [] + for el in self.polygons: + if el.layer.name == layer_name: + if not el.net.is_null: + if net_list and el.net.name in net_list: + objinst.append(el) + else: + objinst.append(el) + return objinst + + def get_primitive_by_layer_and_point(self, point=None, layer=None, nets=None): + """Return primitive given coordinate point [x, y], layer name and nets. + + Parameters + ---------- + point : list + Coordinate [x, y] + + layer : list or str, optional + list of layer name or layer name applied on filter. + + nets : list or str, optional + list of net name or single net name applied on filter + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` + List of primitives, polygons, paths and rectangles. + """ + from ansys.edb.core.primitive.primitive import Circle as GrpcCircle + from ansys.edb.core.primitive.primitive import Path as GrpcPath + from ansys.edb.core.primitive.primitive import Polygon as GrpcPolygon + from ansys.edb.core.primitive.primitive import Rectangle as GrpcRectangle + + if isinstance(layer, str) and layer not in list(self._pedb.stackup.signal_layers.keys()): + layer = None + if not isinstance(point, list) and len(point) == 2: + self._logger.error("Provided point must be a list of two values") + return False + pt = GrpcPointData(point) + if isinstance(nets, str): + nets = [nets] + elif nets and not isinstance(nets, list) and len(nets) == len([net for net in nets if isinstance(net, str)]): + _nets = [] + for net in nets: + if net not in self._pedb.nets: + self._logger.error( + f"Net {net} used to find primitive from layer point and net not found, skipping it." + ) + else: + _nets.append(self._pedb.nets[net]) + if _nets: + nets = _nets + if not isinstance(layer, list) and layer: + layer = [layer] + _obj_instances = self._pedb.layout_instance.query_layout_obj_instances( + layer_filter=layer, net_filter=nets, spatial_filter=pt + ) + returned_obj = [] + for inst in _obj_instances: + primitive = inst.layout_obj.cast() + if isinstance(primitive, GrpcPath): + returned_obj.append(Path(self._pedb, primitive)) + elif isinstance(primitive, GrpcPolygon): + returned_obj.append(Polygon(self._pedb, primitive)) + elif isinstance(primitive, GrpcRectangle): + returned_obj.append(Rectangle(self._pedb, primitive)) + elif isinstance(primitive, GrpcCircle): + returned_obj.append(Circle(self._pedb, primitive)) + return returned_obj + + @staticmethod + def get_polygon_bounding_box(polygon): + """Retrieve a polygon bounding box. + + Parameters + ---------- + polygon : + Name of the polygon. + + Returns + ------- + list + List of bounding box coordinates in the format ``[-x, -y, +x, +y]``. + + Examples + -------- + >>> poly = database.modeler.get_polygons_by_layer("GND") + >>> bounding = database.modeler.get_polygon_bounding_box(poly[0]) + """ + bounding_box = polygon.polygon_data.bbox() + return [ + bounding_box[0].x.value, + bounding_box[0].y.value, + bounding_box[1].x.value, + bounding_box[1].y.value, + ] + + @staticmethod + def get_polygon_points(polygon): + """Retrieve polygon points. + + .. note:: + For arcs, one point is returned. + + Parameters + ---------- + polygon : + class: `dotnet.database.edb_data.primitives_data.Primitive` + + Returns + ------- + list + List of tuples. Each tuple provides x, y point coordinate. If the length of two consecutives tuples + from the list equals 2, a segment is defined. The first tuple defines the starting point while the second + tuple the ending one. If the length of one tuple equals one, that means a polyline is defined and the value + is giving the arc height. Therefore to polyline is defined as starting point for the tuple + before in the list, the current one the arc height and the tuple after the polyline ending point. + + Examples + -------- + + >>> poly = database.modeler.get_polygons_by_layer("GND") + >>> points = database.modeler.get_polygon_points(poly[0]) + + """ + points = [] + i = 0 + continue_iterate = True + prev_point = None + while continue_iterate: + try: + point = polygon.polygon_data.points[i] + if prev_point != point: + if point.is_arc: + points.append([point.x.value]) + else: + points.append([point.x.value, point.y.value]) + prev_point = point + i += 1 + else: + continue_iterate = False + except: + continue_iterate = False + return points + + def parametrize_polygon(self, polygon, selection_polygon, offset_name="offsetx", origin=None): + """Parametrize pieces of a polygon based on another polygon. + + Parameters + ---------- + polygon : + Name of the polygon. + selection_polygon : + Polygon to use as a filter. + offset_name : str, optional + Name of the offset to create. The default is ``"offsetx"``. + origin : list, optional + List of the X and Y origins, which impacts the vector + computation and is needed to determine expansion direction. + The default is ``None``, in which case the vector is + computed from the polygon's center. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + + def calc_slope(point, origin): + if point[0] - origin[0] != 0: + slope = math.atan((point[1] - origin[1]) / (point[0] - origin[0])) + xcoeff = math.sin(slope) + ycoeff = math.cos(slope) + + else: + if point[1] > 0: + xcoeff = 0 + ycoeff = 1 + else: + xcoeff = 0 + ycoeff = -1 + if ycoeff > 0: + ycoeff = "+" + str(ycoeff) + else: + ycoeff = str(ycoeff) + if xcoeff > 0: + xcoeff = "+" + str(xcoeff) + else: + xcoeff = str(xcoeff) + return xcoeff, ycoeff + + selection_polygon_data = selection_polygon.polygon_data + polygon_data = polygon.polygon_data + bound_center = polygon_data.bounding_circle()[0] + bound_center2 = selection_polygon_data.bounding_circle()[0] + center = [bound_center.x.value, bound_center.y.value] + center2 = [bound_center2.x.value, bound_center2.y.value] + x1, y1 = calc_slope(center2, center) + + if not origin: + origin = [center[0] + float(x1) * 10000, center[1] + float(y1) * 10000] + self._pedb.add_design_variable(offset_name, 0.0, is_parameter=True) + i = 0 + continue_iterate = True + prev_point = None + while continue_iterate: + try: + point = polygon_data.points[i] + if prev_point != point: + check_inside = selection_polygon_data.is_inside(point) + if check_inside: + xcoeff, ycoeff = calc_slope([point.x.value, point.x.value], origin) + + new_points = GrpcPointData( + [ + GrpcValue(str(point.x.value) + f"{xcoeff}*{offset_name}"), + GrpcValue(str(point.y.value) + f"{ycoeff}*{offset_name}"), + ] + ) + polygon_data.points[i] = new_points + prev_point = point + i += 1 + else: + continue_iterate = False + except: + continue_iterate = False + polygon.polygon_data = polygon_data + return True + + def _create_path( + self, + points, + layer_name, + width=1, + net_name="", + start_cap_style="Round", + end_cap_style="Round", + corner_style="Round", + ): + """ + Create a path based on a list of points. + + Parameters + ---------- + points: .:class:`dotnet.database.layout.Shape` + List of points. + layer_name : str + Name of the layer on which to create the path. + width : float, optional + Width of the path. The default is ``1``. + net_name: str, optional + Name of the net. The default is ``""``. + start_cap_style : str, optional + Style of the cap at its start. Options are ``"Round"``, + ``"Extended", `` and ``"Flat"``. The default is + ``"Round". + end_cap_style : str, optional + Style of the cap at its end. Options are ``"Round"``, + ``"Extended", `` and ``"Flat"``. The default is + ``"Round". + corner_style : str, optional + Style of the corner. Options are ``"Round"``, + ``"Sharp"`` and ``"Mitered"``. The default is ``"Round". + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` + ``True`` when successful, ``False`` when failed. + """ + net = self._pedb.nets.find_or_create_net(net_name) + if start_cap_style.lower() == "round": + start_cap_style = GrpcPathEndCapType.ROUND + elif start_cap_style.lower() == "extended": + start_cap_style = GrpcPathEndCapType.EXTENDED + else: + start_cap_style = GrpcPathEndCapType.FLAT + if end_cap_style.lower() == "round": + end_cap_style = GrpcPathEndCapType.ROUND + elif end_cap_style.lower() == "extended": + end_cap_style = GrpcPathEndCapType.EXTENDED + else: + end_cap_style = GrpcPathEndCapType.FLAT + if corner_style.lower() == "round": + corner_style = GrpcPathEndCapType.ROUND + elif corner_style.lower() == "sharp": + corner_style = GrpcPathCornerType.SHARP + else: + corner_style = GrpcPathCornerType.MITER + _points = [] + for pt in points: + _pt = [] + for coord in pt: + coord = GrpcValue(coord, self._pedb.active_cell) + _pt.append(coord) + _points.append(_pt) + points = _points + + width = GrpcValue(width, self._pedb.active_cell) + + polygon_data = GrpcPolygonData(points=[GrpcPointData(i) for i in points]) + path = Path.create( + layout=self._active_layout, + layer=layer_name, + net=net, + width=width, + end_cap1=start_cap_style, + end_cap2=end_cap_style, + corner_style=corner_style, + points=polygon_data, + ) + if path.is_null: # pragma: no cover + self._logger.error("Null path created") + return False + return Path(self._pedb, path) + + def create_trace( + self, + path_list, + layer_name, + width=1, + net_name="", + start_cap_style="Round", + end_cap_style="Round", + corner_style="Round", + ): + """ + Create a trace based on a list of points. + + Parameters + ---------- + path_list : list + List of points. + layer_name : str + Name of the layer on which to create the path. + width : float, optional + Width of the path. The default is ``1``. + net_name : str, optional + Name of the net. The default is ``""``. + start_cap_style : str, optional + Style of the cap at its start. Options are ``"Round"``, + ``"Extended",`` and ``"Flat"``. The default is + ``"Round"``. + end_cap_style : str, optional + Style of the cap at its end. Options are ``"Round"``, + ``"Extended",`` and ``"Flat"``. The default is + ``"Round"``. + corner_style : str, optional + Style of the corner. Options are ``"Round"``, + ``"Sharp"`` and ``"Mitered"``. The default is ``"Round"``. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` + """ + + primitive = self._create_path( + points=path_list, + layer_name=layer_name, + net_name=net_name, + width=width, + start_cap_style=start_cap_style, + end_cap_style=end_cap_style, + corner_style=corner_style, + ) + + return primitive + + def create_polygon(self, points, layer_name, voids=[], net_name=""): + """Create a polygon based on a list of points and voids. + + Parameters + ---------- + points : list of points or PolygonData. + - [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 + List of shape objects for voids or points that creates the shapes. The default is``[]``. + net_name : str, optional + Name of the net. The default is ``""``. + + Returns + ------- + bool, :class:`dotnet.database.edb_data.primitives.Primitive` + Polygon when successful, ``False`` when failed. + """ + net = self._pedb.nets.find_or_create_net(net_name) + if isinstance(points, list): + new_points = [] + for idx, i in enumerate(points): + new_points.append( + GrpcPointData([GrpcValue(i[0], self._pedb.active_cell), GrpcValue(i[1], self._pedb.active_cell)]) + ) + polygon_data = GrpcPolygonData(points=new_points) + + elif isinstance(points, GrpcPolygonData): + polygon_data = points + else: + polygon_data = points + if not polygon_data.points: + self._logger.error("Failed to create main shape polygon data") + return False + for void in voids: + if isinstance(void, list): + void_polygon_data = GrpcPolygonData(points=void) + else: + void_polygon_data = void.polygon_data + if not void_polygon_data.points: + self._logger.error("Failed to create void polygon data") + return False + polygon_data.holes.append(void_polygon_data) + polygon = Polygon.create(layout=self._active_layout, layer=layer_name, net=net, polygon_data=polygon_data) + if polygon.is_null or polygon_data is False: # pragma: no cover + self._logger.error("Null polygon created") + return False + return Polygon(self._pedb, polygon) + + def create_rectangle( + self, + layer_name, + net_name="", + lower_left_point="", + upper_right_point="", + center_point="", + width="", + height="", + representation_type="lower_left_upper_right", + corner_radius="0mm", + rotation="0deg", + ): + """Create rectangle. + + Parameters + ---------- + layer_name : str + Name of the layer on which to create the rectangle. + net_name : str + Name of the net. The default is ``""``. + lower_left_point : list + Lower left point when ``representation_type="lower_left_upper_right"``. The default is ``""``. + upper_right_point : list + Upper right point when ``representation_type="lower_left_upper_right"``. The default is ``""``. + center_point : list + Center point when ``representation_type="center_width_height"``. The default is ``""``. + width : str + Width of the rectangle when ``representation_type="center_width_height"``. The default is ``""``. + height : str + Height of the rectangle when ``representation_type="center_width_height"``. The default is ``""``. + representation_type : str, optional + Type of the rectangle representation. The default is ``lower_left_upper_right``. Options are + ``"lower_left_upper_right"`` and ``"center_width_height"``. + corner_radius : str, optional + Radius of the rectangle corner. The default is ``"0mm"``. + rotation : str, optional + Rotation of the rectangle. The default is ``"0deg"``. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` + Rectangle when successful, ``False`` when failed. + """ + edb_net = self._pedb.nets.find_or_create_net(net_name) + if representation_type == "lower_left_upper_right": + rep_type = GrpcRectangleRepresentationType.LOWER_LEFT_UPPER_RIGHT + rect = Rectangle.create( + layout=self._active_layout, + layer=layer_name, + net=edb_net, + rep_type=rep_type, + param1=GrpcValue(lower_left_point[0]), + param2=GrpcValue(lower_left_point[1]), + param3=GrpcValue(upper_right_point[0]), + param4=GrpcValue(upper_right_point[1]), + corner_rad=GrpcValue(corner_radius), + rotation=GrpcValue(rotation), + ) + else: + rep_type = GrpcRectangleRepresentationType.CENTER_WIDTH_HEIGHT + if isinstance(width, str): + if width in self._pedb.variables: + width = GrpcValue(width, self._pedb.active_cell) + else: + width = GrpcValue(width) + else: + width = GrpcValue(width) + if isinstance(height, str): + if height in self._pedb.variables: + height = GrpcValue(height, self._pedb.active_cell) + else: + height = GrpcValue(width) + else: + height = GrpcValue(width) + rect = Rectangle.create( + layout=self._active_layout, + layer=layer_name, + net=edb_net, + rep_type=rep_type, + param1=GrpcValue(center_point[0]), + param2=GrpcValue(center_point[1]), + param3=GrpcValue(width), + param4=GrpcValue(height), + corner_rad=GrpcValue(corner_radius), + rotation=GrpcValue(rotation), + ) + if not rect.is_null: + return Rectangle(self._pedb, rect) + return False + + def create_circle(self, layer_name, x, y, radius, net_name=""): + """Create a circle on a specified layer. + + Parameters + ---------- + layer_name : str + Name of the layer. + x : float + Position on the X axis. + y : float + Position on the Y axis. + radius : float + Radius of the circle. + net_name : str, optional + Name of the net. The default is ``None``, in which case the + default name is assigned. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.primitives_data.Primitive` + Objects of the circle created when successful. + """ + edb_net = self._pedb.nets.find_or_create_net(net_name) + + circle = Circle.create( + layout=self._active_layout, + layer=layer_name, + net=edb_net, + center_x=GrpcValue(x), + center_y=GrpcValue(y), + radius=GrpcValue(radius), + ) + if not circle.is_null: + return Circle(self._pedb, circle) + return False + + def delete_primitives(self, net_names): + """Delete primitives by net names. + + Parameters + ---------- + net_names : str, list + Names of the nets to delete. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + References + ---------- + + >>> Edb.modeler.delete_primitives(net_names=["GND"]) + """ + if not isinstance(net_names, list): # pragma: no cover + net_names = [net_names] + + for p in self.primitives[:]: + if p.net_name in net_names: + p.delete() + return True + + def get_primitives(self, net_name=None, layer_name=None, prim_type=None, is_void=False): + """Get primitives by conditions. + + Parameters + ---------- + net_name : str, optional + Set filter on net_name. Default is `None`. + layer_name : str, optional + Set filter on layer_name. Default is `None`. + prim_type : str, optional + Set filter on primitive type. Default is `None`. + is_void : bool + Set filter on is_void. Default is 'False' + Returns + ------- + list + List of filtered primitives + """ + prims = [] + for el in self.primitives: + if not el.primitive_type: + continue + if net_name: + if not el.net.name == net_name: + continue + if layer_name: + if not el.layer.name == layer_name: + continue + if prim_type: + if not el.primitive_type.name.lower() == prim_type: + continue + if not el.is_void == is_void: + continue + prims.append(el) + return prims + + def fix_circle_void_for_clipping(self): + """Fix issues when circle void are clipped due to a bug in EDB. + + Returns + ------- + bool + ``True`` when successful, ``False`` when no changes were applied. + """ + for void_circle in self.circles: + if not void_circle.is_void: + continue + circ_params = void_circle.get_parameters() + + cloned_circle = Circle.create( + layout=self._active_layout, + layer=void_circle.layer_name, + net=void_circle.net, + center_x=GrpcValue(circ_params[0]), + center_y=GrpcValue(circ_params[1]), + radius=GrpcValue(circ_params[2]), + ) + if not cloned_circle.is_null: + cloned_circle.is_negative = True + void_circle.delete() + return True + + @staticmethod + def add_void(shape, void_shape): + """Add a void into a shape. + + Parameters + ---------- + shape : Polygon + Shape of the main object. + void_shape : list, Path + Shape of the voids. + """ + flag = False + if not isinstance(void_shape, list): + void_shape = [void_shape] + for void in void_shape: + if isinstance(void, Primitive): + shape._edb_object.add_void(void) + flag = True + else: + shape._edb_object.add_void(void) + flag = True + if not flag: + return flag + return True + + def shape_to_polygon_data(self, shape): + """Convert a shape to polygon data. + + Parameters + ---------- + shape : :class:`pyedb.dotnet.database.modeler.Modeler.Shape` + Type of the shape to convert. Options are ``"rectangle"`` and ``"polygon"``. + """ + if shape.type == "polygon": + return self._createPolygonDataFromPolygon(shape) + elif shape.type == "rectangle": + return self._createPolygonDataFromRectangle(shape) + else: + self._logger.error( + "Unsupported shape type %s when creating a polygon primitive.", + shape.type, + ) + return None + + def _createPolygonDataFromPolygon(self, shape): + points = shape.points + if not self._validatePoint(points[0]): + self._logger.error("Error validating point.") + return None + arcs = [] + is_parametric = False + for i in range(len(points) - 1): + if i == 0: + startPoint = points[-1] + endPoint = points[i] + else: + startPoint = points[i - 1] + endPoint = points[i] + + if not self._validatePoint(endPoint): + return None + startPoint = [GrpcValue(i) for i in startPoint] + endPoint = [GrpcValue(i) for i in endPoint] + if len(endPoint) == 2: + is_parametric = ( + is_parametric + or startPoint[0].is_parametric + or startPoint[1].is_parametric + or endPoint[0].is_parametric + or endPoint[1].is_parametric + ) + arc = GrpcArcData( + GrpcPointData([startPoint[0], startPoint[1]]), GrpcPointData([endPoint[0], endPoint[1]]) + ) + arcs.append(arc) + elif len(endPoint) == 3: + is_parametric = ( + is_parametric + or startPoint[0].is_parametric + or startPoint[1].is_parametric + or endPoint[0].is_parametric + or endPoint[1].is_parametric + or endPoint[2].is_parametric + ) + arc = GrpcArcData( + GrpcPointData([startPoint[0], startPoint[1]]), + GrpcPointData([endPoint[0], endPoint[1]]), + kwarg={"height": endPoint[2]}, + ) + arcs.append(arc) + elif len(endPoint) == 5: + is_parametric = ( + is_parametric + or startPoint[0].is_parametric + or startPoint[1].is_parametric + or endPoint[0].is_parametric + or endPoint[1].is_parametric + or endPoint[3].is_parametric + or endPoint[4].is_parametric + ) + if endPoint[2].is_cw: + rotationDirection = GrpcPolygonSenseType.SENSE_CW + elif endPoint[2].is_ccw: + rotationDirection = GrpcPolygonSenseType.SENSE_CCW + else: + self._logger.error("Invalid rotation direction %s is specified.", endPoint[2]) + return None + arc = GrpcArcData( + GrpcPointData(startPoint), + GrpcPointData(endPoint), + ) + # arc.direction = rotationDirection, + # arc.center = GrpcPointData([endPoint[3], endPoint[4]]), + arcs.append(arc) + polygon = GrpcPolygonData(arcs=arcs) + if not is_parametric: + return polygon + else: + k = 0 + for pt in points: + point = [GrpcValue(i) for i in pt] + new_points = GrpcPointData(point) + if len(point) > 2: + k += 1 + polygon.set_point(k, new_points) + k += 1 + return polygon + + def _validatePoint(self, point, allowArcs=True): + if len(point) == 2: + 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 + return True + elif len(point) == 3: + if not allowArcs: # pragma: no cover + self._logger.error("Arc found but arcs are not allowed in _validatePoint.") + return False + if not isinstance(point[0], (int, float, str)): # pragma: no cover + self._logger.error("Point X value must be a number.") + return False + if not isinstance(point[1], (int, float, str)): # pragma: no cover + self._logger.error("Point Y value must be a number.") + return False + if not isinstance(point[1], (int, float, str)): # pragma: no cover + self._logger.error("Invalid point height.") + return False + return True + elif len(point) == 5: + if not allowArcs: # pragma: no cover + self._logger.error("Arc found but arcs are not allowed in _validatePoint.") + return False + if not isinstance(point[0], (int, float, str)): # pragma: no cover + self._logger.error("Point X value must be a number.") + return False + if not isinstance(point[1], (int, float, str)): # pragma: no cover + self._logger.error("Point Y value must be a number.") + return False + if not isinstance(point[2], str) or point[2] not in ["cw", "ccw"]: + self._logger.error("Invalid rotation direction {} is specified.") + return False + if not isinstance(point[3], (int, float, str)): # pragma: no cover + self._logger.error("Arc center point X value must be a number.") + return False + if not isinstance(point[4], (int, float, str)): # pragma: no cover + self._logger.error("Arc center point Y value must be a number.") + return False + return True + else: # pragma: no cover + self._logger.error("Arc point descriptor has incorrect number of elements (%s)", len(point)) + return False + + def _createPolygonDataFromRectangle(self, shape): + # if not self._validatePoint(shape.pointA, False) or not self._validatePoint(shape.pointB, False): + # return None + # pointA = GrpcPointData(pointA[0]), self._get_edb_value(shape.pointA[1]) + # ) + # pointB = self._edb.geometry.point_data( + # self._get_edb_value(shape.pointB[0]), self._get_edb_value(shape.pointB[1]) + # ) + # return self._edb.geometry.polygon_data.create_from_bbox((pointA, pointB)) + pass + + def parametrize_trace_width( + self, + nets_name, + layers_name=None, + parameter_name="trace_width", + variable_value=None, + ): + """Parametrize a Trace on specific layer or all stackup. + + Parameters + ---------- + nets_name : str, list + name of the net or list of nets to parametrize. + layers_name : str, optional + name of the layer or list of layers to which the net to parametrize has to be included. + parameter_name : str, optional + name of the parameter to create. + variable_value : str, float, optional + value with units of parameter to create. + If None, the first trace width of Net will be used as parameter value. + + Returns + ------- + bool + """ + if isinstance(nets_name, str): + nets_name = [nets_name] + if isinstance(layers_name, str): + layers_name = [layers_name] + for net_name in nets_name: + for p in self.paths: + _parameter_name = f"{parameter_name}_{p.id}" + if not p.net.is_null: + if p.net.name == net_name: + if not layers_name: + if not variable_value: + variable_value = p.width + self._pedb.active_cell.add_variable( + name=_parameter_name, value=GrpcValue(variable_value), is_param=True + ) + p.width = GrpcValue(_parameter_name, self._pedb.active_cell) + elif p.layer.name in layers_name: + if not variable_value: + variable_value = p.width + self._pedb.add_design_variable(parameter_name, variable_value, True) + p.width = GrpcValue(_parameter_name, self._pedb.active_cell) + return True + + def unite_polygons_on_layer(self, layer_name=None, delete_padstack_gemometries=False, net_names_list=[]): + """Try to unite all Polygons on specified layer. + + Parameters + ---------- + layer_name : str, optional + Name of layer name to unite objects on. The default is ``None``, in which case all layers are taken. + delete_padstack_gemometries : bool, optional + Whether to delete all padstack geometries. The default is ``False``. + net_names_list : list[str] : optional + Net names list filter. The default is ``[]``, in which case all nets are taken. + + Returns + ------- + bool + ``True`` is successful. + """ + if isinstance(layer_name, str): + layer_name = [layer_name] + if not layer_name: + layer_name = list(self._pedb.stackup.signal_layers.keys()) + + for lay in layer_name: + self._logger.info(f"Uniting Objects on layer {lay}.") + poly_by_nets = {} + all_voids = [] + list_polygon_data = [] + delete_list = [] + if lay in list(self.polygons_by_layer.keys()): + for poly in self.polygons_by_layer[lay]: + poly = poly + if not poly.net.name in list(poly_by_nets.keys()): + if poly.net.name: + poly_by_nets[poly.net.name] = [poly] + else: + if poly.net.name: + poly_by_nets[poly.net.name].append(poly) + for net in poly_by_nets: + if net in net_names_list or not net_names_list: + for i in poly_by_nets[net]: + list_polygon_data.append(i.polygon_data) + delete_list.append(i) + all_voids.append(i.voids) + a = GrpcPolygonData.unite(list_polygon_data) + for item in a: + for v in all_voids: + for void in v: + if item.intersection_type(void.polygon_data) == 2: + item.add_hole(void.polygon_data) + self.create_polygon(item, layer_name=lay, voids=[], net_name=net) + for v in all_voids: + for void in v: + for poly in poly_by_nets[net]: # pragma no cover + if void.polygon_data.intersection_type(poly.polygon_data).value >= 2: + try: + id = delete_list.index(poly) + except ValueError: + id = -1 + if id >= 0: + delete_list.pop(id) + for poly in delete_list: + poly.delete() + + if delete_padstack_gemometries: + self._logger.info("Deleting Padstack Definitions") + for pad in self._pedb.padstacks.definitions: + p1 = self._pedb.padstacks.definitions[pad].edb_padstack.data + if len(p1.get_layer_names()) > 1: + self._pedb.padstacks.remove_pads_from_padstack(pad) + return True + + def defeature_polygon(self, poly, tolerance=0.001): + """Defeature the polygon based on the maximum surface deviation criteria. + + Parameters + ---------- + maximum_surface_deviation : float + poly : Edb Polygon primitive + Polygon to defeature. + tolerance : float, optional + Maximum tolerance criteria. The default is ``0.001``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + new_poly = poly.polygon_data.defeature(tol=tolerance) + if not new_poly.points: + self._pedb.logger.error( + f"Defeaturing on polygon {poly.id} returned empty polygon, tolerance threshold " f"might too large. " + ) + return False + poly.polygon_data = new_poly + return True + + def get_layout_statistics(self, evaluate_area=False, net_list=None): + """Return EDBStatistics object from a layout. + + Parameters + ---------- + + evaluate_area : optional bool + When True evaluates the layout metal surface, can take time-consuming, + avoid using this option on large design. + + Returns + ------- + + EDBStatistics object. + + """ + stat_model = LayoutStatistics() + stat_model.num_layers = len(list(self._pedb.stackup.layers.values())) + stat_model.num_capacitors = len(self._pedb.components.capacitors) + stat_model.num_resistors = len(self._pedb.components.resistors) + stat_model.num_inductors = len(self._pedb.components.inductors) + bbox = self._pedb._hfss.get_layout_bounding_box(self._active_layout) + stat_model._layout_size = bbox[2] - bbox[0], bbox[3] - bbox[1] + stat_model.num_discrete_components = ( + len(self._pedb.components.Others) + len(self._pedb.components.ICs) + len(self._pedb.components.IOs) + ) + stat_model.num_inductors = len(self._pedb.components.inductors) + stat_model.num_resistors = len(self._pedb.components.resistors) + stat_model.num_capacitors = len(self._pedb.components.capacitors) + stat_model.num_nets = len(self._pedb.nets.nets) + stat_model.num_traces = len(self._pedb.modeler.paths) + stat_model.num_polygons = len(self._pedb.modeler.polygons) + stat_model.num_vias = len(self._pedb.padstacks.instances) + stat_model.stackup_thickness = self._pedb.stackup.get_layout_thickness() + if evaluate_area: + outline_surface = stat_model.layout_size[0] * stat_model.layout_size[1] + if net_list: + netlist = list(self._pedb.nets.nets.keys()) + _poly = self._pedb.get_conformal_polygon_from_netlist(netlist) + else: + for layer in list(self._pedb.stackup.signal_layers.keys()): + surface = 0.0 + primitives = self.primitives_by_layer[layer] + for prim in primitives: + if prim.primitive_type.name == "PATH": + surface += Path(self._pedb, prim).length * prim.cast().width.value + if prim.primitive_type.name == "POLYGON": + surface += prim.polygon_data.area() + stat_model.occupying_surface[layer] = surface + stat_model.occupying_ratio[layer] = surface / outline_surface + return stat_model + + def create_bondwire( + self, + definition_name, + placement_layer, + width, + material, + start_layer_name, + start_x, + start_y, + end_layer_name, + end_x, + end_y, + net, + start_cell_instance_name=None, + end_cell_instance_name=None, + bondwire_type="jedec4", + ): + """Create a bondwire object. + + Parameters + ---------- + bondwire_type : :class:`BondwireType` + Type of bondwire: kAPDBondWire or kJDECBondWire types. + definition_name : str + Bondwire definition name. + placement_layer : str + Layer name this bondwire will be on. + width : :class:`Value ` + Bondwire width. + material : str + Bondwire material name. + start_layer_name : str + Name of start layer. + start_x : :class:`Value ` + X value of start point. + start_y : :class:`Value ` + Y value of start point. + end_layer_name : str + Name of end layer. + end_x : :class:`Value ` + X value of end point. + end_y : :class:`Value ` + Y value of end point. + net : str or :class:`Net ` or None + Net of the Bondwire. + start_cell_instance_name : str, optional + Cell instance name where the bondwire starts. + end_cell_instance_name : str, optional + Cell instance name where the bondwire ends. + + Returns + ------- + :class:`pyedb.dotnet.database.dotnet.primitive.BondwireDotNet` + Bondwire object created. + """ + from ansys.edb.core.hierarchy.cell_instance import ( + CellInstance as GrpcCellInstance, + ) + + start_cell_inst = None + end_cell_inst = None + cell_instances = {cell_inst.name: cell_inst for cell_inst in self._active_layout.cell_instances} + if start_cell_instance_name: + if start_cell_instance_name not in cell_instances: + start_cell_inst = GrpcCellInstance.create( + self._pedb.active_layout, start_cell_instance_name, ref=self._pedb.active_layout + ) + else: + start_cell_inst = cell_instances[start_cell_instance_name] + cell_instances = {cell_inst.name: cell_inst for cell_inst in self._active_layout.cell_instances} + if end_cell_instance_name: + if end_cell_instance_name not in cell_instances: + end_cell_inst = GrpcCellInstance.create( + self._pedb.active_layout, end_cell_instance_name, ref=self._pedb.active_layout + ) + else: + end_cell_inst = cell_instances[end_cell_instance_name] + + if bondwire_type == "jedec4": + bondwire_type = GrpcBondwireType.JEDEC4 + elif bondwire_type == "jedec5": + bondwire_type = GrpcBondwireType.JEDEC5 + elif bondwire_type == "apd": + bondwire_type = GrpcBondwireType.APD + else: + bondwire_type = GrpcBondwireType.JEDEC4 + bw = Bondwire.create( + layout=self._active_layout, + bondwire_type=bondwire_type, + definition_name=definition_name, + placement_layer=placement_layer, + width=GrpcValue(width), + material=material, + start_layer_name=start_layer_name, + start_x=GrpcValue(start_x), + start_y=GrpcValue(start_y), + end_layer_name=end_layer_name, + end_x=GrpcValue(end_x), + end_y=GrpcValue(end_y), + net=net, + end_context=end_cell_inst, + start_context=start_cell_inst, + ) + return Bondwire(self._pedb, bw) + + def create_pin_group( + self, + name: str, + pins_by_id=None, + pins_by_aedt_name=None, + pins_by_name=None, + ): + """Create a PinGroup. + + Parameters + name : str, + Name of the PinGroup. + pins_by_id : list[int] or None + List of pins by ID. + pins_by_aedt_name : list[str] or None + List of pins by AEDT name. + pins_by_name : list[str] or None + List of pins by name. + """ + # TODO move this method to components and merge with existing one + pins = {} + if pins_by_id: + if isinstance(pins_by_id, int): + pins_by_id = [pins_by_id] + for p in pins_by_id: + edb_pin = None + if p in self._pedb.padstacks.instances: + edb_pin = self._pedb.padstacks.instances[p] + if edb_pin and not p in pins: + pins[p] = edb_pin + if not pins_by_aedt_name: + pins_by_aedt_name = [] + if not pins_by_name: + pins_by_name = [] + if pins_by_aedt_name or pins_by_name: + if isinstance(pins_by_aedt_name, str): + pins_by_aedt_name = [pins_by_aedt_name] + if isinstance(pins_by_name, str): + pins_by_name = [pins_by_name] + p_inst = self._pedb.layout.padstack_instances + _pins = {pin.id: pin for pin in p_inst if pin.aedt_name in pins_by_aedt_name or pin.name in pins_by_name} + if not pins: + pins = _pins + else: + for id, pin in _pins.items(): + if not id in pins: + pins[id] = pin + if not pins: + self._logger.error("No pin found.") + return False + pins = list(pins.values()) + obj = GrpcPinGroup.create(layout=self._pedb.active_layout, name=name, padstack_instances=pins) + if obj.is_null: + raise RuntimeError(f"Failed to create pin group {name}.") + else: + net_obj = [i.net for i in pins if not i.net.is_null] + if net_obj: + obj.net = net_obj[0] + return self._pedb.siwave.pin_groups[name] diff --git a/src/pyedb/grpc/database/net.py b/src/pyedb/grpc/database/net.py new file mode 100644 index 0000000000..bb5771a23b --- /dev/null +++ b/src/pyedb/grpc/database/net.py @@ -0,0 +1,633 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import # noreorder + +import warnings + +from pyedb.common.nets import CommonNets +from pyedb.generic.general_methods import generate_unique_name +from pyedb.grpc.database.nets.net import Net +from pyedb.grpc.database.primitive.bondwire import Bondwire +from pyedb.grpc.database.primitive.path import Path +from pyedb.grpc.database.primitive.polygon import Polygon +from pyedb.misc.utilities import compute_arc_points + + +class Nets(CommonNets): + """Manages EDB methods for nets management accessible from `Edb.nets` property. + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", edbversion="2021.2") + >>> edb_nets = edbapp.nets + """ + + def __getitem__(self, name): + """Get nets from the Edb project. + + Parameters + ---------- + name : str, int + + Returns + ------- + :class:` .:class:`pyedb.grpc.database.nets_data.EDBNets` + + """ + return Net(self._pedb, Net.find_by_name(self._active_layout, name)) + + def __contains__(self, name): + """Determine if a net is named ``name`` or not. + + Parameters + ---------- + name : str + + Returns + ------- + bool + ``True`` when one of the net is named ``name``, ``False`` otherwise. + + """ + return name in self.nets + + def __init__(self, p_edb): + CommonNets.__init__(self, p_edb) + self._nets_by_comp_dict = {} + self._comps_by_nets_dict = {} + + @property + def _edb(self): + """ """ + return self._pedb + + @property + def _active_layout(self): + """ """ + return self._pedb.active_layout + + @property + def _layout(self): + """ """ + return self._pedb.layout + + @property + def _cell(self): + """ """ + return self._pedb.cell + + @property + def db(self): + """Db object.""" + return self._pedb.active_db + + @property + def _logger(self): + """Edb logger.""" + return self._pedb.logger + + @property + def nets(self): + """Nets. + + Returns + ------- + dict[str, :class:`pyedb.dotnet.database.edb_data.nets_data.EDBNetsData`] + Dictionary of nets. + """ + return {i.name: i for i in self._pedb.layout.nets} + + @property + def netlist(self): + """Return the cell netlist. + + Returns + ------- + list + Net names. + """ + return list(self.nets.keys()) + + @property + def signal(self): + """Signal nets. + + Returns + ------- + dict[str, :class:`pyedb.dotnet.database.edb_data.EDBNetsData`] + Dictionary of signal nets. + """ + nets = {} + for net, value in self.nets.items(): + if not value.is_power_ground: + nets[net] = value + return nets + + @property + def power(self): + """Power nets. + + Returns + ------- + dict[str, :class:`pyedb.dotnet.database.edb_data.EDBNetsData`] + Dictionary of power nets. + """ + nets = {} + for net, value in self.nets.items(): + if value.is_power_ground: + nets[net] = value + return nets + + def eligible_power_nets(self, threshold=0.3): + """Return a list of nets calculated by area to be eligible for PWR/Ground net classification. + It uses the same algorithm implemented in SIwave. + + Parameters + ---------- + threshold : float, optional + Area ratio used by the ``get_power_ground_nets`` method. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.EDBNetsData` + """ + pwr_gnd_nets = [] + for net in self._layout.nets[:]: + total_plane_area = 0.0 + total_trace_area = 0.0 + for primitive in net.primitives: + primitive = primitive + if isinstance(primitive, Bondwire): + continue + if isinstance(primitive, Path) or isinstance(primitive, Polygon): + total_plane_area += primitive.polygon_data.area() + if total_plane_area == 0.0: + continue + if total_trace_area == 0.0: + pwr_gnd_nets.append(Net(self._pedb, net)) + continue + if total_plane_area > 0.0 and total_trace_area > 0.0: + if total_plane_area / (total_plane_area + total_trace_area) > threshold: + pwr_gnd_nets.append(Net(self._pedb, net)) + return pwr_gnd_nets + + @property + def nets_by_components(self): + # type: () -> dict + """Get all nets for each component instance.""" + for comp, i in self._pedb.components.instances.items(): + self._nets_by_comp_dict[comp] = i.nets + return self._nets_by_comp_dict + + @property + def components_by_nets(self): + # type: () -> dict + """Get all component instances grouped by nets.""" + for comp, i in self._pedb.components.instances.items(): + for n in i.nets: + if n in self._comps_by_nets_dict: + self._comps_by_nets_dict[n].append(comp) + else: + self._comps_by_nets_dict[n] = [comp] + return self._comps_by_nets_dict + + def generate_extended_nets( + self, + resistor_below=10, + inductor_below=1, + capacitor_above=1, + exception_list=None, + include_signal=True, + include_power=True, + ): + # type: (int | float, int | float, int |float, list, bool, bool) -> list + """Get extended net and associated components. + + . deprecated:: pyedb 0.30.0 + Use :func:`pyedb.grpc.extended_nets.generate_extended_nets` instead. + + Parameters + ---------- + resistor_below : int, float, optional + Threshold of resistor value. Search extended net across resistors which has value lower than the threshold. + inductor_below : int, float, optional + Threshold of inductor value. Search extended net across inductances which has value lower than the + threshold. + capacitor_above : int, float, optional + Threshold of capacitor value. Search extended net across capacitors which has value higher than the + threshold. + exception_list : list, optional + List of components to bypass when performing threshold checks. Components + in the list are considered as serial components. The default is ``None``. + include_signal : str, optional + Whether to generate extended signal nets. The default is ``True``. + include_power : str, optional + Whether to generate extended power nets. The default is ``True``. + + Returns + ------- + list + List of all extended nets. + + Examples + -------- + >>> from pyedb import Edb + >>> app = Edb() + >>> app.nets.get_extended_nets() + """ + warnings.warn("Use new method :func:`edb.extended_nets.generate_extended_nets` instead.", DeprecationWarning) + self._pedb.extended_nets.generate_extended_nets( + resistor_below, inductor_below, capacitor_above, exception_list, include_signal, include_power + ) + + @staticmethod + def _get_points_for_plot(self, my_net_points): + """ + Get the points to be plotted. + """ + # fmt: off + x = [] + y = [] + for i, point in enumerate(my_net_points): + if not point.is_arc: + x.append(point.x.value) + y.append(point.y.value) + else: + arc_h = point.arc_height.value + p1 = [my_net_points[i - 1].x.value, my_net_points[i - 1].y.value] + if i + 1 < len(my_net_points): + p2 = [my_net_points[i + 1].X.ToDouble(), my_net_points[i + 1].Y.ToDouble()] + else: + p2 = [my_net_points[0].X.ToDouble(), my_net_points[0].Y.ToDouble()] + x_arc, y_arc = compute_arc_points(p1, p2, arc_h) + x.extend(x_arc) + y.extend(y_arc) + # i += 1 + # fmt: on + return x, y + + def classify_nets(self, power_nets=None, signal_nets=None): + """Reassign power/ground or signal nets based on list of nets. + + Parameters + ---------- + power_nets : str, list, optional + List of power nets to assign. Default is `None`. + signal_nets : str, list, optional + List of signal nets to assign. Default is `None`. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if isinstance(power_nets, str): + power_nets = [] + elif not power_nets: + power_nets = [] + if isinstance(signal_nets, str): + signal_nets = [] + elif not signal_nets: + signal_nets = [] + for net in power_nets: + if net in self.nets: + self.nets[net].is_power_ground = True + for net in signal_nets: + if net in self.nets: + self.nets[net].is_power_ground = False + return True + + def is_power_gound_net(self, netname_list): + """Determine if one of the nets in a list is power or ground. + + Parameters + ---------- + netname_list : list + List of net names. + + Returns + ------- + bool + ``True`` when one of the net names is ``"power"`` or ``"ground"``, ``False`` otherwise. + """ + if isinstance(netname_list, str): + netname_list = [netname_list] + power_nets_names = list(self.power.keys()) + for netname in netname_list: + if netname in power_nets_names: + return True + return False + + def get_dcconnected_net_list(self, ground_nets=["GND"], res_value=0.001): + """Get the nets connected to the direct current through inductors. + + .. note:: + Only inductors are considered. + + Parameters + ---------- + ground_nets : list, optional + List of ground nets. The default is ``["GND"]``. + + Returns + ------- + list + List of nets connected to DC through inductors. + """ + temp_list = [] + for _, comp_obj in self._pedb.components.inductors.items(): + numpins = comp_obj.numpins + + if numpins == 2: + nets = comp_obj.nets + if not set(nets).intersection(set(ground_nets)): + temp_list.append(set(nets)) + else: + pass + for _, comp_obj in self._pedb.components.resistors.items(): + numpins = comp_obj.numpins + + if numpins == 2 and comp_obj.res_value <= res_value: + nets = comp_obj.nets + if not set(nets).intersection(set(ground_nets)): + temp_list.append(set(nets)) + else: + pass + dcconnected_net_list = [] + + while not not temp_list: + s = temp_list.pop(0) + interseciton_flag = False + for i in temp_list: + if not not s.intersection(i): + i.update(s) + interseciton_flag = True + + if not interseciton_flag: + dcconnected_net_list.append(s) + + return dcconnected_net_list + + def get_powertree(self, power_net_name, ground_nets): + """Retrieve the power tree. + + Parameters + ---------- + power_net_name : str + Name of the power net. + ground_nets : + + + Returns + ------- + + """ + flag_in_ng = False + net_group = [] + for ng in self.get_dcconnected_net_list(ground_nets): + if power_net_name in ng: + flag_in_ng = True + net_group.extend(ng) + break + + if not flag_in_ng: + net_group.append(power_net_name) + + component_list = [] + rats = self._pedb.components.get_rats() + for net in net_group: + for el in rats: + if net in el["net_name"]: + i = 0 + for n in el["net_name"]: + if n == net: + df = [el["refdes"][i], el["pin_name"][i], net] + component_list.append(df) + i += 1 + + component_type = [] + for el in component_list: + refdes = el[0] + comp_type = self._pedb.components._cmp[refdes].type + component_type.append(comp_type) + el.append(comp_type) + + comp_partname = self._pedb.components._cmp[refdes].partname + el.append(comp_partname) + pins = self._pedb.components.get_pin_from_component(component=refdes, net_name=el[2]) + el.append("-".join([i.name for i in pins])) + + component_list_columns = [ + "refdes", + "pin_name", + "net_name", + "component_type", + "component_partname", + "pin_list", + ] + return component_list, component_list_columns, net_group + + def get_net_by_name(self, net_name): + """Find a net by name.""" + edb_net = Net.find_by_name(self._active_layout, net_name) + if edb_net is not None: + return edb_net + + def delete(self, netlist): + """Delete one or more nets from EDB. + + Parameters + ---------- + netlist : str or list + One or more nets to delete. + + Returns + ------- + list + List of nets that were deleted. + + Examples + -------- + + >>> deleted_nets = database.nets.delete(["Net1","Net2"]) + """ + if isinstance(netlist, str): + netlist = [netlist] + + self._pedb.modeler.delete_primitives(netlist) + self._pedb.padstacks.delete_padstack_instances(netlist) + + nets_deleted = [] + + for i in self._pedb.nets.nets.values(): + if i.name in netlist: + i.delete() + nets_deleted.append(i.name) + return nets_deleted + + def find_or_create_net(self, net_name="", start_with="", contain="", end_with=""): + """Find or create the net with the given name in the layout. + + Parameters + ---------- + net_name : str, optional + Name of the net to find or create. The default is ``""``. + + start_with : str, optional + All net name starting with the string. Not case-sensitive. + + contain : str, optional + All net name containing the string. Not case-sensitive. + + end_with : str, optional + All net name ending with the string. Not case-sensitive. + + Returns + ------- + object + Net Object. + """ + if not net_name and not start_with and not contain and not end_with: + net_name = generate_unique_name("NET_") + net = Net.create(self._active_layout, net_name) + return net + else: + if not start_with and not contain and not end_with: + net = Net.find_by_name(self._active_layout, net_name) + if net.is_null: + net = Net.create(self._active_layout, net_name) + return net + elif start_with: + nets_found = [self.nets[net] for net in list(self.nets.keys()) if net.lower().startswith(start_with)] + return nets_found + elif start_with and end_with: + nets_found = [ + self.nets[net] + for net in list(self.nets.keys()) + if net.lower().startswith(start_with) and net.lower().endswith(end_with) + ] + return nets_found + elif start_with and contain and end_with: + nets_found = [ + self.nets[net].net_object + for net in list(self.nets.keys()) + if net.lower().startswith(start_with) and net.lower().endswith(end_with) and contain in net.lower() + ] + return nets_found + elif start_with and contain: + nets_found = [ + self.nets[net] + for net in list(self.nets.keys()) + if net.lower().startswith(start_with) and contain in net.lower() + ] + return nets_found + elif contain and end_with: + nets_found = [ + self.nets[net] + for net in list(self.nets.keys()) + if net.lower().endswith(end_with) and contain in net.lower() + ] + return nets_found + elif end_with and not start_with and not contain: + nets_found = [self.nets[net] for net in list(self.nets.keys()) if net.lower().endswith(end_with)] + return nets_found + elif contain and not start_with and not end_with: + nets_found = [self.nets[net] for net in list(self.nets.keys()) if contain in net.lower()] + return nets_found + + def is_net_in_component(self, component_name, net_name): + """Check if a net belongs to a component. + + Parameters + ---------- + component_name : str + Name of the component. + net_name : str + Name of the net. + + Returns + ------- + bool + ``True`` if the net is found in component pins. + + """ + if component_name not in self._pedb.components.instances: + return False + for net in self._pedb.components.instances[component_name].nets: + if net_name == net: + return True + return False + + def find_and_fix_disjoint_nets( + self, net_list=None, keep_only_main_net=False, clean_disjoints_less_than=0.0, order_by_area=False + ): + """Find and fix disjoint nets from a given netlist. + + .. deprecated:: + Use new property :func:`edb.layout_validation.disjoint_nets` instead. + + Parameters + ---------- + net_list : str, list, optional + List of nets on which check disjoints. If `None` is provided then the algorithm will loop on all nets. + keep_only_main_net : bool, optional + Remove all secondary nets other than principal one (the one with more objects in it). Default is `False`. + clean_disjoints_less_than : bool, optional + Clean all disjoint nets with area less than specified area in square meters. Default is `0.0` to disable it. + order_by_area : bool, optional + Whether if the naming order has to be by number of objects (fastest) or area (slowest but more accurate). + Default is ``False``. + + Returns + ------- + List + New nets created. + + Examples + -------- + + >>> renamed_nets = database.nets.find_and_fix_disjoint_nets(["GND","Net2"]) + """ + warnings.warn("Use new function :func:`edb.layout_validation.disjoint_nets` instead.", DeprecationWarning) + return self._pedb.layout_validation.disjoint_nets( + net_list, keep_only_main_net, clean_disjoints_less_than, order_by_area + ) + + def merge_nets_polygons(self, net_names_list): + """Convert paths from net into polygons, evaluate all connected polygons and perform the merge. + + Parameters + ---------- + net_names_list : str or list[str] + Net name of list of net name. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + if isinstance(net_names_list, str): + net_names_list = [net_names_list] + return self._pedb.modeler.unite_polygons_on_layer(net_names_list=net_names_list) diff --git a/src/pyedb/grpc/database/nets/__init__.py b/src/pyedb/grpc/database/nets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/pyedb/grpc/database/nets/differential_pair.py b/src/pyedb/grpc/database/nets/differential_pair.py new file mode 100644 index 0000000000..9a3f2fd23e --- /dev/null +++ b/src/pyedb/grpc/database/nets/differential_pair.py @@ -0,0 +1,138 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import re + +from ansys.edb.core.net.differential_pair import ( + DifferentialPair as GrpcDifferentialPair, +) + +from pyedb.grpc.database.nets.net import Net + + +class DifferentialPairs: + def __init__(self, pedb): + self._pedb = pedb + + @property + def items(self): + """Extended nets. + + Returns + ------- + dict[str, :class:`pyedb.dotnet.database.edb_data.nets_data.EDBDifferentialPairData`] + Dictionary of extended nets. + """ + diff_pairs = {} + for diff_pair in self._pedb.layout.differential_pairs: + diff_pairs[diff_pair.name] = DifferentialPair(self._pedb, diff_pair) + return diff_pairs + + def create(self, name, net_p, net_n): + # type: (str, str, str) -> DifferentialPair + """ + + Parameters + ---------- + name : str + Name of the differential pair. + net_p : str + Name of the positive net. + net_n : str + Name of the negative net. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.nets_data.EDBDifferentialPairData` + """ + if name in self.items: + self._pedb.logger.error("{} already exists.".format(name)) + return False + GrpcDifferentialPair.create(layout=self._pedb.layout, name=name, pos_net=net_p, neg_net=net_n) + return self.items[name] + + def auto_identify(self, positive_differentiator="_P", negative_differentiator="_N"): + """Auto identify differential pairs by naming conversion. + + Parameters + ---------- + positive_differentiator: str, optional + Differentiator of the positive net. The default is ``"_P"``. + negative_differentiator: str, optional + Differentiator of the negative net. The default is ``"_N"``. + + Returns + ------- + list + A list containing identified differential pair names. + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", edbversion="2023.1") + >>> edb_nets = edbapp.differential_pairs.auto_identify() + """ + nets = self._pedb.nets.nets + pos_net = [] + neg_net = [] + for name, _ in nets.items(): + if name.endswith(positive_differentiator): + pos_net.append(name) + elif name.endswith(negative_differentiator): + neg_net.append(name) + else: + pass + + temp = [] + for p in pos_net: + pattern_p = r"^(.+){}$".format(positive_differentiator) + match_p = re.findall(pattern_p, p)[0] + + for n in neg_net: + pattern_n = r"^(.+){}$".format(negative_differentiator) + match_n = re.findall(pattern_n, n)[0] + + if match_p == match_n: + diff_name = "DIFF_{}".format(match_p) + self.create(diff_name, p, n) + temp.append(diff_name) + return temp + + +class DifferentialPair(GrpcDifferentialPair): + """Manages EDB functionalities for a primitive. + It inherits EDB object properties. + """ + + def __init__(self, pedb, edb_object): + super().__init__(edb_object.msg) + self._pedb = pedb + + @property + def positive_net(self): + """Positive Net.""" + return Net(self._pedb, super().positive_net) + + @property + def negative_net(self): + """Negative Net.""" + return Net(self._pedb, super().negative_net) diff --git a/src/pyedb/grpc/database/nets/extended_net.py b/src/pyedb/grpc/database/nets/extended_net.py new file mode 100644 index 0000000000..72d5c3ca0d --- /dev/null +++ b/src/pyedb/grpc/database/nets/extended_net.py @@ -0,0 +1,307 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.net.extended_net import ExtendedNet as GrpcExtendedNet + +from pyedb.grpc.database.nets.net import Net + + +class ExtendedNets: + def __init__(self, pedb): + self._pedb = pedb + + @property + def items(self): + """Extended nets. + + Returns + ------- + dict[str, :class:`pyedb.dotnet.database.edb_data.nets_data.EDBExtendedNetsData`] + Dictionary of extended nets. + """ + nets = {} + for extended_net in self._pedb.layout.extended_nets: + nets[extended_net.name] = ExtendedNet(self._pedb, extended_net) + return nets + + def create(self, name, net): + # type: (str, str|list)->ExtendedNet + """Create a new Extended net. + + Parameters + ---------- + name : str + Name of the extended net. + net : str, list + Name of the nets to be added into this extended net. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.nets_data.EDBExtendedNetsData` + """ + if name in self.items: + self._pedb.logger.error("{} already exists.".format(name)) + return False + extended_net = GrpcExtendedNet.create(self._pedb.layout, name) + if isinstance(net, str): + net = [net] + for i in net: + extended_net.add_net(self._pedb.nets.nets[i]) + return self.items[name] + + def auto_identify_signal(self, resistor_below=10, inductor_below=1, capacitor_above=1e-9, exception_list=None): + # type: (int | float, int | float, int |float, list) -> list + """Get extended signal net and associated components. + + Parameters + ---------- + resistor_below : int, float, optional + Threshold for the resistor value. Search the extended net across resistors that + have a value lower than the threshold. + inductor_below : int, float, optional + Threshold for the inductor value. Search the extended net across inductances + that have a value lower than the threshold. + capacitor_above : int, float, optional + Threshold for the capacitor value. Search the extended net across capacitors + that have a value higher than the threshold. + exception_list : list, optional + List of components to bypass when performing threshold checks. Components + in the list are considered as serial components. The default is ``None``. + + Returns + ------- + list + List of all extended nets. + + Examples + -------- + >>> from pyedb import Edb + >>> app = Edb() + >>> app.extended_nets.auto_identify_signal() + """ + return self.generate_extended_nets(resistor_below, inductor_below, capacitor_above, exception_list, True, True) + + def auto_identify_power(self, resistor_below=10, inductor_below=1, capacitor_above=1, exception_list=None): + # type: (int | float, int | float, int |float, list) -> list + """Get all extended power nets and their associated components. + + Parameters + ---------- + resistor_below : int, float, optional + Threshold for the resistor value. Search the extended net across resistors that + have a value lower than the threshold. + inductor_below : int, float, optional + Threshold for the inductor value. Search the extended net across inductances that + have a value lower than the threshold. + capacitor_above : int, float, optional + Threshold for the capacitor value. Search the extended net across capacitors that + have a value higher than the threshold. + exception_list : list, optional + List of components to bypass when performing threshold checks. Components + in the list are considered as serial components. The default is ``None``. + + Returns + ------- + list + List of all extended nets and their associated components. + + Examples + -------- + >>> from pyedb import Edb + >>> app = Edb() + >>> app.extended_nets.auto_identify_power() + """ + return self.generate_extended_nets(resistor_below, inductor_below, capacitor_above, exception_list, True, True) + + def generate_extended_nets( + self, + resistor_below=10, + inductor_below=1, + capacitor_above=1, + exception_list=None, + include_signal=True, + include_power=True, + ): + # type: (int | float, int | float, int |float, list, bool, bool) -> list + """Get extended net and associated components. + + Parameters + ---------- + resistor_below : int, float, optional + Threshold of resistor value. Search extended net across resistors which has value lower than the threshold. + inductor_below : int, float, optional + Threshold of inductor value. Search extended net across inductances which has value lower than the + threshold. + capacitor_above : int, float, optional + Threshold of capacitor value. Search extended net across capacitors which has value higher than the + threshold. + exception_list : list, optional + List of components to bypass when performing threshold checks. Components + in the list are considered as serial components. The default is ``None``. + include_signal : str, optional + Whether to generate extended signal nets. The default is ``True``. + include_power : str, optional + Whether to generate extended power nets. The default is ``True``. + + Returns + ------- + list + List of all extended nets. + + Examples + -------- + >>> from pyedb import Edb + >>> app = Edb() + >>> app.nets.get_extended_nets() + """ + if exception_list is None: + exception_list = [] + _extended_nets = [] + _nets = self._pedb.nets.nets + all_nets = list(_nets.keys())[:] + net_dicts = ( + self._pedb.nets._comps_by_nets_dict + if self._pedb.nets._comps_by_nets_dict + else (self._pedb.nets.components_by_nets) + ) + comp_dict = ( + self._pedb.nets._nets_by_comp_dict + if self._pedb.nets._nets_by_comp_dict + else (self._pedb.nets.nets_by_components) + ) + + def get_net_list(net_name, _net_list): + comps = [] + if net_name in net_dicts: + comps = net_dicts[net_name] + + for vals in comps: + refdes = vals + cmp = self._pedb.components.instances[refdes] + if cmp.type not in ["inductor", "resistor", "capacitor"]: + continue + if not cmp.enabled: + continue + val_value = cmp.rlc_values + if refdes in exception_list: + pass + elif cmp.type == "inductor" and val_value[1] < inductor_below: + pass + elif cmp.type == "resistor" and val_value[0] < resistor_below: + pass + elif cmp.type == "capacitor" and val_value[2] > capacitor_above: + pass + else: + continue + for net in comp_dict[refdes]: + if net not in _net_list: + _net_list.append(net) + get_net_list(net, _net_list) + + while len(all_nets) > 0: + new_ext = [all_nets[0]] + get_net_list(new_ext[0], new_ext) + all_nets = [i for i in all_nets if i not in new_ext] + _extended_nets.append(new_ext) + + if len(new_ext) > 1: + i = new_ext[0] + for i in new_ext: + if not i.lower().startswith("unnamed"): + break + + is_power = False + for i in new_ext: + is_power = is_power or _nets[i].is_power_ground + + if is_power: + if include_power: + ext_net = ExtendedNet.create(self._pedb.layout, i) + ext_net.add_net(self._pedb.nets.nets[i]) + for net in new_ext: + ext_net.add_net(self._pedb.nets.nets[net]) + else: # pragma: no cover + pass + else: + if include_signal: + ext_net = ExtendedNet.create(self._pedb.layout, i) + ext_net.add_net(self._pedb.nets.nets[i]) + for net in new_ext: + ext_net.add_net(self._pedb.nets.nets[net]) + else: # pragma: no cover + pass + + return _extended_nets + + +class ExtendedNet(GrpcExtendedNet): + """Manages EDB functionalities for a primitives. + It Inherits EDB Object properties. + """ + + def __init__(self, pedb, edb_object): + super().__init__(edb_object.msg) + self._pedb = pedb + + @property + def nets(self): + """Nets dictionary.""" + return {net.name: Net(self._pedb, net) for net in super().nets} + + @property + def components(self): + """Dictionary of components.""" + comps = {} + for _, obj in self.nets.items(): + comps.update(obj.components) + return comps + + @property + def rlc(self): + """Dictionary of RLC components.""" + return { + name: comp for name, comp in self.components.items() if comp.type in ["inductor", "resistor", "capacitor"] + } + + @property + def serial_rlc(self): + """Dictionary of serial RLC components.""" + res = {} + nets = self.nets + for comp_name, comp_obj in self.components.items(): + if comp_obj.type not in ["resistor", "inductor", "capacitor"]: + continue + if set(list(nets.keys())).issubset(comp_obj.nets): + res[comp_name] = comp_obj + return res + + @property + def shunt_rlc(self): + """Dictionary of shunt RLC components.""" + res = {} + nets = self.nets + for comp_name, comp_obj in self.components.items(): + if comp_obj.type not in ["resistor", "inductor", "capacitor"]: + continue + if not set(list(nets.keys())).issubset(comp_obj.nets): + res[comp_name] = comp_obj + return res diff --git a/src/pyedb/grpc/database/nets/net.py b/src/pyedb/grpc/database/nets/net.py new file mode 100644 index 0000000000..85617f4d2a --- /dev/null +++ b/src/pyedb/grpc/database/nets/net.py @@ -0,0 +1,193 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.net.net import Net as GrpcNet +from ansys.edb.core.primitive.primitive import PrimitiveType as GrpcPrimitiveType + +from pyedb.grpc.database.primitive.bondwire import Bondwire +from pyedb.grpc.database.primitive.circle import Circle +from pyedb.grpc.database.primitive.padstack_instances import PadstackInstance +from pyedb.grpc.database.primitive.path import Path +from pyedb.grpc.database.primitive.polygon import Polygon +from pyedb.grpc.database.primitive.rectangle import Rectangle + + +class Net(GrpcNet): + """Manages EDB functionalities for a primitives. + It Inherits EDB Object properties. + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb(myedb, edbversion="2021.2") + >>> edb_net = edb.nets.nets["GND"] + >>> edb_net.name # Class Property + >>> edb_net.name # EDB Object Property + """ + + def __init__(self, pedb, raw_net): + super().__init__(raw_net.msg) + self._pedb = pedb + self._core_components = pedb.components + self._core_primitive = pedb.modeler + self._edb_object = raw_net + + @property + def primitives(self): + """Return the list of primitives that belongs to the net. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.primitives_data.EDBPrimitives` + """ + primitives = [] + for primitive in super().primitives: + if primitive.primitive_type == GrpcPrimitiveType.PATH: + primitives.append(Path(self._pedb, primitive)) + elif primitive.primitive_type == GrpcPrimitiveType.POLYGON: + primitives.append(Polygon(self._pedb, primitive)) + elif primitive.primitive_type == GrpcPrimitiveType.CIRCLE: + primitives.append(Circle(self._pedb, primitive)) + elif primitive.primitive_type == GrpcPrimitiveType.RECTANGLE: + primitives.append(Rectangle(self._pedb, primitive)) + elif primitive.primitive_type == GrpcPrimitiveType.BONDWIRE: + primitives.append(Bondwire(self._pedb, primitive)) + return primitives + + @property + def padstack_instances(self): + """Return the list of primitives that belongs to the net. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`""" + return [PadstackInstance(self._pedb, i) for i in super().padstack_instances] + + @property + def components(self): + """Return the list of components that touch the net. + + Returns + ------- + dict[str, :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent`] + """ + components = {} + for padstack_instance in self.padstack_instances: + component = padstack_instance.component + if component: + try: + components[component.name] = component + except: + pass + return components + + def find_dc_short(self, fix=False): + """Find DC-shorted nets. + + Parameters + ---------- + fix : bool, optional + If `True`, rename all the nets. (default) + If `False`, only report dc shorts. + + Returns + ------- + List[List[str, str]] + [[net name, net name]]. + """ + return self._pedb.layout_validation.dc_shorts(self.name, fix) + + def plot( + self, + layers=None, + show_legend=True, + save_plot=None, + outline=None, + size=(2000, 1000), + show=True, + ): + """Plot a net to Matplotlib 2D chart. + + Parameters + ---------- + layers : str, list, optional + Name of the layers to include in the plot. If `None` all the signal layers will be considered. + show_legend : bool, optional + If `True` the legend is shown in the plot. (default) + If `False` the legend is not shown. + save_plot : str, optional + If a path is specified the plot will be saved in this location. + If ``save_plot`` is provided, the ``show`` parameter is ignored. + outline : list, optional + List of points of the outline to plot. + size : tuple, optional + Image size in pixel (width, height). + show : bool, optional + Whether to show the plot or not. Default is `True`. + """ + + self._pedb.nets.plot( + self.name, + layers=layers, + show_legend=show_legend, + save_plot=save_plot, + outline=outline, + size=size, + show=show, + plot_components=True, + plot_vias=True, + ) + + def get_smallest_trace_width(self): + """Retrieve the smallest trace width from paths. + + Returns + ------- + float + Trace smallest width. + """ + + current_value = 1e10 + paths = [prim for prim in self.primitives if prim.primitive_type == GrpcPrimitiveType.PATH] + for path in paths: + if path.width < current_value: + current_value = path.width + return current_value + + @property + def extended_net(self): + """Get extended net and associated components. + + Returns + ------- + :class:` :class:`pyedb.dotnet.database.edb_data.nets_data.EDBExtendedNetData` + + Examples + -------- + >>> from pyedb import Edb + >>> app = Edb() + >>> app.nets["BST_V3P3_S5"].extended_net + """ + if self.name in self._pedb.extended_nets.items: + return self._pedb.extended_nets.items[self.name] + else: + return None diff --git a/src/pyedb/grpc/database/nets/net_class.py b/src/pyedb/grpc/database/nets/net_class.py new file mode 100644 index 0000000000..03d0056dbc --- /dev/null +++ b/src/pyedb/grpc/database/nets/net_class.py @@ -0,0 +1,70 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.net.net_class import NetClass as GrpcNetClass + +from pyedb.grpc.database.nets.net import Net + + +class NetClass(GrpcNetClass): + """Manages EDB functionalities for a primitives. + It inherits EDB Object properties. + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb(myedb, edbversion="2025.1") + >>> edb.net_classes + """ + + def __init__(self, pedb, net_class): + super().__init__(net_class.msg) + self._pedb = pedb + + @property + def nets(self): + """list of :class: `.Net` in the net class.""" + return [Net(self._pedb, i) for i in super().nets] + + def add_net(self, net): + """Add a net to the net class.""" + if isinstance(net, str): + net = Net.find_by_name(self._pedb.active_layout, name=net) + if isinstance(net, Net) and not net.is_null: + self.add_net(net) + return True + return False + + def contains_net(self, net): + """Determine if a net exists in the net class.""" + if isinstance(net, str): + net = Net.find_by_name(self._pedb.active_layout, name=net) + return super().contains_net(net) + + def remove_net(self, net): + """Remove net.""" + if isinstance(net, str): + net = Net.find_by_name(self._pedb.active_layout, name=net) + if isinstance(net, Net) and not net.is_null: + self.remove(net) + return True + return False diff --git a/src/pyedb/grpc/database/padstack.py b/src/pyedb/grpc/database/padstack.py new file mode 100644 index 0000000000..2120df7e6a --- /dev/null +++ b/src/pyedb/grpc/database/padstack.py @@ -0,0 +1,1500 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains the `EdbPadstacks` class. +""" +import math +import warnings + +from ansys.edb.core.definition.padstack_def_data import ( + PadGeometryType as GrpcPadGeometryType, +) +from ansys.edb.core.definition.padstack_def_data import ( + PadstackDefData as GrpcPadstackDefData, +) +from ansys.edb.core.definition.padstack_def_data import ( + PadstackHoleRange as GrpcPadstackHoleRange, +) +from ansys.edb.core.definition.padstack_def_data import ( + SolderballPlacement as GrpcSolderballPlacement, +) +from ansys.edb.core.definition.padstack_def_data import ( + SolderballShape as GrpcSolderballShape, +) +from ansys.edb.core.definition.padstack_def_data import PadType as GrpcPadType +from ansys.edb.core.geometry.point_data import PointData as GrpcPointData +from ansys.edb.core.geometry.polygon_data import PolygonData as GrpcPolygonData +from ansys.edb.core.utility.value import Value as GrpcValue +import rtree + +from pyedb.generic.general_methods import generate_unique_name +from pyedb.grpc.database.definition.padstack_def import PadstackDef +from pyedb.grpc.database.primitive.padstack_instances import PadstackInstance +from pyedb.modeler.geometry_operators import GeometryOperators + + +class Padstacks(object): + """Manages EDB methods for nets management accessible from `Edb.padstacks` property. + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", edbversion="2024.2") + >>> edb_padstacks = edbapp.padstacks + """ + + def __getitem__(self, name): + """Get a padstack definition or instance from the Edb project. + + Parameters + ---------- + name : str, int + + Returns + ------- + :class:`pyedb.dotnet.database.cell.hierarchy.component.EDBComponent` + + """ + if isinstance(name, int) and name in self.instances: + return self.instances(name) + elif name in self.definitions: + return self.definitions[name] + else: + for i in list(self.instances.values()): + if i.name == name or i.aedt_name == name: + return i + self._pedb.logger.error("Component or definition not found.") + return + + def __init__(self, p_edb): + self._pedb = p_edb + self._instances = {} + self._definitions = {} + + @property + def _active_layout(self): + """ """ + return self._pedb.active_layout + + @property + def _layout(self): + """ """ + return self._pedb.layout + + @property + def db(self): + """Db object.""" + return self._pedb.active_db + + @property + def _logger(self): + """ """ + return self._pedb.logger + + @property + def _layers(self): + """ """ + return self._pedb.stackup.layers + + def int_to_pad_type(self, val=0): + """Convert an integer to an EDB.PadGeometryType. + + Parameters + ---------- + val : int + + Returns + ------- + object + EDB.PadType enumerator value. + """ + + if val == 0: + return GrpcPadType.REGULAR_PAD + elif val == 1: + return GrpcPadType.ANTI_PAD + elif val == 2: + return GrpcPadType.THERMAL_PAD + elif val == 3: + return GrpcPadType.HOLE + elif val == 4: + return GrpcPadType.UNKNOWN_GEOM_TYPE + else: + return val + + def int_to_geometry_type(self, val=0): + """Convert an integer to an EDB.PadGeometryType. + + Parameters + ---------- + val : int + + Returns + ------- + object + EDB.PadGeometryType enumerator value. + """ + if val == 0: + return GrpcPadGeometryType.PADGEOMTYPE_NO_GEOMETRY + elif val == 1: + return GrpcPadGeometryType.PADGEOMTYPE_CIRCLE + elif val == 2: + return GrpcPadGeometryType.PADGEOMTYPE_SQUARE + elif val == 3: + return GrpcPadGeometryType.PADGEOMTYPE_RECTANGLE + elif val == 4: + return GrpcPadGeometryType.PADGEOMTYPE_OVAL + elif val == 5: + return GrpcPadGeometryType.PADGEOMTYPE_BULLET + elif val == 6: + return GrpcPadGeometryType.PADGEOMTYPE_NSIDED_POLYGON + elif val == 7: + return GrpcPadGeometryType.PADGEOMTYPE_POLYGON + elif val == 8: + return GrpcPadGeometryType.PADGEOMTYPE_ROUND45 + elif val == 9: + return GrpcPadGeometryType.PADGEOMTYPE_ROUND90 + elif val == 10: + return GrpcPadGeometryType.PADGEOMTYPE_SQUARE45 + elif val == 11: + return GrpcPadGeometryType.PADGEOMTYPE_SQUARE90 + else: + return val + + @property + def definitions(self): + """Padstack definitions. + + Returns + ------- + dict[str, :class:`pyedb.dotnet.database.edb_data.padstacks_data.EdbPadstack`] + List of definitions via padstack definitions. + + """ + if len(self._definitions) == len(self.db.padstack_defs): + return self._definitions + self._definitions = {} + for padstack_def in self._pedb.db.padstack_defs: + if len(padstack_def.data.layer_names) >= 1: + self._definitions[padstack_def.name] = PadstackDef(self._pedb, padstack_def) + return self._definitions + + @property + def instances(self): + """Dictionary of all padstack instances (vias and pins). + + Returns + ------- + dict[int, :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`] + List of padstack instances. + + """ + pad_stack_inst = self._pedb.layout.padstack_instances + if len(self._instances) == len(pad_stack_inst): + return self._instances + self._instances = {i.edb_uid: PadstackInstance(self._pedb, i) for i in pad_stack_inst} + return self._instances + + @property + def instances_by_name(self): + """Dictionary of all padstack instances (vias and pins) by name. + + Returns + ------- + dict[str, :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`] + List of padstack instances. + + """ + padstack_instances = {} + for _, edb_padstack_instance in self.instances.items(): + if edb_padstack_instance.aedt_name: + padstack_instances[edb_padstack_instance.aedt_name] = edb_padstack_instance + return padstack_instances + + def find_instance_by_id(self, value: int): + """Find a padstack instance by database id. + + Parameters + ---------- + value : int + """ + return self._pedb.modeler.find_object_by_id(value) + + @property + def pins(self): + """Dictionary of all pins instances (belonging to component). + + Returns + ------- + dic[str, :class:`dotnet.database.edb_data.definitions.EDBPadstackInstance`] + Dictionary of EDBPadstackInstance Components. + + + Examples + -------- + >>> edbapp = dotnet.Edb("myproject.aedb") + >>> pin_net_name = edbapp.pins[424968329].netname + """ + pins = {} + for instancename, instance in self.instances.items(): + if instance.is_pin and instance.component: + pins[instancename] = instance + return pins + + @property + def vias(self): + """Dictionary of all vias instances not belonging to component. + + Returns + ------- + dic[str, :class:`dotnet.database.edb_data.definitions.EDBPadstackInstance`] + Dictionary of EDBPadstackInstance Components. + + + Examples + -------- + >>> edbapp = dotnet.Edb("myproject.aedb") + >>> pin_net_name = edbapp.pins[424968329].netname + """ + pnames = list(self.pins.keys()) + vias = {i: j for i, j in self.instances.items() if i not in pnames} + return vias + + @property + def pingroups(self): + """All Layout Pin groups. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.layout.pin_groups` instead. + + Returns + ------- + list + List of all layout pin groups. + """ + warnings.warn( + "`pingroups` is deprecated and is now located here " "`pyedb.grpc.core.layout.pin_groups` instead.", + DeprecationWarning, + ) + return self._layout.pin_groups + + @property + def pad_type(self): + """Return a PadType Enumerator.""" + + def create_circular_padstack( + self, + padstackname=None, + holediam="300um", + paddiam="400um", + antipaddiam="600um", + startlayer=None, + endlayer=None, + ): + """Create a circular padstack. + + Parameters + ---------- + padstackname : str, optional + Name of the padstack. The default is ``None``. + holediam : str, optional + Diameter of the hole with units. The default is ``"300um"``. + paddiam : str, optional + Diameter of the pad with units. The default is ``"400um"``. + antipaddiam : str, optional + Diameter of the antipad with units. The default is ``"600um"``. + startlayer : str, optional + Starting layer. The default is ``None``, in which case the top + is the starting layer. + endlayer : str, optional + Ending layer. The default is ``None``, in which case the bottom + is the ending layer. + + Returns + ------- + str + Name of the padstack if the operation is successful. + """ + + padstack_def = PadstackDef.create(self._pedb.db, padstackname) + + padstack_data = GrpcPadstackDefData.create() + list_values = [GrpcValue(holediam), GrpcValue(paddiam), GrpcValue(antipaddiam)] + padstack_data.set_hole_parameters( + offset_x=GrpcValue(0), + offset_y=GrpcValue(0), + rotation=GrpcValue(0), + type_geom=GrpcPadGeometryType.PADGEOMTYPE_CIRCLE, + sizes=list_values, + ) + + padstack_data.hole_range = GrpcPadstackHoleRange.UPPER_PAD_TO_LOWER_PAD + layers = list(self._pedb.stackup.signal_layers.keys()) + if not startlayer: + startlayer = layers[0] + if not endlayer: + endlayer = layers[len(layers) - 1] + + antipad_shape = GrpcPadGeometryType.PADGEOMTYPE_CIRCLE + started = False + padstack_data.set_pad_parameters( + layer="Default", + pad_type=GrpcPadType.REGULAR_PAD, + type_geom=GrpcPadGeometryType.PADGEOMTYPE_CIRCLE, + offset_x=GrpcValue(0), + offset_y=GrpcValue(0), + rotation=GrpcValue(0), + sizes=[GrpcValue(paddiam)], + ) + + padstack_data.set_pad_parameters( + layer="Default", + pad_type=GrpcPadType.ANTI_PAD, + type_geom=GrpcPadGeometryType.PADGEOMTYPE_CIRCLE, + offset_x=GrpcValue(0), + offset_y=GrpcValue(0), + rotation=GrpcValue(0), + sizes=[GrpcValue(antipaddiam)], + ) + + for layer in layers: + if layer == startlayer: + started = True + if layer == endlayer: + started = False + if started: + padstack_data.set_pad_parameters( + layer=layer, + pad_type=GrpcPadType.ANTI_PAD, + type_geom=GrpcPadGeometryType.PADGEOMTYPE_CIRCLE, + offset_x=GrpcValue(0), + offset_y=GrpcValue(0), + rotation=GrpcValue(0), + sizes=[GrpcValue(antipaddiam)], + ) + + padstack_data.set_pad_parameters( + layer=layer, + pad_type=GrpcPadType.ANTI_PAD, + type_geom=GrpcPadGeometryType.PADGEOMTYPE_CIRCLE, + offset_x=GrpcValue(0), + offset_y=GrpcValue(0), + rotation=GrpcValue(0), + sizes=[GrpcValue(antipaddiam)], + ) + + padstack_def.data = padstack_data + + def delete_padstack_instances(self, net_names): # pragma: no cover + """Delete padstack instances by net names. + + Parameters + ---------- + net_names : str, list + Names of the nets to delete. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + References + ---------- + + >>> Edb.padstacks.delete_padstack_instances(net_names=["GND"]) + """ + if not isinstance(net_names, list): # pragma: no cover + net_names = [net_names] + + for p_id, p in self.instances.items(): + if p.net_name in net_names: + if not p.delete(): # pragma: no cover + return False + return True + + def set_solderball(self, padstackInst, sballLayer_name, isTopPlaced=True, ballDiam=100e-6): + """Set solderball for the given PadstackInstance. + + Parameters + ---------- + padstackInst : Edb.Cell.Primitive.PadstackInstance or int + Padstack instance id or object. + sballLayer_name : str, + Name of the layer where the solder ball is placed. No default values. + isTopPlaced : bool, optional. + Bollean triggering is the solder ball is placed on Top or Bottom of the layer stackup. + ballDiam : double, optional, + Solder ball diameter value. + + Returns + ------- + bool + + """ + if isinstance(padstackInst, int): + psdef = self.definitions[self.instances[padstackInst].padstack_definition].edb_padstack + padstackInst = self.instances[padstackInst] + + else: + psdef = padstackInst.padstack_def + newdefdata = GrpcPadstackDefData.create(psdef.data) + newdefdata.solder_ball_shape = GrpcSolderballShape.SOLDERBALL_CYLINDER + newdefdata.solder_ball_param(GrpcValue(ballDiam), GrpcValue(ballDiam)) + sball_placement = ( + GrpcSolderballPlacement.ABOVE_PADSTACK if isTopPlaced else GrpcSolderballPlacement.BELOW_PADSTACK + ) + newdefdata.solder_ball_placement = sball_placement + psdef.data = newdefdata + sball_layer = [lay._edb_layer for lay in list(self._layers.values()) if lay.name == sballLayer_name][0] + if sball_layer is not None: + padstackInst.solder_ball_layer = sball_layer + return True + + return False + + def create_coax_port(self, padstackinstance, use_dot_separator=True, name=None): + """Create HFSS 3Dlayout coaxial lumped port on a pastack + Requires to have solder ball defined before calling this method. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_source_on_component` instead. + + Parameters + ---------- + padstackinstance : `Edb.Cell.Primitive.PadstackInstance` or int + Padstack instance object. + use_dot_separator : bool, optional + Whether to use ``.`` as the separator for the naming convention, which + is ``[component][net][pin]``. The default is ``True``. If ``False``, ``_`` is + used as the separator instead. + name : str + Port name for overwriting the default port-naming convention, + which is ``[component][net][pin]``. The port name must be unique. + If a port with the specified name already exists, the + default naming convention is used so that port creation does + not fail. + + Returns + ------- + str + Terminal name. + + """ + warnings.warn( + "`create_coax_port` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_coax_port` instead.", + DeprecationWarning, + ) + self._pedb.excitations.create_coax_port(self, padstackinstance, use_dot_separator=use_dot_separator, name=name) + + def get_pin_from_component_and_net(self, refdes=None, netname=None): + """Retrieve pins given a component's reference designator and net name. + + Parameters + ---------- + refdes : str, optional + Reference designator of the component. The default is ``None``. + netname : str optional + Name of the net. The default is ``None``. + + Returns + ------- + dict + Dictionary of pins if the operation is successful. + ``False`` is returned if the net does not belong to the component. + + """ + pinlist = [] + if refdes: + if refdes in self._pedb.components.instances: + if netname: + for pin, val in self._pedb.components.instances[refdes].pins.items(): + if val.net_name == netname: + pinlist.append(val) + else: + for pin in self._pedb.components.instances[refdes].pins.values(): + pinlist.append(pin) + elif netname: + for pin in self._pedb.pins: + if pin.net_name == netname: + pinlist.append(pin) + else: + self._logger.error("At least a component or a net name has to be provided") + + return pinlist + + def get_pinlist_from_component_and_net(self, refdes=None, netname=None): + """Retrieve pins given a component's reference designator and net name. + + . deprecated:: pyedb 0.28.0 + Use :func:`get_pin_from_component_and_net` instead. + + Parameters + ---------- + refdes : str, optional + Reference designator of the component. The default is ``None``. + netname : str optional + Name of the net. The default is ``None``. + + Returns + ------- + dict + Dictionary of pins if the operation is successful. + ``False`` is returned if the net does not belong to the component. + + """ + warnings.warn( + "`get_pinlist_from_component_and_net` is deprecated use `get_pin_from_component_and_net` instead.", + DeprecationWarning, + ) + return self.get_pin_from_component_and_net(refdes=refdes, netname=netname) + + def get_pad_parameters(self, pin, layername, pad_type="regular_pad"): + """Get Padstack Parameters from Pin or Padstack Definition. + + Parameters + ---------- + pin : Edb.definition.PadstackDef or Edb.definition.PadstackInstance + Pin or PadstackDef on which get values. + layername : str + Layer on which get properties. + pad_type : str + Pad Type, `"pad"`, `"anti_pad"`, `"thermal_pad"` + + Returns + ------- + tuple + Tuple of (GeometryType, ParameterList, OffsetX, OffsetY, Rot). + """ + if pad_type == "regular_pad": + pad_type = GrpcPadType.REGULAR_PAD + elif pad_type == "anti_pad": + pad_type = GrpcPadType.ANTI_PAD + elif pad_type == "thermal_pad": + pad_type = GrpcPadType.THERMAL_PAD + else: + pad_type = pad_type = GrpcPadType.REGULAR_PAD + padparams = pin.padstack_def.data.get_pad_parameters(layername, pad_type) + if len(padparams) == 5: # non polygon via + geometry_type = padparams[0] + parameters = [i.value for i in padparams[1]] + offset_x = padparams[2].value + offset_y = padparams[3].value + rotation = padparams[4].value + return geometry_type.name, parameters, offset_x, offset_y, rotation + elif len(padparams) == 4: # polygon based + from ansys.edb.core.geometry.polygon_data import ( + PolygonData as GrpcPolygonData, + ) + + if isinstance(padparams[0], GrpcPolygonData): + points = [[pt.x.value, pt.y.value] for pt in padparams[0].points] + offset_x = padparams[1] + offset_y = padparams[2] + rotation = padparams[3] + geometry_type = GrpcPadGeometryType.PADGEOMTYPE_POLYGON + return geometry_type.name, points, offset_x, offset_y, rotation + return 0, [0], 0, 0, 0 + + def set_all_antipad_value(self, value): + """Set all anti-pads from all pad-stack definition to the given value. + + Parameters + ---------- + value : float, str + Anti-pad value. + + Returns + ------- + bool + ``True`` when successful, ``False`` if an anti-pad value fails to be assigned. + """ + if self.definitions: + all_succeed = True + for padstack in list(self.definitions.values()): + cloned_padstack_data = GrpcPadstackDefData(padstack.data.msg) + layers_name = cloned_padstack_data.layer_names + for layer in layers_name: + try: + geom_type, points, offset_x, offset_y, rotation = cloned_padstack_data.get_pad_parameters( + layer, GrpcPadType.ANTI_PAD + ) + if geom_type == GrpcPadGeometryType.PADGEOMTYPE_CIRCLE.name: + cloned_padstack_data.set_pad_parameters( + layer=layer, + pad_type=GrpcPadType.ANTI_PAD, + offset_x=GrpcValue(offset_x), + offset_y=GrpcValue(offset_y), + rotation=GrpcValue(rotation), + type_geom=GrpcPadGeometryType.PADGEOMTYPE_CIRCLE, + sizes=[GrpcValue(value)], + ) + self._logger.info( + "Pad-stack definition {}, anti-pad on layer {}, has been set to {}".format( + padstack.edb_padstack.GetName(), layer, str(value) + ) + ) + else: # pragma no cover + self._logger.error( + f"Failed to reassign anti-pad value {value} on Pads-stack definition {padstack.name}," + f" layer{layer}. This feature only support circular shape anti-pads." + ) + all_succeed = False + except: + self._pedb.logger.info( + f"No antipad defined for padstack definition {padstack.name}-layer{layer}" + ) + padstack.data = cloned_padstack_data + return all_succeed + + def check_and_fix_via_plating(self, minimum_value_to_replace=0.0, default_plating_ratio=0.2): + """Check for minimum via plating ration value, values found below the minimum one are replaced by default + plating ratio. + + Parameters + ---------- + minimum_value_to_replace : float + Plating ratio that is below or equal to this value is to be replaced + with the value specified for the next parameter. Default value ``0.0``. + default_plating_ratio : float + Default value to use for plating ratio. The default value is ``0.2``. + + Returns + ------- + bool + ``True`` when successful, ``False`` if an anti-pad value fails to be assigned. + """ + for padstack_def in list(self.definitions.values()): + if padstack_def.hole_plating_ratio <= minimum_value_to_replace: + padstack_def.hole_plating_ratio = default_plating_ratio + self._logger.info( + "Padstack definition with zero plating ratio, defaulting to 20%".format(padstack_def.name) + ) + return True + + def get_via_instance_from_net(self, net_list=None): + """Get the list for EDB vias from a net name list. + + Parameters + ---------- + net_list : str or list + The list of the net name to be used for filtering vias. If no net is provided the command will + return an all vias list. + + Returns + ------- + list of Edb.Cell.Primitive.PadstackInstance + List of EDB vias. + """ + if net_list and not isinstance(net_list, list): + net_list = [net_list] + via_list = [] + for inst in self._layout.padstack_instances: + pad_layers_name = inst.padstack_def.data.layer_names + if len(pad_layers_name) > 1: + if not net_list: + via_list.append(inst) + elif not inst.net.is_null: + if inst.net.name in net_list: + via_list.append(inst) + return via_list + + def create( + self, + padstackname=None, + holediam="300um", + paddiam="400um", + antipaddiam="600um", + pad_shape="Circle", + antipad_shape="Circle", + x_size="600um", + y_size="600um", + corner_radius="300um", + offset_x="0.0", + offset_y="0.0", + rotation="0.0", + has_hole=True, + pad_offset_x="0.0", + pad_offset_y="0.0", + pad_rotation="0.0", + pad_polygon=None, + antipad_polygon=None, + polygon_hole=None, + start_layer=None, + stop_layer=None, + add_default_layer=False, + anti_pad_x_size="600um", + anti_pad_y_size="600um", + hole_range="upper_pad_to_lower_pad", + ): + """Create a padstack. + + Parameters + ---------- + padstackname : str, optional + Name of the padstack. The default is ``None``. + holediam : str, optional + Diameter of the hole with units. The default is ``"300um"``. + paddiam : str, optional + Diameter of the pad with units, used with ``"Circle"`` shape. The default is ``"400um"``. + antipaddiam : str, optional + Diameter of the antipad with units. The default is ``"600um"``. + pad_shape : str, optional + Shape of the pad. The default is ``"Circle``. Options are ``"Circle"``, ``"Rectangle"`` and ``"Polygon"``. + antipad_shape : str, optional + Shape of the antipad. The default is ``"Circle"``. Options are ``"Circle"`` ``"Rectangle"`` and + ``"Bullet"``. + x_size : str, optional + Only applicable to bullet and rectangle shape. The default is ``"600um"``. + y_size : str, optional + Only applicable to bullet and rectangle shape. The default is ``"600um"``. + corner_radius : + Only applicable to bullet shape. The default is ``"300um"``. + offset_x : str, optional + X offset of antipad. The default is ``"0.0"``. + offset_y : str, optional + Y offset of antipad. The default is ``"0.0"``. + rotation : str, optional + rotation of antipad. The default is ``"0.0"``. + has_hole : bool, optional + Whether this padstack has a hole. + pad_offset_x : str, optional + Padstack offset in X direction. + pad_offset_y : str, optional + Padstack offset in Y direction. + pad_rotation : str, optional + Padstack rotation. + start_layer : str, optional + Start layer of the padstack definition. + stop_layer : str, optional + Stop layer of the padstack definition. + add_default_layer : bool, optional + Add ``"Default"`` to padstack definition. Default is ``False``. + anti_pad_x_size : str, optional + Only applicable to bullet and rectangle shape. The default is ``"600um"``. + anti_pad_y_size : str, optional + Only applicable to bullet and rectangle shape. The default is ``"600um"``. + hole_range : str, optional + Define the padstack hole range. Arguments supported, ``"through"``, ``"begin_on_upper_pad"``, + ``"end_on_lower_pad"``, ``"upper_pad_to_lower_pad"``. + + Returns + ------- + str + Name of the padstack if the operation is successful. + """ + holediam = GrpcValue(holediam) + paddiam = GrpcValue(paddiam) + antipaddiam = GrpcValue(antipaddiam) + layers = list(self._pedb.stackup.signal_layers.keys())[:] + value0 = GrpcValue("0.0") + if not padstackname: + padstackname = generate_unique_name("VIA") + padstack_data = GrpcPadstackDefData.create() + if has_hole and not polygon_hole: + hole_param = [holediam, holediam] + padstack_data.set_hole_parameters( + offset_x=value0, + offset_y=value0, + rotation=value0, + type_geom=GrpcPadGeometryType.PADGEOMTYPE_CIRCLE, + sizes=hole_param, + ) + padstack_data.plating_percentage = GrpcValue(20.0) + elif polygon_hole: + if isinstance(polygon_hole, list): + polygon_hole = GrpcPolygonData(points=polygon_hole) + + padstack_data.set_hole_parameters( + offset_x=value0, + offset_y=value0, + rotation=value0, + type_geom=GrpcPadGeometryType.PADGEOMTYPE_POLYGON, + sizes=polygon_hole, + ) + padstack_data.plating_percentage = GrpcValue(20.0) + else: + pass + + x_size = GrpcValue(x_size) + y_size = GrpcValue(y_size) + corner_radius = GrpcValue(corner_radius) + pad_offset_x = GrpcValue(pad_offset_x) + pad_offset_y = GrpcValue(pad_offset_y) + pad_rotation = GrpcValue(pad_rotation) + anti_pad_x_size = GrpcValue(anti_pad_x_size) + anti_pad_y_size = GrpcValue(anti_pad_y_size) + + if hole_range == "through": # pragma no cover + padstack_data.hole_range = GrpcPadstackHoleRange.THROUGH + elif hole_range == "begin_on_upper_pad": # pragma no cover + padstack_data.hole_range = GrpcPadstackHoleRange.BEGIN_ON_UPPER_PAD + elif hole_range == "end_on_lower_pad": # pragma no cover + padstack_data.hole_range = GrpcPadstackHoleRange.END_ON_LOWER_PAD + elif hole_range == "upper_pad_to_lower_pad": # pragma no cover + padstack_data.hole_range = GrpcPadstackHoleRange.UPPER_PAD_TO_LOWER_PAD + else: # pragma no cover + self._logger.error("Unknown padstack hole range") + padstack_data.material = "copper" + + if start_layer and start_layer in layers: # pragma no cover + layers = layers[layers.index(start_layer) :] + if stop_layer and stop_layer in layers: # pragma no cover + layers = layers[: layers.index(stop_layer) + 1] + if not isinstance(paddiam, list): + pad_array = [paddiam] + else: + pad_array = paddiam + if pad_shape == "Circle": # pragma no cover + pad_shape = GrpcPadGeometryType.PADGEOMTYPE_CIRCLE + elif pad_shape == "Rectangle": # pragma no cover + pad_array = [x_size, y_size] + pad_shape = GrpcPadGeometryType.PADGEOMTYPE_RECTANGLE + elif pad_shape == "Polygon": + if isinstance(pad_polygon, list): + pad_array = GrpcPolygonData(points=pad_polygon) + elif isinstance(pad_polygon, GrpcPolygonData): + pad_array = pad_polygon + if antipad_shape == "Bullet": # pragma no cover + antipad_array = [x_size, y_size, corner_radius] + antipad_shape = GrpcPadGeometryType.PADGEOMTYPE_BULLET + elif antipad_shape == "Rectangle": # pragma no cover + antipad_array = [anti_pad_x_size, anti_pad_y_size] + antipad_shape = GrpcPadGeometryType.PADGEOMTYPE_RECTANGLE + elif antipad_shape == "Polygon": + if isinstance(antipad_polygon, list): + antipad_array = GrpcPolygonData(points=antipad_polygon) + elif isinstance(antipad_polygon, GrpcPolygonData): + antipad_array = antipad_polygon + else: + if not isinstance(antipaddiam, list): + antipad_array = [antipaddiam] + else: + antipad_array = antipaddiam + antipad_shape = GrpcPadGeometryType.PADGEOMTYPE_CIRCLE + if add_default_layer: # pragma no cover + layers = layers + ["Default"] + if antipad_shape == "Polygon" and pad_shape == "Polygon": + for layer in layers: + padstack_data.set_pad_parameters( + layer=layer, + pad_type=GrpcPadType.REGULAR_PAD, + offset_x=pad_offset_x, + offset_y=pad_offset_y, + rotation=pad_rotation, + fp=pad_array, + ) + padstack_data.set_pad_parameters( + layer=layer, + pad_type=GrpcPadType.ANTI_PAD, + offset_x=pad_offset_x, + offset_y=pad_offset_y, + rotation=pad_rotation, + fp=antipad_array, + ) + else: + for layer in layers: + padstack_data.set_pad_parameters( + layer=layer, + pad_type=GrpcPadType.REGULAR_PAD, + offset_x=pad_offset_x, + offset_y=pad_offset_y, + rotation=pad_rotation, + type_geom=pad_shape, + sizes=pad_array, + ) + + padstack_data.set_pad_parameters( + layer=layer, + pad_type=GrpcPadType.ANTI_PAD, + offset_x=pad_offset_x, + offset_y=pad_offset_y, + rotation=pad_rotation, + type_geom=antipad_shape, + sizes=antipad_array, + ) + + padstack_definition = PadstackDef.create(self.db, padstackname) + padstack_definition.data = padstack_data + self._logger.info(f"Padstack {padstackname} create correctly") + return padstackname + + def _get_pin_layer_range(self, pin): + layers = pin.get_layer_range() + if layers: + return layers[0], layers[1] + else: + return False + + def duplicate(self, target_padstack_name, new_padstack_name=""): + """Duplicate a padstack. + + Parameters + ---------- + target_padstack_name : str + Name of the padstack to be duplicated. + new_padstack_name : str, optional + Name of the new padstack. + + Returns + ------- + str + Name of the new padstack. + """ + new_padstack_definition_data = GrpcPadstackDefData(self.definitions[target_padstack_name].data) + if not new_padstack_name: + new_padstack_name = generate_unique_name(target_padstack_name) + padstack_definition = PadstackDef.create(self.db, new_padstack_name) + padstack_definition.data = new_padstack_definition_data + return new_padstack_name + + def place( + self, + position, + definition_name, + net_name="", + via_name="", + rotation=0.0, + fromlayer=None, + tolayer=None, + solderlayer=None, + is_pin=False, + ): + """Place a via. + + Parameters + ---------- + position : list + List of float values for the [x,y] positions where the via is to be placed. + definition_name : str + Name of the padstack definition. + net_name : str, optional + Name of the net. The default is ``""``. + via_name : str, optional + The default is ``""``. + rotation : float, optional + Rotation of the padstack in degrees. The default + is ``0``. + fromlayer : + The default is ``None``. + tolayer : + The default is ``None``. + solderlayer : + The default is ``None``. + is_pin : bool, optional + Whether if the padstack is a pin or not. Default is `False`. + + Returns + ------- + :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance` + """ + padstack_def = None + for pad in list(self.definitions.keys()): + if pad == definition_name: + padstack_def = self.definitions[pad] + position = GrpcPointData( + [GrpcValue(position[0], self._pedb.active_cell), GrpcValue(position[1], self._pedb.active_cell)] + ) + net = self._pedb.nets.find_or_create_net(net_name) + rotation = GrpcValue(rotation * math.pi / 180) + sign_layers_values = {i: v for i, v in self._pedb.stackup.signal_layers.items()} + sign_layers = list(sign_layers_values.keys()) + if not fromlayer: + try: + fromlayer = sign_layers_values[list(self.definitions[pad].pad_by_layer.keys())[0]] + except KeyError: + fromlayer = sign_layers_values[sign_layers[0]] + else: + fromlayer = sign_layers_values[fromlayer] + + if not tolayer: + try: + tolayer = sign_layers_values[list(self.definitions[pad].pad_by_layer.keys())[-1]] + except KeyError: + tolayer = sign_layers_values[sign_layers[-1]] + else: + tolayer = sign_layers_values[tolayer] + if solderlayer: + solderlayer = sign_layers_values[solderlayer] + if not via_name: + via_name = generate_unique_name(padstack_def.name) + if padstack_def: + padstack_instance = PadstackInstance.create( + layout=self._active_layout, + net=net, + name=via_name, + padstack_def=padstack_def, + position_x=position.x, + position_y=position.y, + rotation=rotation, + top_layer=fromlayer, + bottom_layer=tolayer, + solder_ball_layer=solderlayer, + layer_map=None, + ) + padstack_instance.is_layout_pin = is_pin + return PadstackInstance(self._pedb, padstack_instance) + else: + return False + + def remove_pads_from_padstack(self, padstack_name, layer_name=None): + """Remove the Pad from a padstack on a specific layer by setting it as a 0 thickness circle. + + Parameters + ---------- + padstack_name : str + padstack name + layer_name : str, optional + Layer name on which remove the PadParameters. If None, all layers will be taken. + + Returns + ------- + bool + ``True`` if successful. + """ + pad_type = GrpcPadType.REGULAR_PAD + pad_geo = GrpcPadGeometryType.PADGEOMTYPE_CIRCLE + vals = GrpcValue(0) + params = [GrpcValue(0)] + new_padstack_definition_data = GrpcPadstackDefData(self.definitions[padstack_name].data) + if not layer_name: + layer_name = list(self._pedb.stackup.signal_layers.keys()) + elif isinstance(layer_name, str): + layer_name = [layer_name] + for lay in layer_name: + new_padstack_definition_data.set_pad_parameters( + layer=lay, + pad_type=pad_type, + offset_x=vals, + offset_y=vals, + rotation=vals, + type_geom=pad_geo, + sizes=params, + ) + self.definitions[padstack_name].data = new_padstack_definition_data + return True + + def set_pad_property( + self, + padstack_name, + layer_name=None, + pad_shape="Circle", + pad_params=0, + pad_x_offset=0, + pad_y_offset=0, + pad_rotation=0, + antipad_shape="Circle", + antipad_params=0, + antipad_x_offset=0, + antipad_y_offset=0, + antipad_rotation=0, + ): + """Set pad and antipad properties of the padstack. + + Parameters + ---------- + padstack_name : str + Name of the padstack. + layer_name : str, optional + Name of the layer. If None, all layers will be taken. + pad_shape : str, optional + Shape of the pad. The default is ``"Circle"``. Options are ``"Circle"``, ``"Square"``, ``"Rectangle"``, + ``"Oval"`` and ``"Bullet"``. + pad_params : str, optional + Dimension of the pad. The default is ``"0"``. + pad_x_offset : str, optional + X offset of the pad. The default is ``"0"``. + pad_y_offset : str, optional + Y offset of the pad. The default is ``"0"``. + pad_rotation : str, optional + Rotation of the pad. The default is ``"0"``. + antipad_shape : str, optional + Shape of the antipad. The default is ``"0"``. + antipad_params : str, optional + Dimension of the antipad. The default is ``"0"``. + antipad_x_offset : str, optional + X offset of the antipad. The default is ``"0"``. + antipad_y_offset : str, optional + Y offset of the antipad. The default is ``"0"``. + antipad_rotation : str, optional + Rotation of the antipad. The default is ``"0"``. + + Returns + ------- + bool + ``True`` if successful. + """ + shape_dict = { + "Circle": GrpcPadGeometryType.PADGEOMTYPE_CIRCLE, + "Square": GrpcPadGeometryType.PADGEOMTYPE_SQUARE, + "Rectangle": GrpcPadGeometryType.PADGEOMTYPE_RECTANGLE, + "Oval": GrpcPadGeometryType.PADGEOMTYPE_OVAL, + "Bullet": GrpcPadGeometryType.PADGEOMTYPE_BULLET, + } + pad_shape = shape_dict[pad_shape] + if not isinstance(pad_params, list): + pad_params = [pad_params] + pad_params = [GrpcValue(i) for i in pad_params] + pad_x_offset = GrpcValue(pad_x_offset) + pad_y_offset = GrpcValue(pad_y_offset) + pad_rotation = GrpcValue(pad_rotation) + + antipad_shape = shape_dict[antipad_shape] + if not isinstance(antipad_params, list): + antipad_params = [antipad_params] + antipad_params = [GrpcValue(i) for i in antipad_params] + antipad_x_offset = GrpcValue(antipad_x_offset) + antipad_y_offset = GrpcValue(antipad_y_offset) + antipad_rotation = GrpcValue(antipad_rotation) + new_padstack_def = GrpcPadstackDefData(self.definitions[padstack_name].data) + if not layer_name: + layer_name = list(self._pedb.stackup.signal_layers.keys()) + elif isinstance(layer_name, str): + layer_name = [layer_name] + for layer in layer_name: + new_padstack_def.set_pad_parameters( + layer=layer, + pad_type=GrpcPadType.REGULAR_PAD, + offset_x=pad_x_offset, + offset_y=pad_y_offset, + rotation=pad_rotation, + type_geom=pad_shape, + sizes=pad_params, + ) + new_padstack_def.set_pad_parameters( + layer=layer, + pad_type=GrpcPadType.ANTI_PAD, + offset_x=antipad_x_offset, + offset_y=antipad_y_offset, + rotation=antipad_rotation, + type_geom=antipad_shape, + sizes=antipad_params, + ) + self.definitions[padstack_name].data = new_padstack_def + return True + + def get_instances( + self, + name=None, + pid=None, + definition_name=None, + net_name=None, + component_reference_designator=None, + component_pin=None, + ): + """Get padstack instances by conditions. + + Parameters + ---------- + name : str, optional + Name of the padstack. + pid : int, optional + Id of the padstack. + definition_name : str, list, optional + Name of the padstack definition. + net_name : str, optional + The net name to be used for filtering padstack instances. + component_pin: str, optional + Pin Number of the component. + Returns + ------- + list + List of :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`. + """ + + instances_by_id = self.instances + if pid: + return instances_by_id[pid] + elif name: + instances = [inst for inst in list(self.instances.values()) if inst.name == name] + if instances: + return instances + else: + instances = list(instances_by_id.values()) + if definition_name: + definition_name = definition_name if isinstance(definition_name, list) else [definition_name] + instances = [inst for inst in instances if inst.padstack_def.name in definition_name] + if net_name: + net_name = net_name if isinstance(net_name, list) else [net_name] + instances = [inst for inst in instances if inst.net_name in net_name] + if component_reference_designator: + refdes = ( + component_reference_designator + if isinstance(component_reference_designator, list) + else [component_reference_designator] + ) + instances = [inst for inst in instances if inst.component] + instances = [inst for inst in instances if inst.component.refdes in refdes] + if component_pin: + component_pin = component_pin if isinstance(component_pin, list) else [component_pin] + instances = [inst for inst in instances if inst.component_pin in component_pin] + return instances + + def get_reference_pins( + self, positive_pin, reference_net="gnd", search_radius=5e-3, max_limit=0, component_only=True + ): + """Search for reference pins using given criteria. + + Parameters + ---------- + positive_pin : EDBPadstackInstance + Pin used for evaluating the distance on the reference pins found. + reference_net : str, optional + Reference net. The default is ``"gnd"``. + search_radius : float, optional + Search radius for finding padstack instances. The default is ``5e-3``. + max_limit : int, optional + Maximum limit for the padstack instances found. The default is ``0``, in which + case no limit is applied. The maximum limit value occurs on the nearest + reference pins from the positive one that is found. + component_only : bool, optional + Whether to limit the search to component padstack instances only. The + default is ``True``. When ``False``, the search is extended to the entire layout. + + Returns + ------- + list + List of :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`. + + Examples + -------- + >>> edbapp = Edb("target_path") + >>> pin = edbapp.components.instances["J5"].pins["19"] + >>> reference_pins = edbapp.padstacks.get_reference_pins(positive_pin=pin, reference_net="GND", + >>> search_radius=5e-3, max_limit=0, component_only=True) + """ + pinlist = [] + if not positive_pin: + search_radius = 10e-2 + component_only = True + if component_only: + references_pins = [ + pin for pin in list(positive_pin.component.pins.values()) if pin.net_name == reference_net + ] + if not references_pins: + return pinlist + else: + references_pins = self.get_instances(net_name=reference_net) + if not references_pins: + return pinlist + pinlist = [ + p + for p in references_pins + if GeometryOperators.points_distance(positive_pin.position, p.position) <= search_radius + ] + if max_limit and len(pinlist) > max_limit: + pin_dict = {GeometryOperators.points_distance(positive_pin.position, p.position): p for p in pinlist} + pinlist = [pin[1] for pin in sorted(pin_dict.items())[:max_limit]] + return pinlist + + def get_padstack_instances_rtree_index(self, nets=None): + """Returns padstack instances Rtree index. + + Parameters + ---------- + nets : str or list, optional + net name of list of nets name applying filtering on padstack instances selection. If ``None`` is provided + all instances are included in the index. Default value is ``None``. + + Returns + ------- + Rtree index object. + + """ + if isinstance(nets, str): + nets = [nets] + padstack_instances_index = rtree.index.Index() + if nets: + instances = [inst for inst in list(self.instances.values()) if inst.net_name in nets] + else: + instances = list(self.instances.values()) + for inst in instances: + padstack_instances_index.insert(inst.id, inst.position) + return padstack_instances_index + + def get_padstack_instances_intersecting_bounding_box(self, bounding_box, nets=None): + """Returns the list of padstack instances ID intersecting a given bounding box and nets. + + Parameters + ---------- + bounding_box : tuple or list. + bounding box, [x1, y1, x2, y2] + nets : str or list, optional + net name of list of nets name applying filtering on padstack instances selection. If ``None`` is provided + all instances are included in the index. Default value is ``None``. + + Returns + ------- + List of padstack instances ID intersecting the bounding box. + """ + if not bounding_box: + raise Exception("No bounding box was provided") + index = self.get_padstack_instances_rtree_index(nets=nets) + if not len(bounding_box) == 4: + raise Exception("The bounding box length must be equal to 4") + if isinstance(bounding_box, list): + bounding_box = tuple(bounding_box) + return list(index.intersection(bounding_box)) + + def merge_via_along_lines( + self, net_name="GND", distance_threshold=5e-3, minimum_via_number=6, selected_angles=None + ): + """Replace padstack instances along lines into a single polygon. + + Detect all padstack instances that are placed along lines and replace them by a single polygon based one + forming a wall shape. This method is designed to simplify meshing on via fence usually added to shield RF traces + on PCB. + + Parameters + ---------- + net_name : str + Net name used for detected padstack instances. Default value is ``"GND"``. + + distance_threshold : float, None, optional + If two points in a line are separated by a distance larger than `distance_threshold`, + the line is divided in two parts. Default is ``5e-3`` (5mm), in which case the control is not performed. + + minimum_via_number : int, optional + The minimum number of points that a line must contain. Default is ``6``. + + selected_angles : list[int, float] + Specify angle in degrees to detected, for instance [0, 180] is only detecting horizontal and vertical lines. + Other values can be assigned like 45 degrees. When `None` is provided all lines are detected. Default value + is `None`. + + Returns + ------- + bool + ``True`` when succeeded ``False`` when failed. < + + """ + _def = list(set([inst.padstack_def for inst in list(self.instances.values()) if inst.net_name == net_name])) + if not _def: + self._logger.error(f"No padstack definition found for net {net_name}") + return False + _instances_to_delete = [] + padstack_instances = [] + for pdstk_def in _def: + padstack_instances.append( + [inst for inst in self.definitions[pdstk_def.name].instances if inst.net_name == net_name] + ) + for pdstk_series in padstack_instances: + instances_location = [inst.position for inst in pdstk_series] + lines, line_indexes = GeometryOperators.find_points_along_lines( + points=instances_location, + minimum_number_of_points=minimum_via_number, + distance_threshold=distance_threshold, + selected_angles=selected_angles, + ) + for line in line_indexes: + [_instances_to_delete.append(pdstk_series[ind]) for ind in line] + start_point = pdstk_series[line[0]] + stop_point = pdstk_series[line[-1]] + padstack_def = start_point.padstack_def + trace_width = ( + self.definitions[padstack_def.name].pad_by_layer[stop_point.start_layer].parameters_values[0] + ) + trace = self._pedb.modeler.create_trace( + path_list=[start_point.position, stop_point.position], + layer_name=start_point.start_layer, + width=trace_width, + ) + polygon_data = trace.polygon_data + trace.delete() + new_padstack_def = generate_unique_name(padstack_def.name) + if not self.create( + padstackname=new_padstack_def, + pad_shape="Polygon", + antipad_shape="Polygon", + pad_polygon=polygon_data, + antipad_polygon=polygon_data, + polygon_hole=polygon_data, + ): + self._logger.error(f"Failed to create padstack definition {new_padstack_def.name}") + if not self.place(position=[0, 0], definition_name=new_padstack_def, net_name=net_name): + self._logger.error(f"Failed to place padstack instance {new_padstack_def.name}") + for inst in _instances_to_delete: + inst.delete() + return True + + def merge_via(self, contour_boxes, net_filter=None, start_layer=None, stop_layer=None): + """Evaluate padstack instances included on the provided point list and replace all by single instance. + + Parameters + ---------- + contour_boxes : List[List[List[float, float]]] + Nested list of polygon with points [x,y]. + net_filter : optional + List[str: net_name] apply a net filter, + nets included in the filter are excluded from the via merge. + start_layer : optional, str + Padstack instance start layer, if `None` the top layer is selected. + stop_layer : optional, str + Padstack instance stop layer, if `None` the bottom layer is selected. + + Return + ------ + List[str], list of created padstack instances ID. + + """ + merged_via_ids = [] + if not contour_boxes: + self._pedb.logger.error("No contour box provided, you need to pass a nested list as argument.") + return False + if not start_layer: + start_layer = list(self._pedb.stackup.layers.values())[0].name + if not stop_layer: + stop_layer = list(self._pedb.stackup.layers.values())[-1].name + instances_index = {} + for id, inst in self.instances.items(): + instances_index[id] = inst.position + for contour_box in contour_boxes: + instances = self.get_padstack_instances_id_intersecting_polygon( + points=contour_box, padstack_instances_index=instances_index + ) + if net_filter: + instances = [self.instances[id] for id in instances if not self.instances[id].net.name in net_filter] + net = self.instances[instances[0]].net.name + instances_pts = np.array([self.instances[id].position for id in instances]) + convex_hull_contour = ConvexHull(instances_pts) + contour_points = list(instances_pts[convex_hull_contour.vertices]) + layer = list(self._pedb.stackup.layers.values())[0].name + polygon = self._pedb.modeler.create_polygon(main_shape=contour_points, layer_name=layer) + polygon_data = polygon.polygon_data + polygon.delete() + new_padstack_def = generate_unique_name("test") + if not self.create( + padstackname=new_padstack_def, + pad_shape="Polygon", + antipad_shape="Polygon", + pad_polygon=polygon_data, + antipad_polygon=polygon_data, + polygon_hole=polygon_data, + start_layer=start_layer, + stop_layer=stop_layer, + ): + self._logger.error(f"Failed to create padstack definition {new_padstack_def}") + merged_instance = self.place(position=[0, 0], definition_name=new_padstack_def, net_name=net) + merged_via_ids.append(merged_instance.id) + [self.instances[id].delete() for id in instances] + return merged_via_ids diff --git a/src/pyedb/grpc/database/ports/__init__.py b/src/pyedb/grpc/database/ports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/pyedb/grpc/database/ports/ports.py b/src/pyedb/grpc/database/ports/ports.py new file mode 100644 index 0000000000..8998da46f1 --- /dev/null +++ b/src/pyedb/grpc/database/ports/ports.py @@ -0,0 +1,293 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.dotnet.database.cell.terminal.terminal import Terminal +from pyedb.grpc.database.terminal.bundle_terminal import BundleTerminal +from pyedb.grpc.database.terminal.edge_terminal import EdgeTerminal +from pyedb.grpc.database.terminal.padstack_instance_terminal import ( + PadstackInstanceTerminal, +) + + +class GapPort(EdgeTerminal): + """Manages gap port properties. + + Parameters + ---------- + pedb : pyedb.edb.Edb + EDB object from the ``Edblib`` library. + edb_object : Ansys.Ansoft.Edb.Cell.Terminal.EdgeTerminal + Edge terminal instance from EDB. + + Examples + -------- + This example shows how to access the ``GapPort`` class. + >>> from pyedb import Edb + >>> edb = Edb("myaedb.aedb") + >>> gap_port = edb.ports["gap_port"] + """ + + def __init__(self, pedb, edb_object): + super().__init__(pedb, edb_object) + + @property + def magnitude(self): + """Magnitude.""" + return self._edb_object.source_amplitude.value + + @property + def phase(self): + """Phase.""" + return self._edb_object.source_phase.value + + @property + def renormalize(self): + """Whether renormalize is active.""" + return self._edb_object.port_post_processing_prop.do_renormalize + + @property + def deembed(self): + """Inductance value of the deembed gap port.""" + return self._edb_object.port_post_processing_prop.do_deembed + + @property + def renormalize_z0(self): + """Renormalize Z0 value (real, imag).""" + return ( + self._edb_object.port_post_processing_prop.renormalizion_z0[0], + self._edb_object.port_post_processing_prop.renormalizion_z0[1], + ) + + +class CircuitPort(GapPort): + """Manages gap port properties. + Parameters + ---------- + pedb : pyedb.edb.Edb + EDB object from the ``Edblib`` library. + edb_object : Ansys.Ansoft.Edb.Cell.Terminal.EdgeTerminal + Edge terminal instance from EDB. + Examples + -------- + This example shows how to access the ``GapPort`` class. + """ + + def __init__(self, pedb, edb_object): + super().__init__(pedb, edb_object) + + +class WavePort(EdgeTerminal): + """Manages wave port properties. + + Parameters + ---------- + pedb : pyedb.edb.Edb + EDB object from the ``Edblib`` library. + edb_object : Ansys.Ansoft.Edb.Cell.Terminal.EdgeTerminal + Edge terminal instance from EDB. + + Examples + -------- + This example shows how to access the ``WavePort`` class. + + >>> from pyedb import Edb + >>> edb = Edb("myaedb.aedb") + >>> exc = edb.ports + """ + + def __init__(self, pedb, edb_terminal): + super().__init__(pedb, edb_terminal.msg) + + @property + def horizontal_extent_factor(self): + """Horizontal extent factor.""" + return self._hfss_port_property["Horizontal Extent Factor"] + + @horizontal_extent_factor.setter + def horizontal_extent_factor(self, value): + self.p = p + p = self.p + p["Horizontal Extent Factor"] = value + + @property + def vertical_extent_factor(self): + """Vertical extent factor.""" + return self._hfss_port_property["Vertical Extent Factor"] + + @vertical_extent_factor.setter + def vertical_extent_factor(self, value): + p = self._hfss_port_property + p["Vertical Extent Factor"] = value + self._hfss_port_property = p + + @property + def pec_launch_width(self): + """Launch width for the printed electronic component (PEC).""" + return self._hfss_port_property["PEC Launch Width"] + + @pec_launch_width.setter + def pec_launch_width(self, value): + p = self._hfss_port_property + p["PEC Launch Width"] = value + self._hfss_port_property = p + + @property + def deembed(self): + """Whether deembed is active.""" + return self._edb_object.port_post_processing_prop.do_deembed + + @deembed.setter + def deembed(self, value): + p = self._edb_object.port_post_processing_prop + p.DoDeembed = value + self._edb_object.port_post_processing_prop = p + + @property + def deembed_length(self): + """Deembed Length.""" + return self._edb_object.port_post_processing_prop.deembed_length.value + + @deembed_length.setter + def deembed_length(self, value): + p = self._edb_object.port_post_processing_prop + p.deembed_length = GrpcValue(value) + self._edb_object.port_post_processing_prop = p + + +class ExcitationSources(Terminal): + """Manage sources properties. + + Parameters + ---------- + pedb : pyedb.edb.Edb + Edb object from Edblib. + edb_terminal : Ansys.Ansoft.Edb.Cell.Terminal.EdgeTerminal + Edge terminal instance from Edb. + + + + Examples + -------- + This example shows how to access this class. + >>> from pyedb import Edb + >>> edb = Edb("myaedb.aedb") + >>> all_sources = edb.sources + >>> print(all_sources["VSource1"].name) + + """ + + def __init__(self, pedb, edb_terminal): + Terminal.__init__(self, pedb, edb_terminal) + + +class BundleWavePort(BundleTerminal): + """Manages bundle wave port properties. + + Parameters + ---------- + pedb : pyedb.edb.Edb + EDB object from the ``Edblib`` library. + edb_object : Ansys.Ansoft.Edb.Cell.Terminal.BundleTerminal + BundleTerminal instance from EDB. + + """ + + def __init__(self, pedb, edb_object): + super().__init__(pedb, edb_object) + + @property + def _wave_port(self): + return WavePort(self._pedb, self.terminals[0]._edb_object) + + @property + def horizontal_extent_factor(self): + """Horizontal extent factor.""" + return self._wave_port.horizontal_extent_factor + + @horizontal_extent_factor.setter + def horizontal_extent_factor(self, value): + self._wave_port.horizontal_extent_factor = value + + @property + def vertical_extent_factor(self): + """Vertical extent factor.""" + return self._wave_port.vertical_extent_factor + + @vertical_extent_factor.setter + def vertical_extent_factor(self, value): + self._wave_port.vertical_extent_factor = value + + @property + def pec_launch_width(self): + """Launch width for the printed electronic component (PEC).""" + return self._wave_port.pec_launch_width + + @pec_launch_width.setter + def pec_launch_width(self, value): + self._wave_port.pec_launch_width = value + + @property + def deembed(self): + """Whether deembed is active.""" + return self._wave_port.deembed + + @deembed.setter + def deembed(self, value): + self._wave_port.deembed = value + + @property + def deembed_length(self): + """Deembed Length.""" + return self._wave_port.deembed_length + + @deembed_length.setter + def deembed_length(self, value): + self._wave_port.deembed_length = value + + +class CoaxPort(PadstackInstanceTerminal): + """Manages bundle wave port properties. + + Parameters + ---------- + pedb : pyedb.edb.Edb + EDB object from the ``Edblib`` library. + edb_object : Ansys.Ansoft.Edb.Cell.Terminal.PadstackInstanceTerminal + PadstackInstanceTerminal instance from EDB. + + """ + + def __init__(self, pedb, edb_object): + super().__init__(pedb, edb_object) + + @property + def radial_extent_factor(self): + """Radial extent factor.""" + return self._hfss_port_property["Radial Extent Factor"] + + @radial_extent_factor.setter + def radial_extent_factor(self, value): + p = self._hfss_port_property + p["Radial Extent Factor"] = value + self._hfss_port_property = p diff --git a/src/pyedb/grpc/database/primitive/__init__.py b/src/pyedb/grpc/database/primitive/__init__.py new file mode 100644 index 0000000000..c23e620fca --- /dev/null +++ b/src/pyedb/grpc/database/primitive/__init__.py @@ -0,0 +1,3 @@ +from pathlib import Path + +workdir = Path(__file__).parent diff --git a/src/pyedb/grpc/database/primitive/bondwire.py b/src/pyedb/grpc/database/primitive/bondwire.py new file mode 100644 index 0000000000..5c534b913d --- /dev/null +++ b/src/pyedb/grpc/database/primitive/bondwire.py @@ -0,0 +1,155 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.primitive.primitive import ( + BondwireCrossSectionType as GrpcBondwireCrossSectionType, +) +from ansys.edb.core.primitive.primitive import Bondwire as GrpcBondWire +from ansys.edb.core.primitive.primitive import BondwireType as GrpcBondWireType +from ansys.edb.core.utility.value import Value as GrpcValue + + +class Bondwire(GrpcBondWire): + """Class representing a bond-wire object.""" + + def __init__(self, _pedb, edb_object): + super().__init__(edb_object.msg) + self._pedb = _pedb + self._edb_object = edb_object + + @property + def material(self): + return self.get_material().value + + @material.setter + def material(self, value): + self.set_material(value) + + # def __create(self, **kwargs): + # return Bondwire.create( + # self._pedb.layout, + # kwargs.get("net"), + # self._bondwire_type[kwargs.get("bondwire_type")], + # kwargs.get("definition_name"), + # kwargs.get("placement_layer"), + # kwargs.get("width"), + # kwargs.get("material"), + # kwargs.get("start_context"), + # kwargs.get("start_layer_name"), + # kwargs.get("start_x"), + # kwargs.get("start_y"), + # kwargs.get("end_context"), + # kwargs.get("end_layer_name"), + # kwargs.get("end_x"), + # kwargs.get("end_y"), + # ) + + @property + def type(self): + """str: Bondwire-type of a bondwire object. Supported values for setter: `"apd"`, `"jedec4"`, `"jedec5"`, + `"num_of_type"`""" + return super().type.name.lower() + + @type.setter + def type(self, bondwire_type): + mapping = { + "apd": GrpcBondWireType.APD, + "jedec4": GrpcBondWireType.JEDEC4, + "jedec5": GrpcBondWireType.JEDEC5, + "num_of_type": GrpcBondWireType.NUM_OF_TYPE, + } + super(Bondwire, self.__class__).type.__set__(self, mapping[bondwire_type]) + + @property + def cross_section_type(self): + """str: Bondwire-cross-section-type of a bondwire object. Supported values for setter: `"round", + `"rectangle"`""" + return super().cross_section_type.name.lower() + + @cross_section_type.setter + def cross_section_type(self, cross_section_type): + mapping = {"round": GrpcBondwireCrossSectionType.ROUND, "rectangle": GrpcBondwireCrossSectionType.RECTANGLE} + super(Bondwire, self.__class__).cross_section_type.__set__(self, mapping[cross_section_type]) + + @property + def cross_section_height(self): + """float: Bondwire-cross-section height of a bondwire object.""" + return super().cross_section_height.value + + @cross_section_height.setter + def cross_section_height(self, cross_section_height): + super(Bondwire, self.__class__).cross_section_height.__set__(self, GrpcValue(cross_section_height)) + + # @property + # def trajectory(self): + # """Get trajectory parameters of a bondwire object. + # + # Returns + # ------- + # tuple[float, float, float, float] + # + # Returns a tuple of the following format: + # **(x1, y1, x2, y2)** + # **x1** : X value of the start point. + # **y1** : Y value of the start point. + # **x1** : X value of the end point. + # **y1** : Y value of the end point. + # """ + # return [i.value for i in self.get_traj()] + # + # @trajectory.setter + # def trajectory(self, value): + # values = [GrpcValue(i) for i in value] + # self.set_traj(values[0], values[1], values[2], values[3]) + + @property + def width(self): + """:class:`Value `: Width of a bondwire object.""" + return super().width.value + + @width.setter + def width(self, width): + super(Bondwire, self.__class__).width.__set__(self, GrpcValue(width)) + + # @property + # def start_elevation(self): + # layer = self.get_start_elevation(self._pedb.active_cell) + # return layer.name + # + # @start_elevation.setter + # def start_elevation(self, layer): + # if not layer in self._pedb.stackup.layers: + # return + # layer = self._pedb.stackup.layers[layer] + # self.set_start_elevation(self._pedb.active_cell, layer) + # + # @property + # def end_elevation(self): + # layer = self.get_end_elevation(self._pedb.active_cell) + # return layer.name + # + # @end_elevation.setter + # def end_elevation(self, layer): + # if not layer in self._pedb.stackup.layers: + # return + # layer = self._pedb.stackup.layers[layer] + # self.set_end_elevation(self._pedb.active_cell, layer) diff --git a/src/pyedb/grpc/database/primitive/circle.py b/src/pyedb/grpc/database/primitive/circle.py new file mode 100644 index 0000000000..004db83a3a --- /dev/null +++ b/src/pyedb/grpc/database/primitive/circle.py @@ -0,0 +1,41 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.primitive.primitive import Circle as GrpcCircle +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.grpc.database.primitive.primitive import Primitive + + +class Circle(GrpcCircle, Primitive): + def __init__(self, pedb, edb_object): + GrpcCircle.__init__(self, edb_object.msg) + Primitive.__init__(self, pedb, edb_object) + self._pedb = pedb + + def get_parameters(self): + params = super().get_parameters() + return params[0].value, params[1].value, params[2].value + + def set_parameters(self, center_x, center_y, radius): + super().set_parameters(GrpcValue(center_x), GrpcValue(center_y), GrpcValue(radius)) diff --git a/src/pyedb/grpc/database/primitive/padstack_instances.py b/src/pyedb/grpc/database/primitive/padstack_instances.py new file mode 100644 index 0000000000..beba1a54ca --- /dev/null +++ b/src/pyedb/grpc/database/primitive/padstack_instances.py @@ -0,0 +1,999 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import math +import re + +from ansys.edb.core.database import ProductIdType as GrpcProductIdType +from ansys.edb.core.geometry.point_data import PointData as GrpcPointData +from ansys.edb.core.geometry.polygon_data import PolygonData as GrpcPolygonData +from ansys.edb.core.hierarchy.pin_group import PinGroup as GrpcPinGroup +from ansys.edb.core.primitive.primitive import PadstackInstance as GrpcPadstackInstance +from ansys.edb.core.terminal.terminals import PinGroupTerminal as GrpcPinGroupTerminal +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.grpc.database.definition.padstack_def import PadstackDef +from pyedb.grpc.database.terminal.padstack_instance_terminal import ( + PadstackInstanceTerminal, +) +from pyedb.modeler.geometry_operators import GeometryOperators + + +class PadstackInstance(GrpcPadstackInstance): + """Manages EDB functionalities for a padstack. + + Parameters + ---------- + edb_padstackinstance : + + _pedb : + Inherited AEDT object. + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb(myedb, edbversion="2021.2") + >>> edb_padstack_instance = edb.padstacks.instances[0] + """ + + def __init__(self, pedb, edb_instance): + super().__init__(edb_instance.msg) + self._edb_object = edb_instance + self._bounding_box = [] + self._position = [] + self._pdef = None + self._pedb = pedb + self._object_instance = None + + @property + def definition(self): + return PadstackDef(self._pedb, self.padstack_def) + + @property + def padstack_definition(self): + return self.padstack_def.name + + @property + def terminal(self): + """Terminal.""" + from pyedb.grpc.database.terminal.padstack_instance_terminal import ( + PadstackInstanceTerminal, + ) + + term = PadstackInstanceTerminal(self._pedb, self._edb_object) + return term if not term.is_null else None + + def create_terminal(self, name=None): + """Create a padstack instance terminal""" + if not name: + name = self.name + term = PadstackInstanceTerminal.create( + layout=self.layout, + name=name, + padstack_instance=self, + layer=self.get_layer_range()[0], + net=self.net, + is_ref=False, + ) + return PadstackInstanceTerminal(self._pedb, term) + + def get_terminal(self, create_new_terminal=True): + inst_term = self.get_padstack_instance_terminal() + if inst_term.is_null and create_new_terminal: + inst_term = self.create_terminal() + return PadstackInstanceTerminal(self._pedb, inst_term) + + def create_coax_port(self, name=None, radial_extent_factor=0): + """Create a coax port.""" + port = self.create_port(name) + port.radial_extent_factor = radial_extent_factor + return port + + def create_port(self, name=None, reference=None, is_circuit_port=False): + """Create a port on the padstack instance. + + Parameters + ---------- + name : str, optional + Name of the port. The default is ``None``, in which case a name is automatically assigned. + reference : reference net or pingroup optional + Negative terminal of the port. + is_circuit_port : bool, optional + Whether it is a circuit port. + """ + if not reference: + return self.create_terminal(name) + else: + positive_terminal = self.create_terminal() + if positive_terminal.is_null: + self._pedb.logger( + f"Positive terminal on padsatck instance {self.name} is null. Make sure a terminal" + f"is not already defined." + ) + negative_terminal = None + if isinstance(reference, list): + pg = GrpcPinGroup.create(self.layout, name=f"pingroup_{self.name}_ref", padstack_instances=reference) + negative_terminal = GrpcPinGroupTerminal.create( + layout=self.layout, + name=f"pingroup_term{self.name}_ref)", + pin_group=pg, + net=reference[0].net, + is_ref=True, + ) + is_circuit_port = True + else: + if isinstance(reference, PadstackInstance): + negative_terminal = reference.create_terminal() + elif isinstance(reference, str): + if reference in self._pedb.padstacks.instances: + reference = self._pedb.padstacks.instances[reference] + else: + pin_groups = [pg for pg in self._pedb.active_layout.pin_groups if pg.name == reference] + if pin_groups: + reference = pin_groups[0] + else: + self._pedb.logger.error(f"No reference found for {reference}") + return False + negative_terminal = reference.create_terminal() + if negative_terminal: + positive_terminal.reference_terminal = negative_terminal + else: + self._pedb.logger.error("No reference terminal created") + return False + positive_terminal.is_circuit_port = is_circuit_port + negative_terminal.is_circuit_port = is_circuit_port + return positive_terminal + + @property + def _em_properties(self): + """Get EM properties.""" + from ansys.edb.core.database import ProductIdType + + default = ( + r"$begin 'EM properties'\n" + r"\tType('Mesh')\n" + r"\tDataId='EM properties1'\n" + r"\t$begin 'Properties'\n" + r"\t\tGeneral=''\n" + r"\t\tModeled='true'\n" + r"\t\tUnion='true'\n" + r"\t\t'Use Precedence'='false'\n" + r"\t\t'Precedence Value'='1'\n" + r"\t\tPlanarEM=''\n" + r"\t\tRefined='true'\n" + r"\t\tRefineFactor='1'\n" + r"\t\tNoEdgeMesh='false'\n" + r"\t\tHFSS=''\n" + r"\t\t'Solve Inside'='false'\n" + r"\t\tSIwave=''\n" + r"\t\t'DCIR Equipotential Region'='false'\n" + r"\t$end 'Properties'\n" + r"$end 'EM properties'\n" + ) + + p = self.get_product_property(ProductIdType.DESIGNER, 18) + if p: + return p + else: + return default + + @_em_properties.setter + def _em_properties(self, em_prop): + """Set EM properties""" + pid = self._pedb.edb_api.ProductId.Designer + self.set_product_property(pid, 18, em_prop) + + @property + def dcir_equipotential_region(self): + """Check whether dcir equipotential region is enabled. + + Returns + ------- + bool + """ + pattern = r"'DCIR Equipotential Region'='([^']+)'" + em_pp = self._em_properties + result = re.search(pattern, em_pp).group(1) + if result == "true": + return True + else: + return False + + @dcir_equipotential_region.setter + def dcir_equipotential_region(self, value): + """Set dcir equipotential region.""" + pp = r"'DCIR Equipotential Region'='true'" if value else r"'DCIR Equipotential Region'='false'" + em_pp = self._em_properties + pattern = r"'DCIR Equipotential Region'='([^']+)'" + new_em_pp = re.sub(pattern, pp, em_pp) + self._em_properties = new_em_pp + + @property + def object_instance(self): + """Return Ansys.Ansoft.Edb.LayoutInstance.LayoutObjInstance object.""" + if not self._object_instance: + self._object_instance = self.layout.layout_instance.get_layout_obj_instance_in_context(self, None) + return self._object_instance + + @property + def bounding_box(self): + """Get bounding box of the padstack instance. + Because this method is slow, the bounding box is stored in a variable and reused. + + Returns + ------- + list of float + """ + # TODO check to implement in grpc + if self._bounding_box: + return self._bounding_box + return self._bounding_box + + def in_polygon(self, polygon_data, include_partial=True): + """Check if padstack Instance is in given polygon data. + + Parameters + ---------- + polygon_data : PolygonData Object + include_partial : bool, optional + Whether to include partial intersecting instances. The default is ``True``. + simple_check : bool, optional + Whether to perform a single check based on the padstack center or check the padstack bounding box. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + int_val = 1 if polygon_data.point_in_polygon(GrpcPointData(self.position)) else 0 + if int_val == 0: + return False + else: + int_val = polygon_data.intersection_type(GrpcPolygonData(self.bounding_box)) + # Intersection type: + # 0 = objects do not intersect + # 1 = this object fully inside other (no common contour points) + # 2 = other object fully inside this + # 3 = common contour points 4 = undefined intersection + if int_val == 0: + return False + elif include_partial: + return True + elif int_val < 3: + return True + else: + return False + + @property + def start_layer(self): + """Starting layer. + + Returns + ------- + str + Name of the starting layer. + """ + return self.get_layer_range()[0].name + + @start_layer.setter + def start_layer(self, layer_name): + stop_layer = self._pedb.stackup.signal_layers[self.stop_layer] + start_layer = self._pedb.stackup.signal_layers[layer_name] + self.set_layer_range(start_layer, stop_layer) + + @property + def stop_layer(self): + """Stopping layer. + + Returns + ------- + str + Name of the stopping layer. + """ + return self.get_layer_range()[-1].name + + @stop_layer.setter + def stop_layer(self, layer_name): + start_layer = self._pedb.stackup.signal_layers[self.start_layer] + stop_layer = self._pedb.stackup.signal_layers[layer_name] + self.set_layer_range(start_layer, stop_layer) + + @property + def layer_range_names(self): + """List of all layers to which the padstack instance belongs.""" + start_layer, stop_layer = self.get_layer_range() + started = False + layer_list = [] + start_layer_name = start_layer.name + stop_layer_name = stop_layer.name + for layer_name in list(self._pedb.stackup.layers.keys()): + if started: + layer_list.append(layer_name) + if layer_name == stop_layer_name or layer_name == start_layer_name: + break + elif layer_name == start_layer_name: + started = True + layer_list.append(layer_name) + if layer_name == stop_layer_name: + break + elif layer_name == stop_layer_name: + started = True + layer_list.append(layer_name) + if layer_name == start_layer_name: + break + return layer_list + + @property + def net_name(self): + """Net name. + + Returns + ------- + str + Name of the net. + """ + if self.is_null: + return "" + elif self.net.is_null: + return "" + else: + return self.net.name + + @net_name.setter + def net_name(self, val): + if not self.is_null and not self.net.is_null: + self.net = self._pedb.nets.nets[val] + + @property + def layout_object_instance(self): + obj_inst = [ + obj + for obj in self._pedb.layout_instance.query_layout_obj_instances( + spatial_filter=GrpcPointData(self.position) + ) + if obj.layout_obj.id == self.id + ] + return obj_inst[0] if obj_inst else None + + @property + def is_pin(self): + """Determines whether this padstack instance is a layout pin. + + Returns + ------- + bool + True if this padstack type is a layout pin, False otherwise. + """ + return self.is_layout_pin + + @is_pin.setter + def is_pin(self, value): + self.is_layout_pin = value + + @property + def component(self): + """Component.""" + from pyedb.grpc.database.hierarchy.component import Component + + comp = Component(self._pedb, super().component) + return comp if not comp.is_null else False + + @property + def position(self): + """Padstack instance position. + + Returns + ------- + list + List of ``[x, y]`` coordinates for the padstack instance position. + """ + position = self.get_position_and_rotation() + if self.component: + out2 = self.component.transform.transform_point(GrpcPointData(position[:2])) + self._position = [round(out2[0].value, 6), round(out2[1].value, 6)] + else: + self._position = [round(pt.value, 6) for pt in position[:2]] + return self._position + + @position.setter + def position(self, value): + pos = [] + for v in value: + if isinstance(v, (float, int, str)): + pos.append(GrpcValue(v, self._pedb.active_cell)) + else: + pos.append(v) + point_data = GrpcPointData(pos[0], pos[1]) + self.set_position_and_rotation( + x=point_data.x, y=point_data.y, rotation=GrpcValue(self.rotation, self._pedb.active_cell) + ) + + @property + def rotation(self): + """Padstack instance rotation. + + Returns + ------- + float + Rotatation value for the padstack instance. + """ + return self.get_position_and_rotation()[-1].value + + @property + def name(self): + """Padstack Instance Name. If it is a pin, the syntax will be like in AEDT ComponentName-PinName.""" + if not super().name: + return self.aedt_name + else: + return super().name + + @name.setter + def name(self, value): + super(PadstackInstance, self.__class__).name.__set__(self, value) + self.set_product_property(GrpcProductIdType.DESIGNER, 11, value) + + @property + def backdrill_type(self): + return self.get_backdrill_type() + + @property + def backdrill_top(self): + if self.get_back_drill_type(False).value == 0: + return False + else: + try: + if self.get_back_drill_by_layer(from_bottom=False): + return True + except: + return False + + @property + def backdrill_bottom(self): + if self.get_back_drill_type(True).value == 0: + return False + else: + try: + if self.get_back_drill_by_layer(True): + return True + except: + return False + + @property + def metal_volume(self): + """Metal volume of the via hole instance in cubic units (m3). Metal plating ratio is accounted. + + Returns + ------- + float + Metal volume of the via hole instance. + + """ + volume = 0 + if not self.start_layer == self.stop_layer: + start_layer = self.start_layer + stop_layer = self.stop_layer + via_length = ( + self._pedb.stackup.signal_layers[start_layer].upper_elevation + - self._pedb.stackup.signal_layers[stop_layer].lower_elevation + ) + if self.get_backdrill_type == "layer_drill": + layer, _, _ = self.get_back_drill_by_layer() + start_layer = self._pedb.stackup.signal_layers[0] + stop_layer = self._pedb.stackup.signal_layers[layer.name] + via_length = ( + self._pedb.stackup.signal_layers[start_layer].upper_elevation + - self._pedb.stackup.signal_layers[stop_layer].lower_elevation + ) + elif self.get_backdrill_type == "depth_drill": + drill_depth, _ = self.get_back_drill_by_depth() + start_layer = self._pedb.stackup.signal_layers[0] + via_length = self._pedb.stackup.signal_layers[start_layer].upper_elevation - drill_depth + padstack_def = self._pedb.padstacks.definitions[self.padstack_def.name] + hole_diameter = padstack_def.hole_diameter + if hole_diameter: + hole_finished_size = padstack_def.hole_finished_size + volume = (math.pi * (hole_diameter / 2) ** 2 - math.pi * (hole_finished_size / 2) ** 2) * via_length + return volume + + @property + def component_pin(self): + """Get component pin.""" + return self.name + + @property + def aedt_name(self): + """Retrieve the pin name that is shown in AEDT. + + .. note:: + To obtain the EDB core pin name, use `pin.GetName()`. + + Returns + ------- + str + Name of the pin in AEDT. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> edbapp.padstacks.instances[111].get_aedt_pin_name() + + """ + + name = self.get_product_property(GrpcProductIdType.DESIGNER, 11) + return str(name).strip("'") + + def get_backdrill_type(self, from_bottom=True): + """Return backdrill type + Parameters + ---------- + from_bottom : bool, optional + default value is `True.` + + Return + ------ + str + Back drill type, `"layer_drill"`,`"depth_drill"`, `"no_drill"`. + + """ + return super().get_back_drill_type(from_bottom).name.lower() + + def get_back_drill_by_layer(self, from_bottom=True): + """Get backdrill by layer. + + Parameters + ---------- + from_bottom : bool, optional. + Default value is `True`. + + Return + ------ + tuple (layer, offset, diameter) (str, [float, float], float). + + """ + back_drill = super().get_back_drill_by_layer(from_bottom) + layer = back_drill[0].name + offset = round(back_drill[1].value, 9) + diameter = round(back_drill[2].value, 9) + return layer, offset, diameter + + def get_back_drill_by_depth(self, from_bottom=True): + """Get back drill by depth parameters + Parameters + ---------- + from_bottom : bool, optional + Default value is `True`. + + Return + ------ + tuple (drill_depth, drill_diameter) (float, float) + """ + back_drill = super().get_back_drill_by_depth(from_bottom) + drill_depth = back_drill[0].value + drill_diameter = back_drill[1].value + return drill_depth, drill_diameter + + def set_back_drill_by_depth(self, drill_depth, diameter, from_bottom=True): + """Set back drill by depth. + + Parameters + ---------- + drill_depth : str, float + drill depth value + diameter : str, float + drill diameter + from_bottom : bool, optional + Default value is `True`. + """ + super().set_back_drill_by_depth( + drill_depth=GrpcValue(drill_depth), diameter=GrpcValue(diameter), from_bottom=from_bottom + ) + + def set_back_drill_by_layer(self, drill_to_layer, offset, diameter, from_bottom=True): + """Set back drill layer. + + Parameters + ---------- + drill_to_layer : str, Layer + Layer to drill to. + offset : str, float + Offset value + diameter : str, float + Drill diameter + from_bottom : bool, optional + Default value is `True` + """ + if isinstance(drill_to_layer, str): + drill_to_layer = self._pedb.stackup.layers[drill_to_layer] + super().set_back_drill_by_layer( + drill_to_layer=drill_to_layer, + offset=GrpcValue(offset), + diameter=GrpcValue(diameter), + from_bottom=from_bottom, + ) + + def parametrize_position(self, prefix=None): + """Parametrize the instance position. + + Parameters + ---------- + prefix : str, optional + Prefix for the variable name. Default is ``None``. + Example `"MyVariableName"` will create 2 Project variables $MyVariableNamesX and $MyVariableNamesY. + + Returns + ------- + List + List of variables created. + """ + p = self.position + if not prefix: + var_name = "${}_pos".format(self.name) + else: + var_name = "${}".format(prefix) + self._pedb.add_project_variable(var_name + "X", p[0]) + self._pedb.add_project_variable(var_name + "Y", p[1]) + self.position = [var_name + "X", var_name + "Y"] + return [var_name + "X", var_name + "Y"] + + def in_voids(self, net_name=None, layer_name=None): + """Check if this padstack instance is in any void. + + Parameters + ---------- + net_name : str + Net name of the voids to be checked. Default is ``None``. + layer_name : str + Layer name of the voids to be checked. Default is ``None``. + + Returns + ------- + list + List of the voids that include this padstack instance. + """ + x_pos = GrpcValue(self.position[0]) + y_pos = GrpcValue(self.position[1]) + point_data = GrpcPointData([x_pos, y_pos]) + + voids = [] + for prim in self._pedb.modeler.get_primitives(net_name, layer_name, is_void=True): + if prim.polygon_data.point_in_polygon(point_data): + voids.append(prim) + return voids + + @property + def pingroups(self): + """Pin groups that the pin belongs to. + + Returns + ------- + list + List of pin groups that the pin belongs to. + """ + return self.pin_groups + + @property + def placement_layer(self): + """Placement layer name. + + Returns + ------- + str + Name of the placement layer. + """ + return self.component.placement_layer + + @property + def layer(self): + """Placement layer object. + + Returns + ------- + :class:`pyedb.grpc.database.layers.stackup_layer.StackupLayer` + Placement layer. + """ + return self.component.layer + + @property + def lower_elevation(self): + """Lower elevation of the placement layer. + + Returns + ------- + float + Lower elavation of the placement layer. + """ + return self._pedb.stackup.layers[self.component.placement_layer].lower_elevation + + @property + def upper_elevation(self): + """Upper elevation of the placement layer. + + Returns + ------- + float + Upper elevation of the placement layer. + """ + return self._pedb.stackup.layers[self.component.placement_layer].upper_elevation + + @property + def top_bottom_association(self): + """Top/bottom association of the placement layer. + + Returns + ------- + int + Top/bottom association of the placement layer. + + * 0 Top associated. + * 1 No association. + * 2 Bottom associated. + * 4 Number of top/bottom association type. + * -1 Undefined. + """ + return self._pedb.stackup.layers[self.component.placement_layer].top_bottom_association.value + + def create_rectangle_in_pad(self, layer_name, return_points=False, partition_max_order=16): + """Create a rectangle inscribed inside a padstack instance pad. + + The rectangle is fully inscribed in the pad and has the maximum area. + It is necessary to specify the layer on which the rectangle will be created. + + Parameters + ---------- + layer_name : str + Name of the layer on which to create the polygon. + return_points : bool, optional + If `True` does not create the rectangle and just returns a list containing the rectangle vertices. + Default is `False`. + partition_max_order : float, optional + Order of the lattice partition used to find the quasi-lattice polygon that approximates ``polygon``. + Default is ``16``. + + Returns + ------- + bool, List, :class:`pyedb.dotnet.database.edb_data.primitives.EDBPrimitives` + Polygon when successful, ``False`` when failed, list of list if `return_points=True`. + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", edbversion="2021.2") + >>> edb_layout = edbapp.modeler + >>> list_of_padstack_instances = list(edbapp.padstacks.instances.values()) + >>> padstack_inst = list_of_padstack_instances[0] + >>> padstack_inst.create_rectangle_in_pad("TOP") + """ + # TODO check if still used anf fix if yes. + padstack_center = self.position + rotation = self.rotation # in radians + # padstack = self._pedb.padstacks.definitions[self.padstack_def.name] + try: + padstack_pad = PadstackDef(self._pedb, self.padstack_def).pad_by_layer[layer_name] + except KeyError: # pragma: no cover + try: + padstack_pad = PadstackDef(self._pedb, self.padstack_def).pad_by_layer[ + PadstackDef(self._pedb, self.padstack_def).start_layer + ] + except KeyError: # pragma: no cover + return False + + try: + pad_shape = padstack_pad.geometry_type + params = padstack_pad.parameters_values + polygon_data = padstack_pad.polygon_data + except: + self._pedb.logger.warning(f"No pad defined on padstack definition {self.padstack_def.name}") + return False + + def _rotate(p): + x = p[0] * math.cos(rotation) - p[1] * math.sin(rotation) + y = p[0] * math.sin(rotation) + p[1] * math.cos(rotation) + return [x, y] + + def _translate(p): + x = p[0] + padstack_center[0] + y = p[1] + padstack_center[1] + return [x, y] + + rect = None + + if pad_shape == 1: + # Circle + diameter = params[0] + r = diameter * 0.5 + p1 = [r, 0.0] + p2 = [0.0, r] + p3 = [-r, 0.0] + p4 = [0.0, -r] + rect = [_translate(p1), _translate(p2), _translate(p3), _translate(p4)] + elif pad_shape == 2: + # Square + square_size = params[0] + s2 = square_size * 0.5 + p1 = [s2, s2] + p2 = [-s2, s2] + p3 = [-s2, -s2] + p4 = [s2, -s2] + rect = [ + _translate(_rotate(p1)), + _translate(_rotate(p2)), + _translate(_rotate(p3)), + _translate(_rotate(p4)), + ] + elif pad_shape == 3: + # Rectangle + x_size = float(params[0]) + y_size = float(params[1]) + sx2 = x_size * 0.5 + sy2 = y_size * 0.5 + p1 = [sx2, sy2] + p2 = [-sx2, sy2] + p3 = [-sx2, -sy2] + p4 = [sx2, -sy2] + rect = [ + _translate(_rotate(p1)), + _translate(_rotate(p2)), + _translate(_rotate(p3)), + _translate(_rotate(p4)), + ] + elif pad_shape == 4: + # Oval + x_size = params[0] + y_size = params[1] + corner_radius = float(params[2]) + if corner_radius >= min(x_size, y_size): + r = min(x_size, y_size) + else: + r = corner_radius + sx = x_size * 0.5 - r + sy = y_size * 0.5 - r + k = r / math.sqrt(2) + p1 = [sx + k, sy + k] + p2 = [-sx - k, sy + k] + p3 = [-sx - k, -sy - k] + p4 = [sx + k, -sy - k] + rect = [ + _translate(_rotate(p1)), + _translate(_rotate(p2)), + _translate(_rotate(p3)), + _translate(_rotate(p4)), + ] + elif pad_shape == 5: + # Bullet + x_size = params[0] + y_size = params[1] + corner_radius = params[2] + if corner_radius >= min(x_size, y_size): + r = min(x_size, y_size) + else: + r = corner_radius + sx = x_size * 0.5 - r + sy = y_size * 0.5 - r + k = r / math.sqrt(2) + p1 = [sx + k, sy + k] + p2 = [-x_size * 0.5, sy + k] + p3 = [-x_size * 0.5, -sy - k] + p4 = [sx + k, -sy - k] + rect = [ + _translate(_rotate(p1)), + _translate(_rotate(p2)), + _translate(_rotate(p3)), + _translate(_rotate(p4)), + ] + elif pad_shape == 6: + # N-Sided Polygon + size = params[0] + num_sides = params[1] + ext_radius = size * 0.5 + apothem = ext_radius * math.cos(math.pi / num_sides) + p1 = [apothem, 0.0] + p2 = [0.0, apothem] + p3 = [-apothem, 0.0] + p4 = [0.0, -apothem] + rect = [ + _translate(_rotate(p1)), + _translate(_rotate(p2)), + _translate(_rotate(p3)), + _translate(_rotate(p4)), + ] + elif pad_shape == 7 and polygon_data is not None: + # Polygon + points = [] + i = 0 + while i < len(polygon_data.points): + point = polygon_data.points[i] + i += 1 + if point.is_arc: + continue + else: + points.append([point.x.value, point.y.value]) + xpoly, ypoly = zip(*points) + polygon = [list(xpoly), list(ypoly)] + rectangles = GeometryOperators.find_largest_rectangle_inside_polygon( + polygon, partition_max_order=partition_max_order + ) + rect = rectangles[0] + for i in range(4): + rect[i] = _translate(_rotate(rect[i])) + + # if rect is None or len(rect) != 4: + # return False + rect = [GrpcPointData(pt) for pt in rect] + path = GrpcPolygonData(rect) + new_rect = [] + for point in path.points: + if self.component: + p_transf = self.component.transform.transform_point(point) + new_rect.append([p_transf.x.value, p_transf.y.value]) + if return_points: + return new_rect + else: + created_polygon = self._pedb.modeler.create_polygon(path, layer_name) + return created_polygon + + def get_reference_pins(self, reference_net="GND", search_radius=5e-3, max_limit=0, component_only=True): + """Search for reference pins using given criteria. + + Parameters + ---------- + reference_net : str, optional + Reference net. The default is ``"GND"``. + search_radius : float, optional + Search radius for finding padstack instances. The default is ``5e-3``. + max_limit : int, optional + Maximum limit for the padstack instances found. The default is ``0``, in which + case no limit is applied. The maximum limit value occurs on the nearest + reference pins from the positive one that is found. + component_only : bool, optional + Whether to limit the search to component padstack instances only. The + default is ``True``. When ``False``, the search is extended to the entire layout. + + Returns + ------- + list + List of :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance`. + + Examples + -------- + >>> edbapp = Edb("target_path") + >>> pin = edbapp.components.instances["J5"].pins["19"] + >>> reference_pins = pin.get_reference_pins(reference_net="GND", search_radius=5e-3, max_limit=0, + >>> component_only=True) + """ + return self._pedb.padstacks.get_reference_pins( + positive_pin=self, + reference_net=reference_net, + search_radius=search_radius, + max_limit=max_limit, + component_only=component_only, + ) + + def get_connected_objects(self): + """Get connected objects. + + Returns + ------- + list + """ + return self._pedb.get_connected_objects(self.object_instance) diff --git a/src/pyedb/grpc/database/primitive/path.py b/src/pyedb/grpc/database/primitive/path.py new file mode 100644 index 0000000000..de89af46d8 --- /dev/null +++ b/src/pyedb/grpc/database/primitive/path.py @@ -0,0 +1,328 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import math + +from ansys.edb.core.geometry.polygon_data import PolygonData as GrpcPolygonData +from ansys.edb.core.primitive.primitive import Path as GrpcPath +from ansys.edb.core.primitive.primitive import PathCornerType as GrpcPatCornerType +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.grpc.database.primitive.primitive import Primitive + + +class Path(GrpcPath, Primitive): + def __init__(self, pedb, edb_object): + GrpcPath.__init__(self, edb_object.msg) + Primitive.__init__(self, pedb, edb_object) + self._edb_object = edb_object + self._pedb = pedb + + @property + def width(self): + """Path width. + + Returns + ------- + float + Path width or None. + """ + return round(super().width.value, 9) + + @width.setter + def width(self, value): + super(Path, self.__class__).width.__set__(self, GrpcValue(value)) + + @property + def length(self): + """Path length in meters. + + Returns + ------- + float + Path length in meters. + """ + center_line_arcs = self._edb_object.cast().center_line.arc_data + path_length = 0.0 + for arc in center_line_arcs: + path_length += arc.length + end_cap_style = self.get_end_cap_style() + if end_cap_style: + if not end_cap_style[0].value == 1: + path_length += self.width / 2 + if not end_cap_style[1].value == 1: + path_length += self.width / 2 + return round(path_length, 9) + + def add_point(self, x, y, incremental=True): + """Add a point at the end of the path. + + Parameters + ---------- + x: str, int, float + X coordinate. + y: str, in, float + Y coordinate. + incremental: bool + Add point incrementally. If True, coordinates of the added point is incremental to the last point. + The default value is ``True``. + + Returns + ------- + bool + """ + if incremental: + points = self.center_line + points.append([x, y]) + points = GrpcPolygonData(points=points) + GrpcPath.create( + layout=self.layout, + layer=self.layer, + net=self.net, + ) + self.center_line = points + return True + + def clone(self): + """Clone a primitive object with keeping same definition and location. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + mapping = { + "round": GrpcPatCornerType.ROUND, + "mitter": GrpcPatCornerType.MITER, + "sharp": GrpcPatCornerType.SHARP, + } + + cloned_path = GrpcPath.create( + layout=self._pedb.active_layout, + layer=self.layer, + net=self.net, + width=GrpcValue(self.width), + end_cap1=self.get_end_cap_style()[0], + end_cap2=self.get_end_cap_style()[1], + corner_style=mapping[self.corner_style], + points=GrpcPolygonData(self.center_line), + ) + if not cloned_path.is_null: + return Path(self._pedb, cloned_path) + + # + + def create_edge_port( + self, + name, + position="End", + port_type="Wave", + reference_layer=None, + horizontal_extent_factor=5, + vertical_extent_factor=3, + pec_launch_width="0.01mm", + ): + """ + + Parameters + ---------- + name : str + Name of the port. + position : str, optional + Position of the port. The default is ``"End"``, in which case the port is created at the end of the trace. + Options are ``"Start"`` and ``"End"``. + port_type : str, optional + Type of the port. The default is ``"Wave"``, in which case a wave port is created. Options are ``"Wave"`` + and ``"Gap"``. + reference_layer : str, optional + Name of the references layer. The default is ``None``. Only available for gap port. + horizontal_extent_factor : int, optional + Horizontal extent factor of the wave port. The default is ``5``. + vertical_extent_factor : int, optional + Vertical extent factor of the wave port. The default is ``3``. + pec_launch_width : float, str, optional + Perfect electrical conductor width of the wave port. The default is ``"0.01mm"``. + + Returns + ------- + :class:`dotnet.database.edb_data.sources.ExcitationPorts` + + Examples + -------- + >>> edbapp = pyedb.dotnet.Edb("myproject.aedb") + >>> sig = appedb.modeler.create_trace([[0, 0], ["9mm", 0]], "TOP", "1mm", "SIG", "Flat", "Flat") + >>> sig.create_edge_port("pcb_port", "end", "Wave", None, 8, 8) + + """ + center_line = self.center_line + pos = center_line[-1] if position.lower() == "end" else center_line[0] + + # if port_type.lower() == "wave": + # return self._pedb.hfss.create_wave_port( + # self.id, pos, name, 50, horizontal_extent_factor, vertical_extent_factor, pec_launch_width + # ) + # else: + return self._pedb.hfss.create_edge_port_vertical( + self.id, + pos, + name, + 50, + reference_layer, + hfss_type=port_type, + horizontal_extent_factor=horizontal_extent_factor, + vertical_extent_factor=vertical_extent_factor, + pec_launch_width=pec_launch_width, + ) + + def create_via_fence(self, distance, gap, padstack_name, net_name="GND"): + """Create via fences on both sides of the trace. + + Parameters + ---------- + distance: str, float + Distance between via fence and trace center line. + gap: str, float + Gap between vias. + padstack_name: str + Name of the via padstack. + net_name: str, optional + Name of the net. + + Returns + ------- + """ + + def getAngle(v1, v2): # pragma: no cover + v1_mag = math.sqrt(v1[0] ** 2 + v1[1] ** 2) + v2_mag = math.sqrt(v2[0] ** 2 + v2[1] ** 2) + dotsum = v1[0] * v2[0] + v1[1] * v2[1] + if v1[0] * v2[1] - v1[1] * v2[0] > 0: + scale = 1 + else: + scale = -1 + dtheta = scale * math.acos(dotsum / (v1_mag * v2_mag)) + + return dtheta + + def get_locations(line, gap): # pragma: no cover + location = [line[0]] + residual = 0 + + for n in range(len(line) - 1): + x0, y0 = line[n] + x1, y1 = line[n + 1] + length = math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) + dx, dy = (x1 - x0) / length, (y1 - y0) / length + x = x0 - dx * residual + y = y0 - dy * residual + length = length + residual + while length >= gap: + x += gap * dx + y += gap * dy + location.append((x, y)) + length -= gap + + residual = length + return location + + def get_parallet_lines(pts, distance): # pragma: no cover + leftline = [] + rightline = [] + + x0, y0 = pts[0] + x1, y1 = pts[1] + vector = (x1 - x0, y1 - y0) + orientation1 = getAngle((1, 0), vector) + + leftturn = orientation1 + math.pi / 2 + righrturn = orientation1 - math.pi / 2 + leftPt = (x0 + distance * math.cos(leftturn), y0 + distance * math.sin(leftturn)) + leftline.append(leftPt) + rightPt = (x0 + distance * math.cos(righrturn), y0 + distance * math.sin(righrturn)) + rightline.append(rightPt) + + for n in range(1, len(pts) - 1): + x0, y0 = pts[n - 1] + x1, y1 = pts[n] + x2, y2 = pts[n + 1] + + v1 = (x1 - x0, y1 - y0) + v2 = (x2 - x1, y2 - y1) + dtheta = getAngle(v1, v2) + orientation1 = getAngle((1, 0), v1) + + leftturn = orientation1 + dtheta / 2 + math.pi / 2 + righrturn = orientation1 + dtheta / 2 - math.pi / 2 + + distance2 = distance / math.sin((math.pi - dtheta) / 2) + leftPt = (x1 + distance2 * math.cos(leftturn), y1 + distance2 * math.sin(leftturn)) + leftline.append(leftPt) + rightPt = (x1 + distance2 * math.cos(righrturn), y1 + distance2 * math.sin(righrturn)) + rightline.append(rightPt) + + x0, y0 = pts[-2] + x1, y1 = pts[-1] + + vector = (x1 - x0, y1 - y0) + orientation1 = getAngle((1, 0), vector) + leftturn = orientation1 + math.pi / 2 + righrturn = orientation1 - math.pi / 2 + leftPt = (x1 + distance * math.cos(leftturn), y1 + distance * math.sin(leftturn)) + leftline.append(leftPt) + rightPt = (x1 + distance * math.cos(righrturn), y1 + distance * math.sin(righrturn)) + rightline.append(rightPt) + return leftline, rightline + + distance = GrpcValue(distance).value + gap = GrpcValue(gap).value + center_line = self.center_line + leftline, rightline = get_parallet_lines(center_line, distance) + for x, y in get_locations(rightline, gap) + get_locations(leftline, gap): + self._pedb.padstacks.place([x, y], padstack_name, net_name=net_name) + + @property + def center_line(self): + return self.get_center_line() + + def get_center_line(self): + """Retrieve center line points list.""" + return [[pt.x.value, pt.y.value] for pt in super().center_line.points] + + # def set_center_line(self, value): + # if isinstance(value, list): + # points = [GrpcPointData(i) for i in value] + # polygon_data = GrpcPolygonData(points, False) + # super(Path, self.__class__).polygon_data.__set__(self, polygon_data) + + @property + def corner_style(self): + """Return Path's corner style as string. Values supported for the setter `"round"``, `"mitter"``, `"sharpt"`""" + return super().corner_style.name.lower() + + @corner_style.setter + def corner_style(self, corner_type): + if isinstance(corner_type, str): + mapping = { + "round": GrpcPatCornerType.ROUND, + "mitter": GrpcPatCornerType.MITER, + "sharp": GrpcPatCornerType.SHARP, + } + self.corner_style = mapping[corner_type] diff --git a/src/pyedb/grpc/database/primitive/polygon.py b/src/pyedb/grpc/database/primitive/polygon.py new file mode 100644 index 0000000000..4fe4920b64 --- /dev/null +++ b/src/pyedb/grpc/database/primitive/polygon.py @@ -0,0 +1,260 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import math + +from ansys.edb.core.geometry.point_data import PointData as GrpcPointData +from ansys.edb.core.geometry.polygon_data import PolygonData as GrpcPolygonData +from ansys.edb.core.primitive.primitive import Polygon as GrpcPolygon +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.grpc.database.primitive.primitive import Primitive + + +class Polygon(GrpcPolygon, Primitive): + def __init__(self, pedb, edb_object): + GrpcPolygon.__init__(self, edb_object.msg) + Primitive.__init__(self, pedb, edb_object) + self._pedb = pedb + + @property + def type(self): + return self.primitive_type.name.lower() + + @property + def has_self_intersections(self): + """Check if Polygon has self intersections. + + Returns + ------- + bool + """ + return self.polygon_data.has_self_intersections() + + def fix_self_intersections(self): + """Remove self intersections if they exist. + + Returns + ------- + list + All new polygons created from the removal operation. + """ + new_polys = [] + if self.has_self_intersections: + new_polygons = self.polygon_data.remove_self_intersections() + self.polygon_data = new_polygons[0] + for p in new_polygons[1:]: + cloned_poly = self.create( + layout=self._pedb.active_layout, layer=self.layer.name, net=self.net, polygon_data=p + ) + new_polys.append(cloned_poly) + return new_polys + + def clone(self): + """Duplicate polygon""" + polygon_data = self.polygon_data + duplicated_polygon = self.create( + layout=self._pedb.active_layout, layer=self.layer, net=self.net, polygon_data=polygon_data + ) + for void in self.voids: + duplicated_polygon.add_void(void) + return duplicated_polygon + + def duplicate_across_layers(self, layers): + """Duplicate across layer a primitive object. + + Parameters: + + layers: list + list of str, with layer names + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + for layer in layers: + if layer in self._pedb.stackup.layers: + duplicate_polygon = self.create( + layout=self._pedb.active_layout, layer=layer, net=self.net.name, polygon_data=self.polygon_data + ) + if duplicate_polygon: + for void in self.voids: + duplicate_void = self.create( + layout=self._pedb.active_layout, + layer=layer, + net=self.net.name, + polygon_data=void.cast().polygon_data, + ) + duplicate_polygon.add_void(duplicate_void) + else: + return False + return True + + def move(self, vector): + """Move polygon along a vector. + + Parameters + ---------- + vector : List of float or str [x,y]. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + >>> edbapp = ansys.aedt.core.Edb("myproject.aedb") + >>> top_layer_polygon = [poly for poly in edbapp.modeler.polygons if poly.layer_name == "Top Layer"] + >>> for polygon in top_layer_polygon: + >>> polygon.move(vector=["2mm", "100um"]) + """ + if vector and isinstance(vector, list) and len(vector) == 2: + _vector = [GrpcValue(pt).value for pt in vector] + self.polygon_data = self.polygon_data.move(_vector) + return True + return False + + def scale(self, factor, center=None): + """Scales the polygon relative to a center point by a factor. + + Parameters + ---------- + factor : float + Scaling factor. + center : List of float or str [x,y], optional + If None scaling is done from polygon center. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if not isinstance(factor, str): + factor = float(factor) + if not center: + center = self.polygon_data.bounding_circle()[0] + if center: + self.polygon_data = self.polygon_data.scale(factor, center) + return True + else: + self._pedb.logger.error(f"Failed to evaluate center on primitive {self.id}") + elif isinstance(center, list) and len(center) == 2: + center = GrpcPointData([GrpcValue(center[0]), GrpcValue(center[1])]) + self.polygon_data = self.polygon_data.scale(factor, center) + return True + return False + + def rotate(self, angle, center=None): + """Rotate polygon around a center point by an angle. + + Parameters + ---------- + angle : float + Value of the rotation angle in degree. + center : List of float or str [x,y], optional + If None rotation is done from polygon center. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + >>> edbapp = ansys.aedt.core.Edb("myproject.aedb") + >>> top_layer_polygon = [poly for poly in edbapp.modeler.polygons if poly.layer_name == "Top Layer"] + >>> for polygon in top_layer_polygon: + >>> polygon.rotate(angle=45) + """ + if angle: + if not center: + center = self.polygon_data.bounding_circle()[0] + if center: + self.polygon_data = self.polygon_data.rotate(angle * math.pi / 180, center) + return True + elif isinstance(center, list) and len(center) == 2: + self.polygon_data = self.polygon_data.rotate(angle * math.pi / 180, center) + return True + return False + + def move_layer(self, layer): + """Move polygon to given layer. + + Parameters + ---------- + layer : str + layer name. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if layer and isinstance(layer, str) and layer in self._pedb.stackup.signal_layers: + self.layer = self._pedb.stackup.layers[layer] + return True + return False + + def in_polygon( + self, + point_data, + include_partial=True, + ): + """Check if padstack Instance is in given polygon data. + + Parameters + ---------- + point_data : PointData Object or list of float + include_partial : bool, optional + Whether to include partial intersecting instances. The default is ``True``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + int_val = 1 if self.polygon_data.is_inside(GrpcPointData(point_data)) else 0 + if int_val == 0: + return False + else: + int_val = self.polygon_data.intersection_type(GrpcPolygonData(point_data)) + # Intersection type: + # 0 = objects do not intersect + # 1 = this object fully inside other (no common contour points) + # 2 = other object fully inside this + # 3 = common contour points 4 = undefined intersection + if int_val == 0: + return False + elif include_partial: + return True + elif int_val < 3: + return True + else: + return False + + def add_void(self, polygon): + if isinstance(polygon, list): + polygon = self._pedb.modeler.create_polygon(points=polygon, layer_name=self.layer.name) + return self._edb_object.add_void(polygon._edb_object) diff --git a/src/pyedb/grpc/database/primitive/primitive.py b/src/pyedb/grpc/database/primitive/primitive.py new file mode 100644 index 0000000000..86581906ab --- /dev/null +++ b/src/pyedb/grpc/database/primitive/primitive.py @@ -0,0 +1,670 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.geometry.point_data import PointData as GrpcPointData +from ansys.edb.core.primitive.primitive import Circle as GrpcCircle +from ansys.edb.core.primitive.primitive import Primitive as GrpcPrimitive + +from pyedb.misc.utilities import compute_arc_points +from pyedb.modeler.geometry_operators import GeometryOperators + + +class Primitive(GrpcPrimitive): + """Manages EDB functionalities for a primitives. + It inherits EDB Object properties. + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb(myedb, edbversion="2021.2") + >>> edb_prim = edb.modeler.primitives[0] + >>> edb_prim.is_void # Class Property + >>> edb_prim.IsVoid() # EDB Object Property + """ + + def __init__(self, pedb, edb_object): + super().__init__(edb_object.msg) + self._pedb = pedb + self._edb_object = edb_object + self._core_stackup = pedb.stackup + self._core_net = pedb.nets + self._object_instance = None + + @property + def type(self): + """Return the type of the primitive. + + Expected output is among ``"Circle"``, ``"Rectangle"``,``"Polygon"``,``"Path"`` or ``"Bondwire"``. + + Returns + ------- + str + """ + return super().primitive_type.name.lower() + + @property + def polygon_data(self): + return self.cast().polygon_data + + @property + def object_instance(self): + """Return Ansys.Ansoft.Edb.LayoutInstance.LayoutObjInstance object.""" + if not self._object_instance: + self._object_instance = self.layout.layout_instance.get_layout_obj_instance_in_context(self, None) + return self._object_instance + + @property + def net_name(self): + if not self.net.is_null: + return self.net.name + + @net_name.setter + def net_name(self, value): + if value in self._pedb.nets.nets: + self.net = self._pedb.nets.nets[value] + + @property + def layer_name(self): + """Get the primitive layer name. + + Returns + ------- + str + """ + return self.layer.name + + @layer_name.setter + def layer_name(self, value): + if value in self._pedb.stackup.layers: + self.layer = self._pedb.stackup.layers[value] + + @property + def voids(self): + return [Primitive(self._pedb, prim) for prim in super().voids] + + # @property + # def polygon_data(self): + # return self.cast().polygon_data + # + # @polygon_data.setter + # def polygon_data(self, value): + # from pyedb.grpc.database.primitive.polygon import GrpcPolygonData + # + # if isinstance(value, GrpcPolygonData): + # self.cast().polygon_data = value + + def get_connected_objects(self): + """Get connected objects. + + Returns + ------- + list + """ + return self._pedb.get_connected_objects(self.object_instance) + + def area(self, include_voids=True): + """Return the total area. + + Parameters + ---------- + include_voids : bool, optional + Either if the voids have to be included in computation. + The default value is ``True``. + + Returns + ------- + float + """ + area = self.cast().polygon_data.area() + if include_voids: + for el in self.voids: + area -= el.polygon_data.area() + return area + + def _get_points_for_plot(self, my_net_points, num): + """ + Get the points to be plotted. + """ + # fmt: off + x = [] + y = [] + for i, point in enumerate(my_net_points): + if not point.is_arc: + x.append(point.x.value) + y.append(point.y.value) + else: + arc_h = point.arc_height.value + p1 = [my_net_points[i - 1].x.value, my_net_points[i - 1].y.value] + if i + 1 < len(my_net_points): + p2 = [my_net_points[i + 1].x.value, my_net_points[i + 1].y.value] + else: + p2 = [my_net_points[0].x.value, my_net_points[0].y.value] + x_arc, y_arc = compute_arc_points(p1, p2, arc_h, num) + x.extend(x_arc) + y.extend(y_arc) + # fmt: on + return x, y + + @property + def center(self): + """Return the primitive bounding box center coordinate. + + Returns + ------- + list + [x, y] + + """ + center = self.cast().polygon_data.bounding_circle()[0] + return [center.x.value, center.y.value] + + def get_connected_object_id_set(self): + """Produce a list of all geometries physically connected to a given layout object. + + Returns + ------- + list + Found connected objects IDs with Layout object. + """ + layout_inst = self.layout.layout_instance + layout_obj_inst = layout_inst.get_layout_obj_instance_in_context(self._edb_object, None) # 2nd arg was [] + return [loi.layout_obj.id for loi in layout_inst.get_connected_objects(layout_obj_inst)] + + @property + def bbox(self): + """Return the primitive bounding box points. Lower left corner, upper right corner. + + Returns + ------- + list + [lower_left x, lower_left y, upper right x, upper right y] + + """ + bbox = self.cast().polygon_data.bbox() + return [bbox[0].x.value, bbox[0].y.value, bbox[1].x.value, bbox[1].y.value] + + def convert_to_polygon(self): + """Convert path to polygon. + + Returns + ------- + bool, :class:`dotnet.database.edb_data.primitives.EDBPrimitives` + Polygon when successful, ``False`` when failed. + + """ + if self.type == "path": + polygon = self._pedb.modeler.create_polygon(self.polygon_data, self.layer_name, [], self.net.name) + self.delete() + return polygon + else: + return False + + def intersection_type(self, primitive): + """Get intersection type between actual primitive and another primitive or polygon data. + + Parameters + ---------- + primitive : :class:`pyaeedt.database.edb_data.primitives_data.EDBPrimitives` or `PolygonData` + + Returns + ------- + int + Intersection type: + 0 - objects do not intersect, + 1 - this object fully inside other (no common contour points), + 2 - other object fully inside this, + 3 - common contour points, + 4 - undefined intersection. + """ + if self.type in ["path, polygon"]: + poly = primitive.polygon_data + return self.polygon_data.intersection_type(poly).value + else: + return 4 + + def is_intersecting(self, primitive): + """Check if actual primitive and another primitive or polygon data intesects. + + Parameters + ---------- + primitive : :class:`pyaeedt.database.edb_data.primitives_data.EDBPrimitives` or `PolygonData` + + Returns + ------- + bool + """ + return True if self.intersection_type(primitive) >= 1 else False + + def get_closest_point(self, point): + """Get the closest point of the primitive to the input data. + + Parameters + ---------- + point : list of float or PointData + + Returns + ------- + list of float + """ + if isinstance(point, (list, tuple)): + point = GrpcPointData(point) + + p0 = self.cast().polygon_data.closest_point(point) + return [p0.x.value, p0.y.value] + + @property + def arcs(self): + """Get the Primitive Arc Data.""" + return self.polygon_data.arc_data + + @property + def longest_arc(self): + """Get the longest arc.""" + len = 0 + arc = None + for i in self.arcs: + if i.is_segment and i.length > len: + arc = i + len = i.length + return arc + + def subtract(self, primitives): + """Subtract active primitive with one or more primitives. + + Parameters + ---------- + primitives : :class:`dotnet.database.edb_data.EDBPrimitives` or EDB PolygonData or EDB Primitive or list + + Returns + ------- + List of :class:`dotnet.database.edb_data.EDBPrimitives` + """ + poly = self.cast().polygon_data + if not isinstance(primitives, list): + primitives = [primitives] + primi_polys = [] + voids_of_prims = [] + for prim in primitives: + if isinstance(prim, Primitive): + primi_polys.append(prim.cast().polygon_data) + for void in prim.voids: + voids_of_prims.append(void.cast().polygon_data) + else: + try: + primi_polys.append(prim.cast().polygon_data) + except: + primi_polys.append(prim) + for v in self.voids[:]: + primi_polys.append(v.cast().polygon_data) + primi_polys = poly.unite(primi_polys) + p_to_sub = poly.unite([poly] + voids_of_prims) + list_poly = poly.subtract(p_to_sub, primi_polys) + new_polys = [] + if list_poly: + for p in list_poly: + if not p.points: + continue + new_polys.append( + self._pedb.modeler.create_polygon(p, self.layer_name, net_name=self.net.name, voids=[]), + ) + self.delete() + for prim in primitives: + if isinstance(prim, Primitive): + prim.delete() + else: + try: + prim.Delete() + except AttributeError: + continue + return new_polys + + def intersect(self, primitives): + """Intersect active primitive with one or more primitives. + + Parameters + ---------- + primitives : :class:`dotnet.database.edb_data.EDBPrimitives` or EDB PolygonData or EDB Primitive or list + + Returns + ------- + List of :class:`dotnet.database.edb_data.EDBPrimitives` + """ + poly = self.cast().polygon_data + if not isinstance(primitives, list): + primitives = [primitives] + primi_polys = [] + for prim in primitives: + prim = prim.cast() + if isinstance(prim, Primitive): + primi_polys.append(prim.polygon_data) + else: + if isinstance(prim, GrpcCircle): + primi_polys.append(prim.polygon_data) + else: + primi_polys.append(prim.polygon_data) + list_poly = poly.intersect([poly], primi_polys) + new_polys = [] + if list_poly: + voids = self.voids + for p in list_poly: + if not p.points: + continue + list_void = [] + void_to_subtract = [] + if voids: + for void in voids: + void_pdata = void.polygon_data + int_data2 = p.intersection_type(void_pdata).value + if int_data2 > 2 or int_data2 == 1: + void_to_subtract.append(void_pdata) + elif int_data2 == 2: + list_void.append(void_pdata) + if void_to_subtract: + polys_cleans = p.subtract(p, void_to_subtract) + for polys_clean in polys_cleans: + if polys_clean.points: + void_to_append = [v for v in list_void if polys_clean.intersection_type(v) == 2] + new_polys.append( + self._pedb.modeler.create_polygon( + polys_clean, self.layer_name, net_name=self.net.name, voids=void_to_append + ) + ) + else: + new_polys.append( + self._pedb.modeler.create_polygon( + p, self.layer_name, net_name=self.net.name, voids=list_void + ) + ) + else: + new_polys.append( + self._pedb.modeler.create_polygon(p, self.layer_name, net_name=self.net.name, voids=list_void) + ) + self.delete() + for prim in primitives: + prim.delete() + return new_polys + + def unite(self, primitives): + """Unite active primitive with one or more primitives. + + Parameters + ---------- + primitives : :class:`dotnet.database.edb_data.EDBPrimitives` or EDB PolygonData or EDB Primitive or list + + Returns + ------- + List of :class:`dotnet.database.edb_data.EDBPrimitives` + """ + poly = self.polygon_data + if not isinstance(primitives, list): + primitives = [primitives] + primi_polys = [] + for prim in primitives: + if isinstance(prim, Primitive): + primi_polys.append(prim.polygon_data) + else: + primi_polys.append(prim.polygon_data) + primi_polys.append(prim) + list_poly = poly.unite([poly] + primi_polys) + new_polys = [] + if list_poly: + voids = self.voids + for p in list_poly: + if not p.points: + continue + list_void = [] + if voids: + for void in voids: + void_pdata = void.polygon_data + int_data2 = p.intersection_type(void_pdata) + if int_data2 > 1: + list_void.append(void_pdata) + new_polys.append( + self._pedb.modeler.create_polygon(p, self.layer_name, net_name=self.net.name, voids=list_void), + ) + self.delete() + for prim in primitives: + if isinstance(prim, Primitive): + prim.delete() + else: + try: + prim.delete() + except AttributeError: + continue + return new_polys + + def get_closest_arc_midpoint(self, point): + """Get the closest arc midpoint of the primitive to the input data. + + Parameters + ---------- + point : list of float or PointData + + Returns + ------- + list of float + """ + if isinstance(point, GrpcPointData): + point = [point.x.value, point.y.value] + dist = 1e12 + out = None + for arc in self.arcs: + mid_point = arc.midpoint + mid_point = [mid_point.x.value, mid_point.y.value] + if GeometryOperators.points_distance(mid_point, point) < dist: + out = arc.midpoint + dist = GeometryOperators.points_distance(mid_point, point) + return [out.x.value, out.y.value] + + @property + def shortest_arc(self): + """Get the longest arc.""" + len = 1e12 + arc = None + for i in self.arcs: + if i.is_segment and i.length < len: + arc = i + len = i.length + return arc + + @property + def aedt_name(self): + """Name to be visualized in AEDT. + + Returns + ------- + str + Name. + """ + from ansys.edb.core.database import ProductIdType + + return self.get_product_property(ProductIdType.DESIGNER, 1) + + @aedt_name.setter + def aedt_name(self, value): + from ansys.edb.core.database import ProductIdType + + self.set_product_property(ProductIdType.DESIGNER, 1, value) + + def add_void(self, point_list): + """Add a void to current primitive. + + Parameters + ---------- + point_list : list or :class:`pyedb.dotnet.database.edb_data.primitives_data.EDBPrimitives` \ + or EDB Primitive Object. Point list in the format of `[[x1,y1], [x2,y2],..,[xn,yn]]`. + + Returns + ------- + bool + ``True`` if successful, either ``False``. + """ + if isinstance(point_list, list): + plane = self._pedb.modeler.Shape("polygon", points=point_list) + _poly = self._pedb.modeler.shape_to_polygon_data(plane) + if _poly is None or _poly.is_null or _poly is False: + self._pedb.logger.error("Failed to create void polygon data") + return False + void_poly = self._pedb.modeler.create_polygon(_poly, layer_name=self.layer_name, net_name=self.net.name) + return self.add_void(void_poly) + + def points(self, arc_segments=6): + """Return the list of points with arcs converted to segments. + + Parameters + ---------- + arc_segments : int + Number of facets to convert an arc. Default is `6`. + + Returns + ------- + tuple + The tuple contains 2 lists made of X and Y points coordinates. + """ + xt, yt = self._get_points_for_plot(self.polygon_data.points, arc_segments) + if not xt: + return [] + x, y = GeometryOperators.orient_polygon(xt, yt, clockwise=True) + return x, y + + @property + def points_raw(self): + """Return a list of Edb points. + + Returns + ------- + list + Edb Points. + """ + return self.polygon_data.points + + def expand(self, offset=0.001, tolerance=1e-12, round_corners=True, maximum_corner_extension=0.001): + """Expand the polygon shape by an absolute value in all direction. + Offset can be negative for negative expansion. + + Parameters + ---------- + offset : float, optional + Offset value in meters. + tolerance : float, optional + Tolerance in meters. + round_corners : bool, optional + Whether to round corners or not. + If True, use rounded corners in the expansion otherwise use straight edges (can be degenerate). + maximum_corner_extension : float, optional + The maximum corner extension (when round corners are not used) at which point the corner is clipped. + + Return + ------ + List of PolygonData. + """ + return self.cast().polygon_data.expand( + offset=offset, round_corner=round_corners, max_corner_ext=maximum_corner_extension, tol=tolerance + ) + + def scale(self, factor, center=None): + """Scales the polygon relative to a center point by a factor. + + Parameters + ---------- + factor : float + Scaling factor. + center : List of float or str [x,y], optional + If None scaling is done from polygon center. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if not isinstance(factor, str): + factor = float(factor) + from ansys.edb.core.geometry.polygon_data import ( + PolygonData as GrpcPolygonData, + ) + + polygon_data = GrpcPolygonData(points=self.cast().polygon_data.points) + if not center: + center = polygon_data.bounding_circle()[0] + if center: + polygon_data.scale(factor, center) + self.cast().polygon_data = polygon_data + return True + else: + self._pedb.logger.error(f"Failed to evaluate center on primitive {self.id}") + elif isinstance(center, list) and len(center) == 2: + center = GrpcPointData(center) + polygon_data.scale(factor, center) + self.cast().polygon_data = polygon_data + return True + return False + + def plot(self, plot_net=False, show=True, save_plot=None): + """Plot the current polygon on matplotlib. + + Parameters + ---------- + plot_net : bool, optional + Whether if plot the entire net or only the selected polygon. Default is ``False``. + show : bool, optional + Whether if show the plot or not. Default is ``True``. + save_plot : str, optional + Save the plot path. + + Returns + ------- + (ax, fig) + Matplotlib ax and figures. + """ + import matplotlib.pyplot as plt + from shapely.geometry import Polygon + from shapely.plotting import plot_polygon + + dpi = 100.0 + figsize = (2000 / dpi, 1000 / dpi) + if plot_net and self.net_name: + fig, ax = self._pedb.nets.plot([self.net_name], color_by_net=True, show=False, show_legend=False) + else: + fig = plt.figure(figsize=figsize) + ax = fig.add_subplot(1, 1, 1) + xt, yt = self.points() + p1 = [(i, j) for i, j in zip(xt[::-1], yt[::-1])] + + holes = [] + for void in self.voids: + xvt, yvt = void.points(arc_segments=3) + h1 = [(i, j) for i, j in zip(xvt, yvt)] + holes.append(h1) + poly = Polygon(p1, holes) + plot_polygon(poly, add_points=False, color=(1, 0, 0)) + ax.grid(False) + ax.set_axis_off() + # Hide axes ticks + ax.set_xticks([]) + ax.set_yticks([]) + message = f"Polygon {self.id} on net {self.net_name}" + plt.title(message, size=20) + if save_plot: + plt.savefig(save_plot) + elif show: + plt.show() + return ax, fig diff --git a/src/pyedb/grpc/database/primitive/rectangle.py b/src/pyedb/grpc/database/primitive/rectangle.py new file mode 100644 index 0000000000..a3fcaa0799 --- /dev/null +++ b/src/pyedb/grpc/database/primitive/rectangle.py @@ -0,0 +1,132 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.primitive.primitive import ( + RectangleRepresentationType as GrpcRectangleRepresentationType, +) +from ansys.edb.core.primitive.primitive import Rectangle as GrpcRectangle +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.grpc.database.primitive.primitive import Primitive + + +class Rectangle(GrpcRectangle, Primitive): + """Class representing a rectangle object.""" + + def __init__(self, pedb, edb_object): + Primitive.__init__(self, pedb, edb_object) + GrpcRectangle.__init__(self, edb_object.msg) + self._pedb = pedb + self._mapping_representation_type = { + "center_width_height": GrpcRectangleRepresentationType.CENTER_WIDTH_HEIGHT, + "lower_left_upper_right": GrpcRectangleRepresentationType.LOWER_LEFT_UPPER_RIGHT, + } + + @property + def polygon_data(self): + return self.cast().polygon_data + + @property + def representation_type(self): + return super().representation_type.name.lower() + + @representation_type.setter + def representation_type(self, value): + if not value in self._mapping_representation_type: + super().representation_type = GrpcRectangleRepresentationType.INVALID_RECT_TYPE + else: + super().representation_type = self._mapping_representation_type[value] + + def get_parameters(self): + """Get coordinates parameters. + + Returns + ------- + tuple[ + str, + float, + float, + float, + float, + float, + float` + ] + + Returns a tuple of the following format: + + **(representation_type, parameter1, parameter2, parameter3, parameter4, corner_radius, rotation)** + + **representation_type** : Type that defines given parameters meaning. + + **parameter1** : X value of lower left point or center point. + + **parameter2** : Y value of lower left point or center point. + + **parameter3** : X value of upper right point or width. + + **parameter4** : Y value of upper right point or height. + + **corner_radius** : Corner radius. + + **rotation** : Rotation. + """ + parameters = super().get_parameters() + representation_type = parameters[0].name.lower() + parameter1 = parameters[1].value + parameter2 = parameters[2].value + parameter3 = parameters[3].value + parameter4 = parameters[4].value + corner_radius = parameters[5].value + rotation = parameters[6].value + return representation_type, parameter1, parameter2, parameter3, parameter4, corner_radius, rotation + + def set_parameters(self, rep_type, param1, param2, param3, param4, corner_rad, rotation): + """Set coordinates parameters. + + Parameters + ---------- + rep_type : :class:`RectangleRepresentationType` + Type that defines given parameters meaning. + param1 : :class:`Value ` + X value of lower left point or center point. + param2 : :class:`Value ` + Y value of lower left point or center point. + param3 : :class:`Value ` + X value of upper right point or width. + param4 : :class:`Value ` + Y value of upper right point or height. + corner_rad : :class:`Value ` + Corner radius. + rotation : :class:`Value ` + Rotation. + """ + + return super().set_parameters( + self.representation_type[rep_type], + GrpcValue(param1), + GrpcValue(param2), + GrpcValue(param3), + GrpcValue(param4), + GrpcValue(corner_rad), + GrpcValue(rotation), + ) diff --git a/src/pyedb/grpc/database/simulation_setup/__init__.py b/src/pyedb/grpc/database/simulation_setup/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/pyedb/grpc/database/simulation_setup/adaptive_frequency.py b/src/pyedb/grpc/database/simulation_setup/adaptive_frequency.py new file mode 100644 index 0000000000..c2ebea978b --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/adaptive_frequency.py @@ -0,0 +1,33 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.simulation_setup.adaptive_solutions import ( + AdaptiveFrequency as GrpcAdaptiveFrequency, +) + + +class AdaptiveFrequency(GrpcAdaptiveFrequency): + """Manages EDB methods for adaptive frequency data.""" + + def __init__(self, adaptive_frequency): + super().__init__(adaptive_frequency) diff --git a/src/pyedb/grpc/database/simulation_setup/hfss_advanced_meshing_settings.py b/src/pyedb/grpc/database/simulation_setup/hfss_advanced_meshing_settings.py new file mode 100644 index 0000000000..89f3dd6449 --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/hfss_advanced_meshing_settings.py @@ -0,0 +1,31 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.simulation_setup.hfss_simulation_settings import ( + HFSSAdvancedMeshingSettings as GrpcHFSSAdvancedMeshingSettings, +) + + +class HFSSAdvancedMeshingSettings(GrpcHFSSAdvancedMeshingSettings): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb diff --git a/src/pyedb/grpc/database/simulation_setup/hfss_advanced_settings.py b/src/pyedb/grpc/database/simulation_setup/hfss_advanced_settings.py new file mode 100644 index 0000000000..599e16d4d0 --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/hfss_advanced_settings.py @@ -0,0 +1,51 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.simulation_setup.hfss_simulation_settings import ( + HFSSAdvancedSettings as GrpcHFSSAdvancedSettings, +) +from ansys.edb.core.simulation_setup.simulation_settings import ViaStyle as GrpcViaStyle + + +class HFSSAdvancedSettings(GrpcHFSSAdvancedSettings): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb + + @property + def via_model_type(self): + return self.via_model_type.name + + @via_model_type.setter + def via_model_type(self, value): + if isinstance(value, str): + if value.upper() == "WIREBOND": + self.via_model_type = GrpcViaStyle.WIREBOND + elif value.lower() == "RIBBON": + self.via_model_type = GrpcViaStyle.RIBBON + elif value.lower() == "MESH": + self.via_model_type = GrpcViaStyle.MESH + elif value.lower() == "FIELD": + self.via_model_type = GrpcViaStyle.FIELD + elif value.lower() == "NUM_VIA_STYLE": + self.via_model_type = GrpcViaStyle.NUM_VIA_STYLE diff --git a/src/pyedb/grpc/database/simulation_setup/hfss_dcr_settings.py b/src/pyedb/grpc/database/simulation_setup/hfss_dcr_settings.py new file mode 100644 index 0000000000..df9a524ab6 --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/hfss_dcr_settings.py @@ -0,0 +1,33 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.simulation_setup.hfss_simulation_settings import ( + HFSSDCRSettings as GrpcHFSSDCRSettings, +) + + +class HFSSDCRSettings(GrpcHFSSDCRSettings): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._edb_object = edb_object + self._pedb = pedb diff --git a/src/pyedb/grpc/database/simulation_setup/hfss_general_settings.py b/src/pyedb/grpc/database/simulation_setup/hfss_general_settings.py new file mode 100644 index 0000000000..b2a82df97b --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/hfss_general_settings.py @@ -0,0 +1,51 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.simulation_setup.hfss_simulation_settings import ( + AdaptType as GrpcAdaptType, +) +from ansys.edb.core.simulation_setup.hfss_simulation_settings import ( + HFSSGeneralSettings as GrpcHFSSGeneralSettings, +) + + +class HFSSGeneralSettings(GrpcHFSSGeneralSettings): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb + + @property + def adaptive_solution_type(self): + return self.adaptive_solution_type.name + + @adaptive_solution_type.setter + def adaptive_solution_type(self, value): + if isinstance(value, str): + if value.lower() == "singlw": + self.adaptive_solution_type = GrpcAdaptType.SINGLE + elif value.lower() == "multi_frequencies": + self.adaptive_solution_type = GrpcAdaptType.MULTI_FREQUENCIES + elif value.lower() == "broad_band": + self.adaptive_solution_type = GrpcAdaptType.BROADBAND + elif value.lower() == "num_adapt_type": + self.adaptive_solution_type = GrpcAdaptType.NUM_ADAPT_TYPE diff --git a/src/pyedb/grpc/database/simulation_setup/hfss_settings_options.py b/src/pyedb/grpc/database/simulation_setup/hfss_settings_options.py new file mode 100644 index 0000000000..87ac746449 --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/hfss_settings_options.py @@ -0,0 +1,68 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.simulation_setup.hfss_simulation_settings import ( + BasisFunctionOrder as GrpcBasisFunctionOrder, +) +from ansys.edb.core.simulation_setup.hfss_simulation_settings import ( + HFSSSettingsOptions as GrpcHFSSSettingsOptions, +) +from ansys.edb.core.simulation_setup.hfss_simulation_settings import ( + SolverType as GrpcSolverType, +) + + +class HFSSSettingsOptions(GrpcHFSSSettingsOptions): + def __init__(self, _pedb, edb_object): + super().__init__(edb_object) + self._pedb = _pedb + + @property + def order_basis(self): + return self.order_basis.name + + @order_basis.setter + def order_basis(self, value): + if value == "ZERO_ORDER": + self.order_basis = GrpcBasisFunctionOrder.ZERO_ORDER + elif value == "FIRST_ORDER": + self.order_basis = GrpcBasisFunctionOrder.FIRST_ORDER + elif value == "SECOND_ORDER": + self.order_basis = GrpcBasisFunctionOrder.SECOND_ORDER + elif value == "MIXED_ORDER": + self.order_basis = GrpcBasisFunctionOrder.MIXED_ORDER + + @property + def solver_type(self): + return self.solver_type.name() + + @solver_type.setter + def solver_type(self, value): + if value == "AUTO_SOLVER": + self.solver_type = GrpcSolverType.AUTO_SOLVER + elif value == "DIRECT_SOLVER": + self.solver_type = GrpcSolverType.DIRECT_SOLVER + elif value == "ITERATIVE_SOLVER": + self.solver_type = GrpcSolverType.ITERATIVE_SOLVER + elif value == "NUM_SOLVER_TYPES": + self.solver_type = GrpcSolverType.NUM_SOLVER_TYPES diff --git a/src/pyedb/grpc/database/simulation_setup/hfss_simulation_settings.py b/src/pyedb/grpc/database/simulation_setup/hfss_simulation_settings.py new file mode 100644 index 0000000000..ffe21c1084 --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/hfss_simulation_settings.py @@ -0,0 +1,72 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.simulation_setup.hfss_simulation_settings import ( + HFSSSimulationSettings as GrpcHFSSSimulationSettings, +) + +from pyedb.grpc.database.simulation_setup.hfss_advanced_meshing_settings import ( + HFSSAdvancedMeshingSettings, +) +from pyedb.grpc.database.simulation_setup.hfss_advanced_settings import ( + HFSSAdvancedSettings, +) +from pyedb.grpc.database.simulation_setup.hfss_dcr_settings import HFSSDCRSettings +from pyedb.grpc.database.simulation_setup.hfss_general_settings import ( + HFSSGeneralSettings, +) +from pyedb.grpc.database.simulation_setup.hfss_settings_options import ( + HFSSSettingsOptions, +) +from pyedb.grpc.database.simulation_setup.hfss_solver_settings import HFSSSolverSettings + + +class HFSSSimulationSettings(GrpcHFSSSimulationSettings): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._edb_object = edb_object + self._pedb = pedb + + @property + def advanced(self): + return HFSSAdvancedSettings(self._pedb, self.advanced) + + @property + def advanced_meshing(self): + return HFSSAdvancedMeshingSettings(self._pedb, self.advanced_meshing) + + @property + def dcr(self): + return HFSSDCRSettings(self._pedb, self.dcr) + + @property + def general(self): + return HFSSGeneralSettings(self._pedb, self.general) + + @property + def options(self): + return HFSSSettingsOptions(self._pedb, self.options) + + @property + def solver(self): + return HFSSSolverSettings(self._pedb, self.solver) diff --git a/src/pyedb/grpc/database/simulation_setup/hfss_simulation_setup.py b/src/pyedb/grpc/database/simulation_setup/hfss_simulation_setup.py new file mode 100644 index 0000000000..624c6fc597 --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/hfss_simulation_setup.py @@ -0,0 +1,318 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.simulation_setup.adaptive_solutions import ( + AdaptiveFrequency as GrpcAdaptiveFrequency, +) +from ansys.edb.core.simulation_setup.hfss_simulation_settings import ( + AdaptType as GrpcAdaptType, +) +from ansys.edb.core.simulation_setup.hfss_simulation_setup import ( + HfssSimulationSetup as GrpcHfssSimulationSetup, +) + +from pyedb.generic.general_methods import generate_unique_name +from pyedb.grpc.database.simulation_setup.sweep_data import SweepData + + +class HfssSimulationSetup(GrpcHfssSimulationSetup): + """Manages EDB methods for HFSS simulation setup.""" + + def __init__(self, pedb, edb_object, name: str = None): + super().__init__(edb_object.msg) + self._pedb = pedb + self._name = name + + def set_solution_single_frequency(self, frequency="5GHz", max_num_passes=10, max_delta_s=0.02): + """Set HFSS single frequency solution. + Parameters + ---------- + frequency : str, optional + Adaptive frequency. + max_num_passes : int, optional + Maxmímum passes number. Default value `10`. + max_delta_s : float, optional + Maximum delta S value. Default value `0.02`, + + Returns + ------- + bool + """ + try: + self.settings.general.adaptive_solution_type = GrpcAdaptType.SINGLE + sfs = self.settings.general.single_frequency_adaptive_solution + sfs.adaptive_frequency = frequency + sfs.max_passes = max_num_passes + sfs.max_delta = str(max_delta_s) + self.settings.general.single_frequency_adaptive_solution = sfs + return True + except: + return False + + def set_solution_multi_frequencies(self, frequencies="5GHz", max_delta_s=0.02): + """Set HFSS setup multi frequencies adaptive. + + Parameters + ---------- + frequencies : str, List[str]. + Adaptive frequencies. + max_delta_s : float, List[float]. + Max delta S values. + + Returns + ------- + bool. + """ + try: + self.settings.general.adaptive_solution_type = GrpcAdaptType.MULTI_FREQUENCIES + if not isinstance(frequencies, list): + frequencies = [frequencies] + if not isinstance(max_delta_s, list): + max_delta_s = [max_delta_s] + if len(max_delta_s) < len(frequencies): + for _ in frequencies[len(max_delta_s) :]: + max_delta_s.append(max_delta_s[-1]) + adapt_frequencies = [ + GrpcAdaptiveFrequency(frequencies[ind], str(max_delta_s[ind])) for ind in range(len(frequencies)) + ] + self.settings.general.multi_frequency_adaptive_solution.adaptive_frequencies = adapt_frequencies + return True + except: + return False + + def set_solution_broadband(self, low_frequency="1GHz", high_frequency="10GHz", max_delta_s=0.02, max_num_passes=10): + try: + self.settings.general.adaptive_solution_type = GrpcAdaptType.BROADBAND + bfs = self.settings.general.broadband_adaptive_solution + bfs.low_frequency = low_frequency + bfs.high_frequency = high_frequency + bfs.max_delta = str(max_delta_s) + bfs.max_num_passes = max_num_passes + self.settings.general.broadband_adaptive_solution = bfs + return True + except: + return False + + def add_adaptive_frequency_data(self, frequency="5GHz", max_delta_s="0.01"): + try: + adapt_frequencies = self.settings.general.multi_frequency_adaptive_solution.adaptive_frequencies + adapt_frequencies.append(GrpcAdaptiveFrequency(frequency, str(max_delta_s))) + self.settings.general.multi_frequency_adaptive_solution.adaptive_frequencies = adapt_frequencies + return True + except: + return False + + def add_length_mesh_operation( + self, + net_layer_list, + name=None, + max_elements=1000, + max_length="1mm", + restrict_elements=True, + restrict_length=True, + refine_inside=False, + mesh_region=None, + ): + """Add a mesh operation to the setup. + + Parameters + ---------- + net_layer_list : dict + Dictionary containing nets and layers on which enable Mesh operation. Example ``{"A0_N": ["TOP", "PWR"]}``. + name : str, optional + Mesh operation name. + max_elements : int, optional + Maximum number of elements. Default is ``1000``. + max_length : str, optional + Maximum length of elements. Default is ``1mm``. + restrict_elements : bool, optional + Whether to restrict number of elements. Default is ``True``. + restrict_length : bool, optional + Whether to restrict length of elements. Default is ``True``. + mesh_region : str, optional + Mesh region name. + refine_inside : bool, optional + Whether to refine inside or not. Default is ``False``. + + Returns + ------- + :class:`dotnet.database.edb_data.hfss_simulation_setup_data.LengthMeshOperation` + """ + from ansys.edb.core.simulation_setup.mesh_operation import ( + LengthMeshOperation as GrpcLengthMeshOperation, + ) + + if not name: + name = generate_unique_name("skin") + net_layer_op = [] + if net_layer_list: + for net, layers in net_layer_list.items(): + if not isinstance(layers, list): + layers = [layers] + for layer in layers: + net_layer_op.append((net, layer, True)) + + mop = GrpcLengthMeshOperation( + name=name, + net_layer_info=net_layer_op, + refine_inside=refine_inside, + mesh_region=str(net_layer_op), + max_length=str(max_length), + restrict_max_length=restrict_length, + restrict_max_elements=restrict_elements, + max_elements=str(max_elements), + ) + mesh_ops = self.mesh_operations + mesh_ops.append(mop) + self.mesh_operations = mesh_ops + return mop + + def add_skin_depth_mesh_operation( + self, + net_layer_list, + name=None, + max_elements=1000, + skin_depth="1um", + restrict_elements=True, + surface_triangle_length="1mm", + number_of_layers=2, + refine_inside=False, + mesh_region=None, + ): + """Add a mesh operation to the setup. + + Parameters + ---------- + net_layer_list : dict + Dictionary containing nets and layers on which enable Mesh operation. Example ``{"A0_N": ["TOP", "PWR"]}``. + name : str, optional + Mesh operation name. + max_elements : int, optional + Maximum number of elements. Default is ``1000``. + skin_depth : str, optional + Skin Depth. Default is ``1um``. + restrict_elements : bool, optional + Whether to restrict number of elements. Default is ``True``. + surface_triangle_length : bool, optional + Surface Triangle length. Default is ``1mm``. + number_of_layers : int, str, optional + Number of layers. Default is ``2``. + mesh_region : str, optional + Mesh region name. + refine_inside : bool, optional + Whether to refine inside or not. Default is ``False``. + + Returns + ------- + :class:`dotnet.database.edb_data.hfss_simulation_setup_data.LengthMeshOperation` + """ + if not name: + name = generate_unique_name("length") + net_layer_op = [] + if net_layer_list: + for net, layers in net_layer_list.items(): + if not isinstance(layers, list): + layers = [layers] + for layer in layers: + net_layer_op.append((net, layer, True)) + from ansys.edb.core.simulation_setup.mesh_operation import ( + SkinDepthMeshOperation as GrpcSkinDepthMeshOperation, + ) + + mesh_operation = GrpcSkinDepthMeshOperation( + name=name, + net_layer_info=net_layer_op, + refine_inside=refine_inside, + mesh_region=mesh_region, + skin_depth=str(skin_depth), + surface_triangle_length=str(surface_triangle_length), + restrict_max_elements=restrict_elements, + max_elements=str(max_elements), + num_layers=str(number_of_layers), + ) + mesh_ops = self.mesh_operations + mesh_ops.append(mesh_operation) + self.mesh_operations = mesh_ops + return mesh_operation + + def add_sweep( + self, name=None, distribution="linear", start_freq="0GHz", stop_freq="20GHz", step="10MHz", discrete=False + ): + """Add a HFSS frequency sweep. + + Parameters + ---------- + name : str, optional + Sweep name. + distribution : str, optional + Type of the sweep. The default is `"linear"`. Options are: + - `"linear"` + - `"linear_count"` + - `"decade_count"` + - `"octave_count"` + - `"exponential"` + start_freq : str, float, optional + Starting frequency. The default is ``1``. + stop_freq : str, float, optional + Stopping frequency. The default is ``1e9``. + step : str, float, int, optional + Frequency step. The default is ``1e6``. or used for `"decade_count"`, "linear_count"`, "octave_count"` + distribution. Must be integer in that case. + discrete : bool, optional + Whether the sweep is discrete. The default is ``False``. + + Returns + ------- + bool + """ + init_sweep_count = len(self.sweep_data) + start_freq = self._pedb.number_with_units(start_freq, "Hz") + stop_freq = self._pedb.number_with_units(stop_freq, "Hz") + step = str(step) + if distribution.lower() == "linear": + distribution = "LIN" + elif distribution.lower() == "linear_count": + distribution = "LINC" + elif distribution.lower() == "exponential": + distribution = "ESTP" + elif distribution.lower() == "decade_count": + distribution = "DEC" + elif distribution.lower() == "octave_count": + distribution = "OCT" + else: + distribution = "LIN" + if not name: + name = f"sweep_{init_sweep_count + 1}" + sweep_data = [ + SweepData(self._pedb, name=name, distribution=distribution, start_f=start_freq, end_f=stop_freq, step=step) + ] + if discrete: + sweep_data[0].type = sweep_data[0].type.DISCRETE_SWEEP + for sweep in self.sweep_data: + sweep_data.append(sweep) + self.sweep_data = sweep_data + if len(self.sweep_data) == init_sweep_count + 1: + return True + else: + self._pedb.logger.error("Failed to add frequency sweep data") + return False diff --git a/src/pyedb/grpc/database/simulation_setup/hfss_solver_settings.py b/src/pyedb/grpc/database/simulation_setup/hfss_solver_settings.py new file mode 100644 index 0000000000..832f963645 --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/hfss_solver_settings.py @@ -0,0 +1,32 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.simulation_setup.hfss_simulation_settings import ( + HFSSSolverSettings as GrpcHFSSSolverSettings, +) + + +class HFSSSolverSettings(GrpcHFSSSolverSettings): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb diff --git a/src/pyedb/grpc/database/simulation_setup/mesh_operation.py b/src/pyedb/grpc/database/simulation_setup/mesh_operation.py new file mode 100644 index 0000000000..087f368f18 --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/mesh_operation.py @@ -0,0 +1,32 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNE SS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.simulation_setup.mesh_operation import ( + MeshOperation as GrpcMeshOperation, +) + + +class MeshOperation(GrpcMeshOperation): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb diff --git a/src/pyedb/grpc/database/simulation_setup/raptor_x_advanced_settings.py b/src/pyedb/grpc/database/simulation_setup/raptor_x_advanced_settings.py new file mode 100644 index 0000000000..6a435aa71f --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/raptor_x_advanced_settings.py @@ -0,0 +1,32 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNE SS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.simulation_setup.raptor_x_simulation_settings import ( + RaptorXAdvancedSettings as GrpcRaptorXAdvancedSettings, +) + + +class RaptorXAdvancedSettings(GrpcRaptorXAdvancedSettings): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb diff --git a/src/pyedb/grpc/database/simulation_setup/raptor_x_general_settings.py b/src/pyedb/grpc/database/simulation_setup/raptor_x_general_settings.py new file mode 100644 index 0000000000..28f9f9c8c8 --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/raptor_x_general_settings.py @@ -0,0 +1,31 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNE SS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.simulation_setup.raptor_x_simulation_settings import ( + RaptorXGeneralSettings as GrpcRaptorXGeneralSettings, +) + + +class RaptorXGeneralSettings(GrpcRaptorXGeneralSettings): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb diff --git a/src/pyedb/grpc/database/simulation_setup/raptor_x_simulation_settings.py b/src/pyedb/grpc/database/simulation_setup/raptor_x_simulation_settings.py new file mode 100644 index 0000000000..1d12d90089 --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/raptor_x_simulation_settings.py @@ -0,0 +1,46 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNE SS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.simulation_setup.raptor_x_simulation_setup import ( + RaptorXSimulationSettings as GrpcRaptorXSimulationSettings, +) + +from pyedb.grpc.database.simulation_setup.raptor_x_advanced_settings import ( + RaptorXAdvancedSettings, +) +from pyedb.grpc.database.simulation_setup.raptor_x_general_settings import ( + RaptorXGeneralSettings, +) + + +class RaptorXSimulationSettings(GrpcRaptorXSimulationSettings): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb + + @property + def advanced(self): + return RaptorXAdvancedSettings(self._pedb, self.advanced) + + @property + def general(self): + RaptorXGeneralSettings(self._pedb, self.general) diff --git a/src/pyedb/grpc/database/simulation_setup/raptor_x_simulation_setup.py b/src/pyedb/grpc/database/simulation_setup/raptor_x_simulation_setup.py new file mode 100644 index 0000000000..5318d463ab --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/raptor_x_simulation_setup.py @@ -0,0 +1,125 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNE SS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import warnings + +from ansys.edb.core.simulation_setup.raptor_x_simulation_setup import ( + RaptorXSimulationSetup as GrpcRaptorXSimulationSetup, +) + +from pyedb.grpc.database.simulation_setup.sweep_data import SweepData + + +class RaptorXSimulationSetup(GrpcRaptorXSimulationSetup): + """Manages EDB methods for RaptorX simulation setup.""" + + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb + + @property + def frequency_sweeps(self): + """Returns Frequency sweeps + . deprecated:: use sweep_data instead + """ + warnings.warn( + "`frequency_sweeps` is deprecated use `sweep_data` instead.", + DeprecationWarning, + ) + return self.sweep_data + + def add_frequency_sweep( + self, name=None, distribution="linear", start_freq="0GHz", stop_freq="20GHz", step="10MHz", discrete=False + ): + """Add frequency sweep. + + . deprecated:: pyedb 0.31.0 + Use :func:`add sweep` instead. + + """ + warnings.warn( + "`add_frequency_sweep` is deprecated use `add_sweep` instead.", + DeprecationWarning, + ) + return self.add_sweep(name, distribution, start_freq, stop_freq, step, discrete) + + def add_sweep( + self, name=None, distribution="linear", start_freq="0GHz", stop_freq="20GHz", step="10MHz", discrete=False + ): + """Add a HFSS frequency sweep. + + Parameters + ---------- + name : str, optional + Sweep name. + distribution : str, optional + Type of the sweep. The default is `"linear"`. Options are: + - `"linear"` + - `"linear_count"` + - `"decade_count"` + - `"octave_count"` + - `"exponential"` + start_freq : str, float, optional + Starting frequency. The default is ``1``. + stop_freq : str, float, optional + Stopping frequency. The default is ``1e9``. + step : str, float, int, optional + Frequency step. The default is ``1e6``. or used for `"decade_count"`, "linear_count"`, "octave_count"` + distribution. Must be integer in that case. + discrete : bool, optional + Whether the sweep is discrete. The default is ``False``. + + Returns + ------- + bool + """ + init_sweep_count = len(self.sweep_data) + start_freq = self._pedb.number_with_units(start_freq, "Hz") + stop_freq = self._pedb.number_with_units(stop_freq, "Hz") + step = str(step) + if distribution.lower() == "linear": + distribution = "LIN" + elif distribution.lower() == "linear_count": + distribution = "LINC" + elif distribution.lower() == "exponential": + distribution = "ESTP" + elif distribution.lower() == "decade_count": + distribution = "DEC" + elif distribution.lower() == "octave_count": + distribution = "OCT" + else: + distribution = "LIN" + if not name: + name = f"sweep_{init_sweep_count + 1}" + sweep_data = [ + SweepData(self._pedb, name=name, distribution=distribution, start_f=start_freq, end_f=stop_freq, step=step) + ] + if discrete: + sweep_data[0].type = sweep_data[0].type.DISCRETE_SWEEP + for sweep in self.sweep_data: + sweep_data.append(sweep) + self.sweep_data = sweep_data + if len(self.sweep_data) == init_sweep_count + 1: + return True + else: + self._pedb.logger.error("Failed to add frequency sweep data") + return False diff --git a/src/pyedb/grpc/database/simulation_setup/siwave_dcir_simulation_setup.py b/src/pyedb/grpc/database/simulation_setup/siwave_dcir_simulation_setup.py new file mode 100644 index 0000000000..eb42e4d204 --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/siwave_dcir_simulation_setup.py @@ -0,0 +1,32 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.simulation_setup.siwave_dcir_simulation_setup import ( + SIWaveDCIRSimulationSetup as Grpcsiwave_dcir_simulation_setup, +) + + +class SIWaveDCIRSimulationSetup(Grpcsiwave_dcir_simulation_setup): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb diff --git a/src/pyedb/grpc/database/simulation_setup/siwave_simulation_setup.py b/src/pyedb/grpc/database/simulation_setup/siwave_simulation_setup.py new file mode 100644 index 0000000000..9531ef174b --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/siwave_simulation_setup.py @@ -0,0 +1,111 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNE SS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.simulation_setup.simulation_setup import ( + SimulationSetupType as GrpcSimulationSetupType, +) +from ansys.edb.core.simulation_setup.siwave_simulation_setup import ( + SIWaveSimulationSetup as GrpcSIWaveSimulationSetup, +) + +from pyedb.grpc.database.simulation_setup.sweep_data import SweepData + + +class SiwaveSimulationSetup(GrpcSIWaveSimulationSetup): + """Manages EDB methods for SIwave simulation setup.""" + + def __init__(self, pedb, edb_object=None): + super().__init__(edb_object.msg) + self._pedb = pedb + + @property + def type(self): + return super().type.name + + @type.setter + def type(self, value): + if value.upper() == "SI_WAVE": + super(SiwaveSimulationSetup, self.__class__).type.__set__(self, GrpcSimulationSetupType.SI_WAVE) + elif value.upper() == "SI_WAVE_DCIR": + super(SiwaveSimulationSetup, self.__class__).type.__set__(self, GrpcSimulationSetupType.SI_WAVE_DCIR) + + def add_sweep( + self, name=None, distribution="linear", start_freq="0GHz", stop_freq="20GHz", step="10MHz", discrete=False + ): + """Add a HFSS frequency sweep. + + Parameters + ---------- + name : str, optional + Sweep name. + distribution : str, optional + Type of the sweep. The default is `"linear"`. Options are: + - `"linear"` + - `"linear_count"` + - `"decade_count"` + - `"octave_count"` + - `"exponential"` + start_freq : str, float, optional + Starting frequency. The default is ``1``. + stop_freq : str, float, optional + Stopping frequency. The default is ``1e9``. + step : str, float, int, optional + Frequency step. The default is ``1e6``. or used for `"decade_count"`, "linear_count"`, "octave_count"` + distribution. Must be integer in that case. + discrete : bool, optional + Whether the sweep is discrete. The default is ``False``. + + Returns + ------- + bool + """ + init_sweep_count = len(self.sweep_data) + start_freq = self._pedb.number_with_units(start_freq, "Hz") + stop_freq = self._pedb.number_with_units(stop_freq, "Hz") + step = str(step) + if distribution.lower() == "linear": + distribution = "LIN" + elif distribution.lower() == "linear_count": + distribution = "LINC" + elif distribution.lower() == "exponential": + distribution = "ESTP" + elif distribution.lower() == "decade_count": + distribution = "DEC" + elif distribution.lower() == "octave_count": + distribution = "OCT" + else: + distribution = "LIN" + if not name: + name = f"sweep_{init_sweep_count + 1}" + sweep_data = [ + SweepData(self._pedb, name=name, distribution=distribution, start_f=start_freq, end_f=stop_freq, step=step) + ] + if discrete: + sweep_data[0].type = sweep_data[0].type.DISCRETE_SWEEP + for sweep in self.sweep_data: + sweep_data.append(sweep) + self.sweep_data = sweep_data + if len(self.sweep_data) == init_sweep_count + 1: + return True + else: + self._pedb.logger.error("Failed to add frequency sweep data") + return False diff --git a/src/pyedb/grpc/database/simulation_setup/sweep_data.py b/src/pyedb/grpc/database/simulation_setup/sweep_data.py new file mode 100644 index 0000000000..4a5bd90942 --- /dev/null +++ b/src/pyedb/grpc/database/simulation_setup/sweep_data.py @@ -0,0 +1,41 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.simulation_setup.simulation_setup import SweepData as GrpcSweepData + + +class SweepData(GrpcSweepData): + """Manages EDB methods for a frequency sweep. + + Parameters + ---------- + sim_setup : :class:`pyedb.dotnet.database.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup` + name : str, optional + Name of the frequency sweep. + edb_object : :class:`Ansys.Ansoft.Edb.Utility.SIWDCIRSimulationSettings`, optional + EDB object. The default is ``None``. + """ + + def __init__(self, pedb, name, distribution, start_f, end_f, step, edb_object=None): + super().__init__(name=name, distribution=distribution, start_f=start_f, end_f=end_f, step=step) + self._edb_object = edb_object + self._pedb = pedb diff --git a/src/pyedb/grpc/database/siwave.py b/src/pyedb/grpc/database/siwave.py new file mode 100644 index 0000000000..d45ef5bb01 --- /dev/null +++ b/src/pyedb/grpc/database/siwave.py @@ -0,0 +1,1023 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains these classes: ``CircuitPort``, ``CurrentSource``, ``EdbSiwave``, +``PinGroup``, ``ResistorSource``, ``Source``, ``SourceType``, and ``VoltageSource``. +""" +import os +import warnings + +from ansys.edb.core.database import ProductIdType as GrpcProductIdType +from ansys.edb.core.simulation_setup.simulation_setup import SweepData as GrpcSweepData + +from pyedb.misc.siw_feature_config.xtalk_scan.scan_config import SiwaveScanConfig + + +class Siwave(object): + """Manages EDB methods related to Siwave Setup accessible from `Edb.siwave` property. + + Parameters + ---------- + edb_class : :class:`pyedb.edb.Edb` + Inherited parent object. + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", edbversion="2021.2") + >>> edb_siwave = edbapp.siwave + """ + + def __init__(self, p_edb): + self._pedb = p_edb + + @property + def _edb(self): + """EDB.""" + return self._pedb + + @property + def _logger(self): + """EDB.""" + return self._pedb.logger + + @property + def _active_layout(self): + """Active layout.""" + return self._pedb.active_layout + + @property + def _layout(self): + """Active layout.""" + return self._pedb.layout + + @property + def _cell(self): + """Cell.""" + return self._pedb.active_cell + + @property + def _db(self): + """ """ + return self._pedb.active_db + + @property + def excitations(self): + """Get all excitations.""" + return self._pedb.excitations + + @property + def sources(self): + """Get all sources.""" + return self._pedb.sources + + @property + def probes(self): + """Get all probes.""" + return self._pedb.probes + + @property + def pin_groups(self): + """All Layout Pin groups. + + Returns + ------- + list + List of all layout pin groups. + """ + _pingroups = {} + for el in self._pedb.layout.pin_groups: + _pingroups[el.name] = el + return _pingroups + + def _create_terminal_on_pins(self, source): + """Create a terminal on pins. + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations._create_terminal_on_pins` instead. + + Parameters + ---------- + source : VoltageSource, CircuitPort, CurrentSource or ResistorSource + Name of the source. + + """ + warnings.warn( + "`_create_terminal_on_pins` is deprecated and is now located here " + "`pyedb.grpc.core.excitations._create_terminal_on_pins` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation._create_terminal_on_pins(source) + + def create_circuit_port_on_pin(self, pos_pin, neg_pin, impedance=50, port_name=None): + """Create a circuit port on a pin. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_circuit_port_on_pin` instead. + + Parameters + ---------- + pos_pin : Object + Edb Pin + neg_pin : Object + Edb Pin + impedance : float + Port Impedance + port_name : str, optional + Port Name + + Returns + ------- + str + Port Name. + """ + warnings.warn( + "`create_circuit_port_on_pin` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_circuit_port_on_pin` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_circuit_port_on_pin(pos_pin, neg_pin, impedance, port_name) + + def create_port_between_pin_and_layer( + self, component_name=None, pins_name=None, layer_name=None, reference_net=None, impedance=50.0 + ): + """Create circuit port between pin and a reference layer. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_port_between_pin_and_layer` instead. + + Parameters + ---------- + component_name : str + Component name. The default is ``None``. + pins_name : str + Pin name or list of pin names. The default is ``None``. + layer_name : str + Layer name. The default is ``None``. + reference_net : str + Reference net name. The default is ``None``. + impedance : float, optional + Port impedance. The default is ``50.0`` in ohms. + + Returns + ------- + PadstackInstanceTerminal + Created terminal. + """ + warnings.warn( + "`create_port_between_pin_and_layer` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_port_between_pin_and_layer` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_port_between_pin_and_layer( + component_name, pins_name, layer_name, reference_net, impedance + ) + + def create_voltage_source_on_pin(self, pos_pin, neg_pin, voltage_value=3.3, phase_value=0, source_name=""): + """Create a voltage source. + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_voltage_source_on_pin` instead. + + Parameters + ---------- + pos_pin : Object + Positive Pin. + neg_pin : Object + Negative Pin. + voltage_value : float, optional + Value for the voltage. The default is ``3.3``. + phase_value : optional + Value for the phase. The default is ``0``. + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + Source Name. + """ + + warnings.warn( + "`create_voltage_source_on_pin` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_voltage_source_on_pin` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_voltage_source_on_pin( + pos_pin, neg_pin, voltage_value, phase_value, source_name + ) + + def create_current_source_on_pin(self, pos_pin, neg_pin, current_value=0.1, phase_value=0, source_name=""): + """Create a current source. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_current_source_on_pin` instead. + + Parameters + ---------- + pos_pin : Object + Positive pin. + neg_pin : Object + Negative pin. + current_value : float, optional + Value for the current. The default is ``0.1``. + phase_value : optional + Value for the phase. The default is ``0``. + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + Source Name. + """ + warnings.warn( + "`create_current_source_on_pin` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_current_source_on_pin` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_current_source_on_pin( + pos_pin, neg_pin, current_value, phase_value, source_name + ) + + def create_resistor_on_pin(self, pos_pin, neg_pin, rvalue=1, resistor_name=""): + """Create a Resistor boundary between two given pins. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_resistor_on_pin` instead. + + Parameters + ---------- + pos_pin : Object + Positive Pin. + neg_pin : Object + Negative Pin. + rvalue : float, optional + Resistance value. The default is ``1``. + resistor_name : str, optional + Name of the resistor. The default is ``""``. + + Returns + ------- + str + Name of the resistor. + """ + warnings.warn( + "`create_resistor_on_pin` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_resistor_on_pin` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_resistor_on_pin(pos_pin, neg_pin, rvalue, resistor_name) + + def _check_gnd(self, component_name): + """ + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations._check_gnd` instead. + + """ + warnings.warn( + "`_check_gnd` is deprecated and is now located here " "`pyedb.grpc.core.excitations._check_gnd` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation._check_gnd(component_name) + + def create_circuit_port_on_net( + self, + positive_component_name, + positive_net_name, + negative_component_name=None, + negative_net_name=None, + impedance_value=50, + port_name="", + ): + """Create a circuit port on a NET. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_circuit_port_on_net` instead. + + It groups all pins belonging to the specified net and then applies the port on PinGroups. + + Parameters + ---------- + positive_component_name : str + Name of the positive component. + positive_net_name : str + Name of the positive net. + negative_component_name : str, optional + Name of the negative component. The default is ``None``, in which case the name of + the positive net is assigned. + negative_net_name : str, optional + Name of the negative net name. The default is ``None`` which will look for GND Nets. + impedance_value : float, optional + Port impedance value. The default is ``50``. + port_name : str, optional + Name of the port. The default is ``""``. + + Returns + ------- + str + The name of the port. + + """ + warnings.warn( + "`create_circuit_port_on_net` is deprecated and is now located here " + "`pyedb.grpc.core.source_excitation.create_circuit_port_on_net` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_circuit_port_on_net( + positive_component_name, + positive_net_name, + negative_component_name, + negative_net_name, + impedance_value, + port_name, + ) + + def create_voltage_source_on_net( + self, + positive_component_name, + positive_net_name, + negative_component_name=None, + negative_net_name=None, + voltage_value=3.3, + phase_value=0, + source_name="", + ): + """Create a voltage source. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_voltage_source_on_net` instead. + + Parameters + ---------- + positive_component_name : str + Name of the positive component. + positive_net_name : str + Name of the positive net. + negative_component_name : str, optional + Name of the negative component. The default is ``None``, in which case the name of + the positive net is assigned. + negative_net_name : str, optional + Name of the negative net name. The default is ``None`` which will look for GND Nets. + voltage_value : float, optional + Value for the voltage. The default is ``3.3``. + phase_value : optional + Value for the phase. The default is ``0``. + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + The name of the source. + + """ + warnings.warn( + "`create_voltage_source_on_net` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_voltage_source_on_net` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_voltage_source_on_net( + positive_component_name, + positive_net_name, + negative_component_name, + negative_net_name, + voltage_value, + phase_value, + source_name, + ) + + def create_current_source_on_net( + self, + positive_component_name, + positive_net_name, + negative_component_name=None, + negative_net_name=None, + current_value=0.1, + phase_value=0, + source_name="", + ): + """Create a current source. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_current_source_on_net` instead. + + Parameters + ---------- + positive_component_name : str + Name of the positive component. + positive_net_name : str + Name of the positive net. + negative_component_name : str, optional + Name of the negative component. The default is ``None``, in which case the name of + the positive net is assigned. + negative_net_name : str, optional + Name of the negative net name. The default is ``None`` which will look for GND Nets. + current_value : float, optional + Value for the current. The default is ``0.1``. + phase_value : optional + Value for the phase. The default is ``0``. + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + The name of the source. + """ + warnings.warn( + "`create_current_source_on_net` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_current_source_on_net` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_current_source_on_net( + positive_component_name, + positive_net_name, + negative_component_name, + negative_net_name, + current_value, + phase_value, + source_name, + ) + + def create_dc_terminal( + self, + component_name, + net_name, + source_name="", + ): + """Create a dc terminal. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_dc_terminal` instead. + + Parameters + ---------- + component_name : str + Name of the positive component. + net_name : str + Name of the positive net. + + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + The name of the source. + """ + warnings.warn( + "`create_dc_terminal` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_dc_terminal` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_dc_terminal(component_name, net_name, source_name) + + def create_exec_file( + self, add_dc=False, add_ac=False, add_syz=False, export_touchstone=False, touchstone_file_path="" + ): + """Create an executable file. + + Parameters + ---------- + add_dc : bool, optional + Whether to add the DC option in the EXE file. The default is ``False``. + add_ac : bool, optional + Whether to add the AC option in the EXE file. The default is + ``False``. + add_syz : bool, optional + Whether to add the SYZ option in the EXE file + export_touchstone : bool, optional + Add the Touchstone file export option in the EXE file. + The default is ``False``. + touchstone_file_path : str, optional + File path for the Touchstone file. The default is ``""``. When no path is + specified and ``export_touchstone=True``, the path for the project is + used. + """ + workdir = os.path.dirname(self._pedb.edbpath) + file_name = os.path.join(workdir, os.path.splitext(os.path.basename(self._pedb.edbpath))[0] + ".exec") + if os.path.isfile(file_name): + os.remove(file_name) + with open(file_name, "w") as f: + if add_ac: + f.write("ExecAcSim\n") + if add_dc: + f.write("ExecDcSim\n") + if add_syz: + f.write("ExecSyzSim\n") + if export_touchstone: + if touchstone_file_path: # pragma no cover + f.write('ExportTouchstone "{}"\n'.format(touchstone_file_path)) + else: # pragma no cover + touchstone_file_path = os.path.join( + workdir, os.path.splitext(os.path.basename(self._pedb.edbpath))[0] + "_touchstone" + ) + f.write('ExportTouchstone "{}"\n'.format(touchstone_file_path)) + f.write("SaveSiw\n") + + return True if os.path.exists(file_name) else False + + def add_siwave_syz_analysis( + self, + accuracy_level=1, + distribution="linear", + start_freq=1, + stop_freq=1e9, + step_freq=1e6, + discrete_sweep=False, + ): + """Add a SIwave AC analysis to EDB. + + Parameters + ---------- + accuracy_level : int, optional + Level of accuracy of SI slider. The default is ``1``. + distribution : str, optional + Type of the sweep. The default is `"linear"`. Options are: + - `"linear"` + - `"linear_count"` + - `"decade_count"` + - `"octave_count"` + - `"exponential"` + start_freq : str, float, optional + Starting frequency. The default is ``1``. + stop_freq : str, float, optional + Stopping frequency. The default is ``1e9``. + step_freq : str, float, int, optional + Frequency step. The default is ``1e6``. or used for `"decade_count"`, "linear_count"`, "octave_count"` + distribution. Must be integer in that case. + discrete_sweep : bool, optional + Whether the sweep is discrete. The default is ``False``. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup` + Setup object class. + """ + setup = self._pedb.create_siwave_syz_setup() + start_freq = self._pedb.number_with_units(start_freq, "Hz") + stop_freq = self._pedb.number_with_units(stop_freq, "Hz") + setup.settings.general.si_slider_pos = accuracy_level + if distribution.lower() == "linear": + distribution = "LIN" + elif distribution.lower() == "linear_count": + distribution = "LINC" + elif distribution.lower() == "exponential": + distribution = "ESTP" + elif distribution.lower() == "decade_count": + distribution = "DEC" + elif distribution.lower() == "octave_count": + distribution = "OCT" + else: + distribution = "LIN" + sweep_name = f"sweep_{len(setup.sweep_data) + 1}" + sweep_data = [ + GrpcSweepData( + name=sweep_name, distribution=distribution, start_f=start_freq, end_f=stop_freq, step=step_freq + ) + ] + if discrete_sweep: + sweep_data[0].type = sweep_data[0].type.DISCRETE_SWEEP + for sweep in setup.sweep_data: + sweep_data.append(sweep) + setup.sweep_data = sweep_data + self.create_exec_file(add_ac=True) + return setup + + def add_siwave_dc_analysis(self, name=None): + """Add a Siwave DC analysis in EDB. + + If a setup is present, it is deleted and replaced with + actual settings. + + .. note:: + Source Reference to Ground settings works only from 2021.2 + + Parameters + ---------- + name : str, optional + Setup name. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.siwave_simulation_setup_data.SiwaveDCSimulationSetup` + Setup object class. + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb("pathtoaedb", edbversion="2021.2") + >>> edb.siwave.add_siwave_ac_analysis() + >>> edb.siwave.add_siwave_dc_analysis2("my_setup") + + """ + setup = self._pedb.create_siwave_dc_setup(name) + self.create_exec_file(add_dc=True) + return setup + + def create_pin_group_terminal(self, source): + """Create a pin group terminal. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.excitations.create_pin_group_terminal` instead. + + Parameters + ---------- + source : VoltageSource, CircuitPort, CurrentSource, DCTerminal or ResistorSource + Name of the source. + + """ + warnings.warn( + "`create_pin_group_terminal` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_pin_group_terminal` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_pin_group_terminal(source) + + def create_rlc_component( + self, + pins, + component_name="", + r_value=1.0, + c_value=1e-9, + l_value=1e-9, + is_parallel=False, + ): + """Create physical Rlc component. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.components.create_pin_group_terminal` instead. + + Parameters + ---------- + pins : list[Edb.Cell.Primitive.PadstackInstance] + List of EDB pins. + + component_name : str + Component name. + + r_value : float + Resistor value. + + c_value : float + Capacitance value. + + l_value : float + Inductor value. + + is_parallel : bool + Using parallel model when ``True``, series when ``False``. + + Returns + ------- + class:`pyedb.dotnet.database.components.Components` + Created EDB component. + + """ + warnings.warn( + "`create_rlc_component` is deprecated and is now located here " + "`pyedb.grpc.core.components.create_rlc_component` instead.", + DeprecationWarning, + ) + return self._pedb.components.create( + pins, + component_name=component_name, + is_rlc=True, + r_value=r_value, + c_value=c_value, + l_value=l_value, + is_parallel=is_parallel, + ) # pragma no cover + + def create_pin_group(self, reference_designator, pin_numbers, group_name=None): + """Create pin group on the component. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.components.create_pin_group_terminal` instead. + + Parameters + ---------- + reference_designator : str + References designator of the component. + pin_numbers : int, str, list + List of pin names. + group_name : str, optional + Name of the pin group. + + Returns + ------- + PinGroup + """ + warnings.warn( + "`create_pin_group` is deprecated and is now located here " + "`pyedb.grpc.core.components.create_pin_group` instead.", + DeprecationWarning, + ) + return self._pedb.components.create_pin_group(reference_designator, pin_numbers, group_name) + + def create_pin_group_on_net(self, reference_designator, net_name, group_name=None): + """Create pin group on component by net name. + + . deprecated:: pyedb 0.28.0 + Use :func:`pyedb.grpc.core.components.create_pin_group_terminal` instead. + + Parameters + ---------- + reference_designator : str + References designator of the component. + net_name : str + Name of the net. + group_name : str, optional + Name of the pin group. The default value is ``None``. + + Returns + ------- + PinGroup + """ + warnings.warn( + "`create_pin_group_on_net` is deprecated and is now located here " + "`pyedb.grpc.core.components.create_pin_group_on_net` instead.", + DeprecationWarning, + ) + return self._pedb.components.create_pin_group_on_net(reference_designator, net_name, group_name) + + def create_current_source_on_pin_group( + self, pos_pin_group_name, neg_pin_group_name, magnitude=1, phase=0, name=None + ): + """Create current source between two pin groups. + + .deprecated:: pyedb 0.28.0 + Use: func:`pyedb.grpc.core.excitations.create_current_source_on_pin_group` + instead. + + Parameters + ---------- + pos_pin_group_name : str + Name of the positive pin group. + neg_pin_group_name : str + Name of the negative pin group. + magnitude : int, float, optional + Magnitude of the source. + phase : int, float, optional + Phase of the source + name : str, optional + source name. + + Returns + ------- + bool + + """ + warnings.warn( + "`create_current_source_on_pin_group` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_current_source_on_pin_group` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_current_source_on_pin_group( + pos_pin_group_name, neg_pin_group_name, magnitude, phase, name + ) + + def create_voltage_source_on_pin_group( + self, pos_pin_group_name, neg_pin_group_name, magnitude=1, phase=0, name=None, impedance=0.001 + ): + """Create voltage source between two pin groups. + + .deprecated:: pyedb 0.28.0 + Use: func:`pyedb.grpc.core.excitations.create_voltage_source_on_pin_group` + instead. + + Parameters + ---------- + pos_pin_group_name : str + Name of the positive pin group. + neg_pin_group_name : str + Name of the negative pin group. + magnitude : int, float, optional + Magnitude of the source. + phase : int, float, optional + Phase of the source + + Returns + ------- + bool + + """ + warnings.warn( + "`create_voltage_source_on_pin_group` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_voltage_source_on_pin_group` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_voltage_source_on_pin_group( + pos_pin_group_name, neg_pin_group_name, magnitude, phase, name, impedance + ) + + def create_voltage_probe_on_pin_group(self, probe_name, pos_pin_group_name, neg_pin_group_name, impedance=1e6): + """Create voltage probe between two pin groups. + + .deprecated:: pyedb 0.28.0 + Use: func:`pyedb.grpc.core.excitations.create_voltage_probe_on_pin_group` + instead. + + Parameters + ---------- + probe_name : str + Name of the probe. + pos_pin_group_name : str + Name of the positive pin group. + neg_pin_group_name : str + Name of the negative pin group. + impedance : int, float, optional + Phase of the source. + + Returns + ------- + bool + + """ + + warnings.warn( + "`create_voltage_probe_on_pin_group` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_voltage_probe_on_pin_group` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_voltage_probe_on_pin_group( + probe_name, pos_pin_group_name, neg_pin_group_name, impedance=impedance + ) + + def create_circuit_port_on_pin_group(self, pos_pin_group_name, neg_pin_group_name, impedance=50, name=None): + """Create a port between two pin groups. + + .deprecated:: pyedb 0.28.0 + Use: func:`pyedb.grpc.core.excitations.create_circuit_port_on_pin_group` + instead. + + Parameters + ---------- + pos_pin_group_name : str + Name of the positive pin group. + neg_pin_group_name : str + Name of the negative pin group. + impedance : int, float, optional + Impedance of the port. Default is ``50``. + name : str, optional + Port name. + + Returns + ------- + bool + + """ + warnings.warn( + "`create_circuit_port_on_pin_group` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.create_circuit_port_on_pin_group` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.create_circuit_port_on_pin_group( + pos_pin_group_name, neg_pin_group_name, impedance, name + ) + + def place_voltage_probe( + self, + name, + positive_net_name, + positive_location, + positive_layer, + negative_net_name, + negative_location, + negative_layer, + ): + """Place a voltage probe between two points. + + .deprecated:: pyedb 0.28.0 + Use: func:`pyedb.grpc.core.excitations.place_voltage_probe` + instead. + + Parameters + ---------- + name : str, + Name of the probe. + positive_net_name : str + Name of the positive net. + positive_location : list + Location of the positive terminal. + positive_layer : str, + Layer of the positive terminal. + negative_net_name : str, + Name of the negative net. + negative_location : list + Location of the negative terminal. + negative_layer : str + Layer of the negative terminal. + """ + warnings.warn( + "`place_voltage_probe` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.place_voltage_probe` instead.", + DeprecationWarning, + ) + return self._pedb.source_excitation.place_voltage_probe( + name, + positive_net_name, + positive_location, + positive_layer, + negative_net_name, + negative_location, + negative_layer, + ) + + # def create_vrm_module( + # self, + # name=None, + # is_active=True, + # voltage="3V", + # positive_sensor_pin=None, + # negative_sensor_pin=None, + # load_regulation_current="1A", + # load_regulation_percent=0.1, + # ): + # """Create a voltage regulator module. + # + # Parameters + # ---------- + # name : str + # Name of the voltage regulator. + # is_active : bool optional + # Set the voltage regulator active or not. Default value is ``True``. + # voltage ; str, float + # Set the voltage value. + # positive_sensor_pin : int, .class pyedb.dotnet.database.edb_data.padstacks_data.EDBPadstackInstance + # defining the positive sensor pin. + # negative_sensor_pin : int, .class pyedb.dotnet.database.edb_data.padstacks_data.EDBPadstackInstance + # defining the negative sensor pin. + # load_regulation_current : str or float + # definition the load regulation current value. + # load_regulation_percent : float + # definition the load regulation percent value. + # """ + # from pyedb.grpc.database.voltage_regulator import VoltageRegulator + # + # voltage = self._pedb.edb_value(voltage) + # load_regulation_current = self._pedb.edb_value(load_regulation_current) + # load_regulation_percent = self._pedb.edb_value(load_regulation_percent) + # edb_vrm = self._edb_object = self._pedb._edb.Cell.VoltageRegulator.Create( + # self._pedb.active_layout, name, is_active, voltage, load_regulation_current, load_regulation_percent + # ) + # vrm = VoltageRegulator(self._pedb, edb_vrm) + # if positive_sensor_pin: + # vrm.positive_remote_sense_pin = positive_sensor_pin + # if negative_sensor_pin: + # vrm.negative_remote_sense_pin = negative_sensor_pin + # return vrm + + @property + def icepak_use_minimal_comp_defaults(self): + """Icepak default setting. If "True", only resistor are active in Icepak simulation. + The power dissipation of the resistors are calculated from DC results. + """ + return self._pedb.active_cell.get_product_property(GrpcProductIdType.SIWAVE, 422).value + + def create_impedance_crosstalk_scan(self, scan_type="impedance"): + """Create Siwave crosstalk scan object + + Parameters + ---------- + scan_type : str + Scan type to be analyzed. 3 options are available, ``impedance`` for frequency impedance scan, + ``frequency_xtalk`` for frequency domain crosstalk and ``time_xtalk`` for time domain crosstalk. + Default value is ``frequency``. + + """ + return SiwaveScanConfig(self._pedb, scan_type) + + @icepak_use_minimal_comp_defaults.setter + def icepak_use_minimal_comp_defaults(self, value): + value = "True" if bool(value) else "" + self._pedb.active_cell.set_product_property(GrpcProductIdType.SIWAVE, 422, value) + + @property + def icepak_component_file(self): + """Icepak component file path.""" + return self._pedb.active_cell.get_product_property(GrpcProductIdType.SIWAVE, 420).value + return value + + @icepak_component_file.setter + def icepak_component_file(self, value): + self._pedb.active_cell.set_product_property(GrpcProductIdType.SIWAVE, 420, value) diff --git a/src/pyedb/grpc/database/source_excitations.py b/src/pyedb/grpc/database/source_excitations.py new file mode 100644 index 0000000000..14463a89fa --- /dev/null +++ b/src/pyedb/grpc/database/source_excitations.py @@ -0,0 +1,2572 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.database import ProductIdType as GrpcProductIdType +from ansys.edb.core.geometry.point_data import PointData as GrpcPointData +from ansys.edb.core.geometry.polygon_data import PolygonData as GrpcPolygonData +from ansys.edb.core.hierarchy.component_group import ( + ComponentGroup as GrpcComponentGroup, +) +from ansys.edb.core.terminal.terminals import BoundaryType as GrpcBoundaryType +from ansys.edb.core.terminal.terminals import EdgeTerminal as GrpcEdgeTerminal +from ansys.edb.core.terminal.terminals import PrimitiveEdge as GrpcPrimitiveEdge +from ansys.edb.core.utility.rlc import Rlc as GrpcRlc +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.generic.general_methods import generate_unique_name +from pyedb.grpc.database.layers.stackup_layer import StackupLayer +from pyedb.grpc.database.nets.net import Net +from pyedb.grpc.database.ports.ports import BundleWavePort, WavePort +from pyedb.grpc.database.primitive.padstack_instances import PadstackInstance +from pyedb.grpc.database.primitive.primitive import Primitive +from pyedb.grpc.database.terminal.bundle_terminal import BundleTerminal +from pyedb.grpc.database.terminal.padstack_instance_terminal import ( + PadstackInstanceTerminal, +) +from pyedb.grpc.database.terminal.pingroup_terminal import PinGroupTerminal +from pyedb.grpc.database.terminal.point_terminal import PointTerminal +from pyedb.grpc.database.utility.sources import Source, SourceType +from pyedb.modeler.geometry_operators import GeometryOperators + + +class SourceExcitation: + def __init__(self, pedb): + self._pedb = pedb + + @property + def _logger(self): + return self._pedb.logger + + @property + def excitations(self): + """Get all excitations.""" + return self._pedb.excitations + + @property + def sources(self): + """Get all sources.""" + return self._pedb.sources + + @property + def probes(self): + """Get all probes.""" + return self._pedb.probes + + def create_source_on_component(self, sources=None): + """Create voltage, current source, or resistor on component. + + Parameters + ---------- + sources : list[Source] + List of ``pyedb.grpc.utility.sources.Source`` objects. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + + if not sources: # pragma: no cover + return False + if isinstance(sources, Source): # pragma: no cover + sources = [sources] + if isinstance(sources, list): # pragma: no cover + for src in sources: + if not isinstance(src, Source): # pragma: no cover + self._pedb.logger.error("List of source objects must be passed as an argument.") + return False + for source in sources: + positive_pins = self._pedb.padstack.get_instances(source.positive_node.component, source.positive_node.net) + negative_pins = self._pedb.padstack.get_instances(source.negative_node.component, source.negative_node.net) + positive_pin_group = self._pedb.components.create_pingroup_from_pins(positive_pins) + if not positive_pin_group: # pragma: no cover + return False + positive_pin_group = self._pedb.siwave.pin_groups[positive_pin_group.name] + negative_pin_group = self._pedb.components.create_pingroup_from_pins(negative_pins) + if not negative_pin_group: # pragma: no cover + return False + negative_pin_group = self._pedb.siwave.pin_groups[negative_pin_group.GetName()] + if source.source_type == SourceType.Vsource: # pragma: no cover + positive_pin_group_term = self._pedb.components._create_pin_group_terminal( + positive_pin_group, + ) + negative_pin_group_term = self._pedb.components._create_pin_group_terminal( + negative_pin_group, isref=True + ) + positive_pin_group_term.boundary_type = GrpcBoundaryType.VOLTAGE_SOURCE + negative_pin_group_term.boundary_type = GrpcBoundaryType.VOLTAGE_SOURCE + term_name = source.name + positive_pin_group_term.SetName(term_name) + negative_pin_group_term.SetName("{}_ref".format(term_name)) + positive_pin_group_term.source_amplitude = GrpcValue(source.amplitude) + negative_pin_group_term.source_amplitude = GrpcValue(source.amplitude) + positive_pin_group_term.source_phase = GrpcValue(source.phase) + negative_pin_group_term.source_phase = GrpcValue(source.phase) + positive_pin_group_term.impedance = GrpcValue(source.impedance) + negative_pin_group_term.impedance = GrpcValue(source.impedance) + positive_pin_group_term.reference_terminal = negative_pin_group_term + elif source.source_type == SourceType.Isource: # pragma: no cover + positive_pin_group_term = self._pedb.components._create_pin_group_terminal( + positive_pin_group, + ) + negative_pin_group_term = self._pedb.components._create_pin_group_terminal( + negative_pin_group, isref=True + ) + positive_pin_group_term.boundary_type = GrpcBoundaryType.CURRENT_SOURCE + negative_pin_group_term.boundary_type = GrpcBoundaryType.CURRENT_SOURCE + positive_pin_group_term.name = source.name + negative_pin_group_term.name = "{}_ref".format(source.name) + positive_pin_group_term.source_amplitude = GrpcValue(source.amplitude) + negative_pin_group_term.source_amplitude = GrpcValue(source.amplitude) + positive_pin_group_term.source_phase = GrpcValue(source.phase) + negative_pin_group_term.source_phase = GrpcValue(source.phase) + positive_pin_group_term.impedance = GrpcValue(source.impedance) + negative_pin_group_term.impedance = GrpcValue(source.impedance) + positive_pin_group_term.reference_terminal = negative_pin_group_term + elif source.source_type == SourceType.Rlc: # pragma: no cover + self._pedb.components.create( + pins=[positive_pins[0], negative_pins[0]], + component_name=source.name, + is_rlc=True, + r_value=source.r_value, + l_value=source.l_value, + c_value=source.c_value, + ) + return True + + def create_port_on_pins( + self, + refdes, + pins, + reference_pins, + impedance=50.0, + port_name=None, + pec_boundary=False, + pingroup_on_single_pin=False, + ): + """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 + port_name : str, optional + Port name. The default is ``None``, in which case a name is automatically assigned. + pec_boundary : bool, optional + Whether to define the PEC boundary, The default is ``False``. If set to ``True``, + a perfect short is created between the pin and impedance is ignored. This + parameter is only supported on a port created between two pins, such as + when there is no pin group. + pingroup_on_single_pin : bool + If ``True`` force using pingroup definition on single pin to have the port created at the pad center. If + ``False`` the port is created at the pad edge. Default value is ``False``. + + Returns + ------- + EDB terminal created, or False if failed to create. + + Example: + >>> from pyedb 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() + """ + from pyedb.grpc.database.components import Component + + if isinstance(pins, str): + pins = [pins] + elif isinstance(pins, PadstackInstance): + pins = [pins.name] + if not reference_pins: + self._logger.error("No reference pin provided.") + return False + if isinstance(reference_pins, str): + reference_pins = [reference_pins] + elif isinstance(reference_pins, int): + reference_pins = [reference_pins] + elif isinstance(reference_pins, PadstackInstance): + reference_pins = [reference_pins] + if isinstance(reference_pins, list): + _temp = [] + for ref_pin in reference_pins: + if isinstance(ref_pin, int): + pins = self._pedb.padstacks.instances + reference_pins = [pins[ref_pin] for ref_pin in reference_pins if ref_pin in pins] + # if reference_pins in pins: + # reference_pins = pins[reference_pins] + elif isinstance(ref_pin, str): + component_pins = self._pedb.components.instances[refdes].pins + if ref_pin in component_pins: + _temp.append(component_pins[ref_pin]) + else: + p = [pp for pp in list(self._pedb.padstack.instances.values()) if pp.name == ref_pin] + if p: + _temp.extend(p) + elif isinstance(ref_pin, PadstackInstance): + _temp.append(ref_pin) + reference_pins = _temp + if isinstance(refdes, str): + refdes = self._pedb.components.instances[refdes] + elif isinstance(refdes, GrpcComponentGroup): + refdes = Component(self._pedb, refdes) + refdes_pins = refdes.pins + if any(refdes.rlc_values): + return self._pedb.components.deactivate_rlc_component(component=refdes, create_circuit_port=True) + if len([pin for pin in pins if isinstance(pin, str)]) == len(pins): + cmp_pins = [] + for pin_name in pins: + cmp_pins = [pin for pin in list(refdes_pins.values()) if pin_name == pin.name] + if not cmp_pins: + for pin in list(refdes_pins.values()): + if pin.name and "-" in pin.name: + if pin_name == pin.name.split("-")[1]: + cmp_pins.append(pin) + if not cmp_pins: + self._logger.warning("No pin found during port creation. Port is not defined.") + return + pins = cmp_pins + if not len([pin for pin in pins if isinstance(pin, PadstackInstance)]) == len(pins): + self._logger.error("Pin list must contain only pins instances") + return False + if not port_name: + pin = pins[0] + if pin.net.is_null: + pin_net_name = "no_net" + else: + pin_net_name = pin.net.name + port_name = f"Port_{pin_net_name}_{refdes.name}_{pins[0].name}" + + ref_cmp_pins = [] + for ref_pin in reference_pins: + if ref_pin.name in refdes_pins: + ref_cmp_pins.append(ref_pin) + elif "-" in ref_pin.name: + if ref_pin.name.split("-")[1] in refdes_pins: + ref_cmp_pins.append(ref_pin) + elif "via" in ref_pin.name: + _ref_pin = [ + pin for pin in list(self._pedb.padstacks.instances.values()) if pin.aedt_name == ref_pin.name + ] + if _ref_pin: + _ref_pin[0].is_layout_pin = True + ref_cmp_pins.append(_ref_pin[0]) + if not ref_cmp_pins: + self._logger.error("No reference pins found.") + return False + reference_pins = ref_cmp_pins + if len(pins) > 1 or pingroup_on_single_pin: + pec_boundary = False + self._logger.info( + "Disabling PEC boundary creation, this feature is supported on single pin " + "ports only, {} pins found".format(len(pins)) + ) + group_name = "group_{}".format(port_name) + pin_group = self._pedb.components.create_pingroup_from_pins(pins, group_name) + term = self._create_pin_group_terminal(pingroup=pin_group, term_name=port_name) + + else: + term = self._create_terminal(pins[0], term_name=port_name) + term.is_circuit_port = True + if len(reference_pins) > 1 or pingroup_on_single_pin: + pec_boundary = False + self._logger.info( + "Disabling PEC boundary creation. This feature is supported on single pin" + "ports only {} reference pins found.".format(len(reference_pins)) + ) + ref_group_name = "group_{}_ref".format(port_name) + ref_pin_group = self._pedb.components.create_pingroup_from_pins(reference_pins, ref_group_name) + ref_pin_group = self._pedb.siwave.pin_groups[ref_pin_group.name] + ref_term = self._create_pin_group_terminal(pingroup=ref_pin_group, term_name=port_name + "_ref") + + else: + ref_term = self._create_terminal(reference_pins[0], term_name=port_name + "_ref") + ref_term.is_circuit_port = True + term.impedance = GrpcValue(impedance) + term.reference_terminal = ref_term + if pec_boundary: + term.is_circuit_port = False + ref_term.is_circuit_port = False + term.boundary_type = GrpcBoundaryType.PEC + ref_term.boundary_type = GrpcBoundaryType.PEC + self._logger.info( + "PEC boundary created between pin {} and reference pin {}".format(pins[0].name, reference_pins[0].name) + ) + if term: + return term + return False + + def create_port_on_component( + self, + component, + net_list, + port_type=SourceType.CoaxPort, + do_pingroup=True, + reference_net="gnd", + port_name=None, + solder_balls_height=None, + solder_balls_size=None, + solder_balls_mid_size=None, + extend_reference_pins_outside_component=False, + ): + """Create ports on a component. + + Parameters + ---------- + component : str or self._pedb.component + EDB component or str component name. + net_list : str or list of string. + List of nets where ports must be created on the component. + If the net is not part of the component, this parameter is skipped. + port_type : str, optional + Type of port to create. ``coax_port`` generates solder balls. + ``circuit_port`` generates circuit ports on pins belonging to the net list. + do_pingroup : bool + True activate pingroup during port creation (only used with combination of CircPort), + False will take the closest reference pin and generate one port per signal pin. + refnet : string or list of string. + list of the reference net. + port_name : str + Port name for overwriting the default port-naming convention, + which is ``[component][net][pin]``. The port name must be unique. + If a port with the specified name already exists, the + default naming convention is used so that port creation does + not fail. + solder_balls_height : float, optional + Solder balls height used for the component. When provided default value is overwritten and must be + provided in meter. + solder_balls_size : float, optional + Solder balls diameter. When provided auto evaluation based on padstack size will be disabled. + solder_balls_mid_size : float, optional + Solder balls mid-diameter. When provided if value is different than solder balls size, spheroid shape will + be switched. + extend_reference_pins_outside_component : bool + When no reference pins are found on the component extend the pins search with taking the closest one. If + `do_pingroup` is `True` will be set to `False`. Default value is `False`. + + Returns + ------- + double, bool + Salder ball height vale, ``False`` when failed. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder") + >>> net_list = ["M_DQ<1>", "M_DQ<2>", "M_DQ<3>", "M_DQ<4>", "M_DQ<5>"] + >>> edbapp.components.create_port_on_component(cmp="U2A5", net_list=net_list, + >>> port_type=SourceType.CoaxPort, do_pingroup=False, refnet="GND") + + """ + if isinstance(component, str): + component = self._pedb.components.instances[component] + if not isinstance(net_list, list): + net_list = [net_list] + for net in net_list: + if not isinstance(net, str): + try: + net_name = net.name + if net_name: + net_list.append(net_name) + except: + pass + if reference_net in net_list: + net_list.remove(reference_net) + cmp_pins = [p for p in list(component.pins.values()) if p.net_name in net_list] + for p in cmp_pins: # pragma no cover + p.is_layout_pin = True + if len(cmp_pins) == 0: + self._logger.info(f"No pins found on component {component.name}, searching padstack instances instead") + return False + pin_layers = cmp_pins[0].padstack_def.data.layer_names + if port_type == "coax_port": + if not solder_balls_height: + solder_balls_height = self._pedb.components.instances[component.name].solder_ball_height + if not solder_balls_size: + solder_balls_size = self._pedb.components.instances[component.name].solder_ball_diameter[0] + if not solder_balls_mid_size: + solder_balls_mid_size = self._pedb.components.instances[component.name].solder_ball_diameter[1] + ref_pins = [p for p in list(component.pins.values()) if p.net_name in reference_net] + if not ref_pins: + self._logger.error( + "No reference pins found on component. You might consider" + "using Circuit port instead since reference pins can be extended" + "outside the component when not found if argument extend_reference_pins_outside_component is True." + ) + return False + pad_params = self._pedb.padstack.get_pad_parameters(pin=cmp_pins[0], layername=pin_layers[0], pad_type=0) + if not pad_params[0] == 7: + if not solder_balls_size: # pragma no cover + sball_diam = min([GrpcValue(val).value for val in pad_params[1]]) + sball_mid_diam = sball_diam + else: # pragma no cover + sball_diam = solder_balls_size + if solder_balls_mid_size: + sball_mid_diam = solder_balls_mid_size + else: + sball_mid_diam = solder_balls_size + if not solder_balls_height: # pragma no cover + solder_balls_height = 2 * sball_diam / 3 + else: # pragma no cover + if not solder_balls_size: + bbox = pad_params[1] + sball_diam = min([abs(bbox[2] - bbox[0]), abs(bbox[3] - bbox[1])]) * 0.8 + else: + sball_diam = solder_balls_size + if not solder_balls_height: + solder_balls_height = 2 * sball_diam / 3 + if solder_balls_mid_size: + sball_mid_diam = solder_balls_mid_size + else: + sball_mid_diam = sball_diam + sball_shape = "Cylinder" + if not sball_diam == sball_mid_diam: + sball_shape = "Spheroid" + self._pedb.components.set_solder_ball( + component=component, + sball_height=solder_balls_height, + sball_diam=sball_diam, + sball_mid_diam=sball_mid_diam, + shape=sball_shape, + ) + for pin in cmp_pins: + self._pedb.padstack.create_coax_port(padstackinstance=pin, name=port_name) + + elif port_type == "circuit_port": # pragma no cover + ref_pins = [p for p in list(component.pins.values()) if p.net_name in reference_net] + for p in ref_pins: + p.is_layout_pin = True + if not ref_pins: + self._logger.warning("No reference pins found on component") + if not extend_reference_pins_outside_component: + self._logger.warning( + "argument extend_reference_pins_outside_component is False. You might want " + "setting to True to extend the reference pin search outside the component" + ) + else: + do_pingroup = False + if do_pingroup: + if len(ref_pins) == 1: + ref_pins.is_pin = True + ref_pin_group_term = self._create_terminal(ref_pins[0]) + else: + for pin in ref_pins: + pin.is_pin = True + ref_pin_group = self._pedb.components.create_pingroup_from_pins(ref_pins) + if ref_pin_group.is_null: + self._logger.error(f"Failed to create reference pin group on component {component.GetName()}.") + return False + ref_pin_group_term = self._create_pin_group_terminal(ref_pin_group, isref=False) + if not ref_pin_group_term: + self._logger.error( + f"Failed to create reference pin group terminal on component {component.GetName()}" + ) + return False + for net in net_list: + pins = [pin for pin in list(component.pins.values()) if pin.net_name == net] + if pins: + if len(pins) == 1: + pin_term = self._create_terminal(pins[0]) + if pin_term: + pin_term.reference_terminal = ref_pin_group_term + else: + pin_group = self._pedb.components.create_pingroup_from_pins(pins) + if pin_group.is_null: + self._logger.error( + f"Failed to create pin group terminal on component {component.GetName()}" + ) + return False + pin_group_term = self._create_pin_group_terminal(pin_group) + if pin_group_term: + pin_group_term.reference_terminal = ref_pin_group_term + else: + self._logger.info("No pins found on component {} for the net {}".format(component, net)) + else: + for net in net_list: + pins = [pin for pin in list(component.pins.values()) if pin.net_name == net] + for pin in pins: + if ref_pins: + self.create_port_on_pins(component, pin, ref_pins) + else: + if extend_reference_pins_outside_component: + _pin = PadstackInstance(self._pedb, pin) + ref_pin = _pin.get_reference_pins( + reference_net=reference_net[0], + max_limit=1, + component_only=False, + search_radius=3e-3, + ) + if ref_pin: + if not isinstance(ref_pin, list): + ref_pin = [ref_pin] + self.create_port_on_pins(component, [pin.name], ref_pin[0]) + else: + self._logger.error("Skipping port creation no reference pin found.") + return True + + def _create_terminal(self, pin, term_name=None): + """Create terminal on component pin. + + Parameters + ---------- + pin : Edb padstack instance. + + term_name : Terminal name (Optional). + str. + + Returns + ------- + EDB terminal. + """ + + from_layer, _ = pin.get_layer_range() + if term_name is None: + term_name = "{}.{}.{}".format(pin.component.name, pin.name, pin.net.name) + for term in list(self._pedb.active_layout.terminals): + if term.name == term_name: + return term + return PadstackInstanceTerminal.create( + layout=self._pedb.layout, name=term_name, padstack_instance=pin, layer=from_layer, net=pin.net, is_ref=False + ) + + def add_port_on_rlc_component(self, component=None, circuit_ports=True, pec_boundary=False): + """Deactivate RLC component and replace it with a circuit port. + The circuit port supports only two-pin components. + + Parameters + ---------- + component : str + Reference designator of the RLC component. + + circuit_ports : bool + ``True`` will replace RLC component by circuit ports, ``False`` gap ports compatible with HFSS 3D modeler + export. + + pec_boundary : bool, optional + Whether to define the PEC boundary, The default is ``False``. If set to ``True``, + a perfect short is created between the pin and impedance is ignored. This + parameter is only supported on a port created between two pins, such as + when there is no pin group. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + from pyedb.grpc.database.components import Component + + if isinstance(component, str): + component = self._pedb.components.instances[component] + if not isinstance(component, Component): # pragma: no cover + return False + self._pedb.components.set_component_rlc(component.refdes) + pins = list(self._pedb.components.instances[component.refdes].pins.values()) + if len(pins) == 2: + pin_layers = pins[0].get_layer_range() + pos_pin_term = PadstackInstanceTerminal.create( + layout=self._pedb.active_layout, + name=f"{component.name}_{pins[0].name}", + padstack_instance=pins[0], + layer=pin_layers[0], + net=pins[0].net, + is_ref=False, + ) + if not pos_pin_term: # pragma: no cover + return False + neg_pin_term = PadstackInstanceTerminal.create( + layout=self._pedb.active_layout, + name="{}_{}_ref".format(component.name, pins[1].name), + padstack_instance=pins[1], + layer=pin_layers[0], + net=pins[1].net, + is_ref=False, + ) + if not neg_pin_term: # pragma: no cover + return False + if pec_boundary: + pos_pin_term.boundary_type = GrpcBoundaryType.PEC + neg_pin_term.boundary_type = GrpcBoundaryType.PEC + else: + pos_pin_term.boundary_type = GrpcBoundaryType.PORT + neg_pin_term.boundary_type = GrpcBoundaryType.PORT + pos_pin_term.name = component.name + pos_pin_term.reference_terminal = neg_pin_term + if circuit_ports and not pec_boundary: + pos_pin_term.is_circuit_port = True + neg_pin_term.is_circuit_port = True + elif pec_boundary: + pos_pin_term.is_circuit_port = False + neg_pin_term.is_circuit_port = False + else: + pos_pin_term.is_circuit_port = False + neg_pin_term.is_circuit_port = False + self._logger.info(f"Component {component.refdes} has been replaced by port") + return True + return False + + def add_rlc_boundary(self, component=None, circuit_type=True): + """Add RLC gap boundary on component and replace it with a circuit port. + The circuit port supports only 2-pin components. + + Parameters + ---------- + component : str + Reference designator of the RLC component. + circuit_type : bool + When ``True`` circuit type are defined, if ``False`` gap type will be used instead (compatible with HFSS 3D + modeler). Default value is ``True``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + from pyedb.grpc.database.components import Component + + if isinstance(component, str): # pragma: no cover + component = self._pedb.components.instances[component] + if not isinstance(component, Component): # pragma: no cover + return False + self._pedb.components.set_component_rlc(component.name) + pins = list(component.pins.values()) + if len(pins) == 2: # pragma: no cover + pin_layer = pins[0].get_layer_range()[0] + pos_pin_term = PadstackInstanceTerminal.create( + layout=self._pedb.active_layout, + net=pins[0].net, + name=f"{component.name}_{pins[0].name}", + padstack_instance=pins[0], + layer=pin_layer, + is_ref=False, + ) + if not pos_pin_term: # pragma: no cover + return False + neg_pin_term = PadstackInstanceTerminal.create( + layout=self._pedb.active_layout, + net=pins[1].net, + name="{}_{}_ref".format(component.name, pins[1].name), + padstack_instance=pins[1], + layer=pin_layer, + is_ref=True, + ) + if not neg_pin_term: # pragma: no cover + return False + pos_pin_term.boundary_type = GrpcBoundaryType.RLC + if not circuit_type: + pos_pin_term.is_circuit_port = False + else: + pos_pin_term.is_circuit_port = True + pos_pin_term.name = component.name + neg_pin_term.boundary_type = GrpcBoundaryType.RLC + if not circuit_type: + neg_pin_term.is_circuit_port = False + else: + neg_pin_term.is_circuit_port = True + pos_pin_term.reference_terminal = neg_pin_term + rlc_values = component.rlc_values + rlc = GrpcRlc() + if rlc_values[0]: + rlc.r_enabled = True + rlc.r = GrpcValue(rlc_values[0]) + if rlc_values[1]: + rlc.l_enabled = True + rlc.l = GrpcValue(rlc_values[1]) + if rlc_values[2]: + rlc.c_enabled = True + rlc.c = GrpcValue(rlc_values[2]) + rlc.is_parallel = component.is_parallel_rlc + pos_pin_term.rlc_boundary = rlc + self._logger.info("Component {} has been replaced by port".format(component.refdes)) + return True + + def _create_pin_group_terminal(self, pingroup, isref=False, term_name=None, term_type="circuit"): + """Creates an EDB pin group terminal from a given EDB pin group. + + Parameters + ---------- + pingroup : Pin group. + + isref : bool + Specify if this terminal a reference terminal. + + term_name : Terminal name (Optional). If not provided default name is Component name, Pin name, Net name. + str. + + term_type: Type of terminal, gap, circuit or auto. + str. + Returns + ------- + Edb pin group terminal. + """ + if pingroup.is_null: + self._logger.error(f"{pingroup} is null") + pin = PadstackInstance(self._pedb, pingroup.pins[0]) + if term_name is None: + term_name = f"{pin.component.name}.{pin.name}.{pin.net_name}" + for t in self._pedb.active_layout.terminals: + if t.name == term_name: + self._logger.warning( + f"Terminal {term_name} already created in current layout. Returning the " + f"already defined one. Make sure to delete the terminal before to create a new one." + ) + return t + pingroup_term = PinGroupTerminal.create( + layout=self._pedb.active_layout, name=term_name, net=pingroup.net, pin_group=pingroup, is_ref=isref + ) + if term_type == "circuit" or "auto": + pingroup_term.is_circuit_port = True + return pingroup_term + + def create_coax_port(self, padstackinstance, use_dot_separator=True, name=None, create_on_top=True): + """Create HFSS 3Dlayout coaxial lumped port on a pastack + Requires to have solder ball defined before calling this method. + + Parameters + ---------- + padstackinstance : `Edb.Cell.Primitive.PadstackInstance` or int + Padstack instance object. + use_dot_separator : bool, optional + Whether to use ``.`` as the separator for the naming convention, which + is ``[component][net][pin]``. The default is ``True``. If ``False``, ``_`` is + used as the separator instead. + name : str + Port name for overwriting the default port-naming convention, + which is ``[component][net][pin]``. The port name must be unique. + If a port with the specified name already exists, the + default naming convention is used so that port creation does + not fail. + + Returns + ------- + str + Terminal name. + + """ + if isinstance(padstackinstance, int): + padstackinstance = self._pedb.padstacks.instances[padstackinstance] + cmp_name = padstackinstance.component.name + if cmp_name == "": + cmp_name = "no_comp" + net_name = padstackinstance.net.name + if net_name == "": + net_name = "no_net" + pin_name = padstackinstance.name + if pin_name == "": + pin_name = "no_pin_name" + if use_dot_separator: + port_name = "{0}.{1}.{2}".format(cmp_name, pin_name, net_name) + else: + port_name = "{0}_{1}_{2}".format(cmp_name, pin_name, net_name) + padstackinstance.is_layout_pin = True + layer_range = padstackinstance.get_layer_range() + if create_on_top: + terminal_layer = layer_range[0] + else: + terminal_layer = layer_range[1] + if name: + port_name = name + if self._port_exist(port_name): + port_name = generate_unique_name(port_name, n=2) + self._logger.info("An existing port already has this same name. Renaming to {}.".format(port_name)) + PadstackInstanceTerminal.create( + layout=self._pedb._active_layout, + name=port_name, + padstack_instance=padstackinstance, + layer=terminal_layer, + net=padstackinstance.net, + is_ref=False, + ) + return port_name + + def _port_exist(self, port_name): + return any(port for port in list(self._pedb.excitations.keys()) if port == port_name) + + def _create_edge_terminal(self, prim_id, point_on_edge, terminal_name=None, is_ref=False): + """Create an edge terminal. + + Parameters + ---------- + prim_id : int + Primitive ID. + point_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be on the target edge but not on the two + ends of the edge. + terminal_name : str, optional + Name of the terminal. The default is ``None``, in which case the + default name is assigned. + is_ref : bool, optional + Whether it is a reference terminal. The default is ``False``. + + Returns + ------- + Edb.Cell.Terminal.EdgeTerminal + """ + if not terminal_name: + terminal_name = generate_unique_name("Terminal_") + if isinstance(point_on_edge, tuple): + point_on_edge = GrpcPointData(point_on_edge) + prim = [i for i in self._pedb.modeler.primitives if i.id == prim_id] + if not prim: + self._pedb.logger.error(f"No primitive found for ID {prim_id}") + return False + prim = prim[0] + pos_edge = [GrpcPrimitiveEdge.create(prim, point_on_edge)] + return GrpcEdgeTerminal.create( + layout=prim.layout, name=terminal_name, edges=pos_edge, net=prim.net, is_ref=is_ref + ) + + def create_circuit_port_on_pin(self, pos_pin, neg_pin, impedance=50, port_name=None): + """Create a circuit port on a pin. + + Parameters + ---------- + pos_pin : Object + Edb Pin + neg_pin : Object + Edb Pin + impedance : float + Port Impedance + port_name : str, optional + Port Name + + Returns + ------- + str + Port Name. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> pins = edbapp.components.get_pin_from_component("U2A5") + >>> edbapp.siwave.create_circuit_port_on_pin(pins[0], pins[1], 50, "port_name") + """ + if not port_name: + port_name = f"Port_{pos_pin.component.name}_{pos_pin.net_name}_{neg_pin.component.name}_{neg_pin.net_name}" + return self._create_terminal_on_pins( + positive_pin=pos_pin, negative_pin=neg_pin, impedance=impedance, name=port_name + ) + + def _create_terminal_on_pins( + self, + positive_pin, + negative_pin, + name=None, + use_pin_top_layer=True, + source_type="circuit_port", + impedance=50, + magnitude=0, + phase=0, + r=0, + l=0, + c=0, + ): + """Create a terminal on pins. + + Parameters + ---------- + positive_pin : :class: `PadstackInstance` + Positive padstack instance. + negative_pin : :class: `PadstackInstance` + Negative padstack instance. + name : str, optional + terminal name + use_pin_top_layer : bool, optional + Use :class: `PadstackInstance` top layer or bottom for terminal assignment. + source_type : str, optional + Specify the source type created. Supported values: `"circuit_port"`, `"lumped_port"`, `"current_source"`, + `"voltage_port"`, `"rlc"`. + impedance : float, int or str, optional + Terminal impedance value + magnitude : float, int or str, optional + Terminal magnitude. + phase : float, int or str, optional + Terminal phase + r : float, int + Resistor value + l : float, int + Inductor value + c : float, int + Capacitor value + """ + + top_layer_pos, bottom_layer_pos = positive_pin.get_layer_range() + top_layer_neg, bottom_layer_neg = negative_pin.get_layer_range() + pos_term_layer = bottom_layer_pos + neg_term_layer = bottom_layer_neg + if use_pin_top_layer: + pos_term_layer = top_layer_pos + neg_term_layer = top_layer_neg + if not name: + name = positive_pin.name + pos_terminal = PadstackInstanceTerminal.create( + layout=self._pedb.active_layout, + padstack_instance=positive_pin, + name=name, + layer=pos_term_layer, + is_ref=False, + net=positive_pin.net, + ) + + neg_terminal = PadstackInstanceTerminal.create( + layout=self._pedb.active_layout, + padstack_instance=negative_pin, + name=negative_pin.name, + layer=neg_term_layer, + is_ref=False, + net=negative_pin.net, + ) + if source_type in ["circuit_port", "lumped_port"]: + pos_terminal.boundary_type = GrpcBoundaryType.PORT + neg_terminal.boundary_type = GrpcBoundaryType.PORT + pos_terminal.impedance = GrpcValue(impedance) + if source_type == "lumped_port": + pos_terminal.is_circuit_port = False + neg_terminal.is_circuit_port = False + else: + pos_terminal.is_circuit_port = True + neg_terminal.is_circuit_port = True + pos_terminal.reference_terminal = neg_terminal + pos_terminal.name = name + + elif source_type == "current_source": + pos_terminal.boundary_type = GrpcBoundaryType.CURRENT_SOURCE + neg_terminal.boundary_type = GrpcBoundaryType.CURRENT_SOURCE + pos_terminal.source_amplitude = GrpcValue(magnitude) + pos_terminal.source_phase = GrpcValue(phase) + pos_terminal.impedance = GrpcValue(impedance) + pos_terminal.reference_terminal = neg_terminal + pos_terminal.name = name + + elif source_type == "voltage_source": + pos_terminal.boundary_type = GrpcBoundaryType.VOLTAGE_SOURCE + neg_terminal.boundary_type = GrpcBoundaryType.VOLTAGE_SOURCE + pos_terminal.source_amplitude = GrpcValue(magnitude) + pos_terminal.impedance = GrpcValue(impedance) + pos_terminal.source_phase = GrpcValue(phase) + pos_terminal.reference_terminal = neg_terminal + pos_terminal.name = name + + elif source_type == "rlc": + pos_terminal.boundary_type = GrpcBoundaryType.RLC + neg_terminal.boundary_type = GrpcBoundaryType.RLC + pos_terminal.reference_terminal = neg_terminal + rlc = GrpcRlc() + rlc.r_enabled = bool(r) + rlc.l_enabled = bool(l) + rlc.c_enabled = bool(c) + rlc.r = GrpcValue(r) + rlc.l = GrpcValue(l) + rlc.c = GrpcValue(c) + pos_terminal.rlc_boundary_parameters = rlc + pos_terminal.name = name + + else: + self._pedb.logger.error("No valid source type specified.") + return False + return pos_terminal.name + + def create_voltage_source_on_pin(self, pos_pin, neg_pin, voltage_value=0, phase_value=0, source_name=None): + """Create a voltage source. + + Parameters + ---------- + pos_pin : Object + Positive Pin. + neg_pin : Object + Negative Pin. + voltage_value : float, optional + Value for the voltage. The default is ``3.3``. + phase_value : optional + Value for the phase. The default is ``0``. + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + Source Name. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> pins = edbapp.components.get_pin_from_component("U2A5") + >>> edbapp.excitations.create_voltage_source_on_pin(pins[0], pins[1], 50, "source_name") + """ + + if not source_name: + source_name = ( + f"VSource_{pos_pin.component.name}_{pos_pin.net_name}_{neg_pin.component.name}_{neg_pin.net_name}" + ) + return self._create_terminal_on_pins( + positive_pin=pos_pin, + negative_pin=neg_pin, + name=source_name, + magnitude=voltage_value, + phase=phase_value, + source_type="voltage_source", + ) + + def create_current_source_on_pin(self, pos_pin, neg_pin, current_value=0, phase_value=0, source_name=None): + """Create a voltage source. + + Parameters + ---------- + pos_pin : Object + Positive Pin. + neg_pin : Object + Negative Pin. + voltage_value : float, optional + Value for the voltage. The default is ``3.3``. + phase_value : optional + Value for the phase. The default is ``0``. + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + Source Name. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> pins = edbapp.components.get_pin_from_component("U2A5") + >>> edbapp.excitations.create_voltage_source_on_pin(pins[0], pins[1], 50, "source_name") + """ + + if not source_name: + source_name = ( + f"VSource_{pos_pin.component.name}_{pos_pin.net_name}_{neg_pin.component.name}_{neg_pin.net_name}" + ) + return self._create_terminal_on_pins( + positive_pin=pos_pin, + negative_pin=neg_pin, + name=source_name, + magnitude=current_value, + phase=phase_value, + source_type="current_source", + ) + + def create_resistor_on_pin(self, pos_pin, neg_pin, rvalue=1, resistor_name=""): + """Create a Resistor boundary between two given pins.. + + Parameters + ---------- + pos_pin : Object + Positive Pin. + neg_pin : Object + Negative Pin. + rvalue : float, optional + Resistance value. The default is ``1``. + resistor_name : str, optional + Name of the resistor. The default is ``""``. + + Returns + ------- + str + Name of the resistor. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> pins =edbapp.components.get_pin_from_component("U2A5") + >>> edbapp.excitation.create_resistor_on_pin(pins[0], pins[1],50,"res_name") + """ + if not resistor_name: + resistor_name = ( + f"Res_{pos_pin.component.name}_{pos_pin.net.name}_{neg_pin.component.name}_{neg_pin.net.name}" + ) + return self._create_terminal_on_pins( + positive_pin=pos_pin, negative_pin=neg_pin, name=resistor_name, source_type="rlc", r=rvalue + ) + + def create_circuit_port_on_net( + self, + positive_component_name, + positive_net_name, + negative_component_name=None, + negative_net_name=None, + impedance=50, + port_name="", + ): + """Create a circuit port on a NET. + + It groups all pins belonging to the specified net and then applies the port on PinGroups. + + Parameters + ---------- + positive_component_name : str + Name of the positive component. + positive_net_name : str + Name of the positive net. + negative_component_name : str, optional + Name of the negative component. The default is ``None``, in which case the name of + the positive net is assigned. + negative_net_name : str, optional + Name of the negative net name. The default is ``None`` which will look for GND Nets. + impedance_value : float, optional + Port impedance value. The default is ``50``. + port_name : str, optional + Name of the port. The default is ``""``. + + Returns + ------- + str + The name of the port. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> edbapp.excitations.create_circuit_port_on_net("U2A5", "V1P5_S3", "U2A5", "GND", 50, "port_name") + """ + if not negative_component_name: + negative_component_name = positive_component_name + if not negative_net_name: + negative_net_name = self._check_gnd(negative_component_name) + if not port_name: + port_name = ( + f"Port_{positive_component_name}_{positive_net_name}_{negative_component_name}_{negative_net_name}" + ) + positive_pins = [] + for pin in list(self._pedb.components.instances[positive_component_name].pins.values()): + if pin and not pin.net.is_null: + if pin.net_name == positive_net_name: + positive_pins.append(pin) + if not positive_pins: + self._pedb.logger.error( + f"No positive pins found component {positive_component_name} net {positive_net_name}" + ) + return False + negative_pins = [] + for pin in list(self._pedb.components.instances[negative_component_name].pins.values()): + if pin and not pin.net.is_null: + if pin.net_name == negative_net_name: + negative_pins.append(pin) + if not negative_pins: + self._pedb.logger.error( + f"No negative pins found component {negative_component_name} net {negative_net_name}" + ) + return False + + return self.create_pin_group_terminal( + positive_pins=positive_pins, + negatives_pins=negative_pins, + name=port_name, + impedance=impedance, + source_type="circuit_port", + ) + + def create_pin_group_terminal( + self, + positive_pins, + negatives_pins, + name=None, + impedance=50, + source_type="circuit_port", + magnitude=1.0, + phase=0, + r=0.0, + l=0.0, + c=0.0, + ): + """Create a pin group terminal. + + Parameters + ---------- + positive_pins : positive pins used. + :class: `PadstackInstance` or List[:class: ´PadstackInstance´] + negatives_pins : negative pins used. + :class: `PadstackInstance` or List[:class: ´PadstackInstance´] + impedance : float, int or str + Terminal impedance. Default value is `50` Ohms. + source_type : str + Source type assigned on terminal. Supported values : `"circuit_port"`, `"lumped_port"`, `"current_source"`, + `"voltage_source"`, `"rlc"`, `"dc_terminal"`. Default value is `"circuit_port"`. + name : str, optional + Source name. + magnitude : float, int or str, optional + source magnitude. + phase : float, int or str, optional + phase magnitude. + r : float, optional + Resistor value. + l : float, optional + Inductor value. + c : float, optional + Capacitor value. + """ + if isinstance(positive_pins, PadstackInstance): + positive_pins = [positive_pins] + if negatives_pins: + if isinstance(negatives_pins, PadstackInstance): + negatives_pins = [negatives_pins] + if not name: + name = ( + f"Port_{positive_pins[0].component.name}_{positive_pins[0].net.name}_{positive_pins[0].name}_" + f"{negatives_pins.name}" + ) + if name in [i.name for i in self._pedb.active_layout.terminals]: + name = generate_unique_name(name, n=3) + self._logger.warning(f"Port already exists with same name. Renaming to {name}") + + pos_pin_group = self._pedb.components.create_pingroup_from_pins(positive_pins) + pos_pingroup_terminal = PinGroupTerminal.create( + layout=self._pedb.active_layout, + name=name, + pin_group=pos_pin_group, + net=positive_pins[0].net, + is_ref=False, + ) + if not source_type == "dc_terminal": + neg_pin_group = self._pedb.components.create_pingroup_from_pins(negatives_pins) + neg_pingroup_terminal = PinGroupTerminal.create( + layout=self._pedb.active_layout, + name=f"{name}_ref", + pin_group=neg_pin_group, + net=negatives_pins[0].net, + is_ref=False, + ) + if source_type in ["circuit_port", "lumped_port"]: + pos_pingroup_terminal.boundary_type = GrpcBoundaryType.PORT + pos_pingroup_terminal.impedance = GrpcValue(impedance) + if len(positive_pins) > 1 and len(negatives_pins) > 1: + if source_type == "lumped_port": + source_type = "circuit_port" + if source_type == "circuit_port": + pos_pingroup_terminal.is_circuit_port = True + neg_pingroup_terminal.is_circuit_port = True + else: + pos_pingroup_terminal.is_circuit_port = False + neg_pingroup_terminal.is_circuit_port = False + pos_pingroup_terminal.reference_terminal = neg_pingroup_terminal + pos_pingroup_terminal.name = name + + elif source_type == "current_source": + pos_pingroup_terminal.boundary_type = GrpcBoundaryType.CURRENT_SOURCE + neg_pingroup_terminal.boundary_type = GrpcBoundaryType.CURRENT_SOURCE + pos_pingroup_terminal.source_amplitude = GrpcValue(magnitude) + pos_pingroup_terminal.source_phase = GrpcValue(phase) + pos_pingroup_terminal.reference_terminal = neg_pingroup_terminal + pos_pingroup_terminal.name = name + + elif source_type == "voltage_source": + pos_pingroup_terminal.boundary_type = GrpcBoundaryType.VOLTAGE_SOURCE + neg_pingroup_terminal.boundary_type = GrpcBoundaryType.VOLTAGE_SOURCE + pos_pingroup_terminal.source_amplitude = GrpcValue(magnitude) + pos_pingroup_terminal.source_phase = GrpcValue(phase) + pos_pingroup_terminal.reference_terminal = neg_pingroup_terminal + pos_pingroup_terminal.name = name + + elif source_type == "rlc": + pos_pingroup_terminal.boundary_type = GrpcBoundaryType.RLC + neg_pingroup_terminal.boundary_type = GrpcBoundaryType.RLC + pos_pingroup_terminal.reference_terminal = neg_pingroup_terminal + Rlc = GrpcRlc() + Rlc.r_enabled = bool(r) + Rlc.l_enabled = bool(l) + Rlc.c_enabled = bool(c) + Rlc.r = GrpcValue(r) + Rlc.l = GrpcValue(l) + Rlc.c = GrpcValue(c) + pos_pingroup_terminal.rlc_boundary_parameters = Rlc + + elif source_type == "dc_terminal": + pos_pingroup_terminal.boundary_type = GrpcBoundaryType.DC_TERMINAL + else: + pass + return pos_pingroup_terminal.name + + def _check_gnd(self, component_name): + negative_net_name = None + if self._pedb.nets.is_net_in_component(component_name, "GND"): + negative_net_name = "GND" + elif self._pedb.nets.is_net_in_component(component_name, "PGND"): + negative_net_name = "PGND" + elif self._pedb.nets.is_net_in_component(component_name, "AGND"): + negative_net_name = "AGND" + elif self._pedb.nets.is_net_in_component(component_name, "DGND"): + negative_net_name = "DGND" + if not negative_net_name: + raise ValueError("No GND, PGND, AGND, DGND found. Please setup the negative net name manually.") + return negative_net_name + + def create_voltage_source_on_net( + self, + positive_component_name, + positive_net_name, + negative_component_name=None, + negative_net_name=None, + voltage_value=3.3, + phase_value=0, + source_name=None, + ): + """Create a voltage source. + + Parameters + ---------- + positive_component_name : str + Name of the positive component. + positive_net_name : str + Name of the positive net. + negative_component_name : str, optional + Name of the negative component. The default is ``None``, in which case the name of + the positive net is assigned. + negative_net_name : str, optional + Name of the negative net name. The default is ``None`` which will look for GND Nets. + voltage_value : float, optional + Value for the voltage. The default is ``3.3``. + phase_value : optional + Value for the phase. The default is ``0``. + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + The name of the source. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> edb.excitations.create_voltage_source_on_net("U2A5","V1P5_S3","U2A5","GND",3.3,0,"source_name") + """ + if not negative_component_name: + negative_component_name = positive_component_name + if not negative_net_name: + negative_net_name = self._check_gnd(negative_component_name) + pos_node_pins = self._pedb.components.get_pin_from_component(positive_component_name, positive_net_name) + neg_node_pins = self._pedb.components.get_pin_from_component(negative_component_name, negative_net_name) + + if not source_name: + source_name = ( + f"Vsource_{positive_component_name}_{positive_net_name}_" + f"{negative_component_name}_{negative_net_name}" + ) + return self.create_pin_group_terminal( + positive_pins=pos_node_pins, + negatives_pins=neg_node_pins, + name=source_name, + magnitude=voltage_value, + phase=phase_value, + impedance=1e-6, + source_type="voltage_source", + ) + + def create_current_source_on_net( + self, + positive_component_name, + positive_net_name, + negative_component_name=None, + negative_net_name=None, + current_value=3.3, + phase_value=0, + source_name=None, + ): + """Create a voltage source. + + Parameters + ---------- + positive_component_name : str + Name of the positive component. + positive_net_name : str + Name of the positive net. + negative_component_name : str, optional + Name of the negative component. The default is ``None``, in which case the name of + the positive net is assigned. + negative_net_name : str, optional + Name of the negative net name. The default is ``None`` which will look for GND Nets. + voltage_value : float, optional + Value for the voltage. The default is ``3.3``. + phase_value : optional + Value for the phase. The default is ``0``. + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + The name of the source. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> edb.excitations.create_voltage_source_on_net("U2A5","V1P5_S3","U2A5","GND",3.3,0,"source_name") + """ + if not negative_component_name: + negative_component_name = positive_component_name + if not negative_net_name: + negative_net_name = self._check_gnd(negative_component_name) + pos_node_pins = self._pedb.components.get_pin_from_component(positive_component_name, positive_net_name) + neg_node_pins = self._pedb.components.get_pin_from_component(negative_component_name, negative_net_name) + + if not source_name: + source_name = ( + f"Vsource_{positive_component_name}_{positive_net_name}_" + f"{negative_component_name}_{negative_net_name}" + ) + return self.create_pin_group_terminal( + positive_pins=pos_node_pins, + negatives_pins=neg_node_pins, + name=source_name, + magnitude=current_value, + phase=phase_value, + impedance=1e6, + source_type="current_source", + ) + + def create_coax_port_on_component(self, ref_des_list, net_list, delete_existing_terminal=False): + """Create a coaxial port on a component or component list on a net or net list. + The name of the new coaxial port is automatically assigned. + + Parameters + ---------- + ref_des_list : list, str + List of one or more reference designators. + + net_list : list, str + List of one or more nets. + + delete_existing_terminal : bool + Delete existing terminal with same name if exists. + Port naming convention is `ref_des`_`pin.net.name`_`pin.name` + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + coax = [] + if not isinstance(ref_des_list, list): + ref_des_list = [ref_des_list] + if not isinstance(net_list, list): + net_list = [net_list] + for ref in ref_des_list: + for _, pin in self._pedb.components.instances[ref].pins.items(): + try: # trying due to grpc crash when no net is defined on pin. + try: + pin_net = pin.net + except: + pin_net = None + if pin_net and pin.net.is_null: + self._logger.warning(f"Pin {pin.id} has no net defined") + elif pin.net.name in net_list: + pin.is_pin = True + port_name = f"{ref}_{pin.net.name}_{pin.name}" + if self.check_before_terminal_assignement( + connectable=pin, delete_existing_terminal=delete_existing_terminal + ): + top_layer = pin.get_layer_range()[0] + term = PadstackInstanceTerminal.create( + layout=pin.layout, + name=port_name, + padstack_instance=pin, + layer=top_layer, + net=pin.net, + is_ref=False, + ) + if not term.is_null: + coax.append(port_name) + except RuntimeError as error: + self._logger.error(error) + return coax + + def check_before_terminal_assignement(self, connectable, delete_existing_terminal=False): + if not connectable: + return False + existing_terminals = [term for term in self._pedb.active_layout.terminals if term.id == connectable.id] + if existing_terminals: + if not delete_existing_terminal: + self._pedb.logger.error( + f"Terminal {connectable.name} already defined in design, please make sure to have unique name." + ) + return False + else: + if isinstance(connectable, PadstackInstanceTerminal): + self._pedb.logger.error( + f"Terminal {connectable.name} already defined, check status on bug " + f"https://github.com/ansys/pyedb-core/issues/429" + ) + return False + else: + return True + + def create_differential_wave_port( + self, + positive_primitive_id, + positive_points_on_edge, + negative_primitive_id, + negative_points_on_edge, + port_name=None, + horizontal_extent_factor=5, + vertical_extent_factor=3, + pec_launch_width="0.01mm", + ): + """Create a differential wave port. + + Parameters + ---------- + positive_primitive_id : int, EDBPrimitives + Primitive ID of the positive terminal. + positive_points_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be close to the target edge but not on the two + ends of the edge. + negative_primitive_id : int, EDBPrimitives + Primitive ID of the negative terminal. + negative_points_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be close to the target edge but not on the two + ends of the edge. + port_name : str, optional + Name of the port. The default is ``None``. + horizontal_extent_factor : int, float, optional + Horizontal extent factor. The default value is ``5``. + vertical_extent_factor : int, float, optional + Vertical extent factor. The default value is ``3``. + pec_launch_width : str, optional + Launch Width of PEC. The default value is ``"0.01mm"``. + + Returns + ------- + tuple + The tuple contains: (port_name, pyedb.dotnet.database.edb_data.sources.ExcitationDifferential). + + Examples + -------- + >>> edb.hfss.create_differential_wave_port(0, ["-50mm", "-0mm"], 1, ["-50mm", "-0.2mm"]) + """ + if not port_name: + port_name = generate_unique_name("diff") + + if isinstance(positive_primitive_id, Primitive): + positive_primitive_id = positive_primitive_id.id + + if isinstance(negative_primitive_id, Primitive): + negative_primitive_id = negative_primitive_id.id + + _, pos_term = self.create_wave_port( + positive_primitive_id, + positive_points_on_edge, + horizontal_extent_factor=horizontal_extent_factor, + vertical_extent_factor=vertical_extent_factor, + pec_launch_width=pec_launch_width, + ) + _, neg_term = self.create_wave_port( + negative_primitive_id, + negative_points_on_edge, + horizontal_extent_factor=horizontal_extent_factor, + vertical_extent_factor=vertical_extent_factor, + pec_launch_width=pec_launch_width, + ) + edb_list = [pos_term, neg_term] + + boundle_terminal = BundleTerminal.create(edb_list) + boundle_terminal.name = port_name + bundle_term = boundle_terminal.terminals + bundle_term[0].name = port_name + ":T1" + bundle_term[1].mame = port_name + ":T2" + return port_name, boundle_terminal + + def create_wave_port( + self, + prim_id, + point_on_edge, + port_name=None, + impedance=50, + horizontal_extent_factor=5, + vertical_extent_factor=3, + pec_launch_width="0.01mm", + ): + """Create a wave port. + + Parameters + ---------- + prim_id : int, Primitive + Primitive ID. + point_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be on the target edge but not on the two + ends of the edge. + port_name : str, optional + Name of the port. The default is ``None``. + impedance : int, float, optional + Impedance of the port. The default value is ``50``. + horizontal_extent_factor : int, float, optional + Horizontal extent factor. The default value is ``5``. + vertical_extent_factor : int, float, optional + Vertical extent factor. The default value is ``3``. + pec_launch_width : str, optional + Launch Width of PEC. The default value is ``"0.01mm"``. + + Returns + ------- + tuple + The tuple contains: (Port name, pyedb.dotnet.database.edb_data.sources.Excitation). + + Examples + -------- + >>> edb.excitations.create_wave_port(0, ["-50mm", "-0mm"]) + """ + if not port_name: + port_name = generate_unique_name("Terminal_") + + if isinstance(prim_id, Primitive): + prim_id = prim_id.id + pos_edge_term = self._create_edge_terminal(prim_id, point_on_edge, port_name) + pos_edge_term.impedance = GrpcValue(impedance) + wave_port = WavePort(self._pedb, pos_edge_term) + wave_port.horizontal_extent_factor = horizontal_extent_factor + wave_port.vertical_extent_factor = vertical_extent_factor + wave_port.pec_launch_width = pec_launch_width + wave_port.hfss_type = "Wave" + wave_port.do_renormalize = True + if pos_edge_term: + return port_name, wave_port + else: + return False + + def create_edge_port_vertical( + self, + prim_id, + point_on_edge, + port_name=None, + impedance=50, + reference_layer=None, + hfss_type="Gap", + horizontal_extent_factor=5, + vertical_extent_factor=3, + pec_launch_width="0.01mm", + ): + """Create a vertical edge port. + + Parameters + ---------- + prim_id : int + Primitive ID. + point_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be on the target edge but not on the two + ends of the edge. + port_name : str, optional + Name of the port. The default is ``None``. + impedance : int, float, optional + Impedance of the port. The default value is ``50``. + reference_layer : str, optional + Reference layer of the port. The default is ``None``. + hfss_type : str, optional + Type of the port. The default value is ``"Gap"``. Options are ``"Gap"``, ``"Wave"``. + horizontal_extent_factor : int, float, optional + Horizontal extent factor. The default value is ``5``. + vertical_extent_factor : int, float, optional + Vertical extent factor. The default value is ``3``. + radial_extent_factor : int, float, optional + Radial extent factor. The default value is ``0``. + pec_launch_width : str, optional + Launch Width of PEC. The default value is ``"0.01mm"``. + + Returns + ------- + str + Port name. + """ + if not port_name: + port_name = generate_unique_name("Terminal_") + pos_edge_term = self._create_edge_terminal(prim_id, point_on_edge, port_name) + pos_edge_term.impedance = GrpcValue(impedance) + if reference_layer: + reference_layer = self._pedb.stackup.signal_layers[reference_layer] + pos_edge_term.reference_layer = reference_layer + + prop = ", ".join( + [ + f"HFSS('HFSS Type'='{hfss_type}'", + " Orientation='Vertical'", + " 'Layer Alignment'='Upper'", + f" 'Horizontal Extent Factor'='{horizontal_extent_factor}'", + f" 'Vertical Extent Factor'='{vertical_extent_factor}'", + f" 'PEC Launch Width'='{pec_launch_width}')", + ] + ) + pos_edge_term.set_product_solver_option( + GrpcProductIdType.DESIGNER, + "HFSS", + prop, + ) + if not pos_edge_term.is_null: + return pos_edge_term + else: + return False + + def create_edge_port_horizontal( + self, + prim_id, + point_on_edge, + ref_prim_id=None, + point_on_ref_edge=None, + port_name=None, + impedance=50, + layer_alignment="Upper", + ): + """Create a horizontal edge port. + + Parameters + ---------- + prim_id : int + Primitive ID. + point_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be on the target edge but not on the two + ends of the edge. + ref_prim_id : int, optional + Reference primitive ID. The default is ``None``. + point_on_ref_edge : list, optional + Coordinate of the point to define the reference edge + terminal. The point must be on the target edge but not + on the two ends of the edge. The default is ``None``. + port_name : str, optional + Name of the port. The default is ``None``. + impedance : int, float, optional + Impedance of the port. The default value is ``50``. + layer_alignment : str, optional + Layer alignment. The default value is ``Upper``. Options are ``"Upper"``, ``"Lower"``. + + Returns + ------- + str + Name of the port. + """ + pos_edge_term = self._create_edge_terminal(prim_id, point_on_edge, port_name) + neg_edge_term = self._create_edge_terminal(ref_prim_id, point_on_ref_edge, port_name + "_ref", is_ref=True) + + pos_edge_term.impedance = GrpcValue(impedance) + pos_edge_term.reference_terminal = neg_edge_term + if not layer_alignment == "Upper": + layer_alignment = "Lower" + pos_edge_term.set_product_solver_option( + GrpcProductIdType.DESIGNER, + "HFSS", + f"HFSS('HFSS Type'='Gap(coax)', Orientation='Horizontal', 'Layer Alignment'='{layer_alignment}')", + ) + if pos_edge_term: + return port_name + else: + return False + + def create_lumped_port_on_net( + self, nets=None, reference_layer=None, return_points_only=False, digit_resolution=6, at_bounding_box=True + ): + """Create an edge port on nets. This command looks for traces and polygons on the + nets and tries to assign vertical lumped port. + + Parameters + ---------- + nets : list, optional + List of nets, str or Edb net. + + reference_layer : str, Edb layer. + Name or Edb layer object. + + return_points_only : bool, optional + Use this boolean when you want to return only the points from the edges and not creating ports. Default + value is ``False``. + + digit_resolution : int, optional + The number of digits carried for the edge location accuracy. The default value is ``6``. + + at_bounding_box : bool + When ``True`` will keep the edges from traces at the layout bounding box location. This is recommended when + a cutout has been performed before and lumped ports have to be created on ending traces. Default value is + ``True``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if isinstance(nets, str): + nets = [self._pedb.nets.signal[nets]] + if isinstance(nets, Net): + nets = [nets] + nets = [self._pedb.nets.signal[net] for net in nets if isinstance(net, str)] + port_created = False + if nets: + edges_pts = [] + if isinstance(reference_layer, str): + try: + reference_layer = self._pedb.stackup.signal_layers[reference_layer] + except: + raise Exception(f"Failed to get the layer {reference_layer}") + if not isinstance(reference_layer, StackupLayer): + return False + layout_bbox = self._pedb.get_conformal_polygon_from_netlist(self._pedb.nets.netlist) + layout_extent_segments = [pt for pt in list(layout_bbox.arc_data) if pt.is_segment] + first_pt = layout_extent_segments[0] + layout_extent_points = [ + [first_pt.start.x.value, first_pt.end.x.value], + [first_pt.Start.y.value, first_pt.end.y.value], + ] + for segment in layout_extent_segments[1:]: + end_point = (segment.end.x.value, segment.end.y.value) + layout_extent_points[0].append(end_point[0]) + layout_extent_points[1].append(end_point[1]) + for net in nets: + net_primitives = self._pedb.nets[net.name].primitives + net_paths = [pp for pp in net_primitives if pp.type == "Path"] + for path in net_paths: + trace_path_pts = list(path.center_line.Points) + port_name = f"{net.name}_{path.id}" + for pt in trace_path_pts: + _pt = [ + round(pt.x.value, digit_resolution), + round(pt.y.value, digit_resolution), + ] + if at_bounding_box: + if GeometryOperators.point_in_polygon(_pt, layout_extent_points) == 0: + if return_points_only: + edges_pts.append(_pt) + else: + term = self._create_edge_terminal(path.id, pt, port_name) # pragma no cover + term.reference_layer = reference_layer + port_created = True + else: + if return_points_only: # pragma: no cover + edges_pts.append(_pt) + else: + term = self._create_edge_terminal(path.id, pt, port_name) + term.reference_layer = reference_layer + port_created = True + net_poly = [pp for pp in net_primitives if pp.type == "Polygon"] + for poly in net_poly: + poly_segment = [aa for aa in poly.arcs if aa.is_segment] + for segment in poly_segment: + if ( + GeometryOperators.point_in_polygon( + [segment.mid_point.x.value, segment.mid_point.y.value], layout_extent_points + ) + == 0 + ): + if return_points_only: + edges_pts.append(segment.mid_point) + else: + port_name = f"{net.name}_{poly.id}" + term = self._create_edge_terminal( + poly.id, segment.mid_point, port_name + ) # pragma no cover + term.set_reference_layer = reference_layer + port_created = True + if return_points_only: + return edges_pts + return port_created + + def create_vertical_circuit_port_on_clipped_traces(self, nets=None, reference_net=None, user_defined_extent=None): + """Create an edge port on clipped signal traces. + + Parameters + ---------- + nets : list, optional + String of one net or EDB net or a list of multiple nets or EDB nets. + + reference_net : str, Edb net. + Name or EDB reference net. + + user_defined_extent : [x, y], EDB PolygonData + Use this point list or PolygonData object to check if ports are at this polygon border. + + Returns + ------- + [[str]] + Nested list of str, with net name as first value, X value for point at border, Y value for point at border, + and terminal name. + """ + if not isinstance(nets, list): + if isinstance(nets, str): + nets = list(self._pedb.nets.signal.values()) + else: + nets = [self._pedb.nets.signal[net] for net in nets] + if nets: + if isinstance(reference_net, str): + reference_net = self._pedb.nets.nets[reference_net] + if not reference_net: + self._logger.error("No reference net provided for creating port") + return False + if user_defined_extent: + if isinstance(user_defined_extent, GrpcPolygonData): + _points = [pt for pt in list(user_defined_extent.points)] + _x = [] + _y = [] + for pt in _points: + if pt.x.value < 1e100 and pt.y.value < 1e100: + _x.append(pt.x.value) + _y.append(pt.y.value) + user_defined_extent = [_x, _y] + terminal_info = [] + for net in nets: + net_polygons = [prim for prim in self._pedb.modeler.primitives if prim.type in ["polygon", "rectangle"]] + for poly in net_polygons: + mid_points = [[arc.midpoint.x.value, arc.midpoint.y.value] for arc in poly.arcs] + for mid_point in mid_points: + if GeometryOperators.point_in_polygon(mid_point, user_defined_extent) == 0: + port_name = generate_unique_name(f"{poly.net_name}_{poly.id}") + term = self._create_edge_terminal(poly.id, mid_point, port_name) # pragma no cover + if not term.is_null: + self._logger.info(f"Terminal {term.name} created") + term.is_circuit_port = True + terminal_info.append([poly.net_name, mid_point[0], mid_point[1], term.name]) + mid_pt_data = GrpcPointData(mid_point) + ref_prim = [ + prim + for prim in reference_net.primitives + if prim.polygon_data.point_in_polygon(mid_pt_data) + ] + if not ref_prim: + self._logger.warning("no reference primitive found, trying to extend scanning area") + scanning_zone = [ + (mid_point[0] - mid_point[0] * 1e-3, mid_point[1] - mid_point[1] * 1e-3), + (mid_point[0] - mid_point[0] * 1e-3, mid_point[1] + mid_point[1] * 1e-3), + (mid_point[0] + mid_point[0] * 1e-3, mid_point[1] + mid_point[1] * 1e-3), + (mid_point[0] + mid_point[0] * 1e-3, mid_point[1] - mid_point[1] * 1e-3), + ] + for new_point in scanning_zone: + mid_pt_data = GrpcPointData(new_point) + ref_prim = [ + prim + for prim in reference_net.primitives + if prim.polygon_data.point_in_polygon(mid_pt_data) + ] + if ref_prim: + self._logger.info("Reference primitive found") + break + if not ref_prim: + self._logger.error("Failed to collect valid reference primitives for terminal") + if ref_prim: + reference_layer = ref_prim[0].layer + if term.reference_layer == reference_layer: + self._logger.info(f"Port {port_name} created") + return terminal_info + return False + + def create_bundle_wave_port( + self, + primitives_id, + points_on_edge, + port_name=None, + horizontal_extent_factor=5, + vertical_extent_factor=3, + pec_launch_width="0.01mm", + ): + """Create a bundle wave port. + + Parameters + ---------- + primitives_id : list + Primitive ID of the positive terminal. + points_on_edge : list + Coordinate of the point to define the edge terminal. + The point must be close to the target edge but not on the two + ends of the edge. + port_name : str, optional + Name of the port. The default is ``None``. + horizontal_extent_factor : int, float, optional + Horizontal extent factor. The default value is ``5``. + vertical_extent_factor : int, float, optional + Vertical extent factor. The default value is ``3``. + pec_launch_width : str, optional + Launch Width of PEC. The default value is ``"0.01mm"``. + + Returns + ------- + tuple + The tuple contains: (port_name, pyedb.egacy.database.edb_data.sources.ExcitationDifferential). + + Examples + -------- + >>> edb.excitations.create_bundle_wave_port(0, ["-50mm", "-0mm"], 1, ["-50mm", "-0.2mm"]) + """ + if not port_name: + port_name = generate_unique_name("bundle_port") + + if isinstance(primitives_id[0], Primitive): + primitives_id = [i.id for i in primitives_id] + + terminals = [] + _port_name = port_name + for p_id, loc in list(zip(primitives_id, points_on_edge)): + _, term = self.create_wave_port( + p_id, + loc, + port_name=_port_name, + horizontal_extent_factor=horizontal_extent_factor, + vertical_extent_factor=vertical_extent_factor, + pec_launch_width=pec_launch_width, + ) + _port_name = None + terminals.append(term) + + _edb_bundle_terminal = BundleTerminal.create(terminals) + return port_name, BundleWavePort(self._pedb, _edb_bundle_terminal) + + def create_hfss_ports_on_padstack(self, pinpos, portname=None): + """Create an HFSS port on a padstack. + + Parameters + ---------- + pinpos : + Position of the pin. + + portname : str, optional + Name of the port. The default is ``None``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + top_layer, bottom_layer = pinpos.get_layer_range() + + if not portname: + portname = generate_unique_name("Port_" + pinpos.net.name) + edbpointTerm_pos = PadstackInstanceTerminal.create( + padstack_instance=pinpos, name=portname, layer=top_layer, is_ref=False + ) + if edbpointTerm_pos: + return True + else: + return False + + def get_ports_number(self): + """Return the total number of excitation ports in a layout. + + Parameters + ---------- + None + + Returns + ------- + int + Number of ports. + + """ + terms = [term for term in self._pedb.layout.terminals] + return len([i for i in terms if not i.is_reference_terminal]) + + def create_rlc_boundary_on_pins(self, positive_pin=None, negative_pin=None, rvalue=0.0, lvalue=0.0, cvalue=0.0): + """Create hfss rlc boundary on pins. + + Parameters + ---------- + positive_pin : Positive pin. + Edb.Cell.Primitive.PadstackInstance + + negative_pin : Negative pin. + Edb.Cell.Primitive.PadstackInstance + + rvalue : Resistance value + + lvalue : Inductance value + + cvalue . Capacitance value. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + + if positive_pin and negative_pin: + positive_pin_term = positive_pin.get_terminal(create_new_terminal=True) + negative_pin_term = negative_pin.get_terminal(create_new_terminal=True) + positive_pin_term.boundary_type = GrpcBoundaryType.RLC + negative_pin_term.boundary_type = GrpcBoundaryType.RLC + rlc = GrpcRlc() + rlc.is_parallel = True + rlc.r_enabled = True + rlc.l_enabled = True + rlc.c_enabled = True + rlc.r = GrpcValue(rvalue) + rlc.l = GrpcValue(lvalue) + rlc.c = GrpcValue(cvalue) + positive_pin_term.rlc_boundary_parameters = rlc + term_name = f"{positive_pin.component.name}_{positive_pin.net.name}_{positive_pin.name}" + positive_pin_term.name = term_name + negative_pin_term.name = f"{term_name}_ref" + positive_pin_term.reference_terminal = negative_pin_term + return positive_pin_term + return False + + def create_edge_port_on_polygon( + self, + polygon=None, + reference_polygon=None, + terminal_point=None, + reference_point=None, + reference_layer=None, + port_name=None, + port_impedance=50.0, + force_circuit_port=False, + ): + """Create lumped port between two edges from two different polygons. Can also create a vertical port when + the reference layer name is only provided. When a port is created between two edge from two polygons which don't + belong to the same layer, a circuit port will be automatically created instead of lumped. To enforce the circuit + port instead of lumped,use the boolean force_circuit_port. + + Parameters + ---------- + polygon : The EDB polygon object used to assign the port. + Edb.Cell.Primitive.Polygon object. + + reference_polygon : The EDB polygon object used to define the port reference. + Edb.Cell.Primitive.Polygon object. + + terminal_point : The coordinate of the point to define the edge terminal of the port. This point must be + located on the edge of the polygon where the port has to be placed. For instance taking the middle point + of an edge is a good practice but any point of the edge should be valid. Taking a corner might cause unwanted + port location. + list[float, float] with values provided in meter. + + reference_point : same as terminal_point but used for defining the reference location on the edge. + list[float, float] with values provided in meter. + + reference_layer : Name used to define port reference for vertical ports. + str the layer name. + + port_name : Name of the port. + str. + + port_impedance : port impedance value. Default value is 50 Ohms. + float, impedance value. + + force_circuit_port ; used to force circuit port creation instead of lumped. Works for vertical and coplanar + ports. + + Examples + -------- + + >>> edb_path = path_to_edb + >>> edb = Edb(edb_path) + >>> poly_list = [poly for poly in list(edb.layout.primitives) if poly.GetPrimitiveType() == 2] + >>> port_poly = [poly for poly in poly_list if poly.GetId() == 17][0] + >>> ref_poly = [poly for poly in poly_list if poly.GetId() == 19][0] + >>> port_location = [-65e-3, -13e-3] + >>> ref_location = [-63e-3, -13e-3] + >>> edb.hfss.create_edge_port_on_polygon(polygon=port_poly, reference_polygon=ref_poly, + >>> terminal_point=port_location, reference_point=ref_location) + + """ + if not polygon: + self._logger.error("No polygon provided for port {} creation".format(port_name)) + return False + if reference_layer: + reference_layer = self._pedb.stackup.signal_layers[reference_layer] + if not reference_layer: + self._logger.error("Specified layer for port {} creation was not found".format(port_name)) + if not isinstance(terminal_point, list): + self._logger.error("Terminal point must be a list of float with providing the point location in meter") + return False + terminal_point = GrpcPointData(terminal_point) + if reference_point and isinstance(reference_point, list): + reference_point = GrpcPointData(reference_point) + if not port_name: + port_name = generate_unique_name("Port_") + edge = GrpcPrimitiveEdge.create(polygon, terminal_point) + edges = [edge] + edge_term = GrpcEdgeTerminal.create( + layout=polygon.layout, edges=edges, net=polygon.net, name=port_name, is_ref=False + ) + if force_circuit_port: + edge_term.is_circuit_port = True + else: + edge_term.is_circuit_port = False + + if port_impedance: + edge_term.impedance = GrpcValue(port_impedance) + edge_term.name = port_name + if reference_polygon and reference_point: + ref_edge = GrpcPrimitiveEdge.create(reference_polygon, reference_point) + ref_edges = [ref_edge] + ref_edge_term = GrpcEdgeTerminal.create( + layout=reference_polygon.layout, + name=port_name + "_ref", + edges=ref_edges, + net=reference_polygon.net, + is_ref=True, + ) + if reference_layer: + ref_edge_term.reference_layer = reference_layer + if force_circuit_port: + ref_edge_term.is_circuit_port = True + else: + ref_edge_term.is_circuit_port = False + + if port_impedance: + ref_edge_term.impedance = GrpcValue(port_impedance) + edge_term.reference_terminal = ref_edge_term + return True + + def create_port_between_pin_and_layer( + self, component_name=None, pins_name=None, layer_name=None, reference_net=None, impedance=50.0 + ): + """Create circuit port between pin and a reference layer. + + Parameters + ---------- + component_name : str + Component name. The default is ``None``. + pins_name : str + Pin name or list of pin names. The default is ``None``. + layer_name : str + Layer name. The default is ``None``. + reference_net : str + Reference net name. The default is ``None``. + impedance : float, optional + Port impedance. The default is ``50.0`` in ohms. + + Returns + ------- + PadstackInstanceTerminal + Created terminal. + + """ + if not pins_name: + pins_name = [] + if pins_name: + if not isinstance(pins_name, list): # pragma no cover + pins_name = [pins_name] + if not reference_net: + self._logger.info("no reference net provided, searching net {} instead.".format(layer_name)) + reference_net = self._pedb.nets.get_net_by_name(layer_name) + if not reference_net: # pragma no cover + self._logger.error("reference net {} not found.".format(layer_name)) + return False + else: + if not isinstance(reference_net, Net): # pragma no cover + reference_net = self._pedb.nets.get_net_by_name(reference_net) + if not reference_net: + self._logger.error("Net {} not found".format(reference_net)) + return False + terms = [] + pins = self._pedb.components.instances[component_name].pins + for __pin in pins_name: + if __pin in pins: + pin = pins[__pin] + term_name = f"{pin.component.name}_{pin.net.name}_{pin.component}" + start_layer, stop_layer = pin.get_layer_range() + if start_layer: + positive_terminal = PadstackInstanceTerminal.create( + layout=pin.layout, net=pin.net, padstack_instance=pin, name=term_name, layer=start_layer + ) + positive_terminal.boundary_type = GrpcBoundaryType.PORT + positive_terminal.impedance = GrpcValue(impedance) + positive_terminal.Is_circuit_port = True + position = GrpcPointData(self._pedb.components.get_pin_position(pin)) + negative_terminal = PointTerminal.create( + layout=self._pedb.active_layout, + net=reference_net, + layer=self._pedb.stackup.signal_layers[layer_name], + name=f"{term_name}_ref", + point=position, + ) + negative_terminal.boundary_type = GrpcBoundaryType.PORT + negative_terminal.impedance = GrpcValue(impedance) + negative_terminal.is_circuit_port = True + positive_terminal.reference_terminal = negative_terminal + self._logger.info("Port {} successfully created".format(term_name)) + if not positive_terminal.is_null: + terms.append(positive_terminal) + else: + self._logger.error(f"pin {__pin} not found on component {component_name}") + if terms: + return terms + return False + + def create_current_source_on_pin_group( + self, pos_pin_group_name, neg_pin_group_name, magnitude=1, phase=0, name=None + ): + """Create current source between two pin groups. + + Parameters + ---------- + pos_pin_group_name : str + Name of the positive pin group. + neg_pin_group_name : str + Name of the negative pin group. + magnitude : int, float, optional + Magnitude of the source. + phase : int, float, optional + Phase of the source + + Returns + ------- + bool + + """ + pos_pin_group = next(pg for pg in self._pedb.layout.pin_groups if pg.name == pos_pin_group_name) + if not pos_pin_group: + self._pedb.logger.error(f"Pin group {pos_pin_group_name} not found.") + return False + pos_terminal = pos_pin_group.create_current_source_terminal(magnitude, phase) + if name: + pos_terminal.name = name + else: + name = generate_unique_name("isource") + pos_terminal.name = name + neg_pin_group = next(pg for pg in self._pedb.layout.pin_groups if pg.name == neg_pin_group_name) + if not neg_pin_group: + self._pedb.logger.error(f"Pin group {pos_pin_group_name} not found.") + return False + neg_terminal = neg_pin_group.create_current_source_terminal() + neg_terminal.name = f"{name}_ref" + pos_terminal.reference_terminal = neg_terminal + return True + + def create_voltage_source_on_pin_group( + self, pos_pin_group_name, neg_pin_group_name, magnitude=1, phase=0, name=None, impedance=0.001 + ): + """Create voltage source between two pin groups. + + Parameters + ---------- + pos_pin_group_name : str + Name of the positive pin group. + neg_pin_group_name : str + Name of the negative pin group. + magnitude : int, float, optional + Magnitude of the source. + phase : int, float, optional + Phase of the source + + Returns + ------- + bool + + """ + pos_pin_group = next(pg for pg in self._pedb.layout.pin_groups if pg.name == pos_pin_group_name) + if not pos_pin_group: + self._pedb.logger.error(f"Pingroup {pos_pin_group_name} not found.") + return False + pos_terminal = pos_pin_group.create_voltage_source_terminal(magnitude, phase, impedance) + if name: + pos_terminal.name = name + else: + name = generate_unique_name("vsource") + pos_terminal.name = name + neg_pin_group_name = next(pg for pg in self._pedb.layout.pin_groups if pg.name == neg_pin_group_name) + if not neg_pin_group_name: + self._pedb.logger.error(f"Pingroup {neg_pin_group_name} not found.") + return False + neg_terminal = neg_pin_group_name.create_voltage_source_terminal(magnitude, phase) + neg_terminal.name = f"{name}_ref" + pos_terminal.reference_terminal = neg_terminal + return True + + def create_voltage_probe_on_pin_group(self, probe_name, pos_pin_group_name, neg_pin_group_name, impedance=1000000): + """Create voltage probe between two pin groups. + + Parameters + ---------- + probe_name : str + Name of the probe. + pos_pin_group_name : str + Name of the positive pin group. + neg_pin_group_name : str + Name of the negative pin group. + impedance : int, float, optional + Phase of the source. + + Returns + ------- + bool + + """ + pos_pin_group = next(pg for pg in self._pedb.layout.pin_groups if pg.name == pos_pin_group_name) + if not pos_pin_group: + self._pedb.logger.error(f"Pingroup {pos_pin_group_name} not found.") + return False + pos_terminal = pos_pin_group.create_voltage_probe_terminal(impedance) + if probe_name: + pos_terminal.name = probe_name + else: + probe_name = generate_unique_name("vprobe") + pos_terminal.name = probe_name + neg_pin_group = next(pg for pg in self._pedb.layout.pin_groups if pg.name == neg_pin_group_name) + if not neg_pin_group: + self._pedb.logger.error(f"Pingroup {neg_pin_group_name} not found.") + return False + neg_terminal = neg_pin_group.create_voltage_probe_terminal() + neg_terminal.name = f"{probe_name}_ref" + pos_terminal.reference_terminal = neg_terminal + return not pos_terminal.is_null + + def create_dc_terminal( + self, + component_name, + net_name, + source_name=None, + ): + """Create a dc terminal. + + Parameters + ---------- + component_name : str + Name of the positive component. + net_name : str + Name of the positive net. + + source_name : str, optional + Name of the source. The default is ``""``. + + Returns + ------- + str + The name of the source. + + Examples + -------- + + >>> from pyedb import Edb + >>> edbapp = Edb("myaedbfolder", "project name", "release version") + >>> edb.siwave.create_dc_terminal("U2A5", "V1P5_S3", "source_name") + """ + + node_pin = self._pedb.components.get_pin_from_component(component_name, net_name) + if node_pin: + node_pin = node_pin[0] + if not source_name: + source_name = f"DC_{component_name}_{net_name}" + return self.create_pin_group_terminal( + positive_pins=node_pin, name=source_name, source_type="dc_terminal", negatives_pins=None + ) + + def create_circuit_port_on_pin_group(self, pos_pin_group_name, neg_pin_group_name, impedance=50, name=None): + """Create a port between two pin groups. + + Parameters + ---------- + pos_pin_group_name : str + Name of the positive pin group. + neg_pin_group_name : str + Name of the negative pin group. + impedance : int, float, optional + Impedance of the port. Default is ``50``. + name : str, optional + Port name. + + Returns + ------- + bool + + """ + pos_pin_group = next(pg for pg in self._pedb.layout.pin_groups if pg.name == pos_pin_group_name) + if not pos_pin_group: + self._pedb.logger.error("No positive pin group found") + return False + pos_terminal = pos_pin_group.create_port_terminal(impedance) + if name: # pragma: no cover + pos_terminal.name = name + else: + name = generate_unique_name("port") + pos_terminal.name = name + neg_pin_group = next(pg for pg in self._pedb.layout.pin_groups if pg.name == neg_pin_group_name) + neg_terminal = neg_pin_group.create_port_terminal(impedance) + neg_terminal.name = f"{name}_ref" + pos_terminal.reference_terminal = neg_terminal + return True + + def place_voltage_probe( + self, + name, + positive_net_name, + positive_location, + positive_layer, + negative_net_name, + negative_location, + negative_layer, + ): + """Place a voltage probe between two points. + + Parameters + ---------- + name : str, + Name of the probe. + positive_net_name : str + Name of the positive net. + positive_location : list + Location of the positive terminal. + positive_layer : str, + Layer of the positive terminal. + negative_net_name : str, + Name of the negative net. + negative_location : list + Location of the negative terminal. + negative_layer : str + Layer of the negative terminal. + """ + p_terminal = PointTerminal.create( + layout=self._pedb.active_layout, + net=positive_net_name, + layer=positive_layer, + name=name, + point=GrpcPointData(positive_location), + ) + n_terminal = PointTerminal.create( + layout=self._pedb.active_layout, + net=negative_net_name, + layer=negative_layer, + name=f"{name}_ref", + point=GrpcPointData(negative_location), + ) + p_terminal.reference_terminal = n_terminal + return self._pedb.create_voltage_probe(p_terminal, n_terminal) diff --git a/src/pyedb/grpc/database/stackup.py b/src/pyedb/grpc/database/stackup.py new file mode 100644 index 0000000000..0753ca8045 --- /dev/null +++ b/src/pyedb/grpc/database/stackup.py @@ -0,0 +1,2572 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +This module contains the `EdbStackup` class. + +""" + +from __future__ import absolute_import + +from collections import OrderedDict +import json +import logging +import math +import warnings + +from ansys.edb.core.definition.die_property import DieOrientation as GrpcDieOrientation +from ansys.edb.core.definition.solder_ball_property import ( + SolderballPlacement as GrpcSolderballPlacement, +) +from ansys.edb.core.geometry.point3d_data import Point3DData as GrpcPoint3DData +from ansys.edb.core.hierarchy.cell_instance import CellInstance as GrpcCellInstance +from ansys.edb.core.hierarchy.component_group import ComponentType as GrpcComponentType +from ansys.edb.core.layer.layer import LayerType as GrpcLayerType +from ansys.edb.core.layer.layer import TopBottomAssociation as GrpcTopBottomAssociation +from ansys.edb.core.layer.layer_collection import ( + LayerCollectionMode as GrpcLayerCollectionMode, +) +from ansys.edb.core.layer.layer_collection import LayerCollection as GrpcLayerCollection +from ansys.edb.core.layer.layer_collection import LayerTypeSet as GrpcLayerTypeSet +from ansys.edb.core.layer.stackup_layer import StackupLayer as GrpcStackupLayer +from ansys.edb.core.layout.mcad_model import McadModel as GrpcMcadModel +from ansys.edb.core.utility.transform3d import Transform3D as GrpcTransform3D +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.generic.general_methods import ET, generate_unique_name +from pyedb.grpc.database.layers.layer import Layer +from pyedb.grpc.database.layers.stackup_layer import StackupLayer +from pyedb.misc.aedtlib_personalib_install import write_pretty_xml + +colors = None +pd = None +np = None +try: + import matplotlib.colors as colors +except ImportError: + colors = None + +try: + import numpy as np +except ImportError: + np = None + +try: + import pandas as pd +except ImportError: + pd = None + +logger = logging.getLogger(__name__) + + +class LayerCollection(GrpcLayerCollection): + def __init__(self, pedb, edb_object): + super().__init__(edb_object.msg) + self._layer_collection = edb_object + self._pedb = pedb + + def update_layout(self): + """Set layer collection into edb. + + Parameters + ---------- + stackup + """ + self._pedb.layout.layer_collection = self + + def add_layer_top(self, name, layer_type="signal", **kwargs): + """Add a layer on top of the stackup. + + Parameters + ---------- + name : str + Name of the layer. + layer_type: str, optional + Type of the layer. The default to ``"signal"``. Options are ``"signal"``, ``"dielectric"`` + kwargs + + Returns + ------- + + """ + thickness = GrpcValue(0.0) + if "thickness" in kwargs: + thickness = GrpcValue(kwargs["thickness"]) + elevation = GrpcValue(0.0) + _layer_type = GrpcLayerType.SIGNAL_LAYER + if layer_type.lower() == "dielectric": + _layer_type = GrpcLayerType.DIELECTRIC_LAYER + layer = GrpcStackupLayer.create( + name=name, layer_type=_layer_type, thickness=thickness, material="copper", elevation=elevation + ) + return self._layer_collection.add_layer_top(layer) + + def add_layer_bottom(self, name, layer_type="signal", **kwargs): + """Add a layer on bottom of the stackup. + + Parameters + ---------- + name : str + Name of the layer. + layer_type: str, optional + Type of the layer. The default to ``"signal"``. Options are ``"signal"``, ``"dielectric"`` + kwargs + + Returns + ------- + + """ + thickness = GrpcValue(0.0) + if "thickness" in kwargs: + thickness = GrpcValue(kwargs["thickness"]) + elevation = GrpcValue(0.0) + _layer_type = GrpcLayerType.SIGNAL_LAYER + if layer_type.lower() == "dielectric": + _layer_type = GrpcLayerType.DIELECTRIC_LAYER + layer = GrpcStackupLayer.create( + name=name, layer_type=_layer_type, thickness=thickness, material="copper", elevation=elevation + ) + return self._layer_collection.add_layer_bottom(layer) + + def add_layer_below(self, name, base_layer_name, layer_type="signal", **kwargs): + """Add a layer below a layer. + + Parameters + ---------- + name : str + Name of the layer. + base_layer_name: str + Name of the base layer. + layer_type: str, optional + Type of the layer. The default to ``"signal"``. Options are ``"signal"``, ``"dielectric"`` + kwargs + + Returns + ------- + + """ + thickness = GrpcValue(0.0) + if "thickness" in kwargs: + thickness = GrpcValue(kwargs["thickness"]) + elevation = GrpcValue(0.0) + _layer_type = GrpcLayerType.SIGNAL_LAYER + if layer_type.lower() == "dielectric": + _layer_type = GrpcLayerType.DIELECTRIC_LAYER + layer = GrpcStackupLayer.create( + name=name, layer_type=_layer_type, thickness=thickness, material="copper", elevation=elevation + ) + return self._layer_collection.add_layer_below(layer, base_layer_name) + + def add_layer_above(self, name, base_layer_name, layer_type="signal", **kwargs): + """Add a layer above a layer. + + Parameters + ---------- + name : str + Name of the layer. + base_layer_name: str + Name of the base layer. + layer_type: str, optional + Type of the layer. The default to ``"signal"``. Options are ``"signal"``, ``"dielectric"`` + kwargs + + Returns + ------- + + """ + thickness = GrpcValue(0.0) + if "thickness" in kwargs: + thickness = GrpcValue(kwargs["thickness"]) + elevation = GrpcValue(0.0) + _layer_type = GrpcLayerType.SIGNAL_LAYER + if layer_type.lower() == "dielectric": + _layer_type = GrpcLayerType.DIELECTRIC_LAYER + layer = GrpcStackupLayer.create( + name=name, layer_type=_layer_type, thickness=thickness, material="copper", elevation=elevation + ) + return self._layer_collection.add_layer_above(layer, base_layer_name) + + def add_document_layer(self, name, layer_type="user", **kwargs): + """Add a document layer. + + Parameters + ---------- + name : str + Name of the layer. + layer_type: str, optional + Type of the layer. The default is ``"user"``. Options are ``"user"``, ``"outline"`` + kwargs + + Returns + ------- + + """ + added_layer = self.add_layer_top(name) + added_layer.type = GrpcLayerType.USER_LAYER + return added_layer + + @property + def stackup_layers(self): + """Retrieve the dictionary of signal and dielectric layers.""" + warnings.warn("Use new property :func:`layers` instead.", DeprecationWarning) + return self.layers + + @property + def non_stackup_layers(self): + """Retrieve the dictionary of signal layers.""" + return { + layer.name: Layer(self._pedb, layer) for layer in self.get_layers(GrpcLayerTypeSet.NON_STACKUP_LAYER_SET) + } + + @property + def all_layers(self): + return {layer.name: Layer(self._pedb, layer) for layer in self.get_layers(GrpcLayerTypeSet.ALL_LAYER_SET)} + + @property + def signal_layers(self): + return { + layer.name: StackupLayer(self._pedb, layer) for layer in self.get_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET) + } + + @property + def dielectric_layers(self): + return { + layer.name: StackupLayer(self._pedb, layer) + for layer in self.get_layers(GrpcLayerTypeSet.DIELECTRIC_LAYER_SET) + } + + @property + def layers_by_id(self): + """Retrieve the list of layers with their ids.""" + return [[obj.id, name] for name, obj in self.all_layers.items()] + + @property + def layers(self): + """Retrieve the dictionary of layers. + + Returns + ------- + Dict[str, :class:`pyedb.grpc.database.edb_data.layer_data.LayerEdbClass`] + """ + return {obj.name: StackupLayer(self._pedb, obj) for obj in self.get_layers(GrpcLayerTypeSet.STACKUP_LAYER_SET)} + + def find_layer_by_name(self, name: str): + """Finds a layer with the given name. + + . deprecated:: pyedb 0.29.0 + Use :func:`find_by_name` instead. + + """ + warnings.warn( + "`find_layer_by_name` is deprecated and is now located here " + "`pyedb.grpc.core.excitations.find_by_name` instead.", + DeprecationWarning, + ) + layer = self.find_by_name(name) + if layer.is_null: + raise ValueError(f"Layer with name '{name}' was not found.") + return layer + + +class Stackup(LayerCollection): + """Manages EDB methods for stackup accessible from `Edb.stackup` property.""" + + def __init__(self, pedb, edb_object=None): + super().__init__(pedb, edb_object) + self._pedb = pedb + + @property + def _logger(self): + return self._pedb.logger + + @property + def thickness(self): + """Retrieve Stackup thickness. + + Returns + ------- + float + Layout stackup thickness. + + """ + return self.get_layout_thickness() + + @property + def num_layers(self): + """Retrieve the stackup layer number. + + Returns + ------- + int + layer number. + + """ + return len(list(self.layers.keys())) + + def create_symmetric_stackup( + self, + layer_count, + inner_layer_thickness="17um", + outer_layer_thickness="50um", + dielectric_thickness="100um", + dielectric_material="FR4_epoxy", + soldermask=True, + soldermask_thickness="20um", + ): # pragma: no cover + """Create a symmetric stackup. + + Parameters + ---------- + layer_count : int + Number of layer count. + inner_layer_thickness : str, float, optional + Thickness of inner conductor layer. + outer_layer_thickness : str, float, optional + Thickness of outer conductor layer. + dielectric_thickness : str, float, optional + Thickness of dielectric layer. + dielectric_material : str, optional + Material of dielectric layer. + soldermask : bool, optional + Whether to create soldermask layers. The default is``True``. + soldermask_thickness : str, optional + Thickness of soldermask layer. + + Returns + ------- + bool + """ + if not np: + self._pedb.logger.error("Numpy is needed. Please, install it first.") + return False + if not layer_count % 2 == 0: + return False + + self.add_layer( + "BOT", + None, + material="copper", + thickness=outer_layer_thickness, + fillMaterial=dielectric_material, + ) + self.add_layer( + "D" + str(int(layer_count / 2)), + None, + material="FR4_epoxy", + thickness=dielectric_thickness, + layer_type="dielectric", + fillMaterial=dielectric_material, + ) + self.add_layer( + "TOP", + None, + material="copper", + thickness=outer_layer_thickness, + fillMaterial=dielectric_material, + ) + if soldermask: + self.add_layer( + "SMT", + None, + material="SolderMask", + thickness=soldermask_thickness, + layer_type="dielectric", + fillMaterial=dielectric_material, + ) + self.add_layer( + "SMB", + None, + material="SolderMask", + thickness=soldermask_thickness, + layer_type="dielectric", + fillMaterial=dielectric_material, + method="add_on_bottom", + ) + self.layers["TOP"].dielectric_fill = "SolderMask" + self.layers["BOT"].dielectric_fill = "SolderMask" + + for layer_num in np.arange(int(layer_count / 2), 1, -1): + # Generate upper half + self.add_layer( + "L" + str(layer_num), + "TOP", + material="copper", + thickness=inner_layer_thickness, + fillMaterial=dielectric_material, + method="insert_below", + ) + self.add_layer( + "D" + str(layer_num - 1), + "TOP", + material=dielectric_material, + thickness=dielectric_thickness, + layer_type="dielectric", + fillMaterial=dielectric_material, + method="insert_below", + ) + + # Generate lower half + self.add_layer( + "L" + str(layer_count - layer_num + 1), + "BOT", + material="copper", + thickness=inner_layer_thickness, + fillMaterial=dielectric_material, + method="insert_above", + ) + self.add_layer( + "D" + str(layer_count - layer_num + 1), + "BOT", + material=dielectric_material, + thickness=dielectric_thickness, + layer_type="dielectric", + fillMaterial=dielectric_material, + method="insert_above", + ) + return True + + @property + def mode(self): + """Stackup mode. + + Returns + ------- + int, str + Type of the stackup mode, where: + + * 0 - Laminate + * 1 - Overlapping + * 2 - MultiZone + """ + return super().mode.name.lower() + + @mode.setter + def mode(self, value): + if value == 0 or value == GrpcLayerCollectionMode.LAMINATE or value == "laminate": + super(LayerCollection, self.__class__).mode.__set__(self, GrpcLayerCollectionMode.LAMINATE) + elif value == 1 or value == GrpcLayerCollectionMode.OVERLAPPING or value == "overlapping": + super(LayerCollection, self.__class__).mode.__set__(self, GrpcLayerCollectionMode.OVERLAPPING) + elif value == 2 or value == GrpcLayerCollectionMode.MULTIZONE or value == "multizone": + super(LayerCollection, self.__class__).mode.__set__(self, GrpcLayerCollectionMode.MULTIZONE) + self.update_layout() + + def _set_layout_stackup(self, layer_clone, operation, base_layer=None, method=1): + """Internal method. Apply stackup change into EDB. + + Parameters + ---------- + layer_clone : :class:`dotnet.database.EDB_Data.EDBLayer` + operation : str + Options are ``"change_attribute"``, ``"change_name"``,``"change_position"``, ``"insert_below"``, + ``"insert_above"``, ``"add_on_top"``, ``"add_on_bottom"``, ``"non_stackup"``, ``"add_at_elevation"``. + base_layer : str, optional + Name of the base layer. The default value is ``None``. + + Returns + ------- + + """ + lc = self._pedb.layout.layer_collection + if operation in ["change_position", "change_attribute", "change_name"]: + _lc = GrpcLayerCollection.create() + + layers = [i for i in lc.get_layers(GrpcLayerTypeSet.STACKUP_LAYER_SET)] + non_stackup = [i for i in lc.get_layers(GrpcLayerTypeSet.NON_STACKUP_LAYER_SET)] + _lc.mode = lc.mode + if lc.mode.name.lower() == "overlapping": + for layer in layers: + if layer.name == layer_clone.name or layer.name == base_layer: + _lc.add_stackup_layer_at_elevation(layer_clone) + else: + _lc.add_stackup_layer_at_elevation(layer) + else: + for layer in layers: + if layer.name == layer_clone.name or layer.name == base_layer: + _lc.add_layer_bottom(layer_clone) + else: + _lc.add_layer_bottom(layer) + for layer in non_stackup: + _lc.add_layer_bottom(layer) + elif operation == "insert_below": + lc.add_layer_below(layer_clone, base_layer) + elif operation == "insert_above": + lc.add_layer_above(layer_clone, base_layer) + elif operation == "add_on_top": + lc.add_layer_top(layer_clone) + elif operation == "add_on_bottom": + lc.add_layer_bottom(layer_clone) + elif operation == "add_at_elevation": + lc.add_stackup_layer_at_elevation(layer_clone) + elif operation == "non_stackup": + lc.add_layer_bottom(layer_clone) + self._pedb.layout.layer_collection = lc + return True + + def _create_stackup_layer(self, layer_name, thickness, layer_type="signal", material="copper"): + if layer_type == "signal": + _layer_type = GrpcLayerType.SIGNAL_LAYER + else: + _layer_type = GrpcLayerType.DIELECTRIC_LAYER + material = "FR4_epoxy" + thickness = GrpcValue(thickness, self._pedb.active_db) + layer = StackupLayer.create( + name=layer_name, + layer_type=_layer_type, + thickness=thickness, + elevation=GrpcValue(0), + material=material, + ) + return layer + + def _create_nonstackup_layer(self, layer_name, layer_type): + if layer_type == "conducting": # pragma: no cover + _layer_type = GrpcLayerType.CONDUCTING_LAYER + elif layer_type == "airlines": # pragma: no cover + _layer_type = GrpcLayerType.AIRLINES_LAYER + elif layer_type == "error": # pragma: no cover + _layer_type = GrpcLayerType.ERRORS_LAYER + elif layer_type == "symbol": # pragma: no cover + _layer_type = GrpcLayerType.SYMBOL_LAYER + elif layer_type == "measure": # pragma: no cover + _layer_type = GrpcLayerType.MEASURE_LAYER + elif layer_type == "assembly": # pragma: no cover + _layer_type = GrpcLayerType.ASSEMBLY_LAYER + elif layer_type == "silkscreen": # pragma: no cover + _layer_type = GrpcLayerType.SILKSCREEN_LAYER + elif layer_type == "soldermask": # pragma: no cover + _layer_type = GrpcLayerType.SOLDER_MASK_LAYER + elif layer_type == "solderpaste": # pragma: no cover + _layer_type = GrpcLayerType.SOLDER_PASTE_LAYER + elif layer_type == "glue": # pragma: no cover + _layer_type = GrpcLayerType.GLUE_LAYER + elif layer_type == "wirebond": # pragma: no cover + _layer_type = GrpcLayerType.WIREBOND_LAYER + elif layer_type == "user": # pragma: no cover + _layer_type = GrpcLayerType.USER_LAYER + elif layer_type == "siwavehfsssolverregions": # pragma: no cover + _layer_type = GrpcLayerType.SIWAVE_HFSS_SOLVER_REGIONS + elif layer_type == "outline": # pragma: no cover + _layer_type = GrpcLayerType.OUTLINE_LAYER + elif layer_type == "postprocessing": # pragma: no cover + _layer_type = GrpcLayerType.POST_PROCESSING_LAYER + else: # pragma: no cover + _layer_type = GrpcLayerType.UNDEFINED_LAYER_TYPE + + result = Layer.create(layer_name, _layer_type) + return result + + def add_outline_layer(self, outline_name="Outline"): + """Add an outline layer named ``"Outline"`` if it is not present. + + Returns + ------- + bool + "True" if successful, ``False`` if failed. + """ + return self.add_document_layer(name="Outline", layer_type="outline") + + # TODO: Update optional argument material into material_name and fillMaterial into fill_material_name + + def add_layer( + self, + layer_name, + base_layer=None, + method="add_on_top", + layer_type="signal", + material="copper", + fillMaterial="FR4_epoxy", + thickness="35um", + etch_factor=None, + is_negative=False, + enable_roughness=False, + elevation=None, + ): + """Insert a layer into stackup. + + Parameters + ---------- + layer_name : str + Name of the layer. + base_layer : str, optional + Name of the base layer. + method : str, optional + Where to insert the new layer. The default is ``"add_on_top"``. Options are ``"add_on_top"``, + ``"add_on_bottom"``, ``"insert_above"``, ``"insert_below"``, ``"add_at_elevation"``,. + layer_type : str, optional + Type of layer. The default is ``"signal"``. Options are ``"signal"``, ``"dielectric"``, ``"conducting"``, + ``"air_lines"``, ``"error"``, ``"symbol"``, ``"measure"``, ``"assembly"``, ``"silkscreen"``, + ``"solder_mask"``, ``"solder_paste"``, ``"glue"``, ``"wirebond"``, ``"hfss_region"``, ``"user"``. + material : str, optional + Material of the layer. + fillMaterial : str, optional + Fill material of the layer. + thickness : str, float, optional + Thickness of the layer. + etch_factor : int, float, optional + Etch factor of the layer. + is_negative : bool, optional + Whether the layer is negative. + enable_roughness : bool, optional + Whether roughness is enabled. + elevation : float, optional + Elevation of new layer. Only valid for Overlapping Stackup. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.layer_data.LayerEdbClass` + """ + if layer_name in self.layers: + logger.error("layer {} exists.".format(layer_name)) + return False + if not material: + material = "copper" if layer_type == "signal" else "FR4_epoxy" + if not fillMaterial: + fillMaterial = "FR4_epoxy" + + materials = self._pedb.materials + if material not in materials: + material_properties = self._pedb.materials.read_syslib_material(material) + if material_properties: + logger.info(f"Material {material} found in syslib. Adding it to aedb project.") + materials.add_material(material, **material_properties) + else: + logger.warning(f"Material {material} not found. Check the library and retry.") + + if layer_type != "dielectric" and fillMaterial not in materials: + material_properties = self._pedb.materials.read_syslib_material(fillMaterial) + if material_properties: + logger.info(f"Material {fillMaterial} found in syslib. Adding it to aedb project.") + materials.add_material(fillMaterial, **material_properties) + else: + logger.warning(f"Material {fillMaterial} not found. Check the library and retry.") + + if layer_type in ["signal", "dielectric"]: + new_layer = self._create_stackup_layer(layer_name, thickness, layer_type) + new_layer.set_material(material) + if layer_type != "dielectric": + new_layer.set_fill_material(fillMaterial) + new_layer.negative = is_negative + l1 = len(self.layers) + if method == "add_at_elevation" and elevation: + new_layer.lower_elevation = GrpcValue(elevation) + if etch_factor: + new_layer.etch_factor = etch_factor + if enable_roughness: + new_layer.roughness_enabled = True + self._set_layout_stackup(new_layer, method, base_layer) + if len(self.layers) == l1: + self._set_layout_stackup(new_layer, method, base_layer, method=2) + else: + new_layer = self._create_nonstackup_layer(layer_name, layer_type) + self._set_layout_stackup(new_layer, "non_stackup") + return self.layers[layer_name] + + def remove_layer(self, name): + """Remove a layer from stackup. + + Parameters + ---------- + name : str + Name of the layer to remove. + + Returns + ------- + + """ + new_layer_collection = LayerCollection.create() + for lyr in self.layers: + if not (lyr.name == name): + new_layer_collection.add_layer_bottom(lyr) + + self._pedb.layout.layer_collection = new_layer_collection + return True + + def export(self, fpath, file_format="xml", include_material_with_layer=False): + """Export stackup definition to a CSV or JSON file. + + Parameters + ---------- + fpath : str + File path to csv or json file. + file_format : str, optional + Format of the file to export. The default is ``"csv"``. Options are ``"csv"``, ``"xlsx"``, + ``"json"``. + include_material_with_layer : bool, optional. + Whether to include the material definition inside layer ones. This parameter is only used + when a JSON file is exported. The default is ``False``, which keeps the material definition + section in the JSON file. If ``True``, the material definition is included inside the layer ones. + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb() + >>> edb.stackup.export("stackup.xml") + """ + if len(fpath.split(".")) == 1: + fpath = "{}.{}".format(fpath, file_format) + + if fpath.endswith(".csv"): + return self._export_layer_stackup_to_csv_xlsx(fpath, file_format="csv") + elif fpath.endswith(".xlsx"): + return self._export_layer_stackup_to_csv_xlsx(fpath, file_format="xlsx") + elif fpath.endswith(".json"): + return self._export_layer_stackup_to_json(fpath, include_material_with_layer) + elif fpath.endswith(".xml"): + return self._export_xml(fpath) + else: + self._logger.warning("Layer stackup format is not supported. Skipping import.") + return False + + def export_stackup(self, fpath, file_format="xml", include_material_with_layer=False): + """Export stackup definition to a CSV or JSON file. + + .. deprecated:: 0.6.61 + Use :func:`export` instead. + + Parameters + ---------- + fpath : str + File path to CSV or JSON file. + file_format : str, optional + Format of the file to export. The default is ``"csv"``. Options are ``"csv"``, ``"xlsx"`` + and ``"json"``. + include_material_with_layer : bool, optional. + Whether to include the material definition inside layer objects. This parameter is only used + when a JSON file is exported. The default is ``False``, which keeps the material definition + section in the JSON file. If ``True``, the material definition is included inside the layer ones. + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb() + >>> edb.stackup.export_stackup("stackup.xml") + """ + + self._logger.warning("Method export_stackup is deprecated. Use .export.") + return self.export(fpath, file_format=file_format, include_material_with_layer=include_material_with_layer) + + def _export_layer_stackup_to_csv_xlsx(self, fpath=None, file_format=None): + if not pd: + self._pedb.logger.error("Pandas is needed. Please, install it first.") + return False + + data = { + "Type": [], + "Material": [], + "Dielectric_Fill": [], + "Thickness": [], + } + idx = [] + for lyr in self.layers.values(): + idx.append(lyr.name) + data["Type"].append(lyr.type) + data["Material"].append(lyr.material) + data["Dielectric_Fill"].append(lyr.dielectric_fill) + data["Thickness"].append(lyr.thickness) + df = pd.DataFrame(data, index=idx, columns=["Type", "Material", "Dielectric_Fill", "Thickness"]) + if file_format == "csv": # pragma: no cover + if not fpath.endswith(".csv"): + fpath = fpath + ".csv" + df.to_csv(fpath) + else: # pragma: no cover + if not fpath.endswith(".xlsx"): # pragma: no cover + fpath = fpath + ".xlsx" + df.to_excel(fpath) + return True + + def _export_layer_stackup_to_json(self, output_file=None, include_material_with_layer=False): + if not include_material_with_layer: + material_out = {} + for material_name, material in self._pedb.materials.materials.items(): + material_out[material_name] = material.to_dict() + layers_out = {} + for k, v in self.layers.items(): + data = v._json_format() + # FIXME: Update the API to avoid providing following information to our users + del data["pedb"] + del data["edb_object"] + layers_out[k] = data + if v.material in self._pedb.materials.materials: + layer_material = self._pedb.materials.materials[v.material] + if not v.dielectric_fill: + dielectric_fill = False + else: + dielectric_fill = self._pedb.materials.materials[v.dielectric_fill] + if include_material_with_layer: + layers_out[k]["material"] = layer_material.to_dict() + if dielectric_fill: + layers_out[k]["dielectric_fill"] = dielectric_fill.to_dict() + if not include_material_with_layer: + stackup_out = {"materials": material_out, "layers": layers_out} + else: + stackup_out = {"layers": layers_out} + if output_file: + with open(output_file, "w") as write_file: + json.dump(stackup_out, write_file, indent=4) + + return True + else: + return False + + # TODO: This method might need some refactoring + + def _import_layer_stackup(self, input_file=None): + if input_file: + f = open(input_file) + json_dict = json.load(f) # pragma: no cover + for k, v in json_dict.items(): + if k == "materials": + for material in v.values(): + material_name = material["name"] + del material["name"] + if material_name not in self._pedb.materials: + self._pedb.materials.add_material(material_name, **material) + else: + self._pedb.materials.update_material(material_name, material) + if k == "layers": + if len(list(v.values())) == len(list(self.layers.values())): + imported_layers_list = [l_dict["name"] for l_dict in list(v.values())] + layout_layer_list = list(self.layers.keys()) + for layer_name in imported_layers_list: + layer_index = imported_layers_list.index(layer_name) + if layout_layer_list[layer_index] != layer_name: + self.layers[layout_layer_list[layer_index]].name = layer_name + prev_layer = None + for layer_name, layer in v.items(): + if layer["name"] not in self.layers: + if not prev_layer: + self.add_layer( + layer_name, + method="add_on_top", + layer_type=layer["type"], + material=layer["material"], + fillMaterial=layer["dielectric_fill"], + thickness=layer["thickness"], + ) + prev_layer = layer_name + else: + self.add_layer( + layer_name, + base_layer=layer_name, + method="insert_below", + layer_type=layer["type"], + material=layer["material"], + fillMaterial=layer["dielectric_fill"], + thickness=layer["thickness"], + ) + prev_layer = layer_name + if layer_name in self.layers: + self.layers[layer["name"]]._load_layer(layer) + return True + + def limits(self, only_metals=False): + """Retrieve stackup limits. + + Parameters + ---------- + only_metals : bool, optional + Whether to retrieve only metals. The default is ``False``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if only_metals: + input_layers = GrpcLayerTypeSet.SIGNAL_LAYER_SET + else: + input_layers = GrpcLayerTypeSet.STACKUP_LAYER_SET + + res = self.get_top_bottom_stackup_layers(input_layers) + upper_layer = res[0] + upper_layer_top_elevationm = res[1] + lower_layer = res[2] + lower_layer_lower_elevation = res[3] + return upper_layer.name, upper_layer_top_elevationm, lower_layer.name, lower_layer_lower_elevation + + def flip_design(self): + """Flip the current design of a layout. + + Returns + ------- + bool + ``True`` when succeed ``False`` if not. + + Examples + -------- + >>> edb = Edb(edbpath=targetfile, edbversion="2021.2") + >>> edb.stackup.flip_design() + >>> edb.save() + >>> edb.close_edb() + """ + try: + lc = self._layer_collection + new_lc = LayerCollection.create() + new_lc.mode = lc.mode + max_elevation = 0.0 + for layer in lc.get_layers(GrpcLayerTypeSet.STACKUP_LAYER_SET): + if "RadBox" not in layer.name: # Ignore RadBox + lower_elevation = layer.clone().lower_elevation.value * 1.0e6 + upper_elevation = layer.Clone().upper_elevation.value * 1.0e6 + max_elevation = max([max_elevation, lower_elevation, upper_elevation]) + + non_stackup_layers = [] + for layer in lc.get_Layers(): + cloned_layer = layer.clone() + if not cloned_layer.is_stackup_layer: + non_stackup_layers.append(cloned_layer) + continue + if "RadBox" not in cloned_layer.name and not cloned_layer.is_via_layer: + upper_elevation = cloned_layer.upper_elevation.value * 1.0e6 + updated_lower_el = max_elevation - upper_elevation + val = GrpcValue(f"{updated_lower_el}um") + cloned_layer.lower_elevation = val + if cloned_layer.top_bottom_association == GrpcTopBottomAssociation.TOP_ASSOCIATED: + cloned_layer.top_bottom_association = GrpcTopBottomAssociation.BOTTOM_ASSOCIATED + else: + cloned_layer.top_bottom_association = GrpcTopBottomAssociation.TOP_BOTTOM_ASSOCIATION_COUNT + new_lc.add_stackup_layer_at_elevation(cloned_layer) + + vialayers = [lay for lay in lc.get_layers(GrpcLayerTypeSet.STACKUP_LAYER_SET) if lay.clone().is_via_layer] + for layer in vialayers: + cloned_via_layer = layer.clone() + upper_ref_name = cloned_via_layer.get_ref_layer_name(True) + lower_ref_name = cloned_via_layer.get_ref_layer_name(False) + upper_ref = [lay for lay in lc.Layers(GrpcLayerTypeSet.ALL_LAYER_SET) if lay.name == upper_ref_name][0] + lower_ref = [lay for lay in lc.Layers(GrpcLayerTypeSet.ALL_LAYER_SET) if lay.name == lower_ref_name][0] + cloned_via_layer.set_ref_layer(lower_ref, True) + cloned_via_layer.set_ref_layer(upper_ref, False) + ref_layer_in_flipped_stackup = [ + lay for lay in new_lc.get_layers(GrpcLayerTypeSet.ALL_LAYER_SET) if lay.name == upper_ref_name + ][0] + via_layer_lower_elevation = ( + ref_layer_in_flipped_stackup.lower_elevation + ref_layer_in_flipped_stackup.thickness + ) + cloned_via_layer.lower_elevation = via_layer_lower_elevation + new_lc.add_stackup_layer_at_elevation(cloned_via_layer) + new_lc.add_layers(non_stackup_layers) + self._pedb.layout.layer_collection = new_lc + + for pyaedt_cmp in list(self._pedb.components.instances.values()): + cmp = pyaedt_cmp + cmp_type = cmp.type + cmp_prop = cmp.component_property + try: + if cmp_prop.solder_ball_property.placement == GrpcSolderballPlacement.ABOVE_PADSTACK: + sball_prop = cmp_prop.solder_ball_property + sball_prop.placement = GrpcSolderballPlacement.BELOW_PADSTACK + cmp_prop.solder_ball_property = sball_prop + elif cmp_prop.solder_ball_property.placement == GrpcSolderballPlacement.BELOW_PADSTACK: + sball_prop = cmp_prop.solder_ball_property + sball_prop.placement = GrpcSolderballPlacement.ABOVE_PADSTACK + cmp_prop.solder_ball_property = sball_prop + except: + pass + if cmp_type == GrpcComponentType.IC: + die_prop = cmp_prop.die_property + chip_orientation = die_prop.die_orientation + if chip_orientation == GrpcDieOrientation.CHIP_DOWN: + die_prop.die_orientation = GrpcDieOrientation.CHIP_UP + cmp_prop.die_property = die_prop + else: + die_prop.die_orientation = GrpcDieOrientation.CHIP_DOWN + cmp_prop.die_property = die_prop + cmp.component_property = cmp_prop + + lay_list = new_lc.get_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET) + for padstack in list(self._pedb.padstacks.instances.values()): + start_layer_id = [lay.id for lay in lay_list if lay.name == padstack.start_layer] + stop_layer_id = [lay.id for lay in lay_list if lay.name == padstack.stop_layer] + layer_map = padstack.get_layer_map() + layer_map.set_mapping(stop_layer_id[0], start_layer_id[0]) + padstack.set_layer_map(layer_map) + return True + except: + return False + + def get_layout_thickness(self): + """Return the layout thickness. + + Returns + ------- + float + The thickness value. + """ + layers = list(self.layers.values()) + layers.sort(key=lambda lay: lay.lower_elevation) + thickness = 0 + if layers: + top_layer = layers[-1] + bottom_layer = layers[0] + thickness = abs(top_layer.upper_elevation - bottom_layer.lower_elevation) + return round(thickness, 7) + + def _get_solder_height(self, layer_name): + for _, val in self._pedb.components.instances.items(): + if val.solder_ball_height and val.placement_layer == layer_name: + return val.solder_ball_height + return 0 + + def _remove_solder_pec(self, layer_name): + for _, val in self._pedb.components.instances.items(): + if val.solder_ball_height and val.placement_layer == layer_name: + comp_prop = val.component_property + port_property = comp_prop.port_property + port_property.reference_size_auto = False + port_property.reference_size = (GrpcValue(0.0), GrpcValue(0.0)) + comp_prop.port_property = port_property + val.edbcomponent.component_property = comp_prop + + def adjust_solder_dielectrics(self): + """Adjust the stack-up by adding or modifying dielectric layers that contains Solder Balls. + This method identifies the solder-ball height and adjust the dielectric thickness on top (or bottom) to fit + the thickness in order to merge another layout. + + Returns + ------- + bool + """ + for el, val in self._pedb.components.instances.items(): + if val.solder_ball_height: + layer = val.placement_layer + if layer == list(self.layers.keys())[0]: + self.add_layer( + "Bottom_air", + base_layer=list(self.layers.keys())[-1], + method="insert_below", + material="air", + thickness=val.solder_ball_height, + layer_type="dielectric", + ) + elif layer == list(self.layers.keys())[-1]: + self.add_layer( + "Top_Air", + base_layer=layer, + material="air", + thickness=val.solder_ball_height, + layer_type="dielectric", + ) + elif layer == list(self.signal_layers.keys())[-1]: + list(self.layers.values())[-1].thickness = val.solder_ball_height + + elif layer == list(self.signal_layers.keys())[0]: + list(self.layers.values())[0].thickness = val.solder_ball_height + return True + + def place_in_layout( + self, + edb, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + flipped_stackup=True, + place_on_top=True, + ): + """Place current Cell into another cell using layer placement method. + Flip the current layer stackup of a layout if requested. Transform parameters currently not supported. + + Parameters + ---------- + edb : Edb + Cell on which to place the current layout. If None the Cell will be applied on an empty new Cell. + angle : double, optional + The rotation angle applied on the design. + offset_x : double, optional + The x offset value. + offset_y : double, optional + The y offset value. + flipped_stackup : bool, optional + Either if the current layout is inverted. + If `True` and place_on_top is `True` the stackup will be flipped before the merge. + place_on_top : bool, optional + Either if place the current layout on Top or Bottom of destination Layout. + + Returns + ------- + bool + ``True`` when succeed ``False`` if not. + + Examples + -------- + >>> edb1 = Edb(edbpath=targetfile1, edbversion="2021.2") + >>> edb2 = Edb(edbpath=targetfile2, edbversion="2021.2") + + >>> hosting_cmp = edb1.components.get_component_by_name("U100") + >>> mounted_cmp = edb2.components.get_component_by_name("BGA") + + >>> vector, rotation, solder_ball_height = edb1.components.get_component_placement_vector( + ... mounted_component=mounted_cmp, + ... hosting_component=hosting_cmp, + ... mounted_component_pin1="A12", + ... mounted_component_pin2="A14", + ... hosting_component_pin1="A12", + ... hosting_component_pin2="A14") + >>> edb2.stackup.place_in_layout(edb1.active_cell, angle=0.0, offset_x=vector[0], + ... offset_y=vector[1], flipped_stackup=False, place_on_top=True, + ... ) + """ + # if flipped_stackup and place_on_top or (not flipped_stackup and not place_on_top): + self.adjust_solder_dielectrics() + if not place_on_top: + edb.stackup.flip_design() + place_on_top = True + if not flipped_stackup: + self.flip_design() + elif flipped_stackup: + self.flip_design() + edb_cell = edb.active_cell + _angle = GrpcValue(angle * math.pi / 180.0) + _offset_x = GrpcValue(offset_x) + _offset_y = GrpcValue(offset_y) + + if edb_cell.name not in self._pedb.cell_names: + list_cells = self._pedb.copy_cells([edb_cell.api_object]) + edb_cell = list_cells[0] + self._pedb.layout.cell.is_blackbox = True + cell_inst2 = GrpcCellInstance.create( + layout=edb_cell.layout, name=self._pedb.layout.cell.name, ref=self._pedb.active_layout + ) + cell_trans = cell_inst2.transform + cell_trans.rotation = _angle + cell_trans.offset_x = _offset_x + cell_trans.offset_y = _offset_y + cell_trans.mirror = flipped_stackup + cell_inst2.transform = cell_trans + cell_inst2.solve_independent_preference = False + stackup_target = edb_cell.layout.layer_collection + + if place_on_top: + cell_inst2.placement_layer = stackup_target.get_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)[0] + else: + cell_inst2.placement_layer = stackup_target.get_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)[-1] + return True + + def place_in_layout_3d_placement( + self, + edb, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + flipped_stackup=True, + place_on_top=True, + solder_height=0, + ): + """Place current Cell into another cell using 3d placement method. + Flip the current layer stackup of a layout if requested. Transform parameters currently not supported. + + Parameters + ---------- + edb : Edb + Cell on which to place the current layout. If None the Cell will be applied on an empty new Cell. + angle : double, optional + The rotation angle applied on the design. + offset_x : double, optional + The x offset value. + offset_y : double, optional + The y offset value. + flipped_stackup : bool, optional + Either if the current layout is inverted. + If `True` and place_on_top is `True` the stackup will be flipped before the merge. + place_on_top : bool, optional + Either if place the current layout on Top or Bottom of destination Layout. + solder_height : float, optional + Solder Ball or Bumps eight. + This value will be added to the elevation to align the two layouts. + + Returns + ------- + bool + ``True`` when succeed ``False`` if not. + + Examples + -------- + >>> edb1 = Edb(edbpath=targetfile1, edbversion="2021.2") + >>> edb2 = Edb(edbpath=targetfile2, edbversion="2021.2") + >>> hosting_cmp = edb1.components.get_component_by_name("U100") + >>> mounted_cmp = edb2.components.get_component_by_name("BGA") + >>> edb2.stackup.place_in_layout(edb1.active_cell, angle=0.0, offset_x="1mm", + ... offset_y="2mm", flipped_stackup=False, place_on_top=True, + ... ) + """ + _angle = angle * math.pi / 180.0 + + if solder_height <= 0: + if flipped_stackup and not place_on_top or (place_on_top and not flipped_stackup): + minimum_elevation = None + layers_from_the_bottom = sorted(self.signal_layers.values(), key=lambda lay: lay.upper_elevation) + for lay in layers_from_the_bottom: + if minimum_elevation is None: + minimum_elevation = lay.lower_elevation + elif lay.lower_elevation > minimum_elevation: + break + lay_solder_height = self._get_solder_height(lay.name) + solder_height = max(lay_solder_height, solder_height) + self._remove_solder_pec(lay.name) + else: + maximum_elevation = None + layers_from_the_top = sorted(self.signal_layers.values(), key=lambda lay: -lay.upper_elevation) + for lay in layers_from_the_top: + if maximum_elevation is None: + maximum_elevation = lay.upper_elevation + elif lay.upper_elevation < maximum_elevation: + break + lay_solder_height = self._get_solder_height(lay.name) + solder_height = max(lay_solder_height, solder_height) + self._remove_solder_pec(lay.name) + + rotation = GrpcValue(0.0) + if flipped_stackup: + rotation = GrpcValue(math.pi) + + edb_cell = edb.active_cell + _offset_x = GrpcValue(offset_x) + _offset_y = GrpcValue(offset_y) + + if edb_cell.name not in self._pedb.cell_names: + list_cells = self._pedb.copy_cells(edb_cell.api_object) + edb_cell = list_cells[0] + self._pedb.layout.cell.is_blackbox = True + cell_inst2 = GrpcCellInstance.create( + layout=edb_cell.layout, name=self._pedb.layout.cell.name, ref=self._pedb.active_layout + ) + + stackup_target = edb_cell.layout.layer_collection + stackup_source = self._pedb.layout.layer_collection + + if place_on_top: + cell_inst2.placement_layer = stackup_target.Layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)[0] + else: + cell_inst2.placement_layer = stackup_target.Layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)[-1] + cell_inst2.placement_3d = True + res = stackup_target.get_top_bottom_stackup_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET) + target_top_elevation = res[1] + target_bottom_elevation = res[3] + res_s = stackup_source.get_top_bottom_stackup_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET) + source_stack_top_elevation = res_s[1] + source_stack_bot_elevation = res_s[3] + + if place_on_top and flipped_stackup: + elevation = target_top_elevation + source_stack_top_elevation + elif place_on_top: + elevation = target_top_elevation - source_stack_bot_elevation + elif flipped_stackup: + elevation = target_bottom_elevation + source_stack_bot_elevation + solder_height = -solder_height + else: + elevation = target_bottom_elevation - source_stack_top_elevation + solder_height = -solder_height + + h_stackup = GrpcValue(elevation + solder_height) + + zero_data = GrpcValue(0.0) + one_data = GrpcValue(1.0) + point3d_t = GrpcPoint3DData(_offset_x, _offset_y, h_stackup) + point_loc = GrpcPoint3DData(zero_data, zero_data, zero_data) + point_from = GrpcPoint3DData(one_data, zero_data, zero_data) + point_to = GrpcPoint3DData(math.cos(_angle), -1 * math.sin(_angle), zero_data) + cell_inst2.transform3d = GrpcTransform3D(point_loc, point_from, point_to, rotation, point3d_t) # TODO check + return True + + def place_instance( + self, + component_edb, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + offset_z=0.0, + flipped_stackup=True, + place_on_top=True, + solder_height=0, + ): + """Place current Cell into another cell using 3d placement method. + Flip the current layer stackup of a layout if requested. Transform parameters currently not supported. + + Parameters + ---------- + component_edb : Edb + Cell to place in the current layout. + angle : double, optional + The rotation angle applied on the design. + offset_x : double, optional + The x offset value. + The default value is ``0.0``. + offset_y : double, optional + The y offset value. + The default value is ``0.0``. + offset_z : double, optional + The z offset value. (i.e. elevation offset for placement relative to the top layer conductor). + The default value is ``0.0``, which places the cell layout on top of the top conductor + layer of the target EDB. + flipped_stackup : bool, optional + Either if the current layout is inverted. + If `True` and place_on_top is `True` the stackup will be flipped before the merge. + place_on_top : bool, optional + Either if place the component_edb layout on Top or Bottom of destination Layout. + solder_height : float, optional + Solder Ball or Bumps eight. + This value will be added to the elevation to align the two layouts. + + Returns + ------- + bool + ``True`` when succeed ``False`` if not. + + Examples + -------- + >>> edb1 = Edb(edbpath=targetfile1, edbversion="2021.2") + >>> edb2 = Edb(edbpath=targetfile2, edbversion="2021.2") + >>> hosting_cmp = edb1.components.get_component_by_name("U100") + >>> mounted_cmp = edb2.components.get_component_by_name("BGA") + >>> edb1.stackup.place_instance(edb2, angle=0.0, offset_x="1mm", + ... offset_y="2mm", flipped_stackup=False, place_on_top=True, + ... ) + """ + _angle = angle * math.pi / 180.0 + + if solder_height <= 0: + if flipped_stackup and not place_on_top or (place_on_top and not flipped_stackup): + minimum_elevation = None + layers_from_the_bottom = sorted( + component_edb.stackup.signal_layers.values(), key=lambda lay: lay.upper_elevation + ) + for lay in layers_from_the_bottom: + if minimum_elevation is None: + minimum_elevation = lay.lower_elevation + elif lay.lower_elevation > minimum_elevation: + break + lay_solder_height = component_edb.stackup._get_solder_height(lay.name) + solder_height = max(lay_solder_height, solder_height) + component_edb.stackup._remove_solder_pec(lay.name) + else: + maximum_elevation = None + layers_from_the_top = sorted( + component_edb.stackup.signal_layers.values(), key=lambda lay: -lay.upper_elevation + ) + for lay in layers_from_the_top: + if maximum_elevation is None: + maximum_elevation = lay.upper_elevation + elif lay.upper_elevation < maximum_elevation: + break + lay_solder_height = component_edb.stackup._get_solder_height(lay.name) + solder_height = max(lay_solder_height, solder_height) + component_edb.stackup._remove_solder_pec(lay.name) + edb_cell = component_edb.active_cell + _offset_x = GrpcValue(offset_x) + _offset_y = GrpcValue(offset_y) + + if edb_cell.name not in self._pedb.cell_names: + list_cells = self._pedb.copy_cells(edb_cell.api_object) + edb_cell = list_cells[0] + for cell in self._pedb.active_db.top_circuit_cells: + if cell.name == edb_cell.name: + edb_cell = cell + # Keep Cell Independent + edb_cell.is_black_box = True + rotation = GrpcValue(0.0) + if flipped_stackup: + rotation = GrpcValue(math.pi) + + _offset_x = GrpcValue(offset_x) + _offset_y = GrpcValue(offset_y) + + instance_name = generate_unique_name(edb_cell.name, n=2) + + cell_inst2 = GrpcCellInstance.create(layout=self._pedb.active_layout, name=instance_name, ref=edb_cell.layout) + + stackup_source = edb_cell.layout.layer_collection + stackup_target = self._pedb.layout.layer_collection + + if place_on_top: + cell_inst2.placement_layer = stackup_target.get_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)[0] + else: + cell_inst2.placement_layer = stackup_target.get_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)[-1] + cell_inst2.placement_3d = True + res = stackup_target.get_top_bottom_stackup_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET) + target_top_elevation = res[1] + target_bottom_elevation = res[3] + res_s = stackup_source.get_top_bottom_stackup_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET) + source_stack_top_elevation = res_s[1] + source_stack_bot_elevation = res_s[3] + + if place_on_top and flipped_stackup: + elevation = target_top_elevation + source_stack_top_elevation + offset_z + elif place_on_top: + elevation = target_top_elevation - source_stack_bot_elevation + offset_z + elif flipped_stackup: + elevation = target_bottom_elevation + source_stack_bot_elevation - offset_z + solder_height = -solder_height + else: + elevation = target_bottom_elevation - source_stack_top_elevation - offset_z + solder_height = -solder_height + + h_stackup = elevation + solder_height + + zero_data = GrpcValue(0.0) + one_data = GrpcValue(1.0) + point3d_t = GrpcPoint3DData(_offset_x, _offset_y, h_stackup) + point_loc = GrpcPoint3DData(zero_data, zero_data, zero_data) + point_from = GrpcPoint3DData(one_data, zero_data, zero_data) + point_to = GrpcPoint3DData(math.cos(_angle), -1 * math.sin(_angle), zero_data) + cell_inst2.transform3d = (point_loc, point_from, point_to, rotation, point3d_t) # TODO check + return cell_inst2 + + def place_a3dcomp_3d_placement( + self, + a3dcomp_path, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + offset_z=0.0, + place_on_top=True, + ): + """Place a 3D Component into current layout. + 3D Component ports are not visible via EDB. They will be visible after the EDB has been opened in Ansys + Electronics Desktop as a project. + + Parameters + ---------- + a3dcomp_path : str + Path to the 3D Component file (\\*.a3dcomp) to place. + angle : double, optional + Clockwise rotation angle applied to the a3dcomp. + offset_x : double, optional + The x offset value. + The default value is ``0.0``. + offset_y : double, optional + The y offset value. + The default value is ``0.0``. + offset_z : double, optional + The z offset value. (i.e. elevation) + The default value is ``0.0``. + place_on_top : bool, optional + Whether to place the 3D Component on the top or the bottom of this layout. + If ``False`` then the 3D Component will also be flipped over around its X axis. + + Returns + ------- + bool + ``True`` if successful and ``False`` if not. + + Examples + -------- + >>> edb1 = Edb(edbpath=targetfile1, edbversion="2021.2") + >>> a3dcomp_path = "connector.a3dcomp" + >>> edb1.stackup.place_a3dcomp_3d_placement(a3dcomp_path, angle=0.0, offset_x="1mm", + ... offset_y="2mm", flipped_stackup=False, place_on_top=True, + ... ) + """ + zero_data = GrpcValue(0.0) + one_data = GrpcValue(1.0) + local_origin = GrpcPoint3DData(0.0, 0.0, 0.0) + rotation_axis_from = GrpcPoint3DData(1.0, 0.0, 0.0) + _angle = angle * math.pi / 180.0 + rotation_axis_to = GrpcPoint3DData(math.cos(_angle), -1 * math.sin(_angle), 0.0) + + stackup_target = GrpcLayerCollection(self._pedb.layout.layer_collection) + res = stackup_target.get_top_bottom_stackup_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET) + target_top_elevation = res[1] + target_bottom_elevation = res[3] + flip_angle = GrpcValue("0deg") + if place_on_top: + elevation = target_top_elevation + offset_z + else: + flip_angle = GrpcValue("180deg") + elevation = target_bottom_elevation - offset_z + h_stackup = GrpcValue(elevation) + location = GrpcPoint3DData(offset_x, offset_y, h_stackup) + mcad_model = GrpcMcadModel.create_3d_comp(layout=self._pedb.active_layout, filename=a3dcomp_path) + if mcad_model.is_null: # pragma: no cover + logger.error("Failed to create MCAD model from a3dcomp") + return False + + if mcad_model.cell_instance.is_null: # pragma: no cover + logger.error("Cell instance of a3dcomp is null") + return False + + mcad_model.cell_instance.placement_3d = True + mcad_model.cell_instance.transform3d = GrpcTransform3D( + local_origin, rotation_axis_from, rotation_axis_to, flip_angle, location + ) + return True + + def residual_copper_area_per_layer(self): + """Report residual copper area per layer in percentage. + + Returns + ------- + dict + Copper area per layer. + + Examples + -------- + >>> edb = Edb(edbpath=targetfile1, edbversion="2021.2") + >>> edb.stackup.residual_copper_area_per_layer() + """ + temp_data = {name: 0 for name, _ in self.signal_layers.items()} + outline_area = 0 + for i in self._pedb.modeler.primitives: + layer_name = i.layer.name + if layer_name.lower() == "outline": + if i.area() > outline_area: + outline_area = i.area() + elif layer_name not in temp_data: + continue + elif not i.is_void: + temp_data[layer_name] = temp_data[layer_name] + i.area() + else: + pass + temp_data = {name: area / outline_area * 100 for name, area in temp_data.items()} + return temp_data + + # TODO: This method might need some refactoring + + def _import_dict(self, json_dict, rename=False): + """Import stackup from a dictionary.""" + if not "materials" in json_dict: + self._logger.info("Configuration file does not have material definition. Using aedb and syslib materials.") + else: + mats = json_dict["materials"] + for name, material in mats.items(): + try: + material_name = material["name"] + del material["name"] + except KeyError: + material_name = name + if material_name not in self._pedb.materials: + self._pedb.materials.add_material(material_name, **material) + else: + self._pedb.materials.update_material(material_name, material) + temp = json_dict + if "layers" in json_dict: + temp = {i: j for i, j in json_dict["layers"].items() if j["type"] in ["signal", "dielectric"]} + config_file_layers = list(temp.keys()) + layout_layers = list(self.layers.keys()) + renamed_layers = {} + if rename and len(config_file_layers) == len(layout_layers): + for lay_ind in range(len(list(temp.keys()))): + if not config_file_layers[lay_ind] == layout_layers[lay_ind]: + renamed_layers[layout_layers[lay_ind]] = config_file_layers[lay_ind] + layers_names = list(self.layers.keys())[::] + for name in layers_names: + layer = None + if name in temp: + layer = temp[name] + elif name in renamed_layers: + layer = temp[renamed_layers[name]] + self.layers[name].name = renamed_layers[name] + name = renamed_layers[name] + else: # Remove layers not in config file. + self.remove_layer(name) + self._logger.warning(f"Layer {name} were not found in configuration file, removing layer") + default_layer = { + "name": "default", + "type": "signal", + "material": "copper", + "dielectric_fill": "FR4_epoxy", + "thickness": 3.5e-05, + "etch_factor": 0.0, + "roughness_enabled": False, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0, + "color": [242, 140, 102], + } + if layer: + if "color" in layer: + default_layer["color"] = layer["color"] + elif not layer["type"] == "signal": + default_layer["color"] = [27, 110, 76] + + for k, v in layer.items(): + default_layer[k] = v + self.layers[name]._load_layer(default_layer) + for layer_name, layer in temp.items(): # looping over potential new layers to add + if layer_name in self.layers: + continue # if layer exist, skip + # adding layer + default_layer = { + "name": "default", + "type": "signal", + "material": "copper", + "dielectric_fill": "FR4_epoxy", + "thickness": 3.5e-05, + "etch_factor": 0.0, + "roughness_enabled": False, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0, + "color": [242, 140, 102], + } + + if "color" in layer: + default_layer["color"] = layer["color"] + elif not layer["type"] == "signal": + default_layer["color"] = [27, 110, 76] + + for k, v in layer.items(): + default_layer[k] = v + + temp_2 = list(temp.keys()) + if temp_2.index(layer_name) == 0: + new_layer = self.add_layer( + layer_name, + method="add_on_top", + layer_type=default_layer["type"], + material=default_layer["material"], + fillMaterial=default_layer["dielectric_fill"], + thickness=default_layer["thickness"], + ) + + elif temp_2.index(layer_name) == len(temp_2): + new_layer = self.add_layer( + layer_name, + base_layer=layer_name, + method="add_on_bottom", + layer_type=default_layer["type"], + material=default_layer["material"], + fillMaterial=default_layer["dielectric_fill"], + thickness=default_layer["thickness"], + ) + else: + new_layer = self.add_layer( + layer_name, + base_layer=temp_2[temp_2.index(layer_name) - 1], + method="insert_below", + layer_type=default_layer["type"], + material=default_layer["material"], + fillMaterial=default_layer["dielectric_fill"], + thickness=default_layer["thickness"], + ) + + new_layer.color = default_layer["color"] + new_layer.etch_factor = default_layer["etch_factor"] + + new_layer.roughness_enabled = default_layer["roughness_enabled"] + new_layer.top_hallhuray_nodule_radius = default_layer["top_hallhuray_nodule_radius"] + new_layer.top_hallhuray_surface_ratio = default_layer["top_hallhuray_surface_ratio"] + new_layer.bottom_hallhuray_nodule_radius = default_layer["bottom_hallhuray_nodule_radius"] + new_layer.bottom_hallhuray_surface_ratio = default_layer["bottom_hallhuray_surface_ratio"] + new_layer.side_hallhuray_nodule_radius = default_layer["side_hallhuray_nodule_radius"] + new_layer.side_hallhuray_surface_ratio = default_layer["side_hallhuray_surface_ratio"] + + return True + + def _import_json(self, file_path, rename=False): + """Import stackup from a json file.""" + if file_path: + f = open(file_path) + json_dict = json.load(f) # pragma: no cover + return self._import_dict(json_dict, rename) + + def _import_csv(self, file_path): + """Import stackup definition from a CSV file. + + Parameters + ---------- + file_path : str + File path to the CSV file. + """ + if not pd: + self._pedb.logger.error("Pandas is needed. You must install it first.") + return False + + df = pd.read_csv(file_path, index_col=0) + + for name in self.layers.keys(): # pragma: no cover + if not name in df.index: + logger.error(f"{name} doesn't exist in csv") + return False + + for name, layer_info in df.iterrows(): + layer_type = layer_info.Type + if name in self.layers: + layer = self.layers[name] + layer.type = layer_type + else: + layer = self.add_layer(name, layer_type=layer_type, material="copper", fillMaterial="copper") + + layer.material = layer_info.Material + layer.thickness = layer_info.Thickness + if not str(layer_info.Dielectric_Fill) == "nan": + layer.dielectric_fill = layer_info.Dielectric_Fill + + lc_new = GrpcLayerCollection.create() + for name, _ in df.iterrows(): + layer = self.layers[name] + lc_new.add_layer_bottom(layer) + + for name, layer in self.non_stackup_layers.items(): + lc_new.add_layer_bottom(layer) + + self._pedb.layout.layer_collection = lc_new + return True + + def _set(self, layers=None, materials=None, roughness=None, non_stackup_layers=None): + """Update stackup information. + + Parameters + ---------- + layers: dict + Dictionary containing layer information. + materials: dict + Dictionary containing material information. + roughness: dict + Dictionary containing roughness information. + + Returns + ------- + + """ + if materials: + self._add_materials_from_dictionary(materials) + + if layers: + prev_layer = None + for name, val in layers.items(): + etching_factor = float(val["EtchFactor"]) if "EtchFactor" in val else None + + if not self.layers: + self.add_layer( + name, + None, + "add_on_top", + val["Type"], + val["Material"], + val["FillMaterial"] if val["Type"] == "signal" else "", + val["Thickness"], + etching_factor, + ) + else: + if name in self.layers.keys(): + lyr = self.layers[name] + lyr.type = val["Type"] + lyr.material = val["Material"] + lyr.dielectric_fill = val["FillMaterial"] if val["Type"] == "signal" else "" + lyr.thickness = val["Thickness"] + if prev_layer: + self._set_layout_stackup(lyr._edb_layer, "change_position", prev_layer) + else: + if prev_layer and prev_layer in self.layers: + layer_name = prev_layer + else: + layer_name = list(self.layers.keys())[-1] if self.layers else None + self.add_layer( + name, + layer_name, + "insert_above", + val["Type"], + val["Material"], + val["FillMaterial"] if val["Type"] == "signal" else "", + val["Thickness"], + etching_factor, + ) + prev_layer = name + for name in self.layers: + if name not in layers: + self.remove_layer(name) + + if roughness: + for name, attr in roughness.items(): + layer = self.signal_layers[name] + layer.roughness_enabled = True + + attr_name = "HuraySurfaceRoughness" + if attr_name in attr: + on_surface = "top" + layer.assign_roughness_model( + "huray", + attr[attr_name]["NoduleRadius"], + attr[attr_name]["HallHuraySurfaceRatio"], + apply_on_surface=on_surface, + ) + + attr_name = "HurayBottomSurfaceRoughness" + if attr_name in attr: + on_surface = "bottom" + layer.assign_roughness_model( + "huray", + attr[attr_name]["NoduleRadius"], + attr[attr_name]["HallHuraySurfaceRatio"], + apply_on_surface=on_surface, + ) + attr_name = "HuraySideSurfaceRoughness" + if attr_name in attr: + on_surface = "side" + layer.assign_roughness_model( + "huray", + attr[attr_name]["NoduleRadius"], + attr[attr_name]["HallHuraySurfaceRatio"], + apply_on_surface=on_surface, + ) + + attr_name = "GroissSurfaceRoughness" + if attr_name in attr: + on_surface = "top" + layer.assign_roughness_model( + "groisse", groisse_roughness=attr[attr_name]["Roughness"], apply_on_surface=on_surface + ) + + attr_name = "GroissBottomSurfaceRoughness" + if attr_name in attr: + on_surface = "bottom" + layer.assign_roughness_model( + "groisse", groisse_roughness=attr[attr_name]["Roughness"], apply_on_surface=on_surface + ) + + attr_name = "GroissSideSurfaceRoughness" + if attr_name in attr: + on_surface = "side" + layer.assign_roughness_model( + "groisse", groisse_roughness=attr[attr_name]["Roughness"], apply_on_surface=on_surface + ) + + if non_stackup_layers: + for name, val in non_stackup_layers.items(): + if name in self.non_stackup_layers: + continue + else: + self.add_layer(name, layer_type=val["Type"]) + + return True + + def _get(self): + """Get stackup information from layout. + + Returns: + tuple: (dict, dict, dict) + layers, materials, roughness_models + """ + layers = OrderedDict() + roughness_models = OrderedDict() + for name, val in self.layers.items(): + layer = dict() + layer["Material"] = val.material + layer["Name"] = val.name + layer["Thickness"] = val.thickness + layer["Type"] = val.type + if not val.type == "dielectric": + layer["FillMaterial"] = val.dielectric_fill + layer["EtchFactor"] = val.etch_factor + layers[name] = layer + + if val.roughness_enabled: + roughness_models[name] = {} + model = val.get_roughness_model("top") + if model.type.name.endswith("GroissRoughnessModel"): + roughness_models[name]["GroissSurfaceRoughness"] = {"Roughness": model.get_Roughness.value} + else: + roughness_models[name]["HuraySurfaceRoughness"] = { + "HallHuraySurfaceRatio": model.get_nodule_radius().value, + "NoduleRadius": model.get_surface_ratio().value, + } + model = val.get_roughness_model("bottom") + if model.type.name.endswith("GroissRoughnessModel"): + roughness_models[name]["GroissBottomSurfaceRoughness"] = {"Roughness": model.get_roughness().value} + else: + roughness_models[name]["HurayBottomSurfaceRoughness"] = { + "HallHuraySurfaceRatio": model.get_nodule_radius().value, + "NoduleRadius": model.get_surface_ratio().value, + } + model = val.get_roughness_model("side") + if model.ToString().endswith("GroissRoughnessModel"): + roughness_models[name]["GroissSideSurfaceRoughness"] = {"Roughness": model.get_roughness().value} + else: + roughness_models[name]["HuraySideSurfaceRoughness"] = { + "HallHuraySurfaceRatio": model.get_nodule_radius().value, + "NoduleRadius": model.get_surface_ratio().value, + } + + non_stackup_layers = OrderedDict() + for name, val in self.non_stackup_layers.items(): + layer = dict() + layer["Name"] = val.name + layer["Type"] = val.type + non_stackup_layers[name] = layer + + materials = {} + for name, val in self._pedb.materials.materials.items(): + material = {} + if val.conductivity: + if val.conductivity > 4e7: + material["Conductivity"] = val.conductivity + else: + material["Permittivity"] = val.permittivity + material["DielectricLossTangent"] = val.dielectric_loss_tangent + materials[name] = material + + return layers, materials, roughness_models, non_stackup_layers + + def _add_materials_from_dictionary(self, material_dict): + materials = self.self._pedb.materials.materials + for name, material_properties in material_dict.items(): + if not name in materials: + if "Conductivity" in material_properties: + materials.add_conductor_material(name, material_properties["Conductivity"]) + else: + materials.add_dielectric_material( + name, + material_properties["Permittivity"], + material_properties["DielectricLossTangent"], + ) + else: + material = materials[name] + if "Conductivity" in material_properties: + material.conductivity = material_properties["Conductivity"] + else: + material.permittivity = material_properties["Permittivity"] + material.loss_tanget = material_properties["DielectricLossTangent"] + return True + + def _import_xml(self, file_path, rename=False): + """Read external xml file and convert into json file. + You can use xml file to import layer stackup but using json file is recommended. + see :class:`pyedb.dotnet.database.edb_data.simulation_configuration.SimulationConfiguration´ class to + generate files`. + + Parameters + ---------- + file_path: str + Path to external XML file. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + if not colors: + self._pedb.logger.error("Matplotlib is needed. Please, install it first.") + return False + tree = ET.parse(file_path) + root = tree.getroot() + stackup = root.find("Stackup") + stackup_dict = {} + if stackup.find("Materials"): + mats = [] + for m in stackup.find("Materials").findall("Material"): + temp = dict() + for i in list(m): + value = list(i)[0].text + temp[i.tag] = value + mat = {"name": m.attrib["Name"]} + temp_dict = { + "Permittivity": "permittivity", + "Conductivity": "conductivity", + "DielectricLossTangent": "dielectric_loss_tangent", + } + for i in temp_dict.keys(): + value = temp.get(i, None) + if value: + mat[temp_dict[i]] = value + mats.append(mat) + stackup_dict["materials"] = mats + + stackup_section = stackup.find("Layers") + if stackup_section: + length_unit = stackup_section.attrib["LengthUnit"] + layers = [] + for l in stackup.find("Layers").findall("Layer"): + temp = l.attrib + layer = dict() + temp_dict = { + "Name": "name", + "Color": "color", + "Material": "material", + "Thickness": "thickness", + "Type": "type", + "FillMaterial": "fill_material", + } + for i in temp_dict.keys(): + value = temp.get(i, None) + if value: + if i == "Thickness": + value = str(round(float(value), 6)) + length_unit + value = "signal" if value == "conductor" else value + if i == "Color": + value = [int(x * 255) for x in list(colors.to_rgb(value))] + layer[temp_dict[i]] = value + layers.append(layer) + stackup_dict["layers"] = layers + cfg = {"stackup": stackup_dict} + return self._pedb.configuration.load(cfg, apply_file=True) + + def _export_xml(self, file_path): + """Export stackup information to an external XMLfile. + + Parameters + ---------- + file_path: str + Path to external XML file. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + """ + layers, materials, roughness, non_stackup_layers = self._get() + + root = ET.Element("{http://www.ansys.com/control}Control", attrib={"schemaVersion": "1.0"}) + + el_stackup = ET.SubElement(root, "Stackup", {"schemaVersion": "1.0"}) + + el_materials = ET.SubElement(el_stackup, "Materials") + for mat, val in materials.items(): + material = ET.SubElement(el_materials, "Material") + material.set("Name", mat) + for pname, pval in val.items(): + mat_prop = ET.SubElement(material, pname) + value = ET.SubElement(mat_prop, "Double") + value.text = str(pval) + + el_layers = ET.SubElement(el_stackup, "Layers", {"LengthUnit": "meter"}) + for lyr, val in layers.items(): + layer = ET.SubElement(el_layers, "Layer") + val = {i: str(j) for i, j in val.items()} + if val["Type"] == "signal": + val["Type"] = "conductor" + layer.attrib.update(val) + + for lyr, val in non_stackup_layers.items(): + layer = ET.SubElement(el_layers, "Layer") + val = {i: str(j) for i, j in val.items()} + layer.attrib.update(val) + + for lyr, val in roughness.items(): + el = el_layers.find("./Layer[@Name='{}']".format(lyr)) + for pname, pval in val.items(): + pval = {i: str(j) for i, j in pval.items()} + ET.SubElement(el, pname, pval) + + write_pretty_xml(root, file_path) + return True + + def load(self, file_path, rename=False): + """Import stackup from a file. The file format can be XML, CSV, or JSON. Valid control file must + have the same number of signal layers. Signals layers can be renamed. Dielectric layers can be + added and deleted. + + + Parameters + ---------- + file_path : str, dict + Path to stackup file or dict with stackup details. + rename : bool + If rename is ``False`` then layer in layout not found in the stackup file are deleted. + Otherwise, if the number of layer in the stackup file equals the number of stackup layer + in the layout, layers are renamed according the file. + Note that layer order matters, and has to be writtent from top to bottom layer in the file. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb() + >>> edb.stackup.load("stackup.xml") + """ + + if isinstance(file_path, dict): + return self._import_dict(file_path) + elif file_path.endswith(".csv"): + return self._import_csv(file_path) + elif file_path.endswith(".json"): + return self._import_json(file_path, rename=rename) + elif file_path.endswith(".xml"): + return self._import_xml(file_path, rename=rename) + else: + return False + + def plot( + self, + save_plot=None, + size=(2000, 1500), + plot_definitions=None, + first_layer=None, + last_layer=None, + scale_elevation=True, + show=True, + ): + """Plot current stackup and, optionally, overlap padstack definitions. + Plot supports only 'Laminate' and 'Overlapping' stackup types. + + Parameters + ---------- + save_plot : str, optional + If a path is specified the plot will be saved in this location. + If ``save_plot`` is provided, the ``show`` parameter is ignored. + size : tuple, optional + Image size in pixel (width, height). Default value is ``(2000, 1500)`` + plot_definitions : str, list, optional + List of padstack definitions to plot on the stackup. + It is supported only for Laminate mode. + first_layer : str or :class:`pyedb.dotnet.database.edb_data.layer_data.LayerEdbClass` + First layer to plot from the bottom. Default is `None` to start plotting from bottom. + last_layer : str or :class:`pyedb.dotnet.database.edb_data.layer_data.LayerEdbClass` + Last layer to plot from the bottom. Default is `None` to plot up to top layer. + scale_elevation : bool, optional + The real layer thickness is scaled so that max_thickness = 3 * min_thickness. + Default is `True`. + show : bool, optional + Whether to show the plot or not. Default is `True`. + + Returns + ------- + :class:`matplotlib.plt` + """ + + from pyedb.generic.constants import CSS4_COLORS + from pyedb.generic.plot import plot_matplotlib + + layer_names = list(self.layers.keys()) + if first_layer is None or first_layer not in layer_names: + bottom_layer = layer_names[-1] + elif isinstance(first_layer, str): + bottom_layer = first_layer + elif isinstance(first_layer, Layer): + bottom_layer = first_layer.name + else: + raise AttributeError("first_layer must be str or class `dotnet.database.edb_data.layer_data.LayerEdbClass`") + if last_layer is None or last_layer not in layer_names: + top_layer = layer_names[0] + elif isinstance(last_layer, str): + top_layer = last_layer + elif isinstance(last_layer, Layer): + top_layer = last_layer.name + else: + raise AttributeError("last_layer must be str or class `dotnet.database.edb_data.layer_data.LayerEdbClass`") + + stackup_mode = self.mode + if stackup_mode not in ["laminate", "overlapping"]: + raise AttributeError("stackup plot supports only 'laminate' and 'overlapping' stackup types.") + + # build the layers data + layers_data = [] + skip_flag = True + for layer in self.layers.values(): # start from top + if layer.name != top_layer and skip_flag: + continue + else: + skip_flag = False + layers_data.append([layer, layer.lower_elevation, layer.upper_elevation, layer.thickness]) + if layer.name == bottom_layer: + break + layers_data.reverse() # let's start from the bottom + + # separate dielectric and signal if overlapping stackup + if stackup_mode == "overlapping": + dielectric_layers = [l for l in layers_data if l[0].type == "dielectric"] + signal_layers = [l for l in layers_data if l[0].type == "signal"] + + # compress the thicknesses if required + if scale_elevation: + min_thickness = min([i[3] for i in layers_data if i[3] != 0]) + max_thickness = max([i[3] for i in layers_data]) + c = 3 # max_thickness = c * min_thickness + + def _compress_t(y): + m = min_thickness + M = max_thickness + k = (c - 1) * m / (M - m) + if y > 0: + return (y - m) * k + m + else: + return 0.0 + + if stackup_mode == "laminate": + l0 = layers_data[0] + compressed_layers_data = [[l0[0], l0[1], _compress_t(l0[3]), _compress_t(l0[3])]] # the first row + lp = compressed_layers_data[0] + for li in layers_data[1:]: # the other rows + ct = _compress_t(li[3]) + compressed_layers_data.append([li[0], lp[2], lp[2] + ct, ct]) + lp = compressed_layers_data[-1] + layers_data = compressed_layers_data + + elif stackup_mode == "overlapping": + compressed_diels = [] + first_diel = True + for li in dielectric_layers: + ct = _compress_t(li[3]) + if first_diel: + if li[1] > 0: + l0le = _compress_t(li[1]) + else: + l0le = li[1] + compressed_diels.append([li[0], l0le, l0le + ct, ct]) + first_diel = False + else: + lp = compressed_diels[-1] + compressed_diels.append([li[0], lp[2], lp[2] + ct, ct]) + + def _convert_elevation(el): + inside = False + for i, li in enumerate(dielectric_layers): + if li[1] <= el <= li[2]: + inside = True + break + if inside: + u = (el - li[1]) / (li[2] - li[1]) + cli = compressed_diels[i] + cel = cli[1] + u * (cli[2] - cli[1]) + else: + cel = el + return cel + + compressed_signals = [] + for li in signal_layers: + cle = _convert_elevation(li[1]) + cue = _convert_elevation(li[2]) + ct = cue - cle + compressed_signals.append([li[0], cle, cue, ct]) + + dielectric_layers = compressed_diels + signal_layers = compressed_signals + + # create the data for the plot + diel_alpha = 0.4 + signal_alpha = 0.6 + zero_thickness_alpha = 1.0 + annotation_fontsize = 14 + annotation_x_margin = 0.01 + annotations = [] + plot_data = [] + if stackup_mode == "laminate": + min_thickness = min([i[3] for i in layers_data if i[3] != 0]) + for ly in layers_data: + layer = ly[0] + + # set color and label + color = [float(i) / 256 for i in layer.color] + if color == [1.0, 1.0, 1.0]: + color = [0.9, 0.9, 0.9] + label = "{}, {}, thick: {:.3f}um, elev: {:.3f}um".format( + layer.name, layer.material, layer.thickness * 1e6, layer.lower_elevation * 1e6 + ) + + # create patch + x = [0, 0, 1, 1] + if ly[3] > 0: + lower_elevation = ly[1] + upper_elevation = ly[2] + y = [lower_elevation, upper_elevation, upper_elevation, lower_elevation] + plot_data.insert(0, [x, y, color, label, signal_alpha, "fill"]) + else: + lower_elevation = ly[1] - min_thickness * 0.1 # make the zero thickness layers more visible + upper_elevation = ly[2] + min_thickness * 0.1 + y = [lower_elevation, upper_elevation, upper_elevation, lower_elevation] + # put the zero thickness layers on top + plot_data.append([x, y, color, label, zero_thickness_alpha, "fill"]) + + # create annotation + y_pos = (lower_elevation + upper_elevation) / 2 + if layer.type == "dielectric": + x_pos = -annotation_x_margin + annotations.append( + [x_pos, y_pos, layer.name, {"fontsize": annotation_fontsize, "horizontalalignment": "right"}] + ) + elif layer.type == "signal": + x_pos = 1.0 + annotation_x_margin + annotations.append([x_pos, y_pos, layer.name, {"fontsize": annotation_fontsize}]) + + # evaluate the legend reorder + legend_order = [] + for ly in layers_data: + name = ly[0].name + for i, a in enumerate(plot_data): + iname = a[3].split(",")[0] + if name == iname: + legend_order.append(i) + break + + elif stackup_mode == "overlapping": + min_thickness = min([i[3] for i in signal_layers if i[3] != 0]) + columns = [] # first column is x=[0,1], second column is x=[1,2] and so on... + for ly in signal_layers: + lower_elevation = ly[1] # lower elevation + t = ly[3] # thickness + put_in_column = 0 + cell_position = 0 + for c in columns: + uep = c[-1][0][2] # upper elevation of the last entry of that column + tp = c[-1][0][3] # thickness of the last entry of that column + if lower_elevation < uep or (abs(lower_elevation - uep) < 1e-15 and tp == 0 and t == 0): + put_in_column += 1 + cell_position = len(c) + else: + break + if len(columns) < put_in_column + 1: # add a new column if required + columns.append([]) + # put zeros at the beginning of the column until there is the first layer + if cell_position != 0: + fill_cells = cell_position - 1 - len(columns[put_in_column]) + for i in range(fill_cells): + columns[put_in_column].append(0) + # append the layer to the proper column and row + x = [put_in_column + 1, put_in_column + 1, put_in_column + 2, put_in_column + 2] + columns[put_in_column].append([ly, x]) + + # fill the columns matrix with zeros on top + n_rows = max([len(i) for i in columns]) + for c in columns: + while len(c) < n_rows: + c.append(0) + # expand to the right the fill for the signals that have no overlap on the right + width = len(columns) + 1 + for i, c in enumerate(columns[:-1]): + for j, r in enumerate(c): + if r != 0: # and dname == r[0].name: + if columns[i + 1][j] == 0: + # nothing on the right, so expand the fill + x = r[1] + r[1] = [x[0], x[0], width, width] + + for c in columns: + for r in c: + if r != 0: + ly = r[0] + layer = ly[0] + x = r[1] + + # set color and label + color = [float(i) / 256 for i in layer.color] + if color == [1.0, 1.0, 1.0]: + color = [0.9, 0.9, 0.9] + label = "{}, {}, thick: {:.3f}um, elev: {:.3f}um".format( + layer.name, layer.material, layer.thickness * 1e6, layer.lower_elevation * 1e6 + ) + + if ly[3] > 0: + lower_elevation = ly[1] + upper_elevation = ly[2] + y = [lower_elevation, upper_elevation, upper_elevation, lower_elevation] + plot_data.insert(0, [x, y, color, label, signal_alpha, "fill"]) + else: + lower_elevation = ly[1] - min_thickness * 0.1 # make the zero thickness layers more visible + upper_elevation = ly[2] + min_thickness * 0.1 + y = [lower_elevation, upper_elevation, upper_elevation, lower_elevation] + # put the zero thickness layers on top + plot_data.append([x, y, color, label, zero_thickness_alpha, "fill"]) + + # create annotation + x_pos = 1.0 + y_pos = (lower_elevation + upper_elevation) / 2 + annotations.append([x_pos, y_pos, layer.name, {"fontsize": annotation_fontsize}]) + + # order the annotations based on y_pos (it is necessary later to move them to avoid text overlapping) + annotations.sort(key=lambda e: e[1]) + # move all the annotations to the final x (it could be larger than 1 due to additional columns) + width = len(columns) + 1 + for i, a in enumerate(annotations): + a[0] = width + annotation_x_margin * width + + for ly in dielectric_layers: + layer = ly[0] + # set color and label + color = [float(i) / 256 for i in layer.color] + if color == [1.0, 1.0, 1.0]: + color = [0.9, 0.9, 0.9] + label = "{}, {}, thick: {:.3f}um, elev: {:.3f}um".format( + layer.name, layer.material, layer.thickness * 1e6, layer.lower_elevation * 1e6 + ) + # create the patch + lower_elevation = ly[1] + upper_elevation = ly[2] + y = [lower_elevation, upper_elevation, upper_elevation, lower_elevation] + x = [0, 0, width, width] + plot_data.insert(0, [x, y, color, label, diel_alpha, "fill"]) + + # create annotation + x_pos = -annotation_x_margin * width + y_pos = (lower_elevation + upper_elevation) / 2 + annotations.append( + [x_pos, y_pos, layer.name, {"fontsize": annotation_fontsize, "horizontalalignment": "right"}] + ) + + # evaluate the legend reorder + legend_order = [] + for ly in dielectric_layers: + name = ly[0].name + for i, a in enumerate(plot_data): + iname = a[3].split(",")[0] + if name == iname: + legend_order.append(i) + break + for ly in signal_layers: + name = ly[0].name + for i, a in enumerate(plot_data): + iname = a[3].split(",")[0] + if name == iname: + legend_order.append(i) + break + + # calculate the extremities of the plot + x_min = 0.0 + x_max = max([max(i[0]) for i in plot_data]) + if stackup_mode == "laminate": + y_min = layers_data[0][1] + y_max = layers_data[-1][2] + elif stackup_mode == "overlapping": + y_min = min(dielectric_layers[0][1], signal_layers[0][1]) + y_max = max(dielectric_layers[-1][2], signal_layers[-1][2]) + + # move the annotations to avoid text overlapping + new_annotations = [] + for i, a in enumerate(annotations): + if i > 0 and abs(a[1] - annotations[i - 1][1]) < (y_max - y_min) / 75: + new_annotations[-1][2] = str(new_annotations[-1][2]) + ", " + str(a[2]) + else: + new_annotations.append(a) + annotations = new_annotations + + if plot_definitions: + if stackup_mode == "overlapping": + self._logger.warning("Plot of padstacks are supported only for Laminate mode.") + + max_plots = 10 + + if not isinstance(plot_definitions, list): + plot_definitions = [plot_definitions] + color_index = 0 + color_keys = list(CSS4_COLORS.keys()) + delta = 1 / (max_plots + 1) # padstack spacing in plot coordinates + x_start = delta + + # find the max padstack size to calculate the scaling factor + max_padstak_size = 0.0 + for definition in plot_definitions: + if isinstance(definition, str): + definition = self._pedb.padstacks.definitions[definition] + for layer, defs in definition.pad_by_layer.items(): + pad_shape = defs.shape + params = defs.parameters_values + pad_size = max([p for p in params]) + if pad_size > max_padstak_size: + max_padstak_size = pad_size + if not definition.is_null: + hole_d = definition.hole_diameter + max_padstak_size = max(hole_d, max_padstak_size) + scaling_f_pad = (2 / ((max_plots + 1) * 3)) / max_padstak_size + + for definition in plot_definitions: + if isinstance(definition, str): + definition = self._pedb.padstacks.definitions[definition] + min_le = 1e12 + max_ue = -1e12 + max_x = 0 + padstack_name = definition.name + annotations.append([x_start, y_max, padstack_name, {"rotation": 45}]) + + via_start_layer = definition.start_layer + via_stop_layer = definition.stop_layer + + if stackup_mode == "overlapping": + # here search the column using the first and last layer. Pick the column with max index. + pass + + for layer, defs in definition.pad_by_layer.items(): + pad_shape = defs.shape + params = defs.parameters_values + pad_size = max([p for p in params]) + if stackup_mode == "laminate": + x = [ + x_start - pad_size / 2 * scaling_f_pad, + x_start - pad_size / 2 * scaling_f_pad, + x_start + pad_size / 2 * scaling_f_pad, + x_start + pad_size / 2 * scaling_f_pad, + ] + lower_elevation = [e[1] for e in layers_data if e[0].name == layer or layer == "Default"][0] + upper_elevation = [e[2] for e in layers_data if e[0].name == layer or layer == "Default"][0] + y = [lower_elevation, upper_elevation, upper_elevation, lower_elevation] + # create the patch for that signal layer + plot_data.append([x, y, color_keys[color_index], None, 1.0, "fill"]) + elif stackup_mode == "overlapping": + # here evaluate the x based on the column evaluated before and the pad size + pass + + min_le = min(lower_elevation, min_le) + max_ue = max(upper_elevation, max_ue) + if not definition.is_null: + # create patch for the hole + hole_radius = definition.hole_diameter / 2 * scaling_f_pad + x = [x_start - hole_radius, x_start - hole_radius, x_start + hole_radius, x_start + hole_radius] + y = [min_le, max_ue, max_ue, min_le] + plot_data.append([x, y, color_keys[color_index], None, 0.7, "fill"]) + # create patch for the dielectric + max_x = max(max_x, hole_radius) + rad = hole_radius * (100 - definition.hole_plating_ratio) / 100 + x = [x_start - rad, x_start - rad, x_start + rad, x_start + rad] + plot_data.append([x, y, color_keys[color_index], None, 1.0, "fill"]) + + color_index += 1 + if color_index == max_plots: + self._logger.warning("Maximum number of definitions plotted.") + break + x_start += delta + + # plot the stackup + plt = plot_matplotlib( + plot_data, + size=size, + show_legend=False, + xlabel="", + ylabel="", + title="", + save_plot=None, + x_limits=[x_min, x_max], + y_limits=[y_min, y_max], + axis_equal=False, + annotations=annotations, + show=False, + ) + # we have to customize some defaults, so we plot or save the figure here + plt.axis("off") + plt.box(False) + plt.title("Stackup\n ", fontsize=28) + # evaluates the number of legend column based on the layer name max length + ncol = 3 if max([len(n) for n in layer_names]) < 15 else 2 + handles, labels = plt.gca().get_legend_handles_labels() + plt.legend( + [handles[idx] for idx in legend_order], + [labels[idx] for idx in legend_order], + bbox_to_anchor=(0, -0.05), + loc="upper left", + borderaxespad=0, + ncol=ncol, + ) + plt.tight_layout() + if save_plot: + plt.savefig(save_plot) + elif show: + plt.show() + return plt diff --git a/src/pyedb/grpc/database/terminal/__init__.py b/src/pyedb/grpc/database/terminal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/pyedb/grpc/database/terminal/bundle_terminal.py b/src/pyedb/grpc/database/terminal/bundle_terminal.py new file mode 100644 index 0000000000..7230f16a37 --- /dev/null +++ b/src/pyedb/grpc/database/terminal/bundle_terminal.py @@ -0,0 +1,145 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.terminal.terminals import ( + SourceTermToGroundType as GrpcSourceTermToGroundType, +) +from ansys.edb.core.terminal.terminals import BundleTerminal as GrpcBundleTerminal +from ansys.edb.core.terminal.terminals import HfssPIType as GrpcHfssPIType +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.grpc.database.hierarchy.component import Component +from pyedb.grpc.database.layers.layer import Layer +from pyedb.grpc.database.nets.net import Net +from pyedb.grpc.database.terminal.terminal import Terminal +from pyedb.grpc.database.utility.rlc import Rlc + + +class BundleTerminal(GrpcBundleTerminal): + """Manages bundle terminal properties. + + Parameters + ---------- + pedb : pyedb.edb.Edb + EDB object from the ``Edblib`` library. + edb_object : Ansys.Ansoft.Edb.Cell.Terminal.BundleTerminal + BundleTerminal instance from EDB. + """ + + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb + self._edb_object = edb_object + + def decouple(self): + """Ungroup a bundle of terminals.""" + return self.ungroup() + + @property + def component(self): + return Component(self._pedb, self.component) + + @property + def impedance(self): + return self.impedance.value + + @impedance.setter + def impedance(self, value): + self.impedance = GrpcValue(value) + + @property + def net(self): + return Net(self._pedb, self.net) + + @property + def hfss_pi_type(self): + return self.hfss_pi_type.name + + @hfss_pi_type.setter + def hfss_pi_type(self, value): + if value.upper() == "DEFAULT": + self.hfss_pi_type = GrpcHfssPIType.DEFAULT + elif value.upper() == "COAXIAL_OPEN": + self.hfss_pi_type = GrpcHfssPIType.COAXIAL_OPEN + elif value.upper() == "COAXIAL_SHORTENED": + self.hfss_pi_type = GrpcHfssPIType.COAXIAL_SHORTENED + elif value.upper() == "GAP": + self.hfss_pi_type = GrpcHfssPIType.GAP + elif value.upper() == "LUMPED": + self.hfss_pi_type = GrpcHfssPIType.LUMPED + + @property + def reference_layer(self): + return Layer(self._pedb, self.reference_layer) + + @reference_layer.setter + def reference_layer(self, value): + if isinstance(value, Layer): + self.reference_layer = value._edb_object + elif isinstance(value, str): + self.reference_layer = self._pedb.stackup.signal_layer[value]._edb_object + + @property + def reference_terminal(self): + return Terminal(self._pedb, self.reference_terminal) + + @reference_terminal.setter + def reference_terminal(self, value): + if isinstance(value, Terminal): + self.reference_terminal = value._edb_object + + @property + def rlc_boundary_parameters(self): + return Rlc(self._pedb, self.rlc) + + @property + def source_amplitude(self): + return self.source_amplitude.value + + @source_amplitude.setter + def source_amplitude(self, value): + self.source_amplitude = GrpcValue(value) + + @property + def source_phase(self): + return self.source_phase.value + + @source_phase.setter + def source_phase(self, value): + self.source_phase = GrpcValue(value) + + @property + def term_to_ground(self): + return self.term_to_ground.name + + @term_to_ground.setter + def term_to_ground(self, value): + if value.upper() == "NO_GROUND": + self.term_to_ground = GrpcSourceTermToGroundType.NO_GROUND + elif value.upper() == "NEGATIVE": + self.term_to_ground = GrpcSourceTermToGroundType.NEGATIVE + elif value.upper() == "POSITIVE": + self.term_to_ground = GrpcSourceTermToGroundType.POSITIVE + + @property + def terminals(self): + return [Terminal(self._pedb, terminal) for terminal in self.terminals] diff --git a/src/pyedb/grpc/database/terminal/edge_terminal.py b/src/pyedb/grpc/database/terminal/edge_terminal.py new file mode 100644 index 0000000000..4f3530e463 --- /dev/null +++ b/src/pyedb/grpc/database/terminal/edge_terminal.py @@ -0,0 +1,51 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.terminal.terminals import BundleTerminal as GrpcBundleTerminal +from ansys.edb.core.terminal.terminals import EdgeTerminal as GrpcEdgeTerminal + + +class EdgeTerminal(GrpcEdgeTerminal): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb + self._edb_object = edb_object + + def couple_ports(self, port): + """Create a bundle wave port. + + Parameters + ---------- + port : :class:`dotnet.database.ports.WavePort`, :class:`dotnet.database.ports.GapPort`, list, optional + Ports to be added. + + Returns + ------- + :class:`dotnet.database.ports.BundleWavePort` + + """ + if not isinstance(port, (list, tuple)): + port = [port] + temp = [self] + temp.extend([i for i in port]) + bundle_terminal = GrpcBundleTerminal.create(temp) + return self._pedb.ports[bundle_terminal.name] diff --git a/src/pyedb/grpc/database/terminal/padstack_instance_terminal.py b/src/pyedb/grpc/database/terminal/padstack_instance_terminal.py new file mode 100644 index 0000000000..9fb10406bb --- /dev/null +++ b/src/pyedb/grpc/database/terminal/padstack_instance_terminal.py @@ -0,0 +1,129 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.terminal.terminals import ( + PadstackInstanceTerminal as GrpcPadstackInstanceTerminal, +) +from ansys.edb.core.terminal.terminals import BoundaryType as GrpcBoundaryType + + +class PadstackInstanceTerminal(GrpcPadstackInstanceTerminal): + """Manages bundle terminal properties.""" + + def __init__(self, pedb, edb_object=None): + if edb_object: + super().__init__(edb_object.msg) + self._pedb = pedb + + @property + def position(self): + """Return terminal position. + Returns + ------- + Position [x,y] : [float, float] + """ + pos_x, pos_y, rotation = self.padstack_instance.get_position_and_rotation() + return [pos_x.value, pos_y.value] + + @property + def location(self): + p_inst, _ = self.params + pos_x, pos_y, _ = p_inst.get_position_and_rotation() + return [pos_x.value, pos_y.value] + + @property + def net_name(self): + """Net name. + + Returns + ------- + str + Name of the net. + """ + if self.is_null: + return "" + elif self.net.is_null: + return "" + else: + return self.net.name + + @net_name.setter + def net_name(self, val): + if not self.is_null and self.net.is_null: + self.net.name = val + + @property + def magnitude(self): + return self.source_amplitude + + @magnitude.setter + def magnitude(self, value): + self.source_amplitude = value + + @property + def phase(self): + return self.source_phase + + @phase.setter + def phase(self, value): + self.source_phase = value + + @property + def source_amplitude(self): + return super().source_amplitude + + @source_amplitude.setter + def source_amplitude(self, value): + super(PadstackInstanceTerminal, self.__class__).source_amplitude.__set__(self, value) + + @property + def source_phase(self): + return super().source_phase.value + + @source_phase.setter + def source_phase(self, value): + super(PadstackInstanceTerminal, self.__class__).source_phase.__set__(self, value) + + @property + def impedance(self): + return super().impedance.value + + @impedance.setter + def impedance(self, value): + super(PadstackInstanceTerminal, self.__class__).impedance.__set__(self, value) + + @property + def boundary_type(self): + return super().boundary_type.name.lower() + + @boundary_type.setter + def boundary_type(self, value): + mapping = { + "port": GrpcBoundaryType.PORT, + "dc_terminal": GrpcBoundaryType.DC_TERMINAL, + "voltage_probe": GrpcBoundaryType.VOLTAGE_PROBE, + "voltage_source": GrpcBoundaryType.VOLTAGE_SOURCE, + "current_source": GrpcBoundaryType.CURRENT_SOURCE, + "rlc": GrpcBoundaryType.RLC, + "pec": GrpcBoundaryType.PEC, + } + super(PadstackInstanceTerminal, self.__class__).boundary_type.__set__(self, mapping[value.name.lower()]) diff --git a/src/pyedb/grpc/database/terminal/pingroup_terminal.py b/src/pyedb/grpc/database/terminal/pingroup_terminal.py new file mode 100644 index 0000000000..a8dc2fc5b6 --- /dev/null +++ b/src/pyedb/grpc/database/terminal/pingroup_terminal.py @@ -0,0 +1,105 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.terminal.terminals import BoundaryType as GrpcBoundaryType +from ansys.edb.core.terminal.terminals import PinGroupTerminal as GrpcPinGroupTerminal + +from pyedb.grpc.database.nets.net import Net + + +class PinGroupTerminal(GrpcPinGroupTerminal): + """Manages pin group terminal properties.""" + + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._edb_object = edb_object + self._pedb = pedb + + @property + def boundary_type(self): + return super().boundary_type.name.lower() + + @boundary_type.setter + def boundary_type(self, value): + if value == "voltage_source": + value = GrpcBoundaryType.VOLTAGE_SOURCE + if value == "current_source": + value = GrpcBoundaryType.CURRENT_SOURCE + if value == "port": + value = GrpcBoundaryType.PORT + if value == "voltage_probe": + value = GrpcBoundaryType.VOLTAGE_PROBE + super(PinGroupTerminal, self.__class__).boundary_type.__set__(self, value) + + @property + def magnitude(self): + return self.source_amplitude + + @magnitude.setter + def magnitude(self, value): + self.source_amplitude = value + + @property + def phase(self): + return self.source_phase + + @phase.setter + def phase(self, value): + self.source_phase = value + + @property + def source_amplitude(self): + return super().source_amplitude + + @source_amplitude.setter + def source_amplitude(self, value): + super(PinGroupTerminal, self.__class__).source_amplitude.__set__(self, value) + + @property + def source_phase(self): + return super().source_amplitude.value + + @source_phase.setter + def source_phase(self, value): + super(PinGroupTerminal, self.__class__).source_phase.__set__(self, value) + + @property + def impedance(self): + return super().impedance.value + + @impedance.setter + def impedance(self, value): + super(PinGroupTerminal, self.__class__).impedance.__set__(self, value) + + @property + def net(self): + return Net(self._pedb, super().net) + + @net.setter + def net(self, value): + super(PinGroupTerminal, self.__class__).net.__set__(self, value) + + @property + def pin_group(self): + from pyedb.grpc.database.hierarchy.pingroup import PinGroup + + return PinGroup(self._pedb, super().pin_group) diff --git a/src/pyedb/grpc/database/terminal/point_terminal.py b/src/pyedb/grpc/database/terminal/point_terminal.py new file mode 100644 index 0000000000..7db8d7948b --- /dev/null +++ b/src/pyedb/grpc/database/terminal/point_terminal.py @@ -0,0 +1,71 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.geometry.point_data import PointData as GrpcPointData +from ansys.edb.core.terminal.terminals import PointTerminal as GrpcPointTerminal +from ansys.edb.core.utility.value import Value as GrpcValue + + +class PointTerminal(GrpcPointTerminal): + """Manages point terminal properties.""" + + def __init__(self, pedb, edb_object): + super().__init__(edb_object.msg) + self._pedb = pedb + + @property + def location(self): + return [self.point.x.value, self.point.y.value] + + @location.setter + def location(self, value): + if not isinstance(value, list): + return + value = [GrpcValue(i) for i in value] + self.point = GrpcPointData(value) + + @property + def layer(self): + from pyedb.grpc.database.layers.stackup_layer import StackupLayer + + return StackupLayer(self._pedb, super().layer) + + @layer.setter + def layer(self, value): + if value in self._pedb.stackup.layers: + super(PointTerminal, self.__class__).layer.__set__(self, value) + + @property + def ref_terminal(self): + return PointTerminal(self._pedb, self.reference_terminal) + + @ref_terminal.setter + def ref_terminal(self, value): + super().reference_terminal = value + + @property + def reference_terminal(self): + return PointTerminal(self._pedb, super().reference_terminal) + + @reference_terminal.setter + def reference_terminal(self, value): + super(PointTerminal, self.__class__).reference_terminal.__set__(self, value) diff --git a/src/pyedb/grpc/database/terminal/terminal.py b/src/pyedb/grpc/database/terminal/terminal.py new file mode 100644 index 0000000000..8b11d1f962 --- /dev/null +++ b/src/pyedb/grpc/database/terminal/terminal.py @@ -0,0 +1,410 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import re + +from ansys.edb.core.terminal.terminals import BoundaryType as GrpcBoundaryType +from ansys.edb.core.terminal.terminals import EdgeType as GrpcEdgeType +from ansys.edb.core.terminal.terminals import Terminal as GrpcTerminal +from ansys.edb.core.terminal.terminals import TerminalType as GrpcTerminalType +from ansys.edb.core.utility.value import Value as GrpcValue + +from pyedb.dotnet.database.edb_data.padstacks_data import EDBPadstackInstance +from pyedb.dotnet.database.edb_data.primitives_data import cast + + +class Terminal(GrpcTerminal): + def __init__(self, pedb, edb_object): + super().__init__(edb_object.msg) + self._pedb = pedb + self._reference_object = None + self.edb_object = edb_object + + self._boundary_type_mapping = { + "port": GrpcBoundaryType.PORT, + "pec": GrpcBoundaryType.PEC, + "rlc": GrpcBoundaryType.RLC, + "current_source": GrpcBoundaryType.CURRENT_SOURCE, + "voltage_source": GrpcBoundaryType.VOLTAGE_SOURCE, + "nexxim_ground": GrpcBoundaryType.NEXXIM_GROUND, + "nxxim_port": GrpcBoundaryType.NEXXIM_PORT, + "dc_terminal": GrpcBoundaryType.DC_TERMINAL, + "voltage_probe": GrpcBoundaryType.VOLTAGE_PROBE, + } + + self._terminal_type_mapping = { + "edge": GrpcTerminalType.EDGE, + "point": GrpcTerminalType.POINT, + "terminal_instance": GrpcTerminalType.TERM_INST, + "padstack_instance": GrpcTerminalType.PADSTACK_INST, + "bundle": GrpcTerminalType.BUNDLE, + "pin_group": GrpcTerminalType.PIN_GROUP, + } + + @property + def _hfss_port_property(self): + """HFSS port property.""" + hfss_prop = re.search(r"HFSS\(.*?\)", self._edb_properties) + p = {} + if hfss_prop: + hfss_type = re.search(r"'HFSS Type'='([^']+)'", hfss_prop.group()) + orientation = re.search(r"'Orientation'='([^']+)'", hfss_prop.group()) + horizontal_ef = re.search(r"'Horizontal Extent Factor'='([^']+)'", hfss_prop.group()) + vertical_ef = re.search(r"'Vertical Extent Factor'='([^']+)'", hfss_prop.group()) + radial_ef = re.search(r"'Radial Extent Factor'='([^']+)'", hfss_prop.group()) + pec_w = re.search(r"'PEC Launch Width'='([^']+)'", hfss_prop.group()) + + p["HFSS Type"] = hfss_type.group(1) if hfss_type else "" + p["Orientation"] = orientation.group(1) if orientation else "" + p["Horizontal Extent Factor"] = float(horizontal_ef.group(1)) if horizontal_ef else "" + p["Vertical Extent Factor"] = float(vertical_ef.group(1)) if vertical_ef else "" + p["Radial Extent Factor"] = float(radial_ef.group(1)) if radial_ef else "" + p["PEC Launch Width"] = pec_w.group(1) if pec_w else "" + else: + p["HFSS Type"] = "" + p["Orientation"] = "" + p["Horizontal Extent Factor"] = "" + p["Vertical Extent Factor"] = "" + p["Radial Extent Factor"] = "" + p["PEC Launch Width"] = "" + return p + + @property + def ref_terminal(self): + return self.reference_terminal + + @ref_terminal.setter + def ref_terminal(self, value): + self.reference_terminal = value + + @_hfss_port_property.setter + def _hfss_port_property(self, value): + txt = [] + for k, v in value.items(): + txt.append("'{}'='{}'".format(k, v)) + txt = ",".join(txt) + self._edb_properties = "HFSS({})".format(txt) + + @property + def hfss_type(self): + """HFSS port type.""" + return self._hfss_port_property["HFSS Type"] + + @hfss_type.setter + def hfss_type(self, value): + p = self._hfss_port_property + p["HFSS Type"] = value + self._hfss_port_property = p + + @property + def layer(self): + """Get layer of the terminal.""" + return self.reference_layer.name + + @layer.setter + def layer(self, value): + from ansys.edb.core.layer.layer import Layer + + if isinstance(value, Layer): + self.reference_layer = value + if isinstance(value, str): + self.reference_layer = self._pedb.stackup.layers[value] + + @property + def do_renormalize(self): + """Determine whether port renormalization is enabled.""" + return self.port_post_processing_prop.do_renormalize + + @do_renormalize.setter + def do_renormalize(self, value): + self.port_post_processing_prop.do_renormalize = value + + @property + def net_name(self): + """Net name. + + Returns + ------- + str + """ + return self.net.name + + @property + def terminal_type(self): + """Terminal Type. Accepted values for setter: `"eEdge"`, `"point"`, `"terminal_instance"`, + `"padstack_instance"`, `"bundle_terminal"`, `"pin_group"`. + + Returns + ------- + int + """ + return self.type.name.lower() + + @terminal_type.setter + def terminal_type(self, value): + self.type = self._terminal_type_mapping[value] + + @property + def boundary_type(self): + """Boundary type. + + Returns + ------- + str + port, pec, rlc, current_source, voltage_source, nexxim_ground, nexxim_pPort, dc_terminal, voltage_probe. + """ + return super().boundary_type.name.lower() + + @boundary_type.setter + def boundary_type(self, value): + super(Terminal, self.__class__).boundary_type.__set__(self, self._boundary_type_mapping[value]) + + @property + def is_port(self): + """Whether it is a port.""" + return True if self.boundary_type == "port" else False + + @property + def is_current_source(self): + """Whether it is a current source.""" + return True if self.boundary_type == "current_source" else False + + @property + def is_voltage_source(self): + """Whether it is a voltage source.""" + return True if self.boundary_type == "voltage_source" else False + + @property + def impedance(self): + """Impedance of the port.""" + return self.impedance.value + + @impedance.setter + def impedance(self, value): + self.impedance = GrpcValue(value) + + @property + def reference_object(self): # pragma : no cover + """This returns the object assigned as reference. It can be a primitive or a padstack instance. + + + Returns + ------- + :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance` or + :class:`pyedb.dotnet.database.edb_data.primitives_data.EDBPrimitives` + """ + if not self._reference_object: + if self.terminal_type == "edge": + edges = self.edges + edgeType = edges[0].type + if edgeType == GrpcEdgeType.PADSTACK: + self._reference_object = self.get_pad_edge_terminal_reference_pin() + else: + self._reference_object = self.get_edge_terminal_reference_primitive() + elif self.terminal_type == "pin_group": + self._reference_object = self.get_pin_group_terminal_reference_pin() + elif self.terminal_type == "point": + self._reference_object = self.get_point_terminal_reference_primitive() + elif self.terminal_type == "padstack_instance": + self._reference_object = self.get_padstack_terminal_reference_pin() + else: + self._pedb.logger.warning("Invalid Terminal Type={}") + return False + + return self._reference_object + + @property + def reference_net_name(self): + """Net name to which reference_object belongs.""" + if self.reference_object: + return self.reference_object.net_name + + return "" + + def get_padstack_terminal_reference_pin(self, gnd_net_name_preference=None): # pragma : no cover + """Get a list of pad stacks instances and serves Coax wave ports, + pingroup terminals, PadEdge terminals. + + Parameters + ---------- + gnd_net_name_preference : str, optional + Preferred reference net name. + + Returns + ------- + :class:`dotnet.database.edb_data.padstack_data.EDBPadstackInstance` + """ + + if self.is_circuit_port: + return self.get_pin_group_terminal_reference_pin() + _, padStackInstance, _ = self.get_parameters() + + # Get the pastack instance of the terminal + pins = self._pedb.components.get_pin_from_component(self.component.name) + return self._get_closest_pin(padStackInstance, pins, gnd_net_name_preference) + + def get_pin_group_terminal_reference_pin(self, gnd_net_name_preference=None): # pragma : no cover + """Return a list of pins and serves terminals connected to pingroups. + + Parameters + ---------- + gnd_net_name_preference : str, optional + Preferred reference net name. + + Returns + ------- + :class:`dotnet.database.edb_data.padstack_data.EDBPadstackInstance` + """ + + refTerm = self.reference_terminal + if self.type == GrpcTerminalType.PIN_GROUP: + padStackInstance = self.pin_group.pins[0] + pingroup = refTerm.pin_group + refPinList = pingroup.pins + return self._get_closest_pin(padStackInstance, refPinList, gnd_net_name_preference) + elif self.type == GrpcTerminalType.PADSTACK_INST: + _, padStackInstance, _ = self.get_parameters() + if refTerm.type == GrpcTerminalType.PIN_GROUP: + pingroup = refTerm.pin_group + refPinList = pingroup.pins + return self._get_closest_pin(padStackInstance, refPinList, gnd_net_name_preference) + else: + try: + _, refTermPSI, _ = refTerm.get_parameters() + return EDBPadstackInstance(refTermPSI, self._pedb) + except AttributeError: + return False + return False + + def get_edge_terminal_reference_primitive(self): # pragma : no cover + """Check and return a primitive instance that serves Edge ports, + wave ports and coupled edge ports that are directly connedted to primitives. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.primitives_data.EDBPrimitives` + """ + + ref_layer = self.reference_layer + edges = self.edges + _, _, point_data = edges[0].get_parameters() + # shape_pd = self._pedb.edb_api.geometry.point_data(X, Y) + layer_name = ref_layer.name + for primitive in self._pedb.layout.primitives: + if primitive.layer.name == layer_name: + if primitive.polygon_data.point_in_polygon(point_data): + return cast(primitive, self._pedb) + return None # pragma: no cover + + def get_point_terminal_reference_primitive(self): # pragma : no cover + """Find and return the primitive reference for the point terminal or the padstack instance. + + Returns + ------- + :class:`dotnet.database.edb_data.padstacks_data.EDBPadstackInstance` or + :class:`pyedb.dotnet.database.edb_data.primitives_data.EDBPrimitives` + """ + + ref_term = self.reference_terminal # return value is type terminal + _, point_data, layer = ref_term.get_parameters() + # shape_pd = self._pedb.edb_api.geometry.point_data(X, Y) + layer_name = layer.name + for primitive in self._pedb.layout.primitives: + if primitive.layer.name == layer_name: + prim_shape_data = primitive.GetPolygonData() + if primitive.polygon_data.point_in_polygon(point_data): + return cast(primitive, self._pedb) + for vias in self._pedb.padstacks.instances.values(): + if layer_name in vias.layer_range_names: + plane = self._pedb.modeler.Shape( + "rectangle", pointA=vias.position, pointB=vias.padstack_definition.bounding_box[1] + ) + rectangle_data = vias._pedb.modeler.shape_to_polygon_data(plane) + if rectangle_data.point_in_polygon(point_data): + return vias + return False + + def get_pad_edge_terminal_reference_pin(self, gnd_net_name_preference=None): + """Get the closest pin padstack instances and serves any edge terminal connected to a pad. + + Parameters + ---------- + gnd_net_name_preference : str, optional + Preferred reference net name. Optianal, default is `None` which will auto compute the gnd name. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.padstacks_data.EDBPadstackInstance` + """ + comp_inst = self.component + pins = self._pedb.components.get_pin_from_component(comp_inst.name) + edges = self.edges + _, pad_edge_pstack_inst, _, _ = edges[0].get_parameters() + return self._get_closest_pin(pad_edge_pstack_inst, pins, gnd_net_name_preference) + + def _get_closest_pin(self, ref_pin, pin_list, gnd_net=None): + _, pad_stack_inst_point, _ = ref_pin.position_and_rotation # get the xy of the padstack + if gnd_net is not None: + power_ground_net_names = [gnd_net] + else: + power_ground_net_names = [net for net in self._pedb.nets.power.keys()] + comp_ref_pins = [i for i in pin_list if i.net.name in power_ground_net_names] + if len(comp_ref_pins) == 0: # pragma: no cover + self._pedb.logger.error( + "Terminal with PadStack Instance Name {} component has no reference pins.".format(ref_pin.GetName()) + ) + return None + closest_pin_distance = None + pin_obj = None + for pin in comp_ref_pins: # find the distance to all the pins to the terminal pin + if pin.name == ref_pin.name: # skip the reference psi + continue # pragma: no cover + _, pin_point, _ = pin.position_and_rotation + distance = pad_stack_inst_point.distance(pin_point) + if closest_pin_distance is None: + closest_pin_distance = distance + pin_obj = pin + elif closest_pin_distance < distance: + continue + else: + closest_pin_distance = distance + pin_obj = pin + if pin_obj: + return EDBPadstackInstance(pin_obj, self._pedb) + + @property + def magnitude(self): + """Get the magnitude of the source.""" + return self.source_amplitude.value + + @magnitude.setter + def magnitude(self, value): + self.source_amplitude = GrpcValue(value) + + @property + def phase(self): + """Get the phase of the source.""" + return self.source_phase.value + + @phase.setter + def phase(self, value): + self.source_phase = GrpcValue(value) diff --git a/src/pyedb/grpc/database/utility/__init__.py b/src/pyedb/grpc/database/utility/__init__.py new file mode 100644 index 0000000000..c23e620fca --- /dev/null +++ b/src/pyedb/grpc/database/utility/__init__.py @@ -0,0 +1,3 @@ +from pathlib import Path + +workdir = Path(__file__).parent diff --git a/src/pyedb/grpc/database/utility/constants.py b/src/pyedb/grpc/database/utility/constants.py new file mode 100644 index 0000000000..7d29cf8679 --- /dev/null +++ b/src/pyedb/grpc/database/utility/constants.py @@ -0,0 +1,25 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +def get_terminal_supported_boundary_types(): + return ["voltage_source", "current_source", "port", "dc_terminal", "voltage_probe"] diff --git a/src/pyedb/grpc/database/utility/heat_sink.py b/src/pyedb/grpc/database/utility/heat_sink.py new file mode 100644 index 0000000000..e76dcac351 --- /dev/null +++ b/src/pyedb/grpc/database/utility/heat_sink.py @@ -0,0 +1,92 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.utility.heat_sink import ( + HeatSinkFinOrientation as GrpcHeatSinkFinOrientation, +) +from ansys.edb.core.utility.value import Value as GrpcValue + + +class HeatSink: + + """Heatsink model description. + + Parameters + ---------- + pedb : :class:`pyedb.dotnet.edb.Edb` + Inherited object. + edb_object : :class:`Ansys.Ansoft.Edb.Utility.HeatSink`, + """ + + def __init__(self, pedb, edb_object): + self._pedb = pedb + self._edb_object = edb_object + self._fin_orientation_type = { + "x_oriented": GrpcHeatSinkFinOrientation.X_ORIENTED, + "y_oriented": GrpcHeatSinkFinOrientation.Y_ORIENTED, + "other_oriented": GrpcHeatSinkFinOrientation.OTHER_ORIENTED, + } + + @property + def fin_base_height(self): + """The base elevation of the fins.""" + return self._edb_object.fin_base_height.value + + @fin_base_height.setter + def fin_base_height(self, value): + self._edb_object.fin_base_height = GrpcValue(value) + + @property + def fin_height(self): + """The fin height.""" + return self._edb_object.fin_height.value + + @fin_height.setter + def fin_height(self, value): + self._edb_object.fin_height = GrpcValue(value) + + @property + def fin_orientation(self): + """The fin orientation.""" + return self._edb_object.fin_orientation.name.lower() + + @fin_orientation.setter + def fin_orientation(self, value): + self._edb_object.fin_orientation = self._fin_orientation_type[value] + + @property + def fin_spacing(self): + """The fin spacing.""" + return self._edb_object.fin_spacing.value + + @fin_spacing.setter + def fin_spacing(self, value): + self._edb_object.fin_spacing = GrpcValue(value) + + @property + def fin_thickness(self): + """The fin thickness.""" + return self._edb_object.fin_thickness.value + + @fin_thickness.setter + def fin_thickness(self, value): + self._edb_object.fin_thickness = GrpcValue(value) diff --git a/src/pyedb/grpc/database/utility/hfss_extent_info.py b/src/pyedb/grpc/database/utility/hfss_extent_info.py new file mode 100644 index 0000000000..b779f617c1 --- /dev/null +++ b/src/pyedb/grpc/database/utility/hfss_extent_info.py @@ -0,0 +1,335 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.utility.hfss_extent_info import HfssExtentInfo as GrpcHfssExtentInfo +from ansys.edb.core.utility.value import Value as GrpcValue + + +class HfssExtentInfo(GrpcHfssExtentInfo): + """Manages EDB functionalities for HFSS extent information. + + Parameters + ---------- + pedb : :class:`pyedb.edb.Edb` + Inherited EDB object. + """ + + def __init__(self, pedb): + self._pedb = pedb + super().__init__() + self.extent_type_mapping = { + "bounding_box": GrpcHfssExtentInfo.HFSSExtentInfoType.BOUNDING_BOX, + "conforming": GrpcHfssExtentInfo.HFSSExtentInfoType.CONFORMING, + "convex_hull": GrpcHfssExtentInfo.HFSSExtentInfoType.CONVEX_HUL, + "polygon": GrpcHfssExtentInfo.HFSSExtentInfoType.POLYGON, + } + self._open_region_type = { + "radiation": GrpcHfssExtentInfo.OpenRegionType.RADIATION, + "pml": GrpcHfssExtentInfo.OpenRegionType.PML, + } + self.hfss_extent_type = self._hfss_extent_info.extent_type + + def _update_hfss_extent_info(self, hfss_extent): + return self._pedb.active_cell.set_hfss_extent_info(hfss_extent) + + @property + def _hfss_extent_info(self): + return self._pedb.active_cell.hfss_extent_info + + @property + def air_box_horizontal_extent_enabled(self): + """Whether horizontal extent is enabled for the airbox.""" + return self._hfss_extent_info.air_box_horizontal_extent[1] + + @air_box_horizontal_extent_enabled.setter + def air_box_horizontal_extent_enabled(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.air_box_horizontal_extent = value + self._update_hfss_extent_info(hfss_extent) + + @property + def air_box_horizontal_extent(self): + """Size of horizontal extent for the air box. + + Returns: + dotnet.database.edb_data.edbvalue.EdbValue + """ + return self._hfss_extent_info.air_box_horizontal_extent[0] + + @air_box_horizontal_extent.setter + def air_box_horizontal_extent(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.air_box_horizontal_extent = float(value) + self._update_hfss_extent_info(hfss_extent) + + @property + def air_box_positive_vertical_extent_enabled(self): + """Whether positive vertical extent is enabled for the air box.""" + return self._hfss_extent_info.air_box_positive_vertical_extent[1] + + @air_box_positive_vertical_extent_enabled.setter + def air_box_positive_vertical_extent_enabled(self, value): + hfss_exent = self._hfss_extent_info + hfss_exent.air_box_positive_vertical_extent = value + self._update_hfss_extent_info(hfss_exent) + + @property + def air_box_positive_vertical_extent(self): + """Negative vertical extent for the air box.""" + return self._hfss_extent_info.air_box_positive_vertical_extent[0] + + @air_box_positive_vertical_extent.setter + def air_box_positive_vertical_extent(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.air_box_positive_vertical_extent = float(value) + self._update_hfss_extent_info(hfss_extent) + + @property + def air_box_negative_vertical_extent_enabled(self): + """Whether negative vertical extent is enabled for the air box.""" + return self._hfss_extent_info.air_box_negative_vertical_extent[1] + + @air_box_negative_vertical_extent_enabled.setter + def air_box_negative_vertical_extent_enabled(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.air_box_negative_vertical_extent = value + self._update_hfss_extent_info(hfss_extent) + + @property + def air_box_negative_vertical_extent(self): + """Negative vertical extent for the airbox.""" + return self._hfss_extent_info.air_box_negative_vertical_extent[0] + + @air_box_negative_vertical_extent.setter + def air_box_negative_vertical_extent(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.air_box_negative_vertical_extent = float(value) + self._update_hfss_extent_info(hfss_extent) + + @property + def base_polygon(self): + """Base polygon. + + Returns + ------- + :class:`dotnet.database.edb_data.primitives_data.EDBPrimitive` + """ + return self._hfss_extent_info.base_polygon + + @base_polygon.setter + def base_polygon(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.base_polygon = value + self._update_hfss_extent_info(hfss_extent) + + @property + def dielectric_base_polygon(self): + """Dielectric base polygon. + + Returns + ------- + :class:`dotnet.database.edb_data.primitives_data.EDBPrimitive` + """ + return self._hfss_extent_info.dielectric_base_polygon + + @dielectric_base_polygon.setter + def dielectric_base_polygon(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.dielectric_base_polygon = value + self._update_hfss_extent_info(hfss_extent) + + @property + def dielectric_extent_size_enabled(self): + """Whether dielectric extent size is enabled.""" + return self._hfss_extent_info.dielectric_extent_size[1] + + @dielectric_extent_size_enabled.setter + def dielectric_extent_size_enabled(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.dielectric_extent_size = value + self._update_hfss_extent_info(hfss_extent) + + @property + def dielectric_extent_size(self): + """Dielectric extent size.""" + return self._hfss_extent_info.dielectric_extent_size[0] + + @dielectric_extent_size.setter + def dielectric_extent_size(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.dielectric_extent_size = value + self._update_hfss_extent_info(hfss_extent) + + @property + def dielectric_extent_type(self): + """Dielectric extent type.""" + return self._hfss_extent_info.dielectric_extent_type.name.lower() + + @dielectric_extent_type.setter + def dielectric_extent_type(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.dielectric_extent_type = value + self._update_hfss_extent_info(hfss_extent) + + @property + def extent_type(self): + """Extent type.""" + return self._hfss_extent_info.extent_type.name.lower() + + @extent_type.setter + def extent_type(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.extent_type = value + self._update_hfss_extent_info(hfss_extent) + + @property + def honor_user_dielectric(self): + """Honor user dielectric.""" + return self._hfss_extent_info.honor_user_dielectric + + @honor_user_dielectric.setter + def honor_user_dielectric(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.honor_user_dielectric = value + self._update_hfss_extent_info(hfss_extent) + + @property + def is_pml_visible(self): + """Whether visibility of the PML is enabled.""" + return self._hfss_extent_info.is_pml_visible + + @is_pml_visible.setter + def is_pml_visible(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.is_pml_visible = value + self._update_hfss_extent_info(hfss_extent) + + @property + def open_region_type(self): + """Open region type.""" + return self._hfss_extent_info.open_region_type.name.lower() + + @open_region_type.setter + def open_region_type(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.open_region_type = value + self._update_hfss_extent_info(hfss_extent) + + @property + def operating_freq(self): + """PML Operating frequency. + + Returns + ------- + pyedb.dotnet.database.edb_data.edbvalue.EdbValue + """ + return GrpcValue(self._hfss_extent_info.operating_frequency).value + + @operating_freq.setter + def operating_freq(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.operating_frequency = GrpcValue(value) + self._update_hfss_extent_info(hfss_extent) + + @property + def radiation_level(self): + """PML Radiation level to calculate the thickness of boundary.""" + return GrpcValue(self._hfss_extent_info.radiation_level).value + + @radiation_level.setter + def radiation_level(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.RadiationLevel = GrpcValue(value) + self._update_hfss_extent_info(hfss_extent) + + @property + def sync_air_box_vertical_extent(self): + """Vertical extent of the sync air box.""" + return self._hfss_extent_info.sync_air_box_vertical_extent + + @sync_air_box_vertical_extent.setter + def sync_air_box_vertical_extent(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.sync_air_box_vertical_extent = value + self._update_hfss_extent_info(hfss_extent) + + @property + def truncate_air_box_at_ground(self): + """Truncate air box at ground.""" + return self._hfss_extent_info.truncate_air_box_at_ground + + @truncate_air_box_at_ground.setter + def truncate_air_box_at_ground(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.truncate_air_box_at_ground = value + self._update_hfss_extent_info(hfss_extent) + + @property + def use_open_region(self): + """Whether using an open region is enabled.""" + return self._hfss_extent_info.use_open_region + + @use_open_region.setter + def use_open_region(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.use_open_region = value + self._update_hfss_extent_info(hfss_extent) + + @property + def use_xy_data_extent_for_vertical_expansion(self): + """Whether using the xy data extent for vertical expansion is enabled.""" + return self._hfss_extent_info.use_xy_data_extent_for_vertical_expansion + + @use_xy_data_extent_for_vertical_expansion.setter + def use_xy_data_extent_for_vertical_expansion(self, value): + hfss_extent = self._hfss_extent_info + hfss_extent.use_xy_data_extent_for_vertical_expansion = value + self._update_hfss_extent_info(hfss_extent) + + def load_config(self, config): + """Load HFSS extent configuration. + + Parameters + ---------- + config: dict + Parameters of the HFSS extent information. + """ + for i, j in config.items(): + if hasattr(self, i): + setattr(self, i, j) + + def export_config(self): + """Export HFSS extent information. + + Returns: + dict + Parameters of the HFSS extent information. + """ + config = dict() + for i in dir(self): + if i.startswith("_"): + continue + elif i in ["load_config", "export_config"]: + continue + else: + config[i] = getattr(self, i) + return config diff --git a/src/pyedb/grpc/database/utility/layout_statistics.py b/src/pyedb/grpc/database/utility/layout_statistics.py new file mode 100644 index 0000000000..747955b6a1 --- /dev/null +++ b/src/pyedb/grpc/database/utility/layout_statistics.py @@ -0,0 +1,171 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +class LayoutStatistics(object): + """Statistics object + + Object properties example. + >>> stat_model = EDBStatistics() + >>> stat_model.num_capacitors + >>> stat_model.num_resistors + >>> stat_model.num_inductors + >>> stat_model.layout_size + >>> stat_model.num_discrete_components + >>> stat_model.num_inductors + >>> stat_model.num_resistors + >>> stat_model.num_capacitors + >>> stat_model.num_nets + >>> stat_model.num_traces + >>> stat_model.num_polygons + >>> stat_model.num_vias + >>> stat_model.stackup_thickness + >>> stat_model.occupying_surface + >>> stat_model.occupying_ratio + """ + + def __init__(self): + self._nb_layer = 0 + self._stackup_thickness = 0.0 + self._nb_vias = 0 + self._occupying_ratio = {} + self._occupying_surface = {} + self._layout_size = [0.0, 0.0, 0.0, 0.0] + self._nb_polygons = 0 + self._nb_traces = 0 + self._nb_nets = 0 + self._nb_discrete_components = 0 + self._nb_inductors = 0 + self._nb_capacitors = 0 + self._nb_resistors = 0 + + @property + def num_layers(self): + return self._nb_layer + + @num_layers.setter + def num_layers(self, value): + if isinstance(value, int): + self._nb_layer = value + + @property + def stackup_thickness(self): + return self._stackup_thickness + + @stackup_thickness.setter + def stackup_thickness(self, value): + if isinstance(value, float): + self._stackup_thickness = value + + @property + def num_vias(self): + return self._nb_vias + + @num_vias.setter + def num_vias(self, value): + if isinstance(value, int): + self._nb_vias = value + + @property + def occupying_ratio(self): + return self._occupying_ratio + + @occupying_ratio.setter + def occupying_ratio(self, value): + if isinstance(value, float): + self._occupying_ratio = value + + @property + def occupying_surface(self): + return self._occupying_surface + + @occupying_surface.setter + def occupying_surface(self, value): + if isinstance(value, float): + self._occupying_surface = value + + @property + def layout_size(self): + return self._layout_size + + @property + def num_polygons(self): + return self._nb_polygons + + @num_polygons.setter + def num_polygons(self, value): + if isinstance(value, int): + self._nb_polygons = value + + @property + def num_traces(self): + return self._nb_traces + + @num_traces.setter + def num_traces(self, value): + if isinstance(value, int): + self._nb_traces = value + + @property + def num_nets(self): + return self._nb_nets + + @num_nets.setter + def num_nets(self, value): + if isinstance(value, int): + self._nb_nets = value + + @property + def num_discrete_components(self): + return self._nb_discrete_components + + @num_discrete_components.setter + def num_discrete_components(self, value): + if isinstance(value, int): + self._nb_discrete_components = value + + @property + def num_inductors(self): + return self._nb_inductors + + @num_inductors.setter + def num_inductors(self, value): + if isinstance(value, int): + self._nb_inductors = value + + @property + def num_capacitors(self): + return self._nb_capacitors + + @num_capacitors.setter + def num_capacitors(self, value): + if isinstance(value, int): + self._nb_capacitors = value + + @property + def num_resistors(self): + return self._nb_resistors + + @num_resistors.setter + def num_resistors(self, value): + if isinstance(value, int): + self._nb_resistors = value diff --git a/src/pyedb/grpc/database/utility/rlc.py b/src/pyedb/grpc/database/utility/rlc.py new file mode 100644 index 0000000000..951a874f08 --- /dev/null +++ b/src/pyedb/grpc/database/utility/rlc.py @@ -0,0 +1,56 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from ansys.edb.core.utility.rlc import Rlc as GrpcRlc +from ansys.edb.core.utility.value import Value as GrpcValue + + +class Rlc(GrpcRlc): + def __init__(self, pedb, edb_object): + super().__init__(edb_object) + self._pedb = pedb + self._edb_object = edb_object + + @property + def r(self): + return self.r.value + + @r.setter + def r(self, value): + self.r = GrpcValue(value) + + @property + def l(self): + return self.l.value + + @l.setter + def l(self, value): + self.l = GrpcValue(value) + + @property + def c(self): + return self.c.value + + @c.setter + def c(self, value): + self.c = GrpcValue(value) diff --git a/src/pyedb/grpc/database/utility/simulation_configuration.py b/src/pyedb/grpc/database/utility/simulation_configuration.py new file mode 100644 index 0000000000..8d1654e370 --- /dev/null +++ b/src/pyedb/grpc/database/utility/simulation_configuration.py @@ -0,0 +1,3305 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from collections import OrderedDict +import json +import os + +from ansys.edb.core.hierarchy.component_group import ComponentType as GrpcComponentType +from ansys.edb.core.utility.value import Value as GrpcValue + +# from pyedb.dotnet.database.edb_data.sources import Source, SourceType +# from pyedb.dotnet.database.utilities.simulation_setup import AdaptiveType +from pyedb.generic.constants import ( + BasisOrder, + CutoutSubdesignType, + RadiationBoxType, + SolverType, + SweepType, + validate_enum_class_value, +) +from pyedb.generic.general_methods import generate_unique_name + + +class SimulationConfigurationBatch(object): + """Contains all Cutout and Batch analysis settings. + The class is part of `SimulationConfiguration` class as a property. + + + """ + + def __init__(self): + self._signal_nets = [] + self._power_nets = [] + self._components = [] + self._cutout_subdesign_type = CutoutSubdesignType.Conformal # Conformal + self._cutout_subdesign_expansion = 0.001 + self._cutout_subdesign_round_corner = True + self._use_default_cutout = False + self._generate_excitations = True + self._add_frequency_sweep = True + self._include_only_selected_nets = False + self._generate_solder_balls = True + self._coax_solder_ball_diameter = [] + self._use_default_coax_port_radial_extension = True + self._trim_reference_size = False + self._output_aedb = None + self._signal_layers_properties = {} + self._coplanar_instances = [] + self._signal_layer_etching_instances = [] + self._etching_factor_instances = [] + self._use_dielectric_extent_multiple = True + self._dielectric_extent = 0.001 + self._use_airbox_horizontal_multiple = True + self._airbox_horizontal_extent = 0.1 + self._use_airbox_negative_vertical_extent_multiple = True + self._airbox_negative_vertical_extent = 0.1 + self._use_airbox_positive_vertical_extent_multiple = True + self._airbox_positive_vertical_extent = 0.1 + self._honor_user_dielectric = False + self._truncate_airbox_at_ground = False + self._use_radiation_boundary = True + self._do_cutout_subdesign = True + self._do_pin_group = True + self._sources = [] + + @property + def coplanar_instances(self): # pragma: no cover + """Retrieve the list of component to be replaced by circuit ports (obsolete). + + Returns + ------- + list[str] + List of component name. + """ + return self._coplanar_instances + + @coplanar_instances.setter + def coplanar_instances(self, value): # pragma: no cover + if isinstance(value, list): + self._coplanar_instances = value + + @property + def signal_layer_etching_instances(self): # pragma: no cover + """Retrieve the list of layers which has layer etching activated. + + Returns + ------- + list[str] + List of layer name. + """ + return self._signal_layer_etching_instances + + @signal_layer_etching_instances.setter + def signal_layer_etching_instances(self, value): # pragma: no cover + if isinstance(value, list): + self._signal_layer_etching_instances = value + + @property + def etching_factor_instances(self): # pragma: no cover + """Retrieve the list of etching factor with associated layers. + + Returns + ------- + list[str] + list etching parameters with layer name. + """ + return self._etching_factor_instances + + @etching_factor_instances.setter + def etching_factor_instances(self, value): # pragma: no cover + if isinstance(value, list): + self._etching_factor_instances = value + + @property + def dielectric_extent(self): # pragma: no cover + """Retrieve the value of dielectric extent. + + Returns + ------- + float + Value of the dielectric extent. When absolute dimensions are used, + the values are in meters. + """ + return self._dielectric_extent + + @dielectric_extent.setter + def dielectric_extent(self, value): # pragma: no cover + if isinstance(value, (int, float)): + self._dielectric_extent = value + + @property + def use_dielectric_extent_multiple(self): + """Whether the multiple value of the dielectric extent is used. + + Returns + ------- + bool + ``True`` when the multiple value (extent factor) is used. ``False`` when + absolute dimensions are used. + """ + return self._use_dielectric_extent_multiple + + @use_dielectric_extent_multiple.setter + def use_dielectric_extent_multiple(self, value): + if isinstance(value, bool): + self._use_dielectric_extent_multiple = value + + @property + def airbox_horizontal_extent(self): # pragma: no cover + """Horizontal extent of the airbox for HFSS. When absolute dimensions are used, + the values are in meters. + + Returns + ------- + float + Value of the air box horizontal extent. + """ + return self._airbox_horizontal_extent + + @airbox_horizontal_extent.setter + def airbox_horizontal_extent(self, value): # pragma: no cover + if isinstance(value, (int, float)): + self._airbox_horizontal_extent = value + + @property + def use_airbox_horizontal_extent_multiple(self): + """Whether the multiple value is used for the horizontal extent of the air box. + + Returns + ------- + bool + ``True`` when the multiple value (extent factor) is used. ``False`` when + absolute dimensions are used. + + """ + return self._use_airbox_horizontal_multiple + + @use_airbox_horizontal_extent_multiple.setter + def use_airbox_horizontal_extent_multiple(self, value): + if isinstance(value, bool): + self._use_airbox_horizontal_multiple = value + + @property + def airbox_negative_vertical_extent(self): # pragma: no cover + """Negative vertical extent of the airbox for HFSS. When absolute dimensions + are used, the values are in meters. + + Returns + ------- + float + Value of the air box negative vertical extent. + """ + return self._airbox_negative_vertical_extent + + @airbox_negative_vertical_extent.setter + def airbox_negative_vertical_extent(self, value): # pragma: no cover + if isinstance(value, (int, float)): + self._airbox_negative_vertical_extent = value + + @property + def use_airbox_negative_vertical_extent_multiple(self): + """Multiple value for the negative extent of the airbox. + + Returns + ------- + bool + ``True`` when the multiple value (extent factor) is used. ``False`` when + absolute dimensions are used. + + """ + return self._use_airbox_negative_vertical_extent_multiple + + @use_airbox_negative_vertical_extent_multiple.setter + def use_airbox_negative_vertical_extent_multiple(self, value): + if isinstance(value, bool): + self._use_airbox_negative_vertical_extent_multiple = value + + @property + def airbox_positive_vertical_extent(self): # pragma: no cover + """Positive vertical extent of the airbox for HFSS. When absolute dimensions are + used, the values are in meters. + + Returns + ------- + float + Value of the air box positive vertical extent. + """ + return self._airbox_positive_vertical_extent + + @airbox_positive_vertical_extent.setter + def airbox_positive_vertical_extent(self, value): # pragma: no cover + if isinstance(value, (int, float)): + self._airbox_positive_vertical_extent = value + + @property + def use_airbox_positive_vertical_extent_multiple(self): + """Whether the multiple value for the positive extent of the airbox is used. + + Returns + ------- + bool + ``True`` when the multiple value (extent factor) is used. ``False`` when + absolute dimensions are used. + """ + return self._use_airbox_positive_vertical_extent_multiple + + @use_airbox_positive_vertical_extent_multiple.setter + def use_airbox_positive_vertical_extent_multiple(self, value): + if isinstance(value, bool): + self._use_airbox_positive_vertical_extent_multiple = value + + @property + def use_pyaedt_cutout(self): + """Whether the default EDB cutout or a new PyAEDT cutout is used. + + Returns + ------- + bool + """ + return not self._use_default_cutout + + @use_pyaedt_cutout.setter + def use_pyaedt_cutout(self, value): + self._use_default_cutout = not value + + @property + def use_default_cutout(self): # pragma: no cover + """Whether to use the default EDB cutout. The default is ``False``, in which case + a new PyAEDT cutout is used. + + Returns + ------- + bool + """ + + return self._use_default_cutout + + @use_default_cutout.setter + def use_default_cutout(self, value): # pragma: no cover + self._use_default_cutout = value + + @property + def do_pingroup(self): # pragma: no cover + """Do pingroup on multi-pin component. ``True`` all pins from the same net are grouped, ``False`` one port + is created for each pin. + + Returns + ------- + bool + """ + return self._do_pin_group + + @do_pingroup.setter + def do_pingroup(self, value): # pragma: no cover + self._do_pin_group = value + + @property + def generate_solder_balls(self): # pragma: no cover + """Retrieve the boolean for applying solder balls. + + Returns + ------- + bool + ``True`` when applied ``False`` if not. + """ + return self._generate_solder_balls + + @generate_solder_balls.setter + def generate_solder_balls(self, value): + if isinstance(value, bool): # pragma: no cover + self._generate_solder_balls = value + + @property + def signal_nets(self): + """Retrieve the list of signal net names. + + Returns + ------- + List[str] + List of signal net names. + """ + + return self._signal_nets + + @signal_nets.setter + def signal_nets(self, value): + if isinstance(value, list): # pragma: no cover + self._signal_nets = value + + @property + def power_nets(self): + """Retrieve the list of power and reference net names. + + Returns + ------- + list[str] + List of the net name. + """ + return self._power_nets + + @power_nets.setter + def power_nets(self, value): + if isinstance(value, list): + self._power_nets = value + + @property + def components(self): + """Retrieve the list component name to be included in the simulation. + + Returns + ------- + list[str] + List of the component name. + """ + return self._components + + @components.setter + def components(self, value): + if isinstance(value, list): + self._components = value + + @property + def coax_solder_ball_diameter(self): # pragma: no cover + """Retrieve the list of solder balls diameter values when the auto evaluated one is overwritten. + + Returns + ------- + list[float] + List of the solder balls diameter. + """ + return self._coax_solder_ball_diameter + + @coax_solder_ball_diameter.setter + def coax_solder_ball_diameter(self, value): # pragma: no cover + if isinstance(value, list): + self._coax_solder_ball_diameter = value + + @property + def use_default_coax_port_radial_extension(self): + """Retrieve the boolean for using the default coaxial port extension value. + + Returns + ------- + bool + ``True`` when the default value is used ``False`` if not. + """ + return self._use_default_coax_port_radial_extension + + @use_default_coax_port_radial_extension.setter + def use_default_coax_port_radial_extension(self, value): # pragma: no cover + if isinstance(value, bool): + self._use_default_coax_port_radial_extension = value + + @property + def trim_reference_size(self): + """Retrieve the trim reference size when used. + + Returns + ------- + float + The size value. + """ + return self._trim_reference_size + + @trim_reference_size.setter + def trim_reference_size(self, value): # pragma: no cover + if isinstance(value, bool): + self._trim_reference_size = value + + @property + def do_cutout_subdesign(self): + """Retrieve boolean to perform the cutout during the project build. + + Returns + ------- + bool + ``True`` when clipping the design is applied ``False`` is not. + """ + return self._do_cutout_subdesign + + @do_cutout_subdesign.setter + def do_cutout_subdesign(self, value): # pragma: no cover + if isinstance(value, bool): + self._do_cutout_subdesign = value + + @property + def cutout_subdesign_type(self): + """Retrieve the CutoutSubdesignType selection for clipping the design. + + Returns + ------- + CutoutSubdesignType object + """ + return self._cutout_subdesign_type + + @cutout_subdesign_type.setter + def cutout_subdesign_type(self, value): # pragma: no cover + if validate_enum_class_value(CutoutSubdesignType, value): + self._cutout_subdesign_type = value + + @property + def cutout_subdesign_expansion(self): + """Retrieve expansion factor used for clipping the design. + + Returns + ------- + float + The value used as a ratio. + """ + + return self._cutout_subdesign_expansion + + @cutout_subdesign_expansion.setter + def cutout_subdesign_expansion(self, value): # pragma: no cover + self._cutout_subdesign_expansion = value + + @property + def cutout_subdesign_round_corner(self): + """Retrieve boolean to perform the design clipping using round corner for the extent generation. + + Returns + ------- + bool + ``True`` when using round corner, ``False`` if not. + """ + + return self._cutout_subdesign_round_corner + + @cutout_subdesign_round_corner.setter + def cutout_subdesign_round_corner(self, value): # pragma: no cover + if isinstance(value, bool): + self._cutout_subdesign_round_corner = value + + @property + def output_aedb(self): # pragma: no cover + """Retrieve the path for the output aedb folder. When provided will copy the initial aedb to the specified + path. This is used especially to preserve the initial project when several files have to be build based on + the last one. When the path is None, the initial project will be overwritten. So when cutout is applied mand + you want to preserve the project make sure you provide the full path for the new aedb folder. + + Returns + ------- + str + Absolute path for the created aedb folder. + """ + return self._output_aedb + + @output_aedb.setter + def output_aedb(self, value): # pragma: no cover + if isinstance(value, str): + self._output_aedb = value + + @property + def sources(self): # pragma: no cover + """Retrieve the source list. + + Returns + ------- + :class:`dotnet.database.edb_data.sources.Source` + """ + return self._sources + + @sources.setter + def sources(self, value): # pragma: no cover + if isinstance(value, Source): + value = [value] + if isinstance(value, list): + if len([src for src in value if isinstance(src, Source)]) == len(value): + self._sources = value + + def add_source(self, source=None): # pragma: no cover + """Add a new source to configuration. + + Parameters + ---------- + source : :class:`pyedb.dotnet.database.edb_data.sources.Source` + + """ + if isinstance(source, Source): + self._sources.append(source) + + @property + def honor_user_dielectric(self): # pragma: no cover + """Retrieve the boolean to activate the feature "'Honor user dielectric'". + + Returns + ------- + bool + ``True`` activated, ``False`` deactivated. + """ + return self._honor_user_dielectric + + @honor_user_dielectric.setter + def honor_user_dielectric(self, value): # pragma: no cover + if isinstance(value, bool): + self._honor_user_dielectric = value + + @property + def truncate_airbox_at_ground(self): # pragma: no cover + """Retrieve the boolean to truncate hfss air box at ground. + + Returns + ------- + bool + ``True`` activated, ``False`` deactivated. + """ + return self._truncate_airbox_at_ground + + @truncate_airbox_at_ground.setter + def truncate_airbox_at_ground(self, value): # pragma: no cover + if isinstance(value, bool): + self._truncate_airbox_at_ground = value + + @property + def use_radiation_boundary(self): # pragma: no cover + """Retrieve the boolean to use radiation boundary with HFSS. + + Returns + ------- + bool + ``True`` activated, ``False`` deactivated. + """ + return self._use_radiation_boundary + + @use_radiation_boundary.setter + def use_radiation_boundary(self, value): # pragma: no cover + if isinstance(value, bool): + self._use_radiation_boundary = value + + @property + def signal_layers_properties(self): # pragma: no cover + """Retrieve the list of layers to have properties changes. + + Returns + ------- + list[str] + List of layer name. + """ + return self._signal_layers_properties + + @signal_layers_properties.setter + def signal_layers_properties(self, value): # pragma: no cover + if isinstance(value, dict): + self._signal_layers_properties = value + + @property + def generate_excitations(self): + """Activate ports and sources for DC generation when build project with the class. + + Returns + ------- + bool + ``True`` ports are created, ``False`` skip port generation. Default value is ``True``. + + """ + return self._generate_excitations + + @generate_excitations.setter + def generate_excitations(self, value): + if isinstance(value, bool): + self._generate_excitations = value + + @property + def add_frequency_sweep(self): + """Activate the frequency sweep creation when build project with the class. + + Returns + ------- + bool + ``True`` frequency sweep is created, ``False`` skip sweep adding. Default value is ``True``. + """ + return self._add_frequency_sweep + + @add_frequency_sweep.setter + def add_frequency_sweep(self, value): + if isinstance(value, bool): + self._add_frequency_sweep = value + + @property + def include_only_selected_nets(self): + """Include only net selection in the project. It is only used when ``do_cutout`` is set to ``False``. + Will also be ignored if signal_nets and power_nets are ``None``, resulting project will have all nets included. + + Returns + ------- + bool + ``True`` or ``False``. Default value is ``False``. + + """ + return self._include_only_selected_nets + + @include_only_selected_nets.setter + def include_only_selected_nets(self, value): + if isinstance(value, bool): + self._include_only_selected_nets = value + + +class SimulationConfigurationDc(object): + """Contains all DC analysis settings. + The class is part of `SimulationConfiguration` class as a property. + + """ + + def __init__(self): + self._dc_compute_inductance = False + self._dc_contact_radius = "100um" + self._dc_slide_position = 1 + self._dc_use_dc_custom_settings = False + self._dc_plot_jv = True + self._dc_min_plane_area_to_mesh = "8mil2" + self._dc_min_void_area_to_mesh = "0.734mil2" + self._dc_error_energy = 0.02 + self._dc_max_init_mesh_edge_length = "5.0mm" + self._dc_max_num_pass = 5 + self._dc_min_num_pass = 1 + self._dc_mesh_bondwires = True + self._dc_num_bondwire_sides = 8 + self._dc_mesh_vias = True + self._dc_num_via_sides = 8 + self._dc_percent_local_refinement = 0.2 + self._dc_perform_adaptive_refinement = True + self._dc_refine_bondwires = True + self._dc_refine_vias = True + self._dc_report_config_file = "" + self._dc_report_show_Active_devices = True + self._dc_export_thermal_data = True + self._dc_full_report_path = "" + self._dc_icepak_temp_file = "" + self._dc_import_thermal_data = False + self._dc_per_pin_res_path = "" + self._dc_per_pin_use_pin_format = True + self._dc_use_loop_res_for_per_pin = True + self._dc_via_report_path = "" + self._dc_source_terms_to_ground = Dictionary[str, int]() + + @property + def dc_min_plane_area_to_mesh(self): # pragma: no cover + """Retrieve the value of the minimum plane area to be meshed by Siwave for DC solution. + + Returns + ------- + float + The value of the minimum plane area. + """ + return self._dc_min_plane_area_to_mesh + + @dc_min_plane_area_to_mesh.setter + def dc_min_plane_area_to_mesh(self, value): # pragma: no cover + if isinstance(value, str): + self._dc_min_plane_area_to_mesh = value + + @property + def dc_compute_inductance(self): + """Return the boolean for computing the inductance with SIwave DC solver. + + Returns + ------- + bool + ``True`` activate ``False`` deactivated. + """ + return self._dc_compute_inductance + + @dc_compute_inductance.setter + def dc_compute_inductance(self, value): + if isinstance(value, bool): + self._dc_compute_inductance = value + + @property + def dc_contact_radius(self): + """Retrieve the value for SIwave DC contact radius. + + Returns + ------- + str + The contact radius value. + + """ + return self._dc_contact_radius + + @dc_contact_radius.setter + def dc_contact_radius(self, value): + if isinstance(value, str): + self._dc_contact_radius = value + + @dc_compute_inductance.setter + def dc_compute_inductance(self, value): + if isinstance(value, str): + self._dc_contact_radius = value + + @property + def dc_slide_position(self): + """Retrieve the SIwave DC slide position value. + + Returns + ------- + int + The position value, 0 Optimum speed, 1 balanced, 2 optimum accuracy. + """ + return self._dc_slide_position + + @dc_slide_position.setter + def dc_slide_position(self, value): + if isinstance(value, int): + self._dc_slide_position = value + + @property + def dc_use_dc_custom_settings(self): + """Retrieve the value for using DC custom settings. + + Returns + ------- + bool + ``True`` when activated, ``False`` deactivated. + + """ + return self._dc_use_dc_custom_settings + + @dc_use_dc_custom_settings.setter + def dc_use_dc_custom_settings(self, value): + if isinstance(value, bool): + self._dc_use_dc_custom_settings = value + + @property + def dc_plot_jv(self): + """Retrieve the value for computing current density and voltage distribution. + + Returns + ------- + bool + ``True`` when activated, ``False`` deactivated. Default value True + + """ + return self._dc_plot_jv + + @dc_plot_jv.setter + def dc_plot_jv(self, value): + if isinstance(value, bool): + self._dc_plot_jv = value + + @property + def dc_min_void_area_to_mesh(self): + """Retrieve the value for the minimum void surface to mesh. + + Returns + ------- + str + The area value. + + """ + return self._dc_min_void_area_to_mesh + + @dc_min_void_area_to_mesh.setter + def dc_min_void_area_to_mesh(self, value): + if isinstance(value, str): + self._dc_min_void_area_to_mesh = value + + @property + def dc_error_energy(self): + """Retrieve the value for the DC error energy. + + Returns + ------- + float + The error energy value, 0.2 as default. + + """ + return self._dc_error_energy + + @dc_error_energy.setter + def dc_error_energy(self, value): + if isinstance(value, (int, float)): + self._dc_error_energy = value + + @property + def dc_max_init_mesh_edge_length(self): + """Retrieve the maximum initial mesh edge value. + + Returns + ------- + str + maximum mesh length. + + """ + return self._dc_max_init_mesh_edge_length + + @dc_max_init_mesh_edge_length.setter + def dc_max_init_mesh_edge_length(self, value): + if isinstance(value, str): + self._dc_max_init_mesh_edge_length = value + + @property + def dc_max_num_pass(self): + """Retrieve the maximum number of adaptive passes. + + Returns + ------- + int + number of passes. + """ + return self._dc_max_num_pass + + @dc_max_num_pass.setter + def dc_max_num_pass(self, value): + if isinstance(value, int): + self._dc_max_num_pass = value + + @property + def dc_min_num_pass(self): + """Retrieve the minimum number of adaptive passes. + + Returns + ------- + int + number of passes. + """ + return self._dc_min_num_pass + + @dc_min_num_pass.setter + def dc_min_num_pass(self, value): + if isinstance(value, int): + self._dc_min_num_pass = value + + @property + def dc_mesh_bondwires(self): + """Retrieve the value for meshing bondwires. + + Returns + ------- + bool + ``True`` when activated, ``False`` deactivated. + + """ + return self._dc_mesh_bondwires + + @dc_mesh_bondwires.setter + def dc_mesh_bondwires(self, value): + if isinstance(value, bool): + self._dc_mesh_bondwires = value + + @property + def dc_num_bondwire_sides(self): + """Retrieve the number of sides used for cylinder discretization. + + Returns + ------- + int + Number of sides. + + """ + return self._dc_num_bondwire_sides + + @dc_num_bondwire_sides.setter + def dc_num_bondwire_sides(self, value): + if isinstance(value, int): + self._dc_num_bondwire_sides = value + + @property + def dc_mesh_vias(self): + """Retrieve the value for meshing vias. + + Returns + ------- + bool + ``True`` when activated, ``False`` deactivated. + + """ + return self._dc_mesh_vias + + @dc_mesh_vias.setter + def dc_mesh_vias(self, value): + if isinstance(value, bool): + self._dc_mesh_vias = value + + @property + def dc_num_via_sides(self): + """Retrieve the number of sides used for cylinder discretization. + + Returns + ------- + int + Number of sides. + + """ + return self._dc_num_via_sides + + @dc_num_via_sides.setter + def dc_num_via_sides(self, value): + if isinstance(value, int): + self._dc_num_via_sides = value + + @property + def dc_percent_local_refinement(self): + """Retrieve the value for local mesh refinement. + + Returns + ------- + float + The refinement value, 0.2 (20%) as default. + + """ + return self._dc_percent_local_refinement + + @dc_percent_local_refinement.setter + def dc_percent_local_refinement(self, value): + if isinstance(value, (int, float)): + self._dc_percent_local_refinement = value + + @property + def dc_perform_adaptive_refinement(self): + """Retrieve the value for performing adaptive meshing. + + Returns + ------- + bool + ``True`` when activated, ``False`` deactivated. + + """ + return self._dc_perform_adaptive_refinement + + @dc_perform_adaptive_refinement.setter + def dc_perform_adaptive_refinement(self, value): + if isinstance(value, bool): + self._dc_perform_adaptive_refinement = value + + @property + def dc_refine_bondwires(self): + """Retrieve the value for performing bond wire refinement. + + Returns + ------- + bool + ``True`` when activated, ``False`` deactivated. + + """ + return self._dc_refine_bondwires + + @dc_refine_bondwires.setter + def dc_refine_bondwires(self, value): + if isinstance(value, bool): + self._dc_refine_bondwires = value + + @property + def dc_refine_vias(self): + """Retrieve the value for performing vias refinement. + + Returns + ------- + bool + ``True`` when activated, ``False`` deactivated. + + """ + return self._dc_refine_vias + + @dc_refine_vias.setter + def dc_refine_vias(self, value): + if isinstance(value, bool): + self._dc_refine_vias = value + + @property + def dc_report_config_file(self): + """Retrieve the report configuration file path. + + Returns + ------- + str + The file path. + + """ + return self._dc_report_config_file + + @dc_report_config_file.setter + def dc_report_config_file(self, value): + if isinstance(value, str): + self._dc_report_config_file = value + + @property + def dc_report_show_Active_devices(self): + """Retrieve the value for showing active devices. + + Returns + ------- + bool + ``True`` when activated, ``False`` deactivated. + + """ + return self._dc_report_show_Active_devices + + @dc_report_show_Active_devices.setter + def dc_report_show_Active_devices(self, value): + if isinstance(value, bool): + self._dc_report_show_Active_devices = value + + @property + def dc_export_thermal_data(self): + """Retrieve the value for using external data. + + Returns + ------- + bool + ``True`` when activated, ``False`` deactivated. + + """ + return self._dc_export_thermal_data + + @dc_export_thermal_data.setter + def dc_export_thermal_data(self, value): + if isinstance(value, bool): + self._dc_export_thermal_data = value + + @property + def dc_full_report_path(self): + """Retrieve the path for the report. + + Returns + ------- + str + File path. + + """ + return self._dc_full_report_path + + @dc_full_report_path.setter + def dc_full_report_path(self, value): + if isinstance(value, str): + self._dc_full_report_path = value + + @property + def dc_icepak_temp_file(self): + """Retrieve the icepak temp file path. + + Returns + ------- + str + File path. + """ + return self._dc_icepak_temp_file + + @dc_icepak_temp_file.setter + def dc_icepak_temp_file(self, value): + if isinstance(value, str): + self._dc_icepak_temp_file = value + + @property + def dc_import_thermal_data(self): + """Retrieve the value for importing thermal data. + + Returns + ------- + bool + ``True`` when activated,``False`` deactivated. + + """ + return self._dc_import_thermal_data + + @dc_import_thermal_data.setter + def dc_import_thermal_data(self, value): + if isinstance(value, bool): + self._dc_import_thermal_data = value + + @property + def dc_per_pin_res_path(self): + """Retrieve the file path. + + Returns + ------- + str + The file path. + """ + return self._dc_per_pin_res_path + + @dc_per_pin_res_path.setter + def dc_per_pin_res_path(self, value): + if isinstance(value, str): + self._dc_per_pin_res_path = value + + @property + def dc_per_pin_use_pin_format(self): + """Retrieve the value for using pin format. + + Returns + ------- + bool + """ + return self._dc_per_pin_use_pin_format + + @dc_per_pin_use_pin_format.setter + def dc_per_pin_use_pin_format(self, value): + if isinstance(value, bool): + self._dc_per_pin_use_pin_format = value + + @property + def dc_use_loop_res_for_per_pin(self): + """Retrieve the value for using the loop resistor per pin. + + Returns + ------- + bool + """ + return self._dc_use_loop_res_for_per_pin + + @dc_use_loop_res_for_per_pin.setter + def dc_use_loop_res_for_per_pin(self, value): + if isinstance(value, bool): + self._dc_use_loop_res_for_per_pin = value + + @property + def dc_via_report_path(self): + """Retrieve the via report file path. + + Returns + ------- + str + The file path. + + """ + return self._dc_via_report_path + + @dc_via_report_path.setter + def dc_via_report_path(self, value): + if isinstance(value, str): + self._dc_via_report_path = value + + @dc_via_report_path.setter + def dc_via_report_path(self, value): + if isinstance(value, str): + self._dc_via_report_path = value + + @property + def dc_source_terms_to_ground(self): + """Retrieve the dictionary of grounded terminals. + + Returns + ------- + Dictionary + {str, int}, keys is source name, value int 0 unspecified, 1 negative node, 2 positive one. + + """ + return self._dc_source_terms_to_ground + + @dc_source_terms_to_ground.setter + def dc_source_terms_to_ground(self, value): # pragma: no cover + if isinstance(value, OrderedDict): + if len([k for k in value.keys() if isinstance(k, str)]) == len(value.keys()): + if len([v for v in value.values() if isinstance(v, int)]) == len(value.values()): + self._dc_source_terms_to_ground = value + + +class SimulationConfigurationAc(object): + """Contains all AC analysis settings. + The class is part of `SimulationConfiguration` class as a property. + + """ + + def __init__(self): + self._sweep_interpolating = True + self._use_q3d_for_dc = False + self._relative_error = 0.005 + self._use_error_z0 = False + self._percentage_error_z0 = 1 + self._enforce_causality = True + self._enforce_passivity = False + self._passivity_tolerance = 0.0001 + self._sweep_name = "Sweep1" + self._radiation_box = RadiationBoxType.ConvexHull # 'ConvexHull' + self._start_freq = "0.0GHz" # 0.0 + self._stop_freq = "10.0GHz" # 10e9 + self._sweep_type = SweepType.Linear # 'Linear' + self._step_freq = "0.025GHz" # 10e6 + self._decade_count = 100 # Newly Added + self._mesh_freq = "3GHz" # 5e9 + self._max_num_passes = 30 + self._max_mag_delta_s = 0.03 + self._min_num_passes = 1 + self._basis_order = BasisOrder.Mixed # 'Mixed' + self._do_lambda_refinement = True + self._arc_angle = "30deg" # 30 + self._start_azimuth = 0 + self._max_arc_points = 8 + self._use_arc_to_chord_error = True + self._arc_to_chord_error = "1um" # 1e-6 + self._defeature_abs_length = "1um" # 1e-6 + self._defeature_layout = True + self._minimum_void_surface = 0 + self._max_suf_dev = 1e-3 + self._process_padstack_definitions = False + self._return_current_distribution = True + self._ignore_non_functional_pads = True + self._include_inter_plane_coupling = True + self._xtalk_threshold = -50 + self._min_void_area = "0.01mm2" + self._min_pad_area_to_mesh = "0.01mm2" + self._snap_length_threshold = "2.5um" + self._min_plane_area_to_mesh = "4mil2" # Newly Added + self._mesh_sizefactor = 0.0 + self._adaptive_type = AdaptiveType.SingleFrequency + self._adaptive_low_freq = "0GHz" + self._adaptive_high_freq = "20GHz" + + @property + def sweep_interpolating(self): # pragma: no cover + """Retrieve boolean to add a sweep interpolating sweep. + + Returns + ------- + bool + ``True`` when a sweep interpolating is defined, ``False`` when a discrete one is defined instead. + """ + + return self._sweep_interpolating + + @sweep_interpolating.setter + def sweep_interpolating(self, value): # pragma: no cover + if isinstance(value, bool): + self._sweep_interpolating = value + + @property + def use_q3d_for_dc(self): # pragma: no cover + """Retrieve boolean to Q3D solver for DC point value computation. + + Returns + ------- + bool + ``True`` when Q3D solver is used ``False`` when interpolating value is used instead. + """ + + return self._use_q3d_for_dc + + @use_q3d_for_dc.setter + def use_q3d_for_dc(self, value): # pragma: no cover + if isinstance(value, bool): + self._use_q3d_for_dc = value + + @property + def relative_error(self): # pragma: no cover + """Retrieve relative error used for the interpolating sweep convergence. + + Returns + ------- + float + The value of the error interpolating sweep to reach the convergence criteria. + """ + + return self._relative_error + + @relative_error.setter + def relative_error(self, value): # pragma: no cover + if isinstance(value, (int, float)): + self._relative_error = value + + @property + def use_error_z0(self): # pragma: no cover + """Retrieve value for the error on Z0 for the port. + + Returns + ------- + float + The Z0 value. + """ + + return self._use_error_z0 + + @use_error_z0.setter + def use_error_z0(self, value): # pragma: no cover + if isinstance(value, bool): + self._use_error_z0 = value + + @property + def percentage_error_z0(self): # pragma: no cover + """Retrieve boolean to perform the cutout during the project build. + + Returns + ------- + bool + ``True`` when clipping the design is applied ``False`` if not. + """ + + return self._percentage_error_z0 + + @percentage_error_z0.setter + def percentage_error_z0(self, value): # pragma: no cover + if isinstance(value, (int, float)): + self._percentage_error_z0 = value + + @property + def enforce_causality(self): # pragma: no cover + """Retrieve boolean to enforce causality for the frequency sweep. + + Returns + ------- + bool + ``True`` when causality is enforced ``False`` if not. + """ + + return self._enforce_causality + + @enforce_causality.setter + def enforce_causality(self, value): # pragma: no cover + if isinstance(value, bool): + self._enforce_causality = value + + @property + def enforce_passivity(self): # pragma: no cover + """Retrieve boolean to enforce passivity for the frequency sweep. + + Returns + ------- + bool + ``True`` when passivity is enforced ``False`` if not. + """ + return self._enforce_passivity + + @enforce_passivity.setter + def enforce_passivity(self, value): # pragma: no cover + if isinstance(value, bool): + self._enforce_passivity = value + + @property + def passivity_tolerance(self): # pragma: no cover + """Retrieve the value for the passivity tolerance when used. + + Returns + ------- + float + The passivity tolerance criteria for the frequency sweep. + """ + return self._passivity_tolerance + + @passivity_tolerance.setter + def passivity_tolerance(self, value): # pragma: no cover + if isinstance(value, (int, float)): + self._passivity_tolerance = value + + @property + def sweep_name(self): # pragma: no cover + """Retrieve frequency sweep name. + + Returns + ------- + str + The name of the frequency sweep defined in the project. + """ + return self._sweep_name + + @sweep_name.setter + def sweep_name(self, value): # pragma: no cover + if isinstance(value, str): + self._sweep_name = value + + @property + def radiation_box(self): # pragma: no cover + """Retrieve RadiationBoxType object selection defined for the radiation box type. + + Returns + ------- + RadiationBoxType object + 3 values can be chosen, Conformal, BoundingBox or ConvexHull. + """ + return self._radiation_box + + @radiation_box.setter + def radiation_box(self, value): + if validate_enum_class_value(RadiationBoxType, value): + self._radiation_box = value + + @property + def start_freq(self): # pragma: no cover + """Starting frequency for the frequency sweep. + + Returns + ------- + float + Value of the frequency point. + """ + return self._start_freq + + @start_freq.setter + def start_freq(self, value): # pragma: no cover + if isinstance(value, str): + self._start_freq = value + + @property + def stop_freq(self): # pragma: no cover + """Retrieve stop frequency for the frequency sweep. + + Returns + ------- + float + The value of the frequency point. + """ + return self._stop_freq + + @stop_freq.setter + def stop_freq(self, value): # pragma: no cover + if isinstance(value, str): + self._stop_freq = value + + @property + def sweep_type(self): # pragma: no cover + """Retrieve SweepType object for the frequency sweep. + + Returns + ------- + SweepType + The SweepType object,2 selections are supported Linear and LogCount. + """ + return self._sweep_type + + @sweep_type.setter + def sweep_type(self, value): # pragma: no cover + if validate_enum_class_value(SweepType, value): + self._sweep_type = value + + @property + def step_freq(self): # pragma: no cover + """Retrieve step frequency for the frequency sweep. + + Returns + ------- + float + The value of the frequency point. + """ + return self._step_freq + + @step_freq.setter + def step_freq(self, value): # pragma: no cover + if isinstance(value, str): + self._step_freq = value + + @property + def decade_count(self): # pragma: no cover + """Retrieve decade count number for the frequency sweep in case of a log sweep selected. + + Returns + ------- + int + The value of the decade count number. + """ + return self._decade_count + + @decade_count.setter + def decade_count(self, value): # pragma: no cover + if isinstance(value, int): + self._decade_count = value + + @property + def mesh_freq(self): + """Retrieve the meshing frequency for the HFSS adaptive convergence. + + Returns + ------- + float + The value of the frequency point. + """ + return self._mesh_freq + + @mesh_freq.setter + def mesh_freq(self, value): # pragma: no cover + if isinstance(value, str): + self._mesh_freq = value + + @property + def max_num_passes(self): # pragma: no cover + """Retrieve maximum of points for the HFSS adaptive meshing. + + Returns + ------- + int + The maximum number of adaptive passes value. + """ + return self._max_num_passes + + @max_num_passes.setter + def max_num_passes(self, value): # pragma: no cover + if isinstance(value, int): + self._max_num_passes = value + + @property + def max_mag_delta_s(self): # pragma: no cover + """Retrieve the magnitude of the delta S convergence criteria for the interpolating sweep. + + Returns + ------- + float + The value of convergence criteria. + """ + return self._max_mag_delta_s + + @max_mag_delta_s.setter + def max_mag_delta_s(self, value): # pragma: no cover + if isinstance(value, (int, float)): + self._max_mag_delta_s = value + + @property + def min_num_passes(self): # pragma: no cover + """Retrieve the minimum number of adaptive passes for HFSS convergence. + + Returns + ------- + int + The value of minimum number of adaptive passes. + """ + return self._min_num_passes + + @min_num_passes.setter + def min_num_passes(self, value): # pragma: no cover + if isinstance(value, int): + self._min_num_passes = value + + @property + def basis_order(self): # pragma: no cover + """Retrieve the BasisOrder object. + + Returns + ------- + BasisOrder class + This class supports 4 selections Mixed, Zero, single and Double for the HFSS order matrix. + """ + return self._basis_order + + @basis_order.setter + def basis_order(self, value): # pragma: no cover + if validate_enum_class_value(BasisOrder, value): + self._basis_order = value + + @property + def do_lambda_refinement(self): # pragma: no cover + """Retrieve boolean to activate the lambda refinement. + + Returns + ------- + bool + ``True`` Enable the lambda meshing refinement with HFSS, ``False`` deactivate. + """ + return self._do_lambda_refinement + + @do_lambda_refinement.setter + def do_lambda_refinement(self, value): # pragma: no cover + if isinstance(value, bool): + self._do_lambda_refinement = value + + @property + def arc_angle(self): # pragma: no cover + """Retrieve the value for the HFSS meshing arc angle. + + Returns + ------- + float + Value of the arc angle. + """ + return self._arc_angle + + @arc_angle.setter + def arc_angle(self, value): # pragma: no cover + if isinstance(value, str): + self._arc_angle = value + + @property + def start_azimuth(self): # pragma: no cover + """Retrieve the value of the starting azimuth for the HFSS meshing. + + Returns + ------- + float + Value of the starting azimuth. + """ + return self._start_azimuth + + @start_azimuth.setter + def start_azimuth(self, value): # pragma: no cover + if isinstance(value, (int, float)): + self._start_azimuth = value + + @property + def max_arc_points(self): # pragma: no cover + """Retrieve the value of the maximum arc points number for the HFSS meshing. + + Returns + ------- + int + Value of the maximum arc point number. + """ + return self._max_arc_points + + @max_arc_points.setter + def max_arc_points(self, value): # pragma: no cover + if isinstance(value, int): + self._max_arc_points = value + + @property + def use_arc_to_chord_error(self): # pragma: no cover + """Retrieve the boolean for activating the arc to chord for HFSS meshing. + + Returns + ------- + bool + Activate when ``True``, deactivated when ``False``. + """ + return self._use_arc_to_chord_error + + @use_arc_to_chord_error.setter + def use_arc_to_chord_error(self, value): # pragma: no cover + if isinstance(value, bool): + self._use_arc_to_chord_error = value + + @property + def arc_to_chord_error(self): # pragma: no cover + """Retrieve the value of arc to chord error for HFSS meshing. + + Returns + ------- + flot + Value of the arc to chord error. + """ + return self._arc_to_chord_error + + @arc_to_chord_error.setter + def arc_to_chord_error(self, value): # pragma: no cover + if isinstance(value, str): + self._arc_to_chord_error = value + + @property + def defeature_abs_length(self): # pragma: no cover + """Retrieve the value of arc to chord for HFSS meshing. + + Returns + ------- + flot + Value of the arc to chord error. + """ + return self._defeature_abs_length + + @defeature_abs_length.setter + def defeature_abs_length(self, value): # pragma: no cover + if isinstance(value, str): + self._defeature_abs_length = value + + @property + def defeature_layout(self): # pragma: no cover + """Retrieve the boolean to activate the layout defeaturing.This method has been developed to simplify polygons + with reducing the number of points to simplify the meshing with controlling its surface deviation. This method + should be used at last resort when other methods failed. + + Returns + ------- + bool + ``True`` when activated 'False when deactivated. + """ + return self._defeature_layout + + @defeature_layout.setter + def defeature_layout(self, value): # pragma: no cover + if isinstance(value, bool): + self._defeature_layout = value + + @property + def minimum_void_surface(self): # pragma: no cover + """Retrieve the minimum void surface to be considered for the layout defeaturing. + Voids below this value will be ignored. + + Returns + ------- + flot + Value of the minimum surface. + """ + return self._minimum_void_surface + + @minimum_void_surface.setter + def minimum_void_surface(self, value): # pragma: no cover + if isinstance(value, (int, float)): + self._minimum_void_surface = value + + @property + def max_suf_dev(self): # pragma: no cover + """Retrieve the value for the maximum surface deviation for the layout defeaturing. + + Returns + ------- + flot + Value of maximum surface deviation. + """ + return self._max_suf_dev + + @max_suf_dev.setter + def max_suf_dev(self, value): # pragma: no cover + if isinstance(value, (int, float)): + self._max_suf_dev = value + + @property + def process_padstack_definitions(self): # pragma: no cover + """Retrieve the boolean for activating the padstack definition processing. + + Returns + ------- + flot + Value of the arc to chord error. + """ + return self._process_padstack_definitions + + @process_padstack_definitions.setter + def process_padstack_definitions(self, value): # pragma: no cover + if isinstance(value, bool): + self._process_padstack_definitions = value + + @property + def return_current_distribution(self): # pragma: no cover + """Boolean to activate the current distribution return with Siwave. + + Returns + ------- + flot + Value of the arc to chord error. + """ + return self._return_current_distribution + + @return_current_distribution.setter + def return_current_distribution(self, value): # pragma: no cover + if isinstance(value, bool): + self._return_current_distribution = value + + @property + def ignore_non_functional_pads(self): # pragma: no cover + """Boolean to ignore nonfunctional pads with Siwave. + + Returns + ------- + flot + Value of the arc to chord error. + """ + return self._ignore_non_functional_pads + + @ignore_non_functional_pads.setter + def ignore_non_functional_pads(self, value): # pragma: no cover + if isinstance(value, bool): + self._ignore_non_functional_pads = value + + @property + def include_inter_plane_coupling(self): # pragma: no cover + """Boolean to activate the inter-plane coupling with Siwave. + + Returns + ------- + bool + ``True`` activated ``False`` deactivated. + """ + return self._include_inter_plane_coupling + + @include_inter_plane_coupling.setter + def include_inter_plane_coupling(self, value): # pragma: no cover + if isinstance(value, bool): + self._include_inter_plane_coupling = value + + @property + def xtalk_threshold(self): # pragma: no cover + """Return the value for Siwave cross talk threshold. THis value specifies the distance for the solver to + consider lines coupled during the cross-section computation. Decreasing the value below -60dB can + potentially cause solver failure. + + Returns + ------- + flot + Value of cross-talk threshold. + """ + return self._xtalk_threshold + + @xtalk_threshold.setter + def xtalk_threshold(self, value): # pragma: no cover + if isinstance(value, (int, float)): + self._xtalk_threshold = value + + @property + def min_void_area(self): # pragma: no cover + """Retrieve the value of minimum void area to be considered by Siwave. + + Returns + ------- + flot + Value of the arc to chord error. + """ + return self._min_void_area + + @min_void_area.setter + def min_void_area(self, value): # pragma: no cover + if isinstance(value, str): + self._min_void_area = value + + @property + def min_pad_area_to_mesh(self): # pragma: no cover + """Retrieve the value of minimum pad area to be meshed by Siwave. + + Returns + ------- + flot + Value of minimum pad surface. + """ + return self._min_pad_area_to_mesh + + @min_pad_area_to_mesh.setter + def min_pad_area_to_mesh(self, value): # pragma: no cover + if isinstance(value, str): + self._min_pad_area_to_mesh = value + + @property + def snap_length_threshold(self): # pragma: no cover + """Retrieve the boolean to activate the snapping threshold feature. + + Returns + ------- + bool + ``True`` activate ``False`` deactivated. + """ + return self._snap_length_threshold + + @snap_length_threshold.setter + def snap_length_threshold(self, value): # pragma: no cover + if isinstance(value, str): + self._snap_length_threshold = value + + @property + def min_plane_area_to_mesh(self): # pragma: no cover + """Retrieve the minimum plane area to be meshed by Siwave. + + Returns + ------- + flot + Value of the minimum plane area. + """ + return self._min_plane_area_to_mesh + + @min_plane_area_to_mesh.setter + def min_plane_area_to_mesh(self, value): # pragma: no cover + if isinstance(value, str): + self._min_plane_area_to_mesh = value + + @property + def mesh_sizefactor(self): + """Retrieve the Mesh Size factor value. + + Returns + ------- + float + """ + return self._mesh_sizefactor + + @mesh_sizefactor.setter + def mesh_sizefactor(self, value): + if isinstance(value, (int, float)): + self._mesh_sizefactor = value + if value > 0.0: + self._do_lambda_refinement = False + + @property + def adaptive_type(self): + """HFSS adaptive type. + + Returns + ------- + class: pyedb.dotnet.database.edb_data.simulation_setup.AdaptiveType + """ + return self._adaptive_type + + @adaptive_type.setter + def adaptive_type(self, value): + if isinstance(value, int) and value in range(3): + self._adaptive_type = value + + @property + def adaptive_low_freq(self): + """HFSS broadband low frequency adaptive meshing. + + Returns + ------- + str + """ + return self._adaptive_low_freq + + @adaptive_low_freq.setter + def adaptive_low_freq(self, value): + if isinstance(value, str): + self._adaptive_low_freq = value + + @property + def adaptive_high_freq(self): + """HFSS broadband high frequency adaptive meshing. + + Returns + ------- + str + """ + return self._adaptive_high_freq + + @adaptive_high_freq.setter + def adaptive_high_freq(self, value): + if isinstance(value, str): + self._adaptive_high_freq = value + + +class SimulationConfiguration(object): + """Provides an ASCII simulation configuration file parser. + + This parser supports all types of inputs for setting up and automating any kind + of SI or PI simulation with HFSS 3D Layout or Siwave. If fields are omitted, default + values are applied. This class can be instantiated directly from + Configuration file. + + Examples + -------- + This class is very convenient to build HFSS and SIwave simulation projects from layout. + It is leveraging EDB commands from Pyaedt but with keeping high level parameters making more easy PCB automation + flow. SYZ and DC simulation can be addressed with this class. + + The class is instantiated from an open edb: + + >>> from pyedb import Edb + >>> edb = Edb() + >>> sim_setup = edb.new_simulation_configuration() + + The returned object sim_setup is a SimulationConfiguration object. + From this class you can assign a lot of parameters related the project configuration but also solver options. + Here is the list of parameters available: + + >>> from pyedb.generic.constants import SolverType + >>> sim_setup.solver_type = SolverType.Hfss3dLayout + + Solver type can be selected, HFSS 3D Layout and Siwave are supported. + + + >>> sim_setup.signal_nets = ["net1", "net2"] + + Set the list of net names you want to include for the simulation. These nets will + have excitations ports created if corresponding pins are found on selected component. We usually refer to signal + nets but power / reference nets can also be passed into this list if user wants to have ports created on these ones. + + >>> sim_setup.power_nets = ["gnd", "vcc"] + + Set the list on power and reference nets. These nets won't have excitation ports created + on them and will be clipped during the project build if the cutout option is enabled. + + >>> sim_setup.components = ["comp1", "comp2"] + + Set the list of components which will be included in the simulation. These components will have ports created on + pins belonging to the net list. + + >>> sim_setup.do_cutout_subdesign = True + + When true activates the layout cutout based on net signal net selection and cutout expansion. + + >>> from pyedb.generic.constants import CutoutSubdesignType + >>> sim_setup.cutout_subdesign_type = CutoutSubdesignType.Conformal + + Define the type of cutout used for computing the clippingextent polygon. CutoutSubdesignType.Conformal + CutoutSubdesignType.BBox are surpported. + + >>> sim_setup.cutout_subdesign_expansion = "4mm" + + Define the distance used for computing the extent polygon. Integer or string can be passed. + For example 0.001 is in meter so here 1mm. You can also pass the string "1mm" for the same result. + + >>> sim_setup.cutout_subdesign_round_corner = True + + Boolean to allow using rounded corner for the cutout extent or not. + + >>> sim_setup.use_default_cutout = False + + When True use the native edb API command to process the cutout. Using False uses + the Pyaedt one which improves the cutout speed. + + >>> sim_setup.generate_solder_balls = True + + Boolean to activate the solder ball generation on components. When HFSS solver is selected in combination with this + parameter, coaxial ports will be created on solder balls for pins belonging to selected signal nets. If Siwave + solver is selected this parameter will be ignored. + + >>> sim_setup.use_default_coax_port_radial_extension = True + + When ``True`` the default coaxial extent is used for the ports (only for HFSS). + When the design is having dense solder balls close to each other (like typically package design), the default value + might be too large and cause port overlapping, then solver failure. To prevent this issue set this parameter to + ``False`` will use a smaller value. + + >>> sim_setup.output_aedb = r"C:\temp\my_edb.aedb" + + Specify the output edb file after building the project. The parameter must be the complete file path. + leaving this parameter blank will oervwritte the current open edb. + + >>> sim_setup.dielectric_extent = 0.01 + + Gives the dielectric extent after cutout, keeping default value is advised unless for + very specific application. + + >>> sim_setup.airbox_horizontal_extent = "5mm" + + Provide the air box horizonzal extent values. Unitless float value will be + treated as ratio but string value like "5mm" is also supported. + + >>> sim_setup.airbox_negative_vertical_extent = "5mm" + + Provide the air box negative vertical extent values. Unitless float value will be + treated as ratio but string value like "5mm" is also supported. + + >>> sim_setup.airbox_positive_vertical_extent = "5mm" + + Provide the air box positive vertical extent values. Unitless float value will be + treated as ratio but string value like "5mm" is also supported. + + >>> sim_setup.use_radiation_boundary = True + + When ``True`` use radiation airbox boundary condition and perfect metal box when + set to ``False``. Default value is ``True``, using enclosed metal box will greatly change simulation results. + Setting this parameter as ``False`` must be used cautiously. + + >>> sim_setup.do_cutout_subdesign = True + + ``True`` activates the cutout with associated parameters. Setting ``False`` will + keep the entire layout. + Setting to ``False`` can impact the simulation run time or even memory failure if HFSS solver is used. + + >>> sim_setup.do_pin_group = False + + When circuit ports are used, setting to ``True`` will force to create pin groups on + components having pins belonging to same net. Setting to ``False`` will generate port on each signal pin with + taking the closest reference pin. The last configuration is more often used when users are creating ports on PDN + (Power delivery Network) and want to connect all pins individually. + + >>> from pyedb.generic.constants import SweepType + >>> sim_setup.sweep_type = SweepType.Linear + + Specify the frequency sweep type, Linear or Log sweep can be defined. + + SimulationCOnfiguration also inherit from SimulationConfigurationAc class for High frequency settings. + + >>> sim_setup.start_freq = "OHz" + + Define the start frequency from the sweep. + + >>> sim_setup.stop_freq = "40GHz" + + Define the stop frequency from the sweep. + + >>> sim_setup.step_freq = "10MHz" + + Define the step frequency from the sweep. + + >>> sim_setup.decade_count = 100 + + Used when log sweep is defined and specify the number of points per decade. + + >>> sim_setup.enforce_causality = True + + Activate the option ``Enforce Causality`` for the solver, recommended for signal integrity application + + >>> sim_setup.enforce_passivity = True + + Activate the option ``Enforce Passivity`` for the solver, recommended for signal integrity application + + >>> sim_setup.do_lambda_refinement = True + + Activate the lambda refinement for the initial mesh (only for HFSS), default value is ``True``. Keeping this + activated is highly recommended. + + >>> sim_setup.use_q3d_for_dc = False + + Enable when ``True`` the Q3D DC point computation. Only needed when very high accuracy is required for DC point. + Can eventually cause extra computation time. + + >>> sim_setup.sweep_name = "Test_sweep" + + Define the frequency sweep name. + + >>> sim_setup.mesh_freq = "10GHz" + + Define the frequency used for adaptive meshing (available for both HFSS and SIwave). + + >>> from pyedb.generic.constants import RadiationBoxType + >>> sim_setup.radiation_box = RadiationBoxType.ConvexHull + + Defined the radiation box type, Conformal, Bounding box and ConvexHull are supported (HFSS only). + + >>> sim_setup.max_num_passes= 30 + + Default value is 30, specify the maximum number of adaptive passes (only HFSS). Reasonable high value is recommended + to force the solver reaching the convergence criteria. + + >>> sim_setup.max_mag_delta_s = 0.02 + + Define the convergence criteria + + >>> sim_setup.min_num_passes = 2 + + specify the minimum number of consecutive coberged passes. Setting to 2 is a good practice to avoid converging on + local minima. + + >>> from pyedb.generic.constants import BasisOrder + >>> sim_setup.basis_order = BasisOrder.Single + + Select the order basis (HFSS only), Zero, Single, Double and Mixed are supported. For Signal integrity Single or + Mixed should be used. + + >>> sim_setup.minimum_void_surface = 0 + + Only for Siwave, specify the minimum void surface to be meshed. Void with lower surface value will be ignored by + meshing. + + SimulationConfiguration also inherits from SimulationDc class to handle DC simulation projects. + + >>> sim_setup.dc_compute_inductance = True + + ``True`` activate the DC loop inductance computation (Siwave only), ``False`` is deactivated. + + >>> sim_setup.dc_slide_position = 1 + + The provided value must be between 0 and 2 and correspond ti the SIwave DC slide position in GUI. + 0 : coarse + 1 : medium accuracy + 2 : high accuracy + + >>> sim_setup.dc_plot_jv = True + + ``True`` activate the current / voltage plot with Siwave DC solver, ``False`` deactivate. + + >>> sim_setup.dc_error_energy = 0.02 + + Fix the DC error convergence criteria. In this example 2% is defined. + + >>> sim_setup.dc_max_num_pass = 6 + + Provide the maximum number of passes during Siwave DC adaptive meshing. + + >>> sim_setup.dc_min_num_pass = 1 + + Provide the minimum number of passes during Siwave DC adaptive meshing. + + >>> sim_setup.dc_mesh_bondwires = True + + ``True`` bondwires are meshed, ``False`` bond wires are ignored during meshing. + + >>> sim_setup.dc_num_bondwire_sides = 8 + + Gives the number of facets wirebonds are discretized. + + >>> sim_setup.dc_refine_vias = True + + ``True`` meshing refinement on nondwires activated during meshing process. Deactivated when set to ``False``. + + >>> sim_setup.dc_report_show_Active_devices = True + + Activate when ``True`` the components showing in the DC report. + + >>> sim_setup.dc_export_thermal_data = True + + ``True`` thermal data are exported for Icepak simulation. + + >>> sim_setup.dc_full_report_path = r"C:\temp\my_report.html" + + Provides the file path for the DC report. + + >>> sim_setup.dc_icepak_temp_file = r"C:\temp\my_file" + + Provides icepak temporary files location. + + >>> sim_setup.dc_import_thermal_data = False + + Import DC thermal data when `True`` + + >>> sim_setup.dc_per_pin_res_path = r"C:\temp\dc_pin_res_file" + Provides the resistance per pin file path. + + >>> sim_setup.dc_per_pin_use_pin_format = True + + When ``True`` activate the pin format. + + >>> sim_setup.dc_use_loop_res_for_per_pin = True + + Activate the loop resistance usage per pin when ``True`` + + >>> sim_setup.dc_via_report_path = 'C:\\temp\\via_report_file' + + Define the via report path file. + + >>> sim_setup.add_current_source(name="test_isrc", + >>> current_value=1.2, + >>> phase_value=0.0, + >>> impedance=5e7, + >>> positive_node_component="comp1", + >>> positive_node_net="net1", + >>> negative_node_component="comp2", + >>> negative_node_net="net2" + >>> ) + + Define a current source. + + >>> sim_setup.add_dc_ground_source_term(source_name="test_isrc", node_to_ground=1) + + Define the pin from a source which has to be set to reference for DC simulation. + + >>> sim_setup.add_voltage_source(name="test_vsrc", + >>> current_value=1.33, + >>> phase_value=0.0, + >>> impedance=1e-6, + >>> positive_node_component="comp1", + >>> positive_node_net="net1", + >>> negative_node_component="comp2", + >>> negative_node_net="net2" + >>> ) + + Define a voltage source. + + >>> sim_setup.add_dc_ground_source_term(source_name="test_vsrc", node_to_ground=1) + + Define the pin from a source which has to be set to reference for DC simulation. + + >>> edb.build_simulation_project(sim_setup) + + Will build and save your project. + """ + + def __getattr__(self, item): + if item in dir(self): + return self.__getattribute__(item) + elif item in dir(self.dc_settings): + return self.dc_settings.__getattribute__(item) + elif item in dir(self.ac_settings): + return self.ac_settings.__getattribute__(item) + elif item in dir(self.batch_solve_settings): + return self.batch_solve_settings.__getattribute__(item) + else: + raise AttributeError("Attribute {} not present.".format(item)) + + def __setattr__(self, key, value): + if "_dc_settings" in dir(self) and key in dir(self._dc_settings): + return self.dc_settings.__setattr__(key, value) + elif "_ac_settings" in dir(self) and key in dir(self._ac_settings): + return self.ac_settings.__setattr__(key, value) + elif "_batch_solve_settings" in dir(self) and key in dir(self._batch_solve_settings): + return self.batch_solve_settings.__setattr__(key, value) + else: + return super(SimulationConfiguration, self).__setattr__(key, value) + + def __init__(self, filename=None, edb=None): + self._filename = filename + self._open_edb_after_build = True + self._dc_settings = SimulationConfigurationDc() + self._ac_settings = SimulationConfigurationAc() + self._batch_solve_settings = SimulationConfigurationBatch() + self._setup_name = "Pyaedt_setup" + self._solver_type = SolverType.Hfss3dLayout + if self._filename and os.path.splitext(self._filename)[1] == ".json": + self.import_json(filename) + self._read_cfg() + self._pedb = edb + self.SOLVER_TYPE = SolverType + + @property + def open_edb_after_build(self): + """Either if open the Edb after the build or not. + + Returns + ------- + bool + """ + return self._open_edb_after_build + + @open_edb_after_build.setter + def open_edb_after_build(self, value): + if isinstance(value, bool): + self._open_edb_after_build = value + + @property + def dc_settings(self): + # type: () -> SimulationConfigurationDc + """DC Settings class. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.simulation_configuration.SimulationConfigurationDc` + """ + return self._dc_settings + + @property + def ac_settings(self): + # type: () -> SimulationConfigurationAc + """AC Settings class. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.simulation_configuration.SimulationConfigurationAc` + """ + return self._ac_settings + + @property + def batch_solve_settings(self): + # type: () -> SimulationConfigurationBatch + """Cutout and Batch Settings class. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.simulation_configuration.SimulationConfigurationBatch` + """ + return self._batch_solve_settings + + def build_simulation_project(self): + """Build active simulation project. This method requires to be run inside Edb Class. + + Returns + ------- + bool""" + return self._pedb.build_simulation_project(self) + + @property + def solver_type(self): # pragma: no cover + """Retrieve the SolverType class to select the solver to be called during the project build. + + Returns + ------- + :class:`dotnet.generic.constants.SolverType` + selections are supported, Hfss3dLayout and Siwave. + """ + return self._solver_type + + @solver_type.setter + def solver_type(self, value): # pragma: no cover + if isinstance(value, int): + self._solver_type = value + + @property + def filename(self): # pragma: no cover + """Retrieve the file name loaded for mapping properties value. + + Returns + ------- + str + the absolute path for the filename. + """ + return self._filename + + @filename.setter + def filename(self, value): + if isinstance(value, str): # pragma: no cover + self._filename = value + + @property + def setup_name(self): + """Retrieve setup name for the simulation. + + Returns + ------- + str + Setup name. + """ + return self._setup_name + + @setup_name.setter + def setup_name(self, value): + if isinstance(value, str): # pragma: no cover + self._setup_name = value + + def _get_bool_value(self, value): # pragma: no cover + val = value.lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return True + elif val in ("n", "no", "f", "false", "off", "0"): + return False + else: + raise ValueError("Invalid truth value %r" % (val,)) + + def _get_list_value(self, value): # pragma: no cover + value = value[1:-1] + if len(value) == 0: + return [] + else: + value = value.split(",") + if isinstance(value, list): + prop_values = [i.strip() for i in value] + else: + prop_values = [value.strip()] + return prop_values + + def add_dc_ground_source_term(self, source_name=None, node_to_ground=1): + """Add a dc ground source terminal for Siwave. + + Parameters + ---------- + source_name : str, optional + The source name to assign the reference node to. + Default value is ``None``. + + node_to_ground : int, optional + Value must be ``0``: unspecified, ``1``: negative node, ``2``: positive node. + Default value is ``1``. + + """ + if source_name: + if node_to_ground in [0, 1, 2]: + self._dc_source_terms_to_ground[source_name] = node_to_ground + + def _read_cfg(self): # pragma: no cover + if not self.filename or not os.path.exists(self.filename): + # raise Exception("{} does not exist.".format(self.filename)) + return + + try: + with open(self.filename) as cfg_file: + cfg_lines = cfg_file.read().split("\n") + for line in cfg_lines: + if line.strip() != "": + if line.find("=") > 0: + i, prop_value = line.strip().split("=") + value = prop_value.replace("'", "").strip() + if i.lower().startswith("generatesolderballs"): + self.generate_solder_balls = self._get_bool_value(value) + elif i.lower().startswith("signalnets"): + self.signal_nets = value[1:-1].split(",") if value[0] == "[" else value.split(",") + self.signal_nets = [item.strip() for item in self.signal_nets] + elif i.lower().startswith("powernets"): + self.power_nets = value[1:-1].split(",") if value[0] == "[" else value.split(",") + self.power_nets = [item.strip() for item in self.power_nets] + elif i.lower().startswith("components"): + self.components = value[1:-1].split(",") if value[0] == "[" else value.split(",") + self.components = [item.strip() for item in self.components] + elif i.lower().startswith("coaxsolderballsdiams"): + self.coax_solder_ball_diameter = ( + value[1:-1].split(",") if value[0] == "[" else value.split(",") + ) + self.coax_solder_ball_diameter = [ + item.strip() for item in self.coax_solder_ball_diameter + ] + elif i.lower().startswith("usedefaultcoaxportradialextentfactor"): + self.signal_nets = self._get_bool_value(value) + elif i.lower().startswith("trimrefsize"): + self.trim_reference_size = self._get_bool_value(value) + elif i.lower().startswith("cutoutsubdesigntype"): + if value.lower().startswith("conformal"): + self.cutout_subdesign_type = CutoutSubdesignType.Conformal + elif value.lower().startswith("boundingbox"): + self.cutout_subdesign_type = CutoutSubdesignType.BoundingBox + else: + print("Unprocessed value for CutoutSubdesignType '{0}'".format(value)) + elif i.lower().startswith("cutoutsubdesignexpansion"): + self.cutout_subdesign_expansion = value + elif i.lower().startswith("cutoutsubdesignroundcorners"): + self.cutout_subdesign_round_corner = self._get_bool_value(value) + elif i.lower().startswith("sweepinterpolating"): + self.sweep_interpolating = self._get_bool_value(value) + elif i.lower().startswith("useq3dfordc"): + self.use_q3d_for_dc = self._get_bool_value(value) + elif i.lower().startswith("relativeerrors"): + self.relative_error = float(value) + elif i.lower().startswith("useerrorz0"): + self.use_error_z0 = self._get_bool_value(value) + elif i.lower().startswith("percenterrorz0"): + self.percentage_error_z0 = float(value) + elif i.lower().startswith("enforcecausality"): + self.enforce_causality = self._get_bool_value(value) + elif i.lower().startswith("enforcepassivity"): + self.enforce_passivity = self._get_bool_value(value) + elif i.lower().startswith("passivitytolerance"): + self.passivity_tolerance = float(value) + elif i.lower().startswith("sweepname"): + self.sweep_name = value + elif i.lower().startswith("radiationbox"): + if value.lower().startswith("conformal"): + self.radiation_box = RadiationBoxType.Conformal + elif value.lower().startswith("boundingbox"): + self.radiation_box = RadiationBoxType.BoundingBox + elif value.lower().startswith("convexhull"): + self.radiation_box = RadiationBoxType.ConvexHull + else: + print("Unprocessed value for RadiationBox '{0}'".format(value)) + elif i.lower().startswith("startfreq"): + self.start_freq = value + elif i.lower().startswith("stopfreq"): + self.stop_freq = value + elif i.lower().startswith("sweeptype"): + if value.lower().startswith("linear"): + self.sweep_type = SweepType.Linear + elif value.lower().startswith("logcount"): + self.sweep_type = SweepType.LogCount + else: + print("Unprocessed value for SweepType '{0}'".format(value)) + elif i.lower().startswith("stepfreq"): + self.step_freq = value + elif i.lower().startswith("decadecount"): + self.decade_count = int(value) + elif i.lower().startswith("mesh_freq"): + self.mesh_freq = value + elif i.lower().startswith("maxnumpasses"): + self.max_num_passes = int(value) + elif i.lower().startswith("maxmagdeltas"): + self.max_mag_delta_s = float(value) + elif i.lower().startswith("minnumpasses"): + self.min_num_passes = int(value) + elif i.lower().startswith("basisorder"): + if value.lower().startswith("mixed"): + self.basis_order = BasisOrder.Mixed + elif value.lower().startswith("zero"): + self.basis_order = BasisOrder.Zero + elif value.lower().startswith("first"): # single + self.basis_order = BasisOrder.Single + elif value.lower().startswith("second"): # double + self.basis_order = BasisOrder.Double + else: + print("Unprocessed value for BasisOrder '{0}'".format(value)) + elif i.lower().startswith("dolambdarefinement"): + self.do_lambda_refinement = self._get_bool_value(value) + elif i.lower().startswith("arcangle"): + self.arc_angle = value + elif i.lower().startswith("startazimuth"): + self.start_azimuth = float(value) + elif i.lower().startswith("maxarcpoints"): + self.max_arc_points = int(value) + elif i.lower().startswith("usearctochorderror"): + self.use_arc_to_chord_error = self._get_bool_value(value) + elif i.lower().startswith("arctochorderror"): + self.arc_to_chord_error = value + elif i.lower().startswith("defeatureabsLength"): + self.defeature_abs_length = value + elif i.lower().startswith("defeaturelayout"): + self.defeature_layout = self._get_bool_value(value) + elif i.lower().startswith("minimumvoidsurface"): + self.minimum_void_surface = float(value) + elif i.lower().startswith("maxsurfdev"): + self.max_suf_dev = float(value) + elif i.lower().startswith("processpadstackdefinitions"): + self.process_padstack_definitions = self._get_bool_value(value) + elif i.lower().startswith("returncurrentdistribution"): + self.return_current_distribution = self._get_bool_value(value) + elif i.lower().startswith("ignorenonfunctionalpads"): + self.ignore_non_functional_pads = self._get_bool_value(value) + elif i.lower().startswith("includeinterplanecoupling"): + self.include_inter_plane_coupling = self._get_bool_value(value) + elif i.lower().startswith("xtalkthreshold"): + self.xtalk_threshold = float(value) + elif i.lower().startswith("minvoidarea"): + self.min_void_area = value + elif i.lower().startswith("minpadareatomesh"): + self.min_pad_area_to_mesh = value + elif i.lower().startswith("snaplengththreshold"): + self.snap_length_threshold = value + elif i.lower().startswith("minplaneareatomesh"): + self.min_plane_area_to_mesh = value + elif i.lower().startswith("dcminplaneareatomesh"): + self.dc_min_plane_area_to_mesh = value + elif i.lower().startswith("maxinitmeshedgelength"): + self.max_init_mesh_edge_length = value + elif i.lower().startswith("signallayersproperties"): + self._parse_signal_layer_properties = value[1:-1] if value[0] == "[" else value + self._parse_signal_layer_properties = [ + item.strip() for item in self._parse_signal_layer_properties + ] + elif i.lower().startswith("coplanar_instances"): + self.coplanar_instances = value[1:-1] if value[0] == "[" else value + self.coplanar_instances = [item.strip() for item in self.coplanar_instances] + elif i.lower().startswith("signallayersetching"): + self.signal_layer_etching_instances = value[1:-1] if value[0] == "[" else value + self.signal_layer_etching_instances = [ + item.strip() for item in self.signal_layer_etching_instances + ] + elif i.lower().startswith("etchingfactor"): + self.etching_factor_instances = value[1:-1] if value[0] == "[" else value + self.etching_factor_instances = [item.strip() for item in self.etching_factor_instances] + elif i.lower().startswith("docutoutsubdesign"): + self.do_cutout_subdesign = self._get_bool_value(value) + elif i.lower().startswith("solvertype"): + if value.lower() == "hfss": + self.solver_type = 0 + if value.lower() == "hfss3dlayout": + self.solver_type = 6 + elif value.lower().startswith("siwavesyz"): + self.solver_type = 7 + elif value.lower().startswith("siwavedc"): + self.solver_type = 8 + elif value.lower().startswith("q3d"): + self.solver_type = 2 + elif value.lower().startswith("nexxim"): + self.solver_type = 4 + elif value.lower().startswith("maxwell"): + self.solver_type = 3 + elif value.lower().startswith("twinbuilder"): + self.solver_type = 5 + else: + self.solver_type = SolverType.Hfss3dLayout + else: + print("Unprocessed line in cfg file: {0}".format(line)) + else: + continue + except EnvironmentError as e: + print("Error reading cfg file: {}".format(e.message)) + raise + + def _dict_to_json(self, dict_out, dict_in=None): + exclude = ["_pedb", "SOLVER_TYPE"] + for k, v in dict_in.items(): + if k in exclude: + continue + if k[0] == "_": + if k[1:] in ["dc_settings", "ac_settings", "batch_solve_settings"]: + dict_out[k[1:]] = {} + dict_out[k[1:]] = self._dict_to_json(dict_out[k[1:]], self.__getattr__(k).__dict__) + elif k == "_sources": + sources_out = [src._json_format() for src in v] + dict_out[k[1:]] = sources_out + elif k == "_dc_source_terms_to_ground": + dc_term_gnd = {} + for k2 in list(v.Keys): # pragma: no cover + dc_term_gnd[k2] = v[k2] + dict_out[k[1:]] = dc_term_gnd + else: + dict_out[k[1:]] = v + else: + dict_out[k] = v + return dict_out + + def _json_to_dict(self, json_dict): + for k, v in json_dict.items(): + if k == "sources": + for src in json_dict[k]: # pragma: no cover + source = Source() + source._read_json(src) + self.batch_solve_settings.sources.append(source) + elif k == "dc_source_terms_to_ground": + dc_term_gnd = Dictionary[str, int]() + for k1, v1 in json_dict[k]: # pragma: no cover + dc_term_gnd[k1] = v1 + self.dc_source_terms_to_ground = dc_term_gnd + elif k in ["dc_settings", "ac_settings", "batch_solve_settings"]: + self._json_to_dict(v) + else: + self.__setattr__(k, v) + + def export_json(self, output_file): + """Export Json file from SimulationConfiguration object. + + Parameters + ---------- + output_file : str + Json file name. + + Returns + ------- + bool + True when succeeded False when file name not provided. + + Examples + -------- + + >>> from pyedb.grpc.database.utility.simulation_configuration import SimulationConfiguration + >>> config = SimulationConfiguration() + >>> config.export_json(r"C:\Temp\test_json\test.json") + """ + dict_out = {} + dict_out = self._dict_to_json(dict_out, self.__dict__) + if output_file: + with open(output_file, "w") as write_file: + json.dump(dict_out, write_file, indent=4) + return True + else: + return False + + def import_json(self, input_file): + """Import Json file into SimulationConfiguration object instance. + + Parameters + ---------- + input_file : str + Json file name. + + Returns + ------- + bool + True when succeeded False when file name not provided. + + Examples + -------- + >>> from pyedb.grpc.database.utility.simulation_configuration import SimulationConfiguration + >>> test = SimulationConfiguration() + >>> test.import_json(r"C:\Temp\test_json\test.json") + """ + if input_file: + f = open(input_file) + json_dict = json.load(f) # pragma: no cover + self._json_to_dict(json_dict) + self.filename = input_file + return True + else: + return False + + def add_voltage_source( + self, + name="", + voltage_value=1, + phase_value=0, + impedance=1e-6, + positive_node_component="", + positive_node_net="", + negative_node_component="", + negative_node_net="", + ): + """Add a voltage source for the current SimulationConfiguration instance. + + Parameters + ---------- + name : str + Source name. + + voltage_value : float + Amplitude value of the source. Either amperes for current source or volts for + voltage source. + + phase_value : float + Phase value of the source. + + impedance : float + Impedance value of the source. + + positive_node_component : str + Name of the component used for the positive node. + + negative_node_component : str + Name of the component used for the negative node. + + positive_node_net : str + Net used for the positive node. + + negative_node_net : str + Net used for the negative node. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + >>> edb = Edb(target_file) + >>> sim_setup = SimulationConfiguration() + >>> sim_setup.add_voltage_source(voltage_value=1.0, phase_value=0, positive_node_component="V1", + >>> positive_node_net="HSG", negative_node_component="V1", negative_node_net="SW") + + """ + if name == "": # pragma: no cover + name = generate_unique_name("v_source") + source = Source() + source.source_type = SourceType.Vsource + source.name = name + source.amplitude = voltage_value + source.phase = phase_value + source.positive_node.component = positive_node_component + source.positive_node.net = positive_node_net + source.negative_node.component = negative_node_component + source.negative_node.net = negative_node_net + source.impedance_value = impedance + try: # pragma: no cover + self.sources.append(source) + return True + except: # pragma: no cover + return False + + def add_current_source( + self, + name="", + current_value=0.1, + phase_value=0, + impedance=5e7, + positive_node_component="", + positive_node_net="", + negative_node_component="", + negative_node_net="", + ): + """Add a current source for the current SimulationConfiguration instance. + + Parameters + ---------- + name : str + Source name. + + current_value : float + Amplitude value of the source. Either amperes for current source or volts for + voltage source. + + phase_value : float + Phase value of the source. + + impedance : float + Impedance value of the source. + + positive_node_component : str + Name of the component used for the positive node. + + negative_node_component : str + Name of the component used for the negative node. + + positive_node_net : str + Net used for the positive node. + + negative_node_net : str + Net used for the negative node. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + >>> edb = Edb(target_file) + >>> sim_setup = SimulationConfiguration() + >>> sim_setup.add_voltage_source(voltage_value=1.0, phase_value=0, positive_node_component="V1", + >>> positive_node_net="HSG", negative_node_component="V1", negative_node_net="SW") + """ + + if name == "": # pragma: no cover + name = generate_unique_name("I_source") + source = Source() + source.source_type = SourceType.Isource + source.name = name + source.amplitude = current_value + source.phase = phase_value + source.positive_node.component = positive_node_component + source.positive_node.net = positive_node_net + source.negative_node.component = negative_node_component + source.negative_node.net = negative_node_net + source.impedance_value = impedance + try: # pragma: no cover + self.sources.append(source) + return True + except: # pragma: no cover + return False + + def add_rlc( + self, + name="", + r_value=1.0, + c_value=0.0, + l_value=0.0, + positive_node_component="", + positive_node_net="", + negative_node_component="", + negative_node_net="", + create_physical_rlc=True, + ): + """Add a voltage source for the current SimulationConfiguration instance. + + Parameters + ---------- + name : str + Source name. + + r_value : float + Resistor value in Ohms. + + l_value : float + Inductance value in Henry. + + c_value : float + Capacitance value in Farrad. + + positive_node_component : str + Name of the component used for the positive node. + + negative_node_component : str + Name of the component used for the negative node. + + positive_node_net : str + Net used for the positive node. + + negative_node_net : str + Net used for the negative node. + + create_physical_rlc : bool + When True create a physical Rlc component. Recommended setting to True to be compatible with Siwave. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + Examples + -------- + >>> edb = Edb(target_file) + >>> sim_setup = SimulationConfiguration() + >>> sim_setup.add_voltage_source(voltage_value=1.0, phase_value=0, positive_node_component="V1", + >>> positive_node_net="HSG", negative_node_component="V1", negative_node_net="SW") + """ + + if name == "": # pragma: no cover + name = generate_unique_name("Rlc") + source = Source() + source.source_type = SourceType.Rlc + source.name = name + source.r_value = r_value + source.l_value = l_value + source.c_value = c_value + source.create_physical_resistor = create_physical_rlc + source.positive_node.component = positive_node_component + source.positive_node.net = positive_node_net + source.negative_node.component = negative_node_component + source.negative_node.net = negative_node_net + try: # pragma: no cover + self.sources.append(source) + return True + except: # pragma: no cover + return False + + +class ProcessSimulationConfiguration(object): + @staticmethod + def configure_hfss_extents(self, simulation_setup=None): + """Configure the HFSS extent box. + + Parameters + ---------- + simulation_setup : + Edb_DATA.SimulationConfiguration object + + Returns + ------- + bool + True when succeeded, False when failed. + """ + + if not isinstance(simulation_setup, SimulationConfiguration): + self._logger.error( + "Configure HFSS extent requires edb_data.simulation_configuration.SimulationConfiguration object" + ) + return False + hfss_extent = self._edb.utility.utility.HFSSExtentInfo() + if simulation_setup.radiation_box == RadiationBoxType.BoundingBox: + hfss_extent.ExtentType = self._edb.utility.utility.HFSSExtentInfoType.BoundingBox + elif simulation_setup.radiation_box == RadiationBoxType.Conformal: + hfss_extent.ExtentType = self._edb.utility.utility.HFSSExtentInfoType.Conforming + else: + hfss_extent.ExtentType = self._edb.utility.utility.HFSSExtentInfoType.ConvexHull + hfss_extent.dielectric_extent_size = ( + simulation_setup.dielectric_extent, + simulation_setup.use_dielectric_extent_multiple, + ) + hfss_extent.air_box_horizontal_extent = ( + simulation_setup.airbox_horizontal_extent, + simulation_setup.use_airbox_horizontal_extent_multiple, + ) + hfss_extent.air_box_negative_vertical_extent = ( + simulation_setup.airbox_negative_vertical_extent, + simulation_setup.use_airbox_negative_vertical_extent_multiple, + ) + hfss_extent.AirBoxPositiveVerticalExtent = ( + simulation_setup.airbox_positive_vertical_extent, + simulation_setup.use_airbox_positive_vertical_extent_multiple, + ) + hfss_extent.HonorUserDielectric = simulation_setup.honor_user_dielectric + hfss_extent.TruncateAirBoxAtGround = simulation_setup.truncate_airbox_at_ground + hfss_extent.UseOpenRegion = simulation_setup.use_radiation_boundary + self._layout.cell.SetHFSSExtentInfo(hfss_extent) # returns void + return True + + @staticmethod + def configure_hfss_analysis_setup(self, simulation_setup=None): + """ + Configure HFSS analysis setup. + + Parameters + ---------- + simulation_setup : + Edb_DATA.SimulationConfiguration object + + Returns + ------- + bool + True when succeeded, False when failed. + """ + if not isinstance(simulation_setup, SimulationConfiguration): + self._logger.error( + "Configure HFSS analysis requires and edb_data.simulation_configuration.SimulationConfiguration object \ + as argument" + ) + return False + simsetup_info = self._pedb.simsetupdata.SimSetupInfo[self._pedb.simsetupdata.HFSSSimulationSettings]() + simsetup_info.Name = simulation_setup.setup_name + + if simulation_setup.ac_settings.adaptive_type == 0: + adapt = self._pedb.simsetupdata.AdaptiveFrequencyData() + adapt.AdaptiveFrequency = simulation_setup.mesh_freq + adapt.MaxPasses = int(simulation_setup.max_num_passes) + adapt.MaxDelta = str(simulation_setup.max_mag_delta_s) + simsetup_info.SimulationSettings.AdaptiveSettings.AdaptiveFrequencyDataList = convert_py_list_to_net_list( + [adapt] + ) + elif simulation_setup.ac_settings.adaptive_type == 2: + low_freq_adapt_data = self._pedb.simsetupdata.AdaptiveFrequencyData() + low_freq_adapt_data.MaxDelta = str(simulation_setup.max_mag_delta_s) + low_freq_adapt_data.MaxPasses = int(simulation_setup.max_num_passes) + low_freq_adapt_data.AdaptiveFrequency = simulation_setup.ac_settings.adaptive_low_freq + high_freq_adapt_data = self._pedb.simsetupdata.AdaptiveFrequencyData() + high_freq_adapt_data.MaxDelta = str(simulation_setup.max_mag_delta_s) + high_freq_adapt_data.MaxPasses = int(simulation_setup.max_num_passes) + high_freq_adapt_data.AdaptiveFrequency = simulation_setup.ac_settings.adaptive_high_freq + simsetup_info.SimulationSettings.AdaptiveSettings.AdaptType = ( + self._pedb.simsetupdata.AdaptiveSettings.TAdaptType.kBroadband + ) + simsetup_info.SimulationSettings.AdaptiveSettings.AdaptiveFrequencyDataList.Clear() + simsetup_info.SimulationSettings.AdaptiveSettings.AdaptiveFrequencyDataList.Add(low_freq_adapt_data) + simsetup_info.SimulationSettings.AdaptiveSettings.AdaptiveFrequencyDataList.Add(high_freq_adapt_data) + + simsetup_info.SimulationSettings.CurveApproxSettings.ArcAngle = simulation_setup.arc_angle + simsetup_info.SimulationSettings.CurveApproxSettings.UseArcToChordError = ( + simulation_setup.use_arc_to_chord_error + ) + simsetup_info.SimulationSettings.CurveApproxSettings.ArcToChordError = simulation_setup.arc_to_chord_error + + simsetup_info.SimulationSettings.InitialMeshSettings.LambdaRefine = simulation_setup.do_lambda_refinement + if simulation_setup.mesh_sizefactor > 0.0: + simsetup_info.SimulationSettings.InitialMeshSettings.MeshSizefactor = simulation_setup.mesh_sizefactor + simsetup_info.SimulationSettings.InitialMeshSettings.LambdaRefine = False + simsetup_info.SimulationSettings.AdaptiveSettings.MaxRefinePerPass = 30 + simsetup_info.SimulationSettings.AdaptiveSettings.MinPasses = simulation_setup.min_num_passes + simsetup_info.SimulationSettings.AdaptiveSettings.MinConvergedPasses = 1 + simsetup_info.SimulationSettings.HFSSSolverSettings.OrderBasis = simulation_setup.basis_order + simsetup_info.SimulationSettings.HFSSSolverSettings.UseHFSSIterativeSolver = False + simsetup_info.SimulationSettings.DefeatureSettings.UseDefeature = False # set True when using defeature ratio + simsetup_info.SimulationSettings.DefeatureSettings.UseDefeatureAbsLength = simulation_setup.defeature_layout + simsetup_info.SimulationSettings.DefeatureSettings.DefeatureAbsLength = simulation_setup.defeature_abs_length + + try: + if simulation_setup.add_frequency_sweep: + self._logger.info("Adding frequency sweep") + sweep = self._pedb.simsetupdata.SweepData(simulation_setup.sweep_name) + sweep.IsDiscrete = False + sweep.UseQ3DForDC = simulation_setup.use_q3d_for_dc + sweep.RelativeSError = simulation_setup.relative_error + sweep.InterpUsePortImpedance = False + sweep.EnforceCausality = simulation_setup.enforce_causality + # sweep.EnforceCausality = False + sweep.EnforcePassivity = simulation_setup.enforce_passivity + sweep.PassivityTolerance = simulation_setup.passivity_tolerance + sweep.Frequencies.Clear() + + if simulation_setup.sweep_type == SweepType.LogCount: # setup_info.SweepType == 'DecadeCount' + self._setup_decade_count_sweep( + sweep, + str(simulation_setup.start_freq), + str(simulation_setup.stop_freq), + str(simulation_setup.decade_count), + ) # Added DecadeCount as a new attribute + + else: + sweep.Frequencies = self._pedb.simsetupdata.SweepData.SetFrequencies( + simulation_setup.start_freq, + simulation_setup.stop_freq, + simulation_setup.step_freq, + ) + + simsetup_info.SweepDataList.Add(sweep) + else: + self._logger.info("Adding frequency sweep disabled") + + except Exception as err: + self._logger.error("Exception in Sweep configuration: {0}".format(err)) + + sim_setup = self._edb.utility.utility.HFSSSimulationSetup(simsetup_info) + for setup in self._layout.cell.SimulationSetups: + self._layout.cell.DeleteSimulationSetup(setup.GetName()) + self._logger.warning("Setup {} has been deleted".format(setup.GetName())) + return self._layout.cell.AddSimulationSetup(sim_setup) + + def trim_component_reference_size(self, simulation_setup=None, trim_to_terminals=False): + """Trim the common component reference to the minimally acceptable size. + + Parameters + ---------- + simulation_setup : + Edb_DATA.SimulationConfiguration object + + trim_to_terminals : + bool. + True, reduce the reference to a box covering only the active terminals (i.e. those with + ports). + False, reduce the reference to the minimal size needed to cover all pins + + Returns + ------- + bool + True when succeeded, False when failed. + """ + + if not isinstance(simulation_setup, SimulationConfiguration): + self._logger.error( + "Trim component reference size requires an edb_data.simulation_configuration.SimulationConfiguration \ + object as argument" + ) + return False + + if not simulation_setup.components: # pragma: no cover + return + + layout = self._cell.layout + l_inst = layout.layout_instance + + for inst in simulation_setup.components: # pragma: no cover + comp = self._pedb.components.instances[inst] + terms_bbox_pts = self._get_terminals_bbox(comp, l_inst, trim_to_terminals) + if not terms_bbox_pts: + continue + + terms_bbox = self._edb.geometry.polygon_data.create_from_bbox(terms_bbox_pts) + + if trim_to_terminals: + # Remove any pins that aren't interior to the Terminals bbox + pin_list = [ + obj + for obj in list(comp.LayoutObjs) + if obj.GetObjType() == self._edb.cell.layout_object_type.PadstackInstance + ] + for pin in pin_list: + loi = l_inst.GetLayoutObjInstance(pin, None) + bb_c = loi.GetCenter() + if not terms_bbox.PointInPolygon(bb_c): + comp.RemoveMember(pin) + + # Set the port property reference size + cmp_prop = comp.GetComponentProperty().Clone() + port_prop = cmp_prop.GetPortProperty().Clone() + port_prop.SetReferenceSizeAuto(False) + port_prop.SetReferenceSize( + terms_bbox_pts.Item2.X.ToDouble() - terms_bbox_pts.Item1.X.ToDouble(), + terms_bbox_pts.Item2.Y.ToDouble() - terms_bbox_pts.Item1.Y.ToDouble(), + ) + cmp_prop.SetPortProperty(port_prop) + comp.SetComponentProperty(cmp_prop) + return True + + def set_coax_port_attributes(self, simulation_setup=None): + """Set coaxial port attribute with forcing default impedance to 50 Ohms and adjusting the coaxial extent radius. + + Parameters + ---------- + simulation_setup : + Edb_DATA.SimulationConfiguration object. + + Returns + ------- + bool + True when succeeded, False when failed. + """ + + if not isinstance(simulation_setup, SimulationConfiguration): + self._logger.error( + "Set coax port attribute requires an edb_data.simulation_configuration.SimulationConfiguration object \ + as argument." + ) + return False + + net_names = [net.name for net in self._pedb.layout.nets if not net.is_power_ground] + if simulation_setup.components and isinstance(simulation_setup.components[0], str): + cmp_names = ( + simulation_setup.components + if simulation_setup.components + else [gg.name for gg in self._pedb.layout.groups] + ) + elif ( + simulation_setup.components + and isinstance(simulation_setup.components[0], dict) + and "refdes" in simulation_setup.components[0] + ): + cmp_names = [cmp["refdes"] for cmp in simulation_setup.components] + else: + cmp_names = [] + ii = 0 + for cc in cmp_names: + cmp = self._pedb.components.instances[cc] + if cmp.is_null: + self._logger.warning("RenamePorts: could not find component {0}".format(cc)) + continue + terms = [pin for pin in cmp.pins if not pin.get_padstack_instance_terminal().is_null] + for nn in net_names: + for tt in [term for term in terms if term.net.name == nn]: + tt.impedance = GrpcValue("50ohm") + ii += 1 + + if not simulation_setup.use_default_coax_port_radial_extension: + # Set the Radial Extent Factor + typ = cmp.type + if typ in [ + GrpcComponentType.OTHER, + GrpcComponentType.IC, + GrpcComponentType.IO, + ]: + cmp_prop = cmp.component_property + solder_ball_diam = cmp_prop.solder_ball_property.diameter() + if solder_ball_diam[0] and solder_ball_diam[1] > 0: # pragma: no cover + option = ( + "HFSS('HFSS Type'='**Invalid**', " + "Orientation='**Invalid**', " + "'Layer Alignment'='Upper', " + "'Horizontal Extent Factor'='5', " + "'Vertical Extent Factor'='3', " + "'Radial Extent Factor'='0.25', " + "'PEC Launch Width'='0mm')" + ) + for tt in terms: + tt.set_product_solver_option(GrpcComponentType.DESIGNER, "HFSS", option) + return True + + def layout_defeaturing(self, simulation_setup=None): + """Defeature the layout by reducing the number of points for polygons based on surface deviation criteria. + + Parameters + ---------- + simulation_setup : Edb_DATA.SimulationConfiguration object + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + if not isinstance(simulation_setup, SimulationConfiguration): + self._logger.error( + "Layout defeaturing requires an edb_data.simulation_configuration.SimulationConfiguration object." + ) + return False + self._logger.info("Starting Layout Defeaturing") + polygon_list = self._pedb.modeler.polygons + polygon_with_voids = self._pedb.core_layout.get_poly_with_voids(polygon_list) + self._logger.info("Number of polygons with voids found: {0}".format(str(polygon_with_voids.Count))) + for _poly in polygon_list: + voids_from_current_poly = _poly.Voids + new_poly_data = self._pedb.core_layout.defeature_polygon(setup_info=simulation_setup, poly=_poly) + _poly.SetPolygonData(new_poly_data) + if len(voids_from_current_poly) > 0: + for void in voids_from_current_poly: + void_data = void.GetPolygonData() + if void_data.Area() < float(simulation_setup.minimum_void_surface): + void.Delete() + self._logger.warning( + "Defeaturing Polygon {0}: Deleting Void {1} area is lower than the minimum criteria".format( + str(_poly.GetId()), str(void.GetId()) + ) + ) + else: + self._logger.info( + "Defeaturing polygon {0}: void {1}".format(str(_poly.GetId()), str(void.GetId())) + ) + new_void_data = self._pedb.core_layout.defeature_polygon( + setup_info=simulation_setup, poly=void_data + ) + void.SetPolygonData(new_void_data) + + return True diff --git a/src/pyedb/grpc/database/utility/sources.py b/src/pyedb/grpc/database/utility/sources.py new file mode 100644 index 0000000000..2b0ffe0bec --- /dev/null +++ b/src/pyedb/grpc/database/utility/sources.py @@ -0,0 +1,388 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from pyedb.generic.constants import NodeType, SourceType +from pyedb.grpc.database.hierarchy.pingroup import PinGroup + + +class Node(object): + """Provides for handling nodes for Siwave sources.""" + + def __init__(self): + self._component = None + self._net = None + self._node_type = NodeType.Positive + self._name = "" + + @property + def component(self): # pragma: no cover + """Component name containing the node.""" + return self._component + + @component.setter + def component(self, value): # pragma: no cover + if isinstance(value, str): + self._component = value + + @property + def net(self): # pragma: no cover + """Net of the node.""" + return self._net + + @net.setter + def net(self, value): # pragma: no cover + if isinstance(value, str): + self._net = value + + @property + def node_type(self): # pragma: no cover + """Type of the node.""" + return self._node_type + + @node_type.setter + def node_type(self, value): # pragma: no cover + if isinstance(value, int): + self._node_type = value + + @property + def name(self): # pragma: no cover + """Name of the node.""" + return self._name + + @name.setter + def name(self, value): # pragma: no cover + if isinstance(value, str): + self._name = value + + def _json_format(self): # pragma: no cover + dict_out = {} + for k, v in self.__dict__.items(): + dict_out[k[1:]] = v + return dict_out + + def _read_json(self, node_dict): # pragma: no cover + for k, v in node_dict.items(): + self.__setattr__(k, v) + + +class Source(object): + """Provides for handling Siwave sources.""" + + def __init__(self, pedb): + self._pedb = pedb + self._name = "" + self._source_type = SourceType.Vsource + self._positive_node = PinGroup(self._pedb) + self._negative_node = PinGroup(self._pedb) + self._amplitude = 1.0 + self._phase = 0.0 + self._impedance = 1.0 + self._r = 1.0 + self._l = 0.0 + self._c = 0.0 + self._create_physical_resistor = True + self._positive_node.node_type = int(NodeType.Positive) + self._positive_node.name = "pos_term" + self._negative_node.node_type = int(NodeType.Negative) + self._negative_node.name = "neg_term" + + @property + def name(self): # pragma: no cover + """Source name.""" + return self._name + + @name.setter + def name(self, value): # pragma: no cover + if isinstance(value, str): + self._name = value + + @property + def source_type(self): # pragma: no cover + """Source type.""" + return self._source_type + + @source_type.setter + def source_type(self, value): # pragma: no cover + if isinstance(value, int): + self._source_type = value + if value == 3: + self._impedance = 1e-6 + if value == 4: + self._impedance = 5e7 + if value == 5: + self._r = 1.0 + self._l = 0.0 + self._c = 0.0 + + @property + def positive_node(self): # pragma: no cover + """Positive node of the source.""" + return self._positive_node + + @positive_node.setter + def positive_node(self, value): # pragma: no cover + if isinstance(value, (Node, PinGroup)): + self._positive_node = value + + @property + def negative_node(self): # pragma: no cover + """Negative node of the source.""" + return self._negative_node + + @negative_node.setter + def negative_node(self, value): # pragma: no cover + if isinstance(value, (Node, PinGroup)): + self._negative_node = value + # + + @property + def amplitude(self): # pragma: no cover + """Amplitude value of the source. Either amperes for current source or volts for + voltage source.""" + return self._amplitude + + @amplitude.setter + def amplitude(self, value): # pragma: no cover + if isinstance(float(value), float): + self._amplitude = value + + @property + def phase(self): # pragma: no cover + """Phase of the source.""" + return self._phase + + @phase.setter + def phase(self, value): # pragma: no cover + if isinstance(float(value), float): + self._phase = value + + @property + def impedance(self): # pragma: no cover + """Impedance values of the source.""" + return self._impedance + + @impedance.setter + def impedance(self, value): # pragma: no cover + if isinstance(float(value), float): + self._impedance = value + + @property + def r_value(self): + return self._r + + @r_value.setter + def r_value(self, value): + if isinstance(float(value), float): + self._r = value + + @property + def l_value(self): + return self._l + + @l_value.setter + def l_value(self, value): + if isinstance(float(value), float): + self._l = value + + @property + def c_value(self): + return self._c + + @c_value.setter + def c_value(self, value): + if isinstance(float(value), float): + self._c = value + + @property + def create_physical_resistor(self): + return self._create_physical_resistor + + @create_physical_resistor.setter + def create_physical_resistor(self, value): + if isinstance(value, bool): + self._create_physical_resistor = value + + def _json_format(self): # pragma: no cover + dict_out = {} + for k, v in self.__dict__.items(): + if k == "_positive_node" or k == "_negative_node": + nodes = v._json_format() + dict_out[k[1:]] = nodes + else: + dict_out[k[1:]] = v + return dict_out + + def _read_json(self, source_dict): # pragma: no cover + for k, v in source_dict.items(): + if k == "positive_node": + self.positive_node._read_json(v) + elif k == "negative_node": + self.negative_node._read_json(v) + else: + self.__setattr__(k, v) + + +class CircuitPort(Source): + """Manages a circuit port.""" + + def __init__(self, pedb, impedance="50"): + self._impedance = impedance + super().__init__(self) + self._source_type = SourceType.CircPort + + @property + def impedance(self): + """Impedance.""" + return self._impedance + + @impedance.setter + def impedance(self, value): + self._impedance = value + + @property + def get_type(self): + """Get type.""" + return self._source_type + + +class VoltageSource(Source): + """Manages a voltage source.""" + + def __init__(self): + super(VoltageSource, self).__init__() + self._magnitude = "1V" + self._phase = "0Deg" + self._impedance = "0.05" + self._source_type = SourceType.Vsource + + @property + def magnitude(self): + """Magnitude.""" + return self._magnitude + + @magnitude.setter + def magnitude(self, value): + self._magnitude = value + + @property + def phase(self): + """Phase.""" + return self._phase + + @phase.setter + def phase(self, value): + self._phase = value + + @property + def impedance(self): + """Impedance.""" + return self._impedance + + @impedance.setter + def impedance(self, value): + self._impedance = value + + @property + def source_type(self): + """Source type.""" + return self._source_type + + +class CurrentSource(Source): + """Manages a current source.""" + + def __init__(self): + super(CurrentSource, self).__init__() + self._magnitude = "0.1A" + self._phase = "0Deg" + self._impedance = "1e7" + self._source_type = SourceType.Isource + + @property + def magnitude(self): + """Magnitude.""" + return self._magnitude + + @magnitude.setter + def magnitude(self, value): + self._magnitude = value + + @property + def phase(self): + """Phase.""" + return self._phase + + @phase.setter + def phase(self, value): + self._phase = value + + @property + def impedance(self): + """Impedance.""" + return self._impedance + + @impedance.setter + def impedance(self, value): + self._impedance = value + + @property + def source_type(self): + """Source type.""" + return self._source_type + + +class DCTerminal(Source): + """Manages a dc terminal source.""" + + def __init__(self): + super(DCTerminal, self).__init__() + + self._source_type = SourceType.DcTerminal + + @property + def source_type(self): + """Source type.""" + return self._source_type + + +class ResistorSource(Source): + """Manages a resistor source.""" + + def __init__(self): + super(ResistorSource, self).__init__() + self._rvalue = "50" + self._source_type = SourceType.Rlc + + @property + def rvalue(self): + """Resistance value.""" + return self._rvalue + + @rvalue.setter + def rvalue(self, value): + self._rvalue = value + + @property + def source_type(self): + """Source type.""" + return self._source_type diff --git a/src/pyedb/grpc/database/utility/sweep_data_distribution.py b/src/pyedb/grpc/database/utility/sweep_data_distribution.py new file mode 100644 index 0000000000..0380183a18 --- /dev/null +++ b/src/pyedb/grpc/database/utility/sweep_data_distribution.py @@ -0,0 +1,83 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNE SS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from ansys.edb.core.utility.value import Value as GrpcValue + + +class SweepDataDistribution: + @staticmethod + def get_distribution( + sweep_type="linear", start="0Ghz", stop="10GHz", step="10MHz", count=10, decade_number=6, octave_number=5 + ): + """Return the Sweep data distribution. + + Parameters + ---------- + sweep_type : str + Sweep type. Supported values : `"linear"`, `"linear_count"`, `"exponential"`, `"decade_count"`, + `"octave_count"` + start : str, float + Start frequency. + stop : str, float + Stop frequency + step : str, float + Step frequency + count : int + Count number + decade_number : int + Decade number + octave_number : int + Octave number + + Return + ------ + str + Sweep Data distribution. + + """ + if sweep_type.lower() == "linear": + if isinstance(start, str) and isinstance(stop, str) and isinstance(step, str): + return f"LIN {start} {stop} {step}" + else: + return f"LIN {GrpcValue(start).value} {GrpcValue(stop).value} {GrpcValue(step).value}" + elif sweep_type.lower() == "linear_count": + if isinstance(start, str) and isinstance(stop, str) and isinstance(count, int): + return f"LINC {start} {stop} {count}" + else: + return f"LINC {GrpcValue(start).value} {GrpcValue(stop).value} {int(GrpcValue(count).value)}" + elif sweep_type.lower() == "exponential": + if isinstance(start, str) and isinstance(stop, str) and isinstance(count, int): + return f"ESTP {start} {stop} {count}" + else: + return f"ESTP {GrpcValue(start).value} {GrpcValue(stop).value} {int(GrpcValue(count).value)}" + elif sweep_type.lower() == "decade_count": + if isinstance(start, str) and isinstance(stop, str) and isinstance(decade_number, int): + return f"DEC {start} {stop} {decade_number}" + else: + return f"DEC {GrpcValue(start).value} {GrpcValue(stop).value} {int(GrpcValue(decade_number).value)}" + elif sweep_type.lower() == "octave_count": + if isinstance(start, str) and isinstance(stop, str) and isinstance(octave_number, int): + return f"OCT {start} {stop} {octave_number}" + else: + return f"OCT {GrpcValue(start).value} {GrpcValue(stop).value} {int(GrpcValue(octave_number).value)}" + else: + return "" diff --git a/src/pyedb/grpc/database/utility/xml_control_file.py b/src/pyedb/grpc/database/utility/xml_control_file.py new file mode 100644 index 0000000000..7bffc083f2 --- /dev/null +++ b/src/pyedb/grpc/database/utility/xml_control_file.py @@ -0,0 +1,1277 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import copy +import os +import re +import subprocess +import sys + +from pyedb.edb_logger import pyedb_logger +from pyedb.generic.general_methods import ET, env_path, env_value, is_linux +from pyedb.misc.aedtlib_personalib_install import write_pretty_xml +from pyedb.misc.misc import list_installed_ansysem + + +def convert_technology_file(tech_file, edbversion=None, control_file=None): + """Convert a technology file to edb control file (xml). + + Parameters + ---------- + tech_file : str + Full path to technology file + edbversion : str, optional + Edb version to use. Default is `None` to use latest available version of Edb. + control_file : str, optional + Control file output file. Default is `None` to use same path and same name of `tech_file`. + + Returns + ------- + str + Control file full path if created. + """ + if is_linux: # pragma: no cover + if not edbversion: + edbversion = "20{}.{}".format(list_installed_ansysem()[0][-3:-1], list_installed_ansysem()[0][-1:]) + if env_value(edbversion) in os.environ: + base_path = env_path(edbversion) + sys.path.append(base_path) + else: + pyedb_logger.error("No Edb installation found. Check environment variables") + return False + os.environ["HELIC_ROOT"] = os.path.join(base_path, "helic") + if os.getenv("ANSYSLMD_LICENCE_FILE", None) is None: + lic = os.path.join(base_path, "..", "..", "shared_files", "licensing", "ansyslmd.ini") + if os.path.exists(lic): + with open(lic, "r") as fh: + lines = fh.read().splitlines() + for line in lines: + if line.startswith("SERVER="): + os.environ["ANSYSLMD_LICENSE_FILE"] = line.split("=")[1] + break + else: + pyedb_logger.error("ANSYSLMD_LICENSE_FILE is not defined.") + vlc_file_name = os.path.splitext(tech_file)[0] + if not control_file: + control_file = vlc_file_name + ".xml" + vlc_file = vlc_file_name + ".vlc.tech" + commands = [] + command = [ + os.path.join(base_path, "helic", "tools", "bin", "afet", "tech2afet"), + "-i", + tech_file, + "-o", + vlc_file, + "--backplane", + "False", + ] + commands.append(command) + command = [ + os.path.join(base_path, "helic", "tools", "raptorh", "bin", "make-edb"), + "--dielectric-simplification-method", + "1", + "-t", + vlc_file, + "-o", + vlc_file_name, + "--export-xml", + control_file, + ] + commands.append(command) + commands.append(["rm", "-r", vlc_file_name + ".aedb"]) + my_env = os.environ.copy() + for command in commands: + p = subprocess.Popen(command, env=my_env) + p.wait() + if os.path.exists(control_file): + pyedb_logger.info("Xml file created.") + return control_file + pyedb_logger.error("Technology files are supported only in Linux. Use control file instead.") + return False + + +class ControlProperty: + def __init__(self, property_name, value): + self.name = property_name + self.value = value + if isinstance(value, str): + self.type = 1 + elif isinstance(value, list): + self.type = 2 + else: + try: + float(value) + self.type = 0 + except TypeError: + pass + + def _write_xml(self, root): + try: + if self.type == 0: + content = ET.SubElement(root, self.name) + double = ET.SubElement(content, "Double") + double.text = str(self.value) + else: + pass + except: + pass + + +class ControlFileMaterial: + def __init__(self, name, properties): + self.name = name + self.properties = {} + for name, property in properties.items(): + self.properties[name] = ControlProperty(name, property) + + def _write_xml(self, root): + content = ET.SubElement(root, "Material") + content.set("Name", self.name) + for property_name, property in self.properties.items(): + property._write_xml(content) + + +class ControlFileDielectric: + def __init__(self, name, properties): + self.name = name + self.properties = {} + for name, prop in properties.items(): + self.properties[name] = prop + + def _write_xml(self, root): + content = ET.SubElement(root, "Layer") + for property_name, property in self.properties.items(): + if not property_name == "Index": + content.set(property_name, str(property)) + + +class ControlFileLayer: + def __init__(self, name, properties): + self.name = name + self.properties = {} + for name, prop in properties.items(): + self.properties[name] = prop + + def _write_xml(self, root): + content = ET.SubElement(root, "Layer") + content.set("Color", self.properties.get("Color", "#5c4300")) + if self.properties.get("Elevation"): + content.set("Elevation", self.properties["Elevation"]) + if self.properties.get("GDSDataType"): + content.set("GDSDataType", self.properties["GDSDataType"]) + if self.properties.get("GDSIIVia") or self.properties.get("GDSDataType"): + content.set("GDSIIVia", self.properties.get("GDSIIVia", "false")) + if self.properties.get("Material"): + content.set("Material", self.properties.get("Material", "air")) + content.set("Name", self.name) + if self.properties.get("StartLayer"): + content.set("StartLayer", self.properties["StartLayer"]) + if self.properties.get("StopLayer"): + content.set("StopLayer", self.properties["StopLayer"]) + if self.properties.get("TargetLayer"): + content.set("TargetLayer", self.properties["TargetLayer"]) + if self.properties.get("Thickness"): + content.set("Thickness", self.properties.get("Thickness", "0.001")) + if self.properties.get("Type"): + content.set("Type", self.properties.get("Type", "conductor")) + + +class ControlFileVia(ControlFileLayer): + def __init__(self, name, properties): + ControlFileLayer.__init__(self, name, properties) + self.create_via_group = False + self.check_containment = True + self.method = "proximity" + self.persistent = False + self.tolerance = "1um" + self.snap_via_groups = False + self.snap_method = "areaFactor" + self.remove_unconnected = True + self.snap_tolerance = 3 + + def _write_xml(self, root): + content = ET.SubElement(root, "Layer") + content.set("Color", self.properties.get("Color", "#5c4300")) + if self.properties.get("Elevation"): + content.set("Elevation", self.properties["Elevation"]) + if self.properties.get("GDSDataType"): + content.set("GDSDataType", self.properties["GDSDataType"]) + if self.properties.get("Material"): + content.set("Material", self.properties.get("Material", "air")) + content.set("Name", self.name) + content.set("StartLayer", self.properties.get("StartLayer", "")) + content.set("StopLayer", self.properties.get("StopLayer", "")) + if self.properties.get("TargetLayer"): + content.set("TargetLayer", self.properties["TargetLayer"]) + if self.properties.get("Thickness"): + content.set("Thickness", self.properties.get("Thickness", "0.001")) + if self.properties.get("Type"): + content.set("Type", self.properties.get("Type", "conductor")) + if self.create_via_group: + viagroup = ET.SubElement(content, "CreateViaGroups") + viagroup.set("CheckContainment", "true" if self.check_containment else "false") + viagroup.set("Method", self.method) + viagroup.set("Persistent", "true" if self.persistent else "false") + viagroup.set("Tolerance", self.tolerance) + if self.snap_via_groups: + snapgroup = ET.SubElement(content, "SnapViaGroups") + snapgroup.set("Method", self.snap_method) + snapgroup.set("RemoveUnconnected", "true" if self.remove_unconnected else "false") + snapgroup.set("Tolerance", str(self.snap_tolerance)) + + +class ControlFileStackup: + """Class that manages the Stackup info.""" + + def __init__(self, units="mm"): + self._materials = {} + self._layers = [] + self._dielectrics = [] + self._vias = [] + self.units = units + self.metal_layer_snapping_tolerance = None + self.dielectrics_base_elevation = 0 + + @property + def vias(self): + """Via list. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileVia` + + """ + return self._vias + + @property + def materials(self): + """Material list. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileMaterial` + + """ + return self._materials + + @property + def dielectrics(self): + """Dielectric layer list. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileLayer` + + """ + return self._dielectrics + + @property + def layers(self): + """Layer list. + + Returns + ------- + list of :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileLayer` + + """ + return self._layers + + def add_material( + self, + material_name, + permittivity=1.0, + dielectric_loss_tg=0.0, + permeability=1.0, + conductivity=0.0, + properties=None, + ): + """Add a new material with specific properties. + + Parameters + ---------- + material_name : str + Material name. + permittivity : float, optional + Material permittivity. The default is ``1.0``. + dielectric_loss_tg : float, optional + Material tangent losses. The default is ``0.0``. + permeability : float, optional + Material permeability. The default is ``1.0``. + conductivity : float, optional + Material conductivity. The default is ``0.0``. + properties : dict, optional + Specific material properties. The default is ``None``. + Dictionary with key and material property value. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileMaterial` + """ + if isinstance(properties, dict): + self._materials[material_name] = ControlFileMaterial(material_name, properties) + return self._materials[material_name] + else: + properties = { + "Name": material_name, + "Permittivity": permittivity, + "Permeability": permeability, + "Conductivity": conductivity, + "DielectricLossTangent": dielectric_loss_tg, + } + self._materials[material_name] = ControlFileMaterial(material_name, properties) + return self._materials[material_name] + + def add_layer( + self, + layer_name, + elevation=0.0, + material="", + gds_type=0, + target_layer="", + thickness=0.0, + layer_type="conductor", + solve_inside=True, + properties=None, + ): + """Add a new layer. + + Parameters + ---------- + layer_name : str + Layer name. + elevation : float + Layer elevation. + material : str + Material for the layer. + gds_type : int + GDS type assigned on the layer. The value must be the same as in the GDS file otherwise geometries won't be + imported. + target_layer : str + Layer name assigned in EDB or HFSS 3D layout after import. + thickness : float + Layer thickness + layer_type : str + Define the layer type, default value for a layer is ``"conductor"`` + solve_inside : bool + When ``True`` solver will solve inside metal, and not id ``False``. Default value is ``True``. + properties : dict + Dictionary with key and property value. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileLayer` + """ + if isinstance(properties, dict): + self._layers.append(ControlFileLayer(layer_name, properties)) + return self._layers[-1] + else: + properties = { + "Name": layer_name, + "GDSDataType": str(gds_type), + "TargetLayer": target_layer, + "Type": layer_type, + "Material": material, + "Thickness": str(thickness), + "Elevation": str(elevation), + "SolveInside": str(solve_inside).lower(), + } + self._layers.append(ControlFileDielectric(layer_name, properties)) + return self._layers[-1] + + def add_dielectric( + self, + layer_name, + layer_index=None, + material="", + thickness=0.0, + properties=None, + base_layer=None, + add_on_top=True, + ): + """Add a new dielectric. + + Parameters + ---------- + layer_name : str + Layer name. + layer_index : int, optional + Dielectric layer index as they must be stacked. If not provided the layer index will be incremented. + material : str + Material name. + thickness : float + Layer thickness. + properties : dict + Dictionary with key and property value. + base_layer : str, optional + Layer name used for layer placement. Default value is ``None``. This option is used for inserting + dielectric layer between two existing ones. When no argument is provided the dielectric layer will be placed + on top of the stacked ones. + method : bool, Optional. + Provides the method to use when the argument ``base_layer`` is provided. When ``True`` the layer is added + on top on the base layer, when ``False`` it will be added below. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileDielectric` + """ + if isinstance(properties, dict): + self._dielectrics.append(ControlFileDielectric(layer_name, properties)) + return self._dielectrics[-1] + else: + if not layer_index and self.dielectrics and not base_layer: + layer_index = max([diel.properties["Index"] for diel in self.dielectrics]) + 1 + elif base_layer and self.dielectrics: + if base_layer in [diel.properties["Name"] for diel in self.dielectrics]: + base_layer_index = next( + diel.properties["Index"] for diel in self.dielectrics if diel.properties["Name"] == base_layer + ) + if add_on_top: + layer_index = base_layer_index + 1 + for diel_layer in self.dielectrics: + if diel_layer.properties["Index"] > base_layer_index: + diel_layer.properties["Index"] += 1 + else: + layer_index = base_layer_index + for diel_layer in self.dielectrics: + if diel_layer.properties["Index"] >= base_layer_index: + diel_layer.properties["Index"] += 1 + elif not layer_index: + layer_index = 0 + properties = {"Index": layer_index, "Material": material, "Name": layer_name, "Thickness": thickness} + self._dielectrics.append(ControlFileDielectric(layer_name, properties)) + return self._dielectrics[-1] + + def add_via( + self, + layer_name, + material="", + gds_type=0, + target_layer="", + start_layer="", + stop_layer="", + solve_inside=True, + via_group_method="proximity", + via_group_tol=1e-6, + via_group_persistent=True, + snap_via_group_method="distance", + snap_via_group_tol=10e-9, + properties=None, + ): + """Add a new via layer. + + Parameters + ---------- + layer_name : str + Layer name. + material : str + Define the material for this layer. + gds_type : int + Define the gds type. + target_layer : str + Target layer used after layout import in EDB and HFSS 3D layout. + start_layer : str + Define the start layer for the via + stop_layer : str + Define the stop layer for the via. + solve_inside : bool + When ``True`` solve inside this layer is anbled. Default value is ``True``. + via_group_method : str + Define the via group method, default value is ``"proximity"`` + via_group_tol : float + Define the via group tolerance. + via_group_persistent : bool + When ``True`` activated otherwise when ``False``is deactivated. Default value is ``True``. + snap_via_group_method : str + Define the via group method, default value is ``"distance"`` + snap_via_group_tol : float + Define the via group tolerance, default value is 10e-9. + properties : dict + Dictionary with key and property value. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileVia` + """ + if isinstance(properties, dict): + self._vias.append(ControlFileVia(layer_name, properties)) + return self._vias[-1] + else: + properties = { + "Name": layer_name, + "GDSDataType": str(gds_type), + "TargetLayer": target_layer, + "Material": material, + "StartLayer": start_layer, + "StopLayer": stop_layer, + "SolveInside": str(solve_inside).lower(), + "ViaGroupMethod": via_group_method, + "Persistent": via_group_persistent, + "ViaGroupTolerance": via_group_tol, + "SnapViaGroupMethod": snap_via_group_method, + "SnapViaGroupTolerance": snap_via_group_tol, + } + self._vias.append(ControlFileVia(layer_name, properties)) + return self._vias[-1] + + def _write_xml(self, root): + content = ET.SubElement(root, "Stackup") + content.set("schemaVersion", "1.0") + materials = ET.SubElement(content, "Materials") + for materialname, material in self.materials.items(): + material._write_xml(materials) + elayers = ET.SubElement(content, "ELayers") + elayers.set("LengthUnit", self.units) + if self.metal_layer_snapping_tolerance: + elayers.set("MetalLayerSnappingTolerance", str(self.metal_layer_snapping_tolerance)) + dielectrics = ET.SubElement(elayers, "Dielectrics") + dielectrics.set("BaseElevation", str(self.dielectrics_base_elevation)) + # sorting dielectric layers + self._dielectrics = list(sorted(list(self._dielectrics), key=lambda x: x.properties["Index"], reverse=False)) + for layer in self.dielectrics: + layer._write_xml(dielectrics) + layers = ET.SubElement(elayers, "Layers") + + for layer in self.layers: + layer._write_xml(layers) + vias = ET.SubElement(elayers, "Vias") + + for layer in self.vias: + layer._write_xml(vias) + + +class ControlFileImportOptions: + """Import Options.""" + + def __init__(self): + self.auto_close = False + self.convert_closed_wide_lines_to_polys = False + self.round_to = 0 + self.defeature_tolerance = 0.0 + self.flatten = True + self.enable_default_component_values = True + self.import_dummy_nets = False + self.gdsii_convert_polygon_to_circles = False + self.import_cross_hatch_shapes_as_lines = True + self.max_antipad_radius = 0.0 + self.extracta_use_pin_names = False + self.min_bondwire_width = 0.0 + self.antipad_repalce_radius = 0.0 + self.gdsii_scaling_factor = 0.0 + self.delte_empty_non_laminate_signal_layers = False + + def _write_xml(self, root): + content = ET.SubElement(root, "ImportOptions") + content.set("AutoClose", str(self.auto_close).lower()) + if self.round_to != 0: + content.set("RoundTo", str(self.round_to)) + if self.defeature_tolerance != 0.0: + content.set("DefeatureTolerance", str(self.defeature_tolerance)) + content.set("Flatten", str(self.flatten).lower()) + content.set("EnableDefaultComponentValues", str(self.enable_default_component_values).lower()) + content.set("ImportDummyNet", str(self.import_dummy_nets).lower()) + content.set("GDSIIConvertPolygonToCircles", str(self.convert_closed_wide_lines_to_polys).lower()) + content.set("ImportCrossHatchShapesAsLines", str(self.import_cross_hatch_shapes_as_lines).lower()) + content.set("ExtractaUsePinNames", str(self.extracta_use_pin_names).lower()) + if self.max_antipad_radius != 0.0: + content.set("MaxAntiPadRadius", str(self.max_antipad_radius)) + if self.antipad_repalce_radius != 0.0: + content.set("AntiPadReplaceRadius", str(self.antipad_repalce_radius)) + if self.min_bondwire_width != 0.0: + content.set("MinBondwireWidth", str(self.min_bondwire_width)) + if self.gdsii_scaling_factor != 0.0: + content.set("GDSIIScalingFactor", str(self.gdsii_scaling_factor)) + content.set("DeleteEmptyNonLaminateSignalLayers", str(self.delte_empty_non_laminate_signal_layers).lower()) + + +class ControlExtent: + """Extent options.""" + + def __init__( + self, + type="bbox", + dieltype="bbox", + diel_hactor=0.25, + airbox_hfactor=0.25, + airbox_vr_p=0.25, + airbox_vr_n=0.25, + useradiation=True, + honor_primitives=True, + truncate_at_gnd=True, + ): + self.type = type + self.dieltype = dieltype + self.diel_hactor = diel_hactor + self.airbox_hfactor = airbox_hfactor + self.airbox_vr_p = airbox_vr_p + self.airbox_vr_n = airbox_vr_n + self.useradiation = useradiation + self.honor_primitives = honor_primitives + self.truncate_at_gnd = truncate_at_gnd + + def _write_xml(self, root): + content = ET.SubElement(root, "Extents") + content.set("Type", self.type) + content.set("DielType", self.dieltype) + content.set("DielHorizFactor", str(self.diel_hactor)) + content.set("AirboxHorizFactor", str(self.airbox_hfactor)) + content.set("AirboxVertFactorPos", str(self.airbox_vr_p)) + content.set("AirboxVertFactorNeg", str(self.airbox_vr_n)) + content.set("UseRadiationBoundary", str(self.useradiation).lower()) + content.set("DielHonorPrimitives", str(self.honor_primitives).lower()) + content.set("AirboxTruncateAtGround", str(self.truncate_at_gnd).lower()) + + +class ControlCircuitPt: + """Circuit Port.""" + + def __init__(self, name, x1, y1, lay1, x2, y2, lay2, z0): + self.name = name + self.x1 = x1 + self.x2 = x2 + self.lay1 = lay1 + self.lay2 = lay2 + self.y1 = y1 + self.y2 = y2 + self.z0 = z0 + + def _write_xml(self, root): + content = ET.SubElement(root, "CircuitPortPt") + content.set("Name", self.name) + content.set("x1", self.x1) + content.set("y1", self.y1) + content.set("Layer1", self.lay1) + content.set("x2", self.x2) + content.set("y2", self.y2) + content.set("Layer2", self.lay2) + content.set("Z0", self.z0) + + +class ControlFileComponent: + """Components.""" + + def __init__(self): + self.refdes = "U1" + self.partname = "BGA" + self.parttype = "IC" + self.die_type = "None" + self.die_orientation = "Chip down" + self.solderball_shape = "None" + self.solder_diameter = "65um" + self.solder_height = "65um" + self.solder_material = "solder" + self.pins = [] + self.ports = [] + + def add_pin(self, name, x, y, layer): + self.pins.append({"Name": name, "x": x, "y": y, "Layer": layer}) + + def add_port(self, name, z0, pospin, refpin=None, pos_type="pin", ref_type="pin"): + args = {"Name": name, "Z0": z0} + if pos_type == "pin": + args["PosPin"] = pospin + elif pos_type == "pingroup": + args["PosPinGroup"] = pospin + if refpin: + if ref_type == "pin": + args["RefPin"] = refpin + elif ref_type == "pingroup": + args["RefPinGroup"] = refpin + elif ref_type == "net": + args["RefNet"] = refpin + self.ports.append(args) + + def _write_xml(self, root): + content = ET.SubElement(root, "GDS_COMPONENT") + for p in self.pins: + prop = ET.SubElement(content, "GDS_PIN") + for pname, value in p.items(): + prop.set(pname, value) + + prop = ET.SubElement(content, "Component") + prop.set("RefDes", self.refdes) + prop.set("PartName", self.partname) + prop.set("PartType", self.parttype) + prop2 = ET.SubElement(prop, "DieProperties") + prop2.set("Type", self.die_type) + prop2.set("Orientation", self.die_orientation) + prop2 = ET.SubElement(prop, "SolderballProperties") + prop2.set("Shape", self.solderball_shape) + prop2.set("Diameter", self.solder_diameter) + prop2.set("Height", self.solder_height) + prop2.set("Material", self.solder_material) + for p in self.ports: + prop = ET.SubElement(prop, "ComponentPort") + for pname, value in p.items(): + prop.set(pname, value) + + +class ControlFileComponents: + """Class for component management.""" + + def __init__(self): + self.units = "um" + self.components = [] + + def add_component(self, ref_des, partname, component_type, die_type="None", solderball_shape="None"): + """Create a new component. + + Parameters + ---------- + ref_des : str + Reference Designator name. + partname : str + Part name. + component_type : str + Component Type. Can be `"IC"`, `"IO"` or `"Other"`. + die_type : str, optional + Die Type. Can be `"None"`, `"Flip chip"` or `"Wire bond"`. + solderball_shape : str, optional + Solderball Type. Can be `"None"`, `"Cylinder"` or `"Spheroid"`. + + Returns + ------- + + """ + comp = ControlFileComponent() + comp.refdes = ref_des + comp.partname = partname + comp.parttype = component_type + comp.die_type = die_type + comp.solderball_shape = solderball_shape + self.components.append(comp) + return comp + + +class ControlFileBoundaries: + """Boundaries management.""" + + def __init__(self, units="um"): + self.ports = {} + self.extents = [] + self.circuit_models = {} + self.circuit_elements = {} + self.units = units + + def add_port(self, name, x1, y1, layer1, x2, y2, layer2, z0=50): + """Add a new port to the gds. + + Parameters + ---------- + name : str + Port name. + x1 : str + Pin 1 x position. + y1 : str + Pin 1 y position. + layer1 : str + Pin 1 layer. + x2 : str + Pin 2 x position. + y2 : str + Pin 2 y position. + layer2 : str + Pin 2 layer. + z0 : str + Characteristic impedance. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlCircuitPt` + """ + self.ports[name] = ControlCircuitPt(name, str(x1), str(y1), layer1, str(x2), str(y2), layer2, str(z0)) + return self.ports[name] + + def add_extent( + self, + type="bbox", + dieltype="bbox", + diel_hactor=0.25, + airbox_hfactor=0.25, + airbox_vr_p=0.25, + airbox_vr_n=0.25, + useradiation=True, + honor_primitives=True, + truncate_at_gnd=True, + ): + """Add a new extent. + + Parameters + ---------- + type + dieltype + diel_hactor + airbox_hfactor + airbox_vr_p + airbox_vr_n + useradiation + honor_primitives + truncate_at_gnd + + Returns + ------- + + """ + self.extents.append( + ControlExtent( + type=type, + dieltype=dieltype, + diel_hactor=diel_hactor, + airbox_hfactor=airbox_hfactor, + airbox_vr_p=airbox_vr_p, + airbox_vr_n=airbox_vr_n, + useradiation=useradiation, + honor_primitives=honor_primitives, + truncate_at_gnd=truncate_at_gnd, + ) + ) + return self.extents[-1] + + def _write_xml(self, root): + content = ET.SubElement(root, "Boundaries") + content.set("LengthUnit", self.units) + for p in self.circuit_models.values(): + p._write_xml(content) + for p in self.circuit_elements.values(): + p._write_xml(content) + for p in self.ports.values(): + p._write_xml(content) + for p in self.extents: + p._write_xml(content) + + +class ControlFileSweep: + def __init__(self, name, start, stop, step, sweep_type, step_type, use_q3d): + self.name = name + self.start = start + self.stop = stop + self.step = step + self.sweep_type = sweep_type + self.step_type = step_type + self.use_q3d = use_q3d + + def _write_xml(self, root): + sweep = ET.SubElement(root, "FreqSweep") + prop = ET.SubElement(sweep, "Name") + prop.text = self.name + prop = ET.SubElement(sweep, "UseQ3DForDC") + prop.text = str(self.use_q3d).lower() + prop = ET.SubElement(sweep, self.sweep_type) + prop2 = ET.SubElement(prop, self.step_type) + prop3 = ET.SubElement(prop2, "Start") + prop3.text = self.start + prop3 = ET.SubElement(prop2, "Stop") + prop3.text = self.stop + if self.step_type == "LinearStep": + prop3 = ET.SubElement(prop2, "Step") + prop3.text = str(self.step) + else: + prop3 = ET.SubElement(prop2, "Count") + prop3.text = str(self.step) + + +class ControlFileMeshOp: + def __init__(self, name, region, type, nets_layers): + self.name = name + self.region = name + self.type = type + self.nets_layers = nets_layers + self.num_max_elem = 1000 + self.restrict_elem = False + self.restrict_length = True + self.max_length = "20um" + self.skin_depth = "1um" + self.surf_tri_length = "1mm" + self.num_layers = 2 + self.region_solve_inside = False + + def _write_xml(self, root): + mop = ET.SubElement(root, "MeshOperation") + prop = ET.SubElement(mop, "Name") + prop.text = self.name + prop = ET.SubElement(mop, "Enabled") + prop.text = "true" + prop = ET.SubElement(mop, "Region") + prop.text = self.region + prop = ET.SubElement(mop, "Type") + prop.text = self.type + prop = ET.SubElement(mop, "NetsLayers") + for net, layer in self.nets_layers.items(): + prop2 = ET.SubElement(prop, "NetsLayer") + prop3 = ET.SubElement(prop2, "Net") + prop3.text = net + prop3 = ET.SubElement(prop2, "Layer") + prop3.text = layer + prop = ET.SubElement(mop, "RestrictElem") + prop.text = self.restrict_elem + prop = ET.SubElement(mop, "NumMaxElem") + prop.text = self.num_max_elem + if self.type == "MeshOperationLength": + prop = ET.SubElement(mop, "RestrictLength") + prop.text = self.restrict_length + prop = ET.SubElement(mop, "MaxLength") + prop.text = self.max_length + else: + prop = ET.SubElement(mop, "SkinDepth") + prop.text = self.skin_depth + prop = ET.SubElement(mop, "SurfTriLength") + prop.text = self.surf_tri_length + prop = ET.SubElement(mop, "NumLayers") + prop.text = self.num_layers + prop = ET.SubElement(mop, "RegionSolveInside") + prop.text = self.region_solve_inside + + +class ControlFileSetup: + """Setup Class.""" + + def __init__(self, name): + self.name = name + self.enabled = True + self.save_fields = False + self.save_rad_fields = False + self.frequency = "1GHz" + self.maxpasses = 10 + self.max_delta = 0.02 + self.union_polygons = True + self.small_voids_area = 0 + self.mode_type = "IC" + self.ic_model_resolution = "Auto" + self.order_basis = "FirstOrder" + self.solver_type = "Auto" + self.low_freq_accuracy = False + self.mesh_operations = [] + self.sweeps = [] + + def add_sweep(self, name, start, stop, step, sweep_type="Interpolating", step_type="LinearStep", use_q3d=True): + """Add a new sweep. + + Parameters + ---------- + name : str + Sweep name. + start : str + Frequency start. + stop : str + Frequency stop. + step : str + Frequency step or count. + sweep_type : str + Sweep type. It can be `"Discrete"` or `"Interpolating"`. + step_type : str + Sweep type. It can be `"LinearStep"`, `"DecadeCount"` or `"LinearCount"`. + use_q3d + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileSweep` + """ + self.sweeps.append(ControlFileSweep(name, start, stop, step, sweep_type, step_type, use_q3d)) + return self.sweeps[-1] + + def add_mesh_operation(self, name, region, type, nets_layers): + """Add mesh operations. + + Parameters + ---------- + name : str + Mesh name. + region : str + Region to apply mesh operation. + type : str + Mesh operation type. It can be `"MeshOperationLength"` or `"MeshOperationSkinDepth"`. + nets_layers : dict + Dictionary containing nets and layers on which apply mesh. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileMeshOp` + + """ + mop = ControlFileMeshOp(name, region, type, nets_layers) + self.mesh_operations.append(mop) + return mop + + def _write_xml(self, root): + setups = ET.SubElement(root, "HFSSSetup") + setups.set("schemaVersion", "1.0") + setups.set("Name", self.name) + setup = ET.SubElement(setups, "HFSSSimulationSettings") + prop = ET.SubElement(setup, "Enabled") + prop.text = str(self.enabled).lower() + prop = ET.SubElement(setup, "SaveFields") + prop.text = str(self.save_fields).lower() + prop = ET.SubElement(setup, "SaveRadFieldsOnly") + prop.text = str(self.save_rad_fields).lower() + prop = ET.SubElement(setup, "HFSSAdaptiveSettings") + prop = ET.SubElement(prop, "AdaptiveSettings") + prop = ET.SubElement(prop, "SingleFrequencyDataList") + prop = ET.SubElement(prop, "AdaptiveFrequencyData") + prop2 = ET.SubElement(prop, "AdaptiveFrequency") + prop2.text = self.frequency + prop2 = ET.SubElement(prop, "MaxPasses") + prop2.text = str(self.maxpasses) + prop2 = ET.SubElement(prop, "MaxDelta") + prop2.text = str(self.max_delta) + prop = ET.SubElement(setup, "HFSSDefeatureSettings") + prop2 = ET.SubElement(prop, "UnionPolygons") + prop2.text = str(self.union_polygons).lower() + + prop2 = ET.SubElement(prop, "SmallVoidArea") + prop2.text = str(self.small_voids_area) + prop2 = ET.SubElement(prop, "ModelType") + prop2.text = str(self.mode_type) + prop2 = ET.SubElement(prop, "ICModelResolutionType") + prop2.text = str(self.ic_model_resolution) + + prop = ET.SubElement(setup, "HFSSSolverSettings") + prop2 = ET.SubElement(prop, "OrderBasis") + prop2.text = str(self.order_basis) + prop2 = ET.SubElement(prop, "SolverType") + prop2.text = str(self.solver_type) + prop = ET.SubElement(setup, "HFSSMeshOperations") + for mesh in self.mesh_operations: + mesh._write_xml(prop) + prop = ET.SubElement(setups, "HFSSSweepDataList") + for sweep in self.sweeps: + sweep._write_xml(prop) + + +class ControlFileSetups: + """Setup manager class.""" + + def __init__(self): + self.setups = [] + + def add_setup(self, name, frequency): + """Add a new setup + + Parameters + ---------- + name : str + Setup name. + frequency : str + Setup Frequency. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.control_file.ControlFileSetup` + """ + setup = ControlFileSetup(name) + setup.frequency = frequency + self.setups.append(setup) + return setup + + def _write_xml(self, root): + content = ET.SubElement(root, "SimulationSetups") + for setup in self.setups: + setup._write_xml(content) + + +class ControlFile: + """Control File Class. It helps the creation and modification of edb xml control files.""" + + def __init__(self, xml_input=None, tecnhology=None, layer_map=None): + self.stackup = ControlFileStackup() + if xml_input: + self.parse_xml(xml_input) + if tecnhology: + self.parse_technology(tecnhology) + if layer_map: + self.parse_layer_map(layer_map) + self.boundaries = ControlFileBoundaries() + self.remove_holes = False + self.remove_holes_area_minimum = 30 + self.remove_holes_units = "um" + self.setups = ControlFileSetups() + self.components = ControlFileComponents() + self.import_options = ControlFileImportOptions() + pass + + def parse_technology(self, tecnhology, edbversion=None): + """Parse technology files using Helic and convert it to xml file. + + Parameters + ---------- + layer_map : str + Full path to technology file. + + Returns + ------- + bool + """ + xml_temp = os.path.splitext(tecnhology)[0] + "_temp.xml" + xml_temp = convert_technology_file(tech_file=tecnhology, edbversion=edbversion, control_file=xml_temp) + if xml_temp: + return self.parse_xml(xml_temp) + + def parse_layer_map(self, layer_map): + """Parse layer map and adds info to the stackup info. + This operation must be performed after a tech file is imported. + + Parameters + ---------- + layer_map : str + Full path to `".map"` file. + + Returns + ------- + + """ + with open(layer_map, "r") as f: + lines = f.readlines() + for line in lines: + if not line.startswith("#") and re.search(r"\w+", line.strip()): + out = re.split(r"\s+", line.strip()) + layer_name = out[0] + layer_id = out[2] + layer_type = out[3] + for layer in self.stackup.layers[:]: + if layer.name == layer_name: + layer.properties["GDSDataType"] = layer_type + layer.name = layer_id + layer.properties["TargetLayer"] = layer_name + break + elif layer.properties.get("TargetLayer", None) == layer_name: + new_layer = ControlFileLayer(layer_id, copy.deepcopy(layer.properties)) + new_layer.properties["GDSDataType"] = layer_type + new_layer.name = layer_id + new_layer.properties["TargetLayer"] = layer_name + self.stackup.layers.append(new_layer) + break + for layer in self.stackup.vias[:]: + if layer.name == layer_name: + layer.properties["GDSDataType"] = layer_type + layer.name = layer_id + layer.properties["TargetLayer"] = layer_name + break + elif layer.properties.get("TargetLayer", None) == layer_name: + new_layer = ControlFileVia(layer_id, copy.deepcopy(layer.properties)) + new_layer.properties["GDSDataType"] = layer_type + new_layer.name = layer_id + new_layer.properties["TargetLayer"] = layer_name + self.stackup.vias.append(new_layer) + self.stackup.vias.append(new_layer) + break + return True + + def parse_xml(self, xml_input): + """Parse an xml and populate the class with materials and Stackup only. + + Parameters + ---------- + xml_input : str + Full path to xml. + + Returns + ------- + bool + """ + tree = ET.parse(xml_input) + root = tree.getroot() + for el in root: + if el.tag == "Stackup": + for st_el in el: + if st_el.tag == "Materials": + for mat in st_el: + mat_name = mat.attrib["Name"] + properties = {} + for prop in mat: + if prop[0].tag == "Double": + properties[prop.tag] = prop[0].text + self.stackup.add_material(mat_name, properties) + elif st_el.tag == "ELayers": + if st_el.attrib == "LengthUnits": + self.stackup.units = st_el.attrib + for layers_el in st_el: + if "BaseElevation" in layers_el.attrib: + self.stackup.dielectrics_base_elevation = layers_el.attrib["BaseElevation"] + for layer_el in layers_el: + properties = {} + layer_name = layer_el.attrib["Name"] + for propname, prop_val in layer_el.attrib.items(): + properties[propname] = prop_val + if layers_el.tag == "Dielectrics": + self.stackup.add_dielectric( + layer_name=layer_name, + material=properties["Material"], + thickness=properties["Thickness"], + ) + elif layers_el.tag == "Layers": + self.stackup.add_layer(layer_name=layer_name, properties=properties) + elif layers_el.tag == "Vias": + via = self.stackup.add_via(layer_name, properties=properties) + for i in layer_el: + if i.tag == "CreateViaGroups": + via.create_via_group = True + if "CheckContainment" in i.attrib: + via.check_containment = ( + True if i.attrib["CheckContainment"] == "true" else False + ) + if "Tolerance" in i.attrib: + via.tolerance = i.attrib["Tolerance"] + if "Method" in i.attrib: + via.method = i.attrib["Method"] + if "Persistent" in i.attrib: + via.persistent = True if i.attrib["Persistent"] == "true" else False + elif i.tag == "SnapViaGroups": + if "Method" in i.attrib: + via.snap_method = i.attrib["Method"] + if "Tolerance" in i.attrib: + via.snap_tolerance = i.attrib["Tolerance"] + if "RemoveUnconnected" in i.attrib: + via.remove_unconnected = ( + True if i.attrib["RemoveUnconnected"] == "true" else False + ) + return True + + def write_xml(self, xml_output): + """Write xml to output file + + Parameters + ---------- + xml_output : str + Path to the output xml file. + + Returns + ------- + bool + """ + control = ET.Element("{http://www.ansys.com/control}Control", attrib={"schemaVersion": "1.0"}) + self.stackup._write_xml(control) + if self.boundaries.ports or self.boundaries.extents: + self.boundaries._write_xml(control) + if self.remove_holes: + hole = ET.SubElement(control, "RemoveHoles") + hole.set("HoleAreaMinimum", str(self.remove_holes_area_minimum)) + hole.set("LengthUnit", self.remove_holes_units) + if self.setups.setups: + setups = ET.SubElement(control, "SimulationSetups") + for setup in self.setups.setups: + setup._write_xml(setups) + self.import_options._write_xml(control) + if self.components.components: + comps = ET.SubElement(control, "GDS_COMPONENTS") + comps.set("LengthUnit", self.components.units) + for comp in self.components.components: + comp._write_xml(comps) + write_pretty_xml(control, xml_output) + return True if os.path.exists(xml_output) else False diff --git a/src/pyedb/grpc/edb.py b/src/pyedb/grpc/edb.py new file mode 100644 index 0000000000..8305f8800e --- /dev/null +++ b/src/pyedb/grpc/edb.py @@ -0,0 +1,4105 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""This module contains the ``Edb`` class. + +This module is implicitly loaded in HFSS 3D Layout when launched. + +""" + +from itertools import combinations +import os +import re +import shutil +import subprocess +import sys +import tempfile +import time +import traceback +import warnings +from zipfile import ZipFile as zpf + +from ansys.edb.core.geometry.polygon_data import PolygonData as GrpcPolygonData +from ansys.edb.core.simulation_setup.siwave_dcir_simulation_setup import ( + SIWaveDCIRSimulationSetup as GrpcSIWaveDCIRSimulationSetup, +) +from ansys.edb.core.utility.value import Value as GrpcValue +import rtree + +from pyedb.configuration.configuration import Configuration +from pyedb.generic.general_methods import ( + generate_unique_name, + get_string_version, + is_linux, + is_windows, +) +from pyedb.generic.process import SiwaveSolve +from pyedb.generic.settings import settings +from pyedb.grpc.database.components import Components +from pyedb.grpc.database.control_file import ControlFile, convert_technology_file +from pyedb.grpc.database.definition.materials import Materials +from pyedb.grpc.database.hfss import Hfss +from pyedb.grpc.database.layout.layout import Layout +from pyedb.grpc.database.layout_validation import LayoutValidation +from pyedb.grpc.database.modeler import Modeler +from pyedb.grpc.database.net import Nets +from pyedb.grpc.database.nets.differential_pair import DifferentialPairs +from pyedb.grpc.database.nets.extended_net import ExtendedNets +from pyedb.grpc.database.nets.net_class import NetClass +from pyedb.grpc.database.padstack import Padstacks +from pyedb.grpc.database.ports.ports import BundleWavePort, CoaxPort, GapPort, WavePort +from pyedb.grpc.database.primitive.circle import Circle +from pyedb.grpc.database.primitive.padstack_instances import PadstackInstance +from pyedb.grpc.database.primitive.path import Path +from pyedb.grpc.database.primitive.polygon import Polygon +from pyedb.grpc.database.primitive.rectangle import Rectangle +from pyedb.grpc.database.simulation_setup.hfss_simulation_setup import ( + HfssSimulationSetup, +) +from pyedb.grpc.database.simulation_setup.raptor_x_simulation_setup import ( + RaptorXSimulationSetup, +) +from pyedb.grpc.database.simulation_setup.siwave_dcir_simulation_setup import ( + SIWaveDCIRSimulationSetup, +) +from pyedb.grpc.database.simulation_setup.siwave_simulation_setup import ( + SiwaveSimulationSetup, +) +from pyedb.grpc.database.siwave import Siwave +from pyedb.grpc.database.source_excitations import SourceExcitation +from pyedb.grpc.database.stackup import Stackup +from pyedb.grpc.database.terminal.padstack_instance_terminal import ( + PadstackInstanceTerminal, +) +from pyedb.grpc.database.terminal.terminal import Terminal +from pyedb.grpc.database.utility.constants import get_terminal_supported_boundary_types +from pyedb.grpc.edb_init import EdbInit +from pyedb.ipc2581.ipc2581 import Ipc2581 +from pyedb.modeler.geometry_operators import GeometryOperators +from pyedb.workflow import Workflow + + +class EdbGrpc(EdbInit): + """Provides the EDB application interface. + + This module inherits all objects that belong to EDB. + + Parameters + ---------- + edbpath : str, optional + Full path to the ``aedb`` folder. The variable can also contain + the path to a layout to import. Allowed formats are BRD, MCM, + XML (IPC2581), GDS, and DXF. The default is ``None``. + For GDS import, the Ansys control file (also XML) should have the same + name as the GDS file. Only the file extension differs. + cellname : str, optional + Name of the cell to select. The default is ``None``. + isreadonly : bool, optional + Whether to open EBD in read-only mode when it is + owned by HFSS 3D Layout. The default is ``False``. + edbversion : str, int, float, optional + Version of EDB to use. The default is ``None``. + Examples of input values are ``232``, ``23.2``,``2023.2``,``"2023.2"``. + isaedtowned : bool, optional + Whether to launch EDB from HFSS 3D Layout. The + default is ``False``. + oproject : optional + Reference to the AEDT project object. + technology_file : str, optional + Technology file full path to be converted to XML before importing or XML. Supported by GDS format only. + restart_rpc_server : bool, optional + ``True`` RPC server is terminated and restarted. This will close all open EDB. RPC server is running on single + instance loading all EDB, enabling this option should be used with caution but can be a solution to release + memory in case the server is draining resources. Default value is ``False``. + + Examples + -------- + Create an .::Edb object and a new EDB cell. + + >>> from pyedb.grpc.edb import EdbGrpc as Edb + >>> app = Edb() + + Add a new variable named "s1" to the ``Edb`` instance. + + >>> app['s1'] = "0.25 mm" + >>> app['s1'] + >>> 0.00025 + + Create an ``Edb`` object and open the specified project. + + >>> app = Edb(edbpath="myfile.aedb") + + Create an ``Edb`` object from GDS and control files. + The XML control file resides in the same directory as the GDS file: (myfile.xml). + + >>> app = Edb("/path/to/file/myfile.gds") + + Loading Ansys layout + + >>> from ansys.aedt.core.generic.general_methods import generate_unique_folder_name + >>> import pyedb.misc.downloads as downloads + >>> temp_folder = generate_unique_folder_name() + >>> targetfile = downloads.download_file("edb/ANSYS-HSD_V1.aedb", destination=temp_folder) + >>> from pyedb.grpc.edb import EdbGrpc as Edb + >>> edbapp = Edb(edbpath=targetfile) + + Retrieving signal nets dictionary + + >>> edbapp.nets.signal + + Retrieving layers + + >>> edbapp.stackup.layers + + Retrieving all component instances + + >>> edbapp.components.instances + + Retrieving all padstacks definitions + + >>> edbapp.padstacks.definitions + + Retrieving component pins + + >>> edbapp.components["U1"].pins + + """ + + def __init__( + self, + edbpath=None, + cellname=None, + isreadonly=False, + edbversion=None, + isaedtowned=False, + oproject=None, + use_ppe=False, + technology_file=None, + restart_rpc_server=False, + kill_all_instances=False, + ): + edbversion = get_string_version(edbversion) + self._clean_variables() + EdbInit.__init__(self, edbversion=edbversion) + self.standalone = True + self.oproject = oproject + self._main = sys.modules["__main__"] + self.edbversion = edbversion + self.isaedtowned = isaedtowned + self.isreadonly = isreadonly + self._setups = {} + if cellname: + self.cellname = cellname + else: + self.cellname = "" + if not edbpath: + if is_windows: + edbpath = os.getenv("USERPROFILE") + if not edbpath: + edbpath = os.path.expanduser("~") + edbpath = os.path.join(edbpath, "Documents", generate_unique_name("layout") + ".aedb") + else: + edbpath = os.getenv("HOME") + if not edbpath: + edbpath = os.path.expanduser("~") + edbpath = os.path.join(edbpath, generate_unique_name("layout") + ".aedb") + self.logger.info("No EDB is provided. Creating a new EDB {}.".format(edbpath)) + self.edbpath = edbpath + self.log_name = None + if edbpath: + self.log_name = os.path.join( + os.path.dirname(edbpath), "pyaedt_" + os.path.splitext(os.path.split(edbpath)[-1])[0] + ".log" + ) + if edbpath[-3:] == "zip": + self.edbpath = edbpath[:-4] + ".aedb" + working_dir = os.path.dirname(edbpath) + zipped_file = zpf(edbpath, "r") + top_level_folders = {item.split("/")[0] for item in zipped_file.namelist()} + if len(top_level_folders) == 1: + self.logger.info("Unzipping ODB++...") + zipped_file.extractall(working_dir) + else: + self.logger.info("Unzipping ODB++ before translating to EDB...") + zipped_file.extractall(edbpath[:-4]) + self.logger.info("ODB++ unzipped successfully.") + zipped_file.close() + control_file = None + if technology_file: + if os.path.splitext(technology_file)[1] == ".xml": + control_file = technology_file + else: + control_file = convert_technology_file(technology_file, edbversion=edbversion) + self.logger.info("Translating ODB++ to EDB...") + self.import_layout_pcb(edbpath[:-4], working_dir, use_ppe=use_ppe, control_file=control_file) + if settings.enable_local_log_file and self.log_name: + self.logger.add_file_logger(self.log_name, "Edb") + self.logger.info("EDB %s was created correctly from %s file.", self.edbpath, edbpath) + + elif edbpath[-3:] in ["brd", "mcm", "gds", "xml", "dxf", "tgz"]: + self.edbpath = edbpath[:-4] + ".aedb" + working_dir = os.path.dirname(edbpath) + control_file = None + if technology_file: + if os.path.splitext(technology_file)[1] == ".xml": + control_file = technology_file + else: + control_file = convert_technology_file(technology_file, edbversion=edbversion) + self.import_layout_pcb(edbpath, working_dir, use_ppe=use_ppe, control_file=control_file) + self.logger.info("EDB %s was created correctly from %s file.", self.edbpath, edbpath[-2:]) + elif edbpath.endswith("edb.def"): + self.edbpath = os.path.dirname(edbpath) + self.open_edb(restart_rpc_server=restart_rpc_server) + elif not os.path.exists(os.path.join(self.edbpath, "edb.def")): + self.create_edb(restart_rpc_server=restart_rpc_server) + self.logger.info("EDB %s created correctly.", self.edbpath) + elif ".aedb" in edbpath: + self.edbpath = edbpath + self.open_edb(restart_rpc_server=restart_rpc_server) + if self.active_cell: + self.logger.info("EDB initialized.") + else: + self.logger.info("Failed to initialize EDB.") + self._layout_instance = None + + def __enter__(self): + return self + + def __exit__(self, ex_type, ex_value, ex_traceback): + if ex_type: + self.edb_exception(ex_value, ex_traceback) + + def __getitem__(self, variable_name): + """Get or Set a variable to the Edb project. The variable can be project using ``$`` prefix or + it can be a design variable, in which case the ``$`` is omitted. + + Parameters + ---------- + variable_name : str + + Returns + ------- + variable object. + + """ + if self.variable_exists(variable_name): + return self.variables[variable_name] + return + + def __setitem__(self, variable_name, variable_value): + type_error_message = "Allowed values are str, numeric or two-item list with variable description." + if type(variable_value) in [ + list, + tuple, + ]: # Two-item list or tuple. 2nd argument is a str description. + if len(variable_value) == 2: + if type(variable_value[1]) is str: + description = variable_value[1] if len(variable_value[1]) > 0 else None + else: + description = None + self.logger.warning("Invalid type for Edb variable description is ignored.") + val = variable_value[0] + else: + raise TypeError(type_error_message) + else: + description = None + val = variable_value + if self.variable_exists(variable_name): + self.change_design_variable_value(variable_name, val) + else: + if variable_name.startswith("$"): + self.add_project_variable(variable_name, val) + else: + self.add_design_variable(variable_name, val) + if description: # Add the variable description if a two-item list is passed for variable_value. + self.__getitem__(variable_name).description = description + + def _check_remove_project_files(self, edbpath: str, remove_existing_aedt: bool) -> None: + aedt_file = os.path.splitext(edbpath)[0] + ".aedt" + files = [aedt_file, aedt_file + ".lock"] + for file in files: + if os.path.isfile(file): + if not remove_existing_aedt: + self.logger.warning( + f"AEDT project-related file {file} exists and may need to be deleted before opening the EDB in " + f"HFSS 3D Layout." + # noqa: E501 + ) + else: + try: + os.unlink(file) + self.logger.info(f"Deleted AEDT project-related file {file}.") + except: + self.logger.info(f"Failed to delete AEDT project-related file {file}.") + + def _clean_variables(self): + """Initialize internal variables and perform garbage collection.""" + self.grpc = True + self._materials = None + self._components = None + self._core_primitives = None + self._stackup = None + self._padstack = None + self._siwave = None + self._hfss = None + self._nets = None + self._layout_instance = None + self._variables = None + self._active_cell = None + self._layout = None + self._configuration = None + + def _init_objects(self): + self._components = Components(self) + self._stackup = Stackup(self, self.layout.layer_collection) + self._padstack = Padstacks(self) + self._siwave = Siwave(self) + self._hfss = Hfss(self) + self._nets = Nets(self) + self._modeler = Modeler(self) + self._materials = Materials(self) + self._source_excitation = SourceExcitation(self) + self._differential_pairs = DifferentialPairs(self) + self._extended_nets = ExtendedNets(self) + + @property + def cell_names(self): + """Cell name container. + + Returns + ------- + list of cell names : List[str] + """ + return [cell.name for cell in self.active_db.top_circuit_cells] + + @property + def design_variables(self): + """Get all edb design variables. + + Returns + ------- + variable dictionary : Dict{str[variable name]: float[variable value]} + """ + return {i: self.active_cell.get_variable_value(i).value for i in self.active_cell.get_all_variable_names()} + + @property + def project_variables(self): + """Get all project variables. + + Returns + ------- + variables dictionary : Dict{str[variable name]: float[variable value]} + + """ + return {i: self.active_db.get_variable_value(i).value for i in self.active_db.get_all_variable_names()} + + @property + def layout_validation(self): + """Return LayoutValidation object. + + Returns + ------- + layout validation object: .:class:`pyedb.grpc.database.layout_validation.LayoutValidation` + """ + return LayoutValidation(self) + + @property + def variables(self): + """Get all Edb variables. + + Returns + ------- + variables dictionary : Dict[str, :class:`pyedb.dotnet.database.edb_data.variables.Variable`] + + """ + all_vars = dict() + for i, j in self.project_variables.items(): + all_vars[i] = j + for i, j in self.design_variables.items(): + all_vars[i] = j + return all_vars + + @property + def terminals(self): + """Get terminals belonging to active layout. + + Returns + ------- + Dict + """ + return {i.name: i for i in self.layout.terminals} + + @property + def excitations(self): + """Get all layout excitations.""" + terms = [term for term in self.layout.terminals if term.boundary_type == "port"] + temp = {} + for term in terms: + if not term.bundle_terminal.is_null: + temp[term.name] = BundleWavePort(self, term) + else: + temp[term.name] = GapPort(self, term) + return temp + + @property + def ports(self): + """Get all ports. + + Returns + ------- + port dictionary : Dict[str, [:class:`pyedb.dotnet.database.edb_data.ports.GapPort`, + :class:`pyedb.dotnet.database.edb_data.ports.WavePort`,]] + + """ + terminals = [term for term in self.layout.terminals if not term.is_reference_terminal] + ports = {} + from pyedb.grpc.database.terminal.bundle_terminal import BundleTerminal + from pyedb.grpc.database.terminal.padstack_instance_terminal import ( + PadstackInstanceTerminal, + ) + + for t in terminals: + if isinstance(t, BundleTerminal): + bundle_ter = WavePort(self, t) + ports[bundle_ter.name] = bundle_ter + elif isinstance(t, PadstackInstanceTerminal): + ports[t.name] = CoaxPort(self, t) + else: + ports[t.name] = GapPort(self, t) + return ports + + @property + def excitations_nets(self): + """Get all excitations net names.""" + return list(set([i.net.name for i in self.layout.terminals if not i.is_reference_terminal])) + + @property + def sources(self): + """Get all layout sources.""" + return self.terminals + + @property + def voltage_regulator_modules(self): + """Get all voltage regulator modules""" + vrms = self.layout.voltage_regulators + _vrms = {} + for vrm in vrms: + _vrms[vrm.name] = vrm + return _vrms + + @property + def probes(self): + """Get all layout probes.""" + terms = [term for term in self.layout.terminals if term.boundary_type.value == 8] + return {ter.name: ter for ter in terms} + + def open_edb(self, restart_rpc_server=False, kill_all_instances=False): + """Open EDB. + + Returns + ------- + ``True`` when succeed ``False`` if failed : bool + """ + self.standalone = self.standalone + n_try = 10 + while not self.db and n_try: + try: + self.open( + self.edbpath, + self.isreadonly, + restart_rpc_server=restart_rpc_server, + kill_all_instances=kill_all_instances, + ) + n_try -= 1 + except Exception as e: + self.logger.error(e.args[0]) + if not self.db: + raise ValueError("Failed during EDB loading.") + else: + if self.db.is_null: + self.logger.warning("Error Opening db") + self._active_cell = None + self.logger.info(f"Database {os.path.split(self.edbpath)[-1]} Opened in {self.edbversion}") + self._active_cell = None + if self.cellname: + for cell in self.active_db.circuit_cells: + if cell.name == self.cellname: + self._active_cell = cell + if self._active_cell is None: + self._active_cell = self._db.circuit_cells[0] + self.logger.info("Cell %s Opened", self._active_cell.name) + if self._active_cell: + self._init_objects() + self.logger.info("Builder was initialized.") + else: + self.logger.error("Builder was not initialized.") + return True + + def create_edb(self, restart_rpc_server=False, kill_all_instances=False): + """Create EDB. + + Returns + ------- + ``True`` when succeed ``False`` if failed : bool + """ + from ansys.edb.core.layout.cell import Cell as GrpcCell + from ansys.edb.core.layout.cell import CellType as GrpcCellType + + self.standalone = self.standalone + n_try = 10 + while not self.db and n_try: + try: + self.create(self.edbpath, restart_rpc_server=restart_rpc_server, kill_all_instances=kill_all_instances) + n_try -= 1 + except Exception as e: + self.logger.error(e.args[0]) + if not self.db: + raise ValueError("Failed creating EDB.") + self._active_cell = None + else: + if not self.cellname: + self.cellname = generate_unique_name("Cell") + self._active_cell = GrpcCell.create( + db=self.active_db, cell_type=GrpcCellType.CIRCUIT_CELL, cell_name=self.cellname + ) + if self._active_cell: + self._init_objects() + return True + return None + + def import_layout_pcb( + self, + input_file, + working_dir, + anstranslator_full_path="", + use_ppe=False, + control_file=None, + ): + """Import a board file and generate an ``edb.def`` file in the working directory. + + This function supports all AEDT formats, including DXF, GDS, SML (IPC2581), BRD, MCM, SIP, ZIP and TGZ. + + Parameters + ---------- + input_file : str + Full path to the board file. + working_dir : str + Directory in which to create the ``aedb`` folder. The name given to the AEDB file + is the same as the name of the board file. + anstranslator_full_path : str, optional + Full path to the Ansys translator. The default is ``""``. + use_ppe : bool + Whether to use the PPE License. The default is ``False``. + control_file : str, optional + Path to the XML file. The default is ``None``, in which case an attempt is made to find + the XML file in the same directory as the board file. To succeed, the XML file and board file + must have the same name. Only the extension differs. + + Returns + ------- + Full path to the AEDB file : str + + """ + self._components = None + self._core_primitives = None + self._stackup = None + self._padstack = None + self._siwave = None + self._hfss = None + self._nets = None + aedb_name = os.path.splitext(os.path.basename(input_file))[0] + ".aedb" + if anstranslator_full_path and os.path.exists(anstranslator_full_path): + command = anstranslator_full_path + else: + command = os.path.join(self.base_path, "anstranslator") + if is_windows: + command += ".exe" + + if not working_dir: + working_dir = os.path.dirname(input_file) + cmd_translator = [ + command, + input_file, + os.path.join(working_dir, aedb_name), + "-l={}".format(os.path.join(working_dir, "Translator.log")), + ] + if not use_ppe: + cmd_translator.append("-ppe=false") + if control_file and input_file[-3:] not in ["brd", "mcm"]: + if is_linux: + cmd_translator.append("-c={}".format(control_file)) + else: + cmd_translator.append('-c="{}"'.format(control_file)) + p = subprocess.Popen(cmd_translator) + p.wait() + if not os.path.exists(os.path.join(working_dir, aedb_name)): + self.logger.error("Translator failed to translate.") + return False + else: + self.logger.info("Translation correctly completed") + self.edbpath = os.path.join(working_dir, aedb_name) + return self.open_edb() + + def export_to_ipc2581(self, ipc_path=None, units="MILLIMETER"): + """Create an XML IPC2581 file from the active EDB. + + .. note:: + The method works only in CPython because of some limitations on Ironpython in XML parsing and + because it's time-consuming. + This method is still being tested and may need further debugging. + Any feedback is welcome. Back drills and custom pads are not supported yet. + + Parameters + ---------- + ipc_path : str, optional + Path to the XML IPC2581 file. The default is ``None``, in which case + an attempt is made to find the XML IPC2581 file in the same directory + as the active EDB. To succeed, the XML IPC2581 file and the active + EDT must have the same name. Only the extension differs. + units : str, optional + Units of the XML IPC2581 file. Options are ``"millimeter"``, + ``"inch"``, and ``"micron"``. The default is ``"millimeter"``. + + Returns + ------- + ``True`` if successful, ``False`` if failed : bool + + """ + if units.lower() not in ["millimeter", "inch", "micron"]: # pragma no cover + self.logger.warning("The wrong unit is entered. Setting to the default, millimeter.") + units = "millimeter" + + if not ipc_path: + ipc_path = self.edbpath[:-4] + "xml" + self.logger.info("Export IPC 2581 is starting. This operation can take a while.") + start = time.time() + ipc = Ipc2581(self, units) + ipc.load_ipc_model() + ipc.file_path = ipc_path + result = ipc.write_xml() + + if result: # pragma no cover + self.logger.info_timer("Export IPC 2581 completed.", start) + self.logger.info("File saved as %s", ipc_path) + return ipc_path + self.logger.info("Error exporting IPC 2581.") + return False + + @property + def configuration(self): + """Edb project configuration from file.""" + if not self._configuration: + self._configuration = Configuration(self) + return self._configuration + + def edb_exception(self, ex_value, tb_data): + """Write the trace stack to AEDT when a Python error occurs. + + Parameters + ---------- + ex_value : + + tb_data : + + + Returns + ------- + None + + """ + tb_trace = traceback.format_tb(tb_data) + tblist = tb_trace[0].split("\n") + self.logger.error(str(ex_value)) + for el in tblist: + self.logger.error(el) + + @property + def active_db(self): + """Database object.""" + return self.db + + @property + def active_cell(self): + """Active cell.""" + return self._active_cell + + @property + def components(self): + """Edb Components methods and properties. + + Returns + ------- + Instance of :class:`pyedb.dotnet.database.components.Components` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myproject.aedb") + >>> comp = edbapp.components.get_component_by_name("J1") + """ + if not self._components and self.active_db: + self._components = Components(self) + return self._components + + @property + def design_options(self): + """Edb Design Settings and Options. + + Returns + ------- + Instance of :class:`pyedb.dotnet.database.edb_data.design_options.EdbDesignOptions` + """ + # return EdbDesignOptions(self.active_cell) + # TODO check is really needed + return None + + @property + def stackup(self): + """Stackup manager. + + Returns + ------- + Instance of :class: 'pyedb.dotnet.database.Stackup` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myproject.aedb") + >>> edbapp.stackup.layers["TOP"].thickness = 4e-5 + >>> edbapp.stackup.layers["TOP"].thickness == 4e-05 + >>> edbapp.stackup.add_layer("Diel", "GND", layer_type="dielectric", thickness="0.1mm", material="FR4_epoxy") + """ + if self.active_db: + self._stackup = Stackup(self, self.active_cell.layout.layer_collection) + return self._stackup + + @property + def source_excitation(self): + if self.active_db: + return self._source_excitation + + @property + def materials(self): + """Material Database. + + Returns + ------- + Instance of :class: `pyedb.dotnet.database.Materials` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb() + >>> edbapp.materials.add_material("air", permittivity=1.0) + >>> edbapp.materials.add_debye_material("debye_mat", 5, 3, 0.02, 0.05, 1e5, 1e9) + >>> edbapp.materials.add_djordjevicsarkar_material("djord_mat", 3.3, 0.02, 3.3) + """ + if self.active_db: + self._materials = Materials(self) + return self._materials + + @property + def padstacks(self): + """Core padstack. + + + Returns + ------- + Instance of :class: `legacy.database.padstack.EdbPadstack` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myproject.aedb") + >>> p = edbapp.padstacks.create(padstackname="myVia_bullet", antipad_shape="Bullet") + >>> edbapp.padstacks.get_pad_parameters( + >>> ... p, "TOP", edbapp.padstacks.pad_type.RegularPad + >>> ... ) + """ + + if not self._padstack and self.active_db: + self._padstack = Padstacks(self) + return self._padstack + + @property + def siwave(self): + """Core SIWave methods and properties. + + Returns + ------- + Instance of :class: `pyedb.dotnet.database.siwave.EdbSiwave` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myproject.aedb") + >>> p2 = edbapp.siwave.create_circuit_port_on_net("U2A5", "V3P3_S0", "U2A5", "GND", 50, "test") + """ + if not self._siwave and self.active_db: + self._siwave = Siwave(self) + return self._siwave + + @property + def hfss(self): + """Core HFSS methods and properties. + + Returns + ------- + :class:`pyedb.grpc.edb_core.hfss.Hfss` + + See Also + -------- + :class:`legacy.database.edb_data.simulation_configuration.SimulationConfiguration` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myproject.aedb") + >>> sim_config = edbapp.new_simulation_configuration() + >>> sim_config.mesh_freq = "10Ghz" + >>> edbapp.hfss.configure_hfss_analysis_setup(sim_config) + """ + if not self._hfss and self.active_db: + self._hfss = Hfss(self) + return self._hfss + + @property + def nets(self): + """Core nets. + + Returns + ------- + :class:`legacy.database.nets.EdbNets` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb"myproject.aedb") + >>> edbapp.nets.find_or_create_net("GND") + >>> edbapp.nets.find_and_fix_disjoint_nets("GND", keep_only_main_net=True) + """ + + if not self._nets and self.active_db: + self._nets = Nets(self) + return self._nets + + @property + def net_classes(self): + """Get all net classes. + + Returns + ------- + :class:`legacy.database.nets.EdbNetClasses` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myproject.aedb") + >>> edbapp.net_classes + """ + + if self.active_db: + return {net.name: NetClass(self, net) for net in self.active_layout.net_classes} + + @property + def extended_nets(self): + """Get all extended nets. + + Returns + ------- + :class:`legacy.database.nets.EdbExtendedNets` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myproject.aedb") + >>> edbapp.extended_nets + """ + + if not self._extended_nets: + self._extended_nets = ExtendedNets(self) + return self._extended_nets + + @property + def differential_pairs(self): + """Get all differential pairs. + + Returns + ------- + :class:`legacy.database.nets.EdbDifferentialPairs` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myproject.aedb") + >>> edbapp.differential_pairs + """ + if not self._differential_pairs and self.active_db: + self._differential_pairs = DifferentialPairs(self) + return self._differential_pairs + + @property + def modeler(self): + """Core primitives modeler. + + Returns + ------- + Instance of :class: `legacy.database.layout.EdbLayout` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb("myproject.aedb") + >>> top_prims = edbapp.modeler.primitives_by_layer["TOP"] + """ + if not self._modeler and self.active_db: + self._modeler = Modeler(self) + return self._modeler + + @property + def layout(self): + """Layout object. + + Returns + ------- + :class:`legacy.database.dotnet.layout.Layout` + """ + return Layout(self) + + @property + def active_layout(self): + """Active layout. + + Returns + ------- + Instance of EDB API Layout Class. + """ + return self.layout + + @property + def layout_instance(self): + """Edb Layout Instance.""" + if not self._layout_instance: + self._layout_instance = self.layout.layout_instance + return self._layout_instance + + def get_connected_objects(self, layout_object_instance): + """Get connected objects. + + Returns + ------- + list + """ + from ansys.edb.core.terminal.terminals import ( + PadstackInstanceTerminal as GrpcPadstackInstanceTerminal, + ) + + temp = [] + try: + for i in self.layout_instance.get_connected_objects(layout_object_instance, True): + if isinstance(i.layout_obj, GrpcPadstackInstanceTerminal): + temp.append(PadstackInstanceTerminal(self, i.layout_obj)) + else: + layout_obj_type = i.layout_obj.layout_obj_type.name + if layout_obj_type == "PADSTACK_INSTANCE": + temp.append(PadstackInstance(self, i.layout_obj)) + elif layout_obj_type == "PATH": + temp.append(Path(self, i.layout_obj)) + elif layout_obj_type == "RECTANGLE": + temp.append(Rectangle(self, i.layout_obj)) + elif layout_obj_type == "CIRCLE": + temp.append(Circle(self, i.layout_obj)) + elif layout_obj_type == "POLYGON": + temp.append(Polygon(self, i.layout_obj)) + else: + continue + except: + self.logger.warning( + f"Failed to find connected objects on layout_obj " f"{layout_object_instance.layout_obj.id}, skipping." + ) + pass + return temp + + def point_3d(self, x, y, z=0.0): + """Compute the Edb 3d Point Data. + + Parameters + ---------- + x : float, int or str + X value. + y : float, int or str + Y value. + z : float, int or str, optional + Z value. + + Returns + ------- + ``Geometry.Point3DData``. + """ + from pyedb.grpc.database.geometry.point_3d_data import Point3DData + + return Point3DData(x, y, z) + + def point_data(self, x, y=None): + """Compute the Edb Point Data. + + Parameters + ---------- + x : float, int or str + X value. + y : float, int or str, optional + Y value. + + + Returns + ------- + ``Geometry.PointData``. + """ + from pyedb.grpc.database.geometry.point_data import PointData + + if y is None: + return PointData(x) + else: + return PointData(x, y) + + @staticmethod + def _is_file_existing_and_released(filename): + if os.path.exists(filename): + try: + os.rename(filename, filename + "_") + os.rename(filename + "_", filename) + return True + except OSError as e: + return False + else: + return False + + @staticmethod + def _is_file_existing(filename): + if os.path.exists(filename): + return True + else: + return False + + def _wait_for_file_release(self, timeout=30, file_to_release=None): + if not file_to_release: + file_to_release = os.path.join(self.edbpath) + tstart = time.time() + while True: + if self._is_file_existing_and_released(file_to_release): + return True + elif time.time() - tstart > timeout: + return False + else: + time.sleep(0.250) + + def _wait_for_file_exists(self, timeout=30, file_to_release=None, wait_count=4): + if not file_to_release: + file_to_release = os.path.join(self.edbpath) + tstart = time.time() + times = 0 + while True: + if self._is_file_existing(file_to_release): + # print 'File is released' + times += 1 + if times == wait_count: + return True + elif time.time() - tstart > timeout: + # print 'Timeout reached' + return False + else: + times = 0 + time.sleep(0.250) + + def close_edb(self): + """Close EDB and cleanup variables. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + self.close() + start_time = time.time() + self._wait_for_file_release() + elapsed_time = time.time() - start_time + self.logger.info("EDB file release time: {0:.2f}ms".format(elapsed_time * 1000.0)) + self._clean_variables() + return True + + def save_edb(self): + """Save the EDB file. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + self.save() + start_time = time.time() + self._wait_for_file_release() + elapsed_time = time.time() - start_time + self.logger.info("EDB file save time: {0:.2f}ms".format(elapsed_time * 1000.0)) + return True + + def save_edb_as(self, fname): + """Save the EDB file as another file. + + Parameters + ---------- + fname : str + Name of the new file to save to. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + self.save_as(fname) + start_time = time.time() + self._wait_for_file_release() + elapsed_time = time.time() - start_time + self.logger.info("EDB file save time: {0:.2f}ms".format(elapsed_time * 1000.0)) + self.edbpath = self.directory + self.log_name = os.path.join( + os.path.dirname(fname), "pyedb_" + os.path.splitext(os.path.split(fname)[-1])[0] + ".log" + ) + return True + + def execute(self, func): + """Execute a function. + + Parameters + ---------- + func : str + Function to execute. + + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + # return self.edb_api.utility.utility.Command.Execute(func) + pass + + def import_cadence_file(self, inputBrd, WorkDir=None, anstranslator_full_path="", use_ppe=False): + """Import a board file and generate an ``edb.def`` file in the working directory. + + Parameters + ---------- + inputBrd : str + Full path to the board file. + WorkDir : str, optional + Directory in which to create the ``aedb`` folder. The default value is ``None``, + in which case the AEDB file is given the same name as the board file. Only + the extension differs. + anstranslator_full_path : str, optional + Full path to the Ansys translator. + use_ppe : bool, optional + Whether to use the PPE License. The default is ``False``. + + Returns + ------- + bool + ``True`` when successful, ``False`` when failed. + + """ + if self.import_layout_pcb( + inputBrd, + working_dir=WorkDir, + anstranslator_full_path=anstranslator_full_path, + use_ppe=use_ppe, + ): + return True + else: + return False + + def import_gds_file( + self, + inputGDS, + anstranslator_full_path="", + use_ppe=False, + control_file=None, + tech_file=None, + map_file=None, + layer_filter=None, + ): + """Import a GDS file and generate an ``edb.def`` file in the working directory. + + ..note:: + `ANSYSLMD_LICENSE_FILE` is needed to run the translator. + + Parameters + ---------- + inputGDS : str + Full path to the GDS file. + anstranslator_full_path : str, optional + Full path to the Ansys translator. + use_ppe : bool, optional + Whether to use the PPE License. The default is ``False``. + control_file : str, optional + Path to the XML file. The default is ``None``, in which case an attempt is made to find + the XML file in the same directory as the GDS file. To succeed, the XML file and GDS file must + have the same name. Only the extension differs. + tech_file : str, optional + Technology file. For versions<2024.1 it uses Helic to convert tech file to xml and then imports + the gds. Works on Linux only. + For versions>=2024.1 it can directly parse through supported foundry tech files. + map_file : str, optional + Layer map file. + layer_filter:str,optional + Layer filter file. + + """ + control_file_temp = os.path.join(tempfile.gettempdir(), os.path.split(inputGDS)[-1][:-3] + "xml") + if float(self.edbversion) < 2024.1: + if not is_linux and tech_file: + self.logger.error("Technology files are supported only in Linux. Use control file instead.") + return False + + ControlFile(xml_input=control_file, tecnhology=tech_file, layer_map=map_file).write_xml(control_file_temp) + if self.import_layout_pcb( + inputGDS, + anstranslator_full_path=anstranslator_full_path, + use_ppe=use_ppe, + control_file=control_file_temp, + ): + return True + else: + return False + else: + temp_map_file = os.path.splitext(inputGDS)[0] + ".map" + temp_layermap_file = os.path.splitext(inputGDS)[0] + ".layermap" + + if map_file is None: + if os.path.isfile(temp_map_file): + map_file = temp_map_file + elif os.path.isfile(temp_layermap_file): + map_file = temp_layermap_file + else: + self.logger.error("Unable to define map file.") + + if tech_file is None: + if control_file is None: + temp_control_file = os.path.splitext(inputGDS)[0] + ".xml" + if os.path.isfile(temp_control_file): + control_file = temp_control_file + else: + self.logger.error("Unable to define control file.") + + command = [anstranslator_full_path, inputGDS, f'-g="{map_file}"', f'-c="{control_file}"'] + else: + command = [ + anstranslator_full_path, + inputGDS, + f'-o="{control_file_temp}"' f'-t="{tech_file}"', + f'-g="{map_file}"', + f'-f="{layer_filter}"', + ] + + result = subprocess.run(command, capture_output=True, text=True, shell=True) + print(result.stdout) + print(command) + temp_inputGDS = inputGDS.split(".gds")[0] + self.edbpath = temp_inputGDS + ".aedb" + return self.open_edb() + + def _create_extent( + self, + net_signals, + extent_type, + expansion_size, + use_round_corner, + use_pyaedt_extent=False, + smart_cut=False, + reference_list=[], + include_pingroups=True, + pins_to_preserve=None, + inlcude_voids_in_extents=False, + ): + from ansys.edb.core.geometry.polygon_data import ExtentType as GrpcExtentType + + if extent_type in [ + "Conforming", + GrpcExtentType.CONFORMING, + 1, + ]: + if use_pyaedt_extent: + _poly = self._create_conformal( + net_signals, + expansion_size, + 1e-12, + use_round_corner, + expansion_size, + smart_cut, + reference_list, + pins_to_preserve, + inlcude_voids_in_extents=inlcude_voids_in_extents, + ) + else: + _poly = self.layout.expanded_extent( + net_signals, + GrpcExtentType.CONFORMING, + expansion_size, + False, + use_round_corner, + 1, + ) + elif extent_type in [ + "Bounding", + GrpcExtentType.BOUNDING_BOX, + 0, + ]: + _poly = self.layout.expanded_extent( + net_signals, + GrpcExtentType.BOUNDING_BOX, + expansion_size, + False, + use_round_corner, + 1, + ) + else: + if use_pyaedt_extent: + _poly = self._create_convex_hull( + net_signals, + expansion_size, + 1e-12, + use_round_corner, + expansion_size, + smart_cut, + reference_list, + pins_to_preserve, + ) + else: + _poly = self.layout.expanded_extent( + net_signals, + GrpcExtentType.CONFORMING, + expansion_size, + False, + use_round_corner, + 1, + ) + if not isinstance(_poly, list): + _poly = [_poly] + _poly = GrpcPolygonData.convex_hull(_poly) + return _poly + + def _create_conformal( + self, + net_signals, + expansion_size, + tolerance, + round_corner, + round_extension, + smart_cutout=False, + reference_list=[], + pins_to_preserve=None, + inlcude_voids_in_extents=False, + ): + names = [] + _polys = [] + for net in net_signals: + names.append(net.name) + if pins_to_preserve: + insts = self.padstacks.instances + for i in pins_to_preserve: + p = insts[i].position + pos_1 = [i - expansion_size for i in p] + pos_2 = [i + expansion_size for i in p] + plane = self.modeler.Shape("rectangle", pointA=pos_1, pointB=pos_2) + rectangle_data = self.modeler.shape_to_polygon_data(plane) + _polys.append(rectangle_data) + + for prim in self.modeler.primitives: + if prim is not None and prim.net_name in names: + _polys.append(prim) + if smart_cutout: + objs_data = self._smart_cut(reference_list, expansion_size) + _polys.extend(objs_data) + k = 0 + delta = expansion_size / 5 + while k < 10: + unite_polys = [] + for i in _polys: + if "PolygonData" not in str(i): + obj_data = i.polygon_data.expand(expansion_size, tolerance, round_corner, round_extension) + else: + obj_data = i.expand(expansion_size, tolerance, round_corner, round_extension) + if inlcude_voids_in_extents and "PolygonData" not in str(i) and i.has_voids and obj_data: + for void in i.voids: + void_data = void.polygon_data.expand( + -1 * expansion_size, tolerance, round_corner, round_extension + ) + if void_data: + for v in list(void_data): + obj_data[0].holes.append(v) + if obj_data: + if not inlcude_voids_in_extents: + unite_polys.extend(list(obj_data)) + else: + voids_poly = [] + try: + if i.has_voids: + area = i.area() + for void in i.voids: + void_polydata = void.polygon_data + if void_polydata.area() >= 0.05 * area: + voids_poly.append(void_polydata) + if voids_poly: + obj_data = obj_data[0].subtract(list(obj_data), voids_poly) + except: + pass + finally: + unite_polys.extend(list(obj_data)) + _poly_unite = GrpcPolygonData.unite(unite_polys) + if len(_poly_unite) == 1: + self.logger.info("Correctly computed Extension at first iteration.") + return _poly_unite[0] + k += 1 + expansion_size += delta + if len(_poly_unite) == 1: + self.logger.info(f"Correctly computed Extension in {k} iterations.") + return _poly_unite[0] + else: + self.logger.info("Failed to Correctly computed Extension.") + areas = [i.area() for i in _poly_unite] + return _poly_unite[areas.index(max(areas))] + + def _smart_cut(self, reference_list=[], expansion_size=1e-12): + from ansys.edb.core.geometry.point_data import PointData as GrpcPointData + + _polys = [] + boundary_types = [ + "port", + ] + terms = [term for term in self.layout.terminals if term.boundary_type in [0, 3, 4, 7, 8]] + locations = [] + for term in terms: + if term.type == "PointTerminal" and term.net.name in reference_list: + pd = term.get_parameters()[1] + locations.append([pd.x.value, pd.y.value]) + for point in locations: + pointA = GrpcPointData([point[0] - expansion_size, point[1] - expansion_size]) + pointB = GrpcPointData([point[0] + expansion_size, point[1] + expansion_size]) + points = [pointA, GrpcPointData([pointB.x, pointA.y]), pointB, GrpcPointData([pointA.x, pointB.y])] + _polys.append(GrpcPolygonData(points=points)) + return _polys + + def _create_convex_hull( + self, + net_signals, + expansion_size, + tolerance, + round_corner, + round_extension, + smart_cut=False, + reference_list=[], + pins_to_preserve=None, + ): + names = [] + _polys = [] + for net in net_signals: + names.append(net.name) + if pins_to_preserve: + insts = self.padstacks.instances + for i in pins_to_preserve: + p = insts[i].position + pos_1 = [i - 1e-12 for i in p] + pos_2 = [i + 1e-12 for i in p] + pos_3 = [pos_2[0], pos_1[1]] + pos_4 = pos_1[0], pos_2[1] + rectangle_data = GrpcPolygonData(points=[pos_1, pos_3, pos_2, pos_4]) + _polys.append(rectangle_data) + for prim in self.modeler.primitives: + if not prim.is_null and not prim.net.is_null: + if prim.net.name in names: + _polys.append(prim.polygon_data) + if smart_cut: + objs_data = self._smart_cut(reference_list, expansion_size) + _polys.extend(objs_data) + _poly = GrpcPolygonData.convex_hull(_polys) + _poly = _poly.expand( + offset=expansion_size, round_corner=round_corner, max_corner_ext=round_extension, tol=tolerance + )[0] + return _poly + + def cutout( + self, + signal_list=None, + reference_list=None, + extent_type="ConvexHull", + expansion_size=0.002, + use_round_corner=False, + output_aedb_path=None, + open_cutout_at_end=True, + use_pyaedt_cutout=True, + number_of_threads=4, + use_pyaedt_extent_computing=True, + extent_defeature=0, + remove_single_pin_components=False, + custom_extent=None, + custom_extent_units="mm", + include_partial_instances=False, + keep_voids=True, + check_terminals=False, + include_pingroups=False, + expansion_factor=0, + maximum_iterations=10, + preserve_components_with_model=False, + simple_pad_check=True, + keep_lines_as_path=False, + include_voids_in_extents=False, + ): + """Create a cutout using an approach entirely based on PyAEDT. + This method replaces all legacy cutout methods in PyAEDT. + It does in sequence: + - delete all nets not in list, + - create a extent of the nets, + - check and delete all vias not in the extent, + - check and delete all the primitives not in extent, + - check and intersect all the primitives that intersect the extent. + + Parameters + ---------- + signal_list : list + List of signal strings. + reference_list : list, optional + List of references to add. The default is ``["GND"]``. + extent_type : str, optional + Type of the extension. Options are ``"Conforming"``, ``"ConvexHull"``, and + ``"Bounding"``. The default is ``"Conforming"``. + expansion_size : float, str, optional + Expansion size ratio in meters. The default is ``0.002``. + use_round_corner : bool, optional + Whether to use round corners. The default is ``False``. + output_aedb_path : str, optional + Full path and name for the new AEDB file. If None, then current aedb will be cutout. + open_cutout_at_end : bool, optional + Whether to open the cutout at the end. The default is ``True``. + use_pyaedt_cutout : bool, optional + Whether to use new PyAEDT cutout method or EDB API method. + New method is faster than native API method since it benefits of multithread. + number_of_threads : int, optional + Number of thread to use. Default is 4. Valid only if ``use_pyaedt_cutout`` is set to ``True``. + use_pyaedt_extent_computing : bool, optional + Whether to use legacy extent computing (experimental) or EDB API. + extent_defeature : float, optional + Defeature the cutout before applying it to produce simpler geometry for mesh (Experimental). + It applies only to Conforming bounding box. Default value is ``0`` which disable it. + remove_single_pin_components : bool, optional + Remove all Single Pin RLC after the cutout is completed. Default is `False`. + custom_extent : list + Points list defining the cutout shape. This setting will override `extent_type` field. + custom_extent_units : str + Units of the point list. The default is ``"mm"``. Valid only if `custom_extend` is provided. + include_partial_instances : bool, optional + Whether to include padstack instances that have bounding boxes intersecting with point list polygons. + This operation may slow down the cutout export.Valid only if `custom_extend` and + `use_pyaedt_cutout` is provided. + keep_voids : bool + Boolean used for keep or not the voids intersecting the polygon used for clipping the layout. + Default value is ``True``, ``False`` will remove the voids.Valid only if `custom_extend` is provided. + check_terminals : bool, optional + Whether to check for all reference terminals and increase extent to include them into the cutout. + This applies to components which have a model (spice, touchstone or netlist) associated. + include_pingroups : bool, optional + Whether to check for all pingroups terminals and increase extent to include them into the cutout. + It requires ``check_terminals``. + expansion_factor : int, optional + The method computes a float representing the largest number between + the dielectric thickness or trace width multiplied by the expansion_factor factor. + The trace width search is limited to nets with ports attached. Works only if `use_pyaedt_cutout`. + Default is `0` to disable the search. + maximum_iterations : int, optional + Maximum number of iterations before stopping a search for a cutout with an error. + Default is `10`. + preserve_components_with_model : bool, optional + Whether to preserve all pins of components that have associated models (Spice or NPort). + This parameter is applicable only for a PyAEDT cutout (except point list). + simple_pad_check : bool, optional + Whether to use the center of the pad to find the intersection with extent or use the bounding box. + Second method is much slower and requires to disable multithread on padstack removal. + Default is `True`. + keep_lines_as_path : bool, optional + Whether to keep the lines as Path after they are cutout or convert them to PolygonData. + This feature works only in Electronics Desktop (3D Layout). + If the flag is set to ``True`` it can cause issues in SiWave once the Edb is imported. + Default is ``False`` to generate PolygonData of cut lines. + include_voids_in_extents : bool, optional + Whether to compute and include voids in pyaedt extent before the cutout. Cutout time can be affected. + It works only with Conforming cutout. + Default is ``False`` to generate extent without voids. + + + Returns + ------- + List + List of coordinate points defining the extent used for clipping the design. If it failed return an empty + list. + + Examples + -------- + >>> from pyedb import Edb + >>> edb = Edb(r'C:\\test.aedb', edbversion="2022.2") + >>> edb.logger.info_timer("Edb Opening") + >>> edb.logger.reset_timer() + >>> start = time.time() + >>> signal_list = [] + >>> for net in edb.nets.netlist: + >>> if "3V3" in net: + >>> signal_list.append(net) + >>> power_list = ["PGND"] + >>> edb.cutout(signal_list=signal_list, reference_list=power_list, extent_type="Conforming") + >>> end_time = str((time.time() - start)/60) + >>> edb.logger.info("Total legacy cutout time in min %s", end_time) + >>> edb.nets.plot(signal_list, None, color_by_net=True) + >>> edb.nets.plot(power_list, None, color_by_net=True) + >>> edb.save_edb() + >>> edb.close_edb() + + + """ + if expansion_factor > 0: + expansion_size = self.calculate_initial_extent(expansion_factor) + if signal_list is None: + signal_list = [] + if isinstance(reference_list, str): + reference_list = [reference_list] + elif reference_list is None: + reference_list = [] + if not use_pyaedt_cutout and custom_extent: + return self._create_cutout_on_point_list( + custom_extent, + units=custom_extent_units, + output_aedb_path=output_aedb_path, + open_cutout_at_end=open_cutout_at_end, + nets_to_include=signal_list + reference_list, + include_partial_instances=include_partial_instances, + keep_voids=keep_voids, + ) + elif not use_pyaedt_cutout: + return self._create_cutout_legacy( + signal_list=signal_list, + reference_list=reference_list, + extent_type=extent_type, + expansion_size=expansion_size, + use_round_corner=use_round_corner, + output_aedb_path=output_aedb_path, + open_cutout_at_end=open_cutout_at_end, + use_pyaedt_extent_computing=use_pyaedt_extent_computing, + check_terminals=check_terminals, + include_pingroups=include_pingroups, + inlcude_voids_in_extents=include_voids_in_extents, + ) + else: + legacy_path = self.edbpath + if expansion_factor > 0 and not custom_extent: + start = time.time() + self.save_edb() + dummy_path = self.edbpath.replace(".aedb", "_smart_cutout_temp.aedb") + working_cutout = False + i = 1 + expansion = expansion_size + while i <= maximum_iterations: + self.logger.info("-----------------------------------------") + self.logger.info(f"Trying cutout with {expansion * 1e3}mm expansion size") + self.logger.info("-----------------------------------------") + result = self._create_cutout_multithread( + signal_list=signal_list, + reference_list=reference_list, + extent_type=extent_type, + expansion_size=expansion, + use_round_corner=use_round_corner, + number_of_threads=number_of_threads, + custom_extent=custom_extent, + output_aedb_path=dummy_path, + remove_single_pin_components=remove_single_pin_components, + use_pyaedt_extent_computing=use_pyaedt_extent_computing, + extent_defeature=extent_defeature, + custom_extent_units=custom_extent_units, + check_terminals=check_terminals, + include_pingroups=include_pingroups, + preserve_components_with_model=preserve_components_with_model, + include_partial=include_partial_instances, + simple_pad_check=simple_pad_check, + keep_lines_as_path=keep_lines_as_path, + inlcude_voids_in_extents=include_voids_in_extents, + ) + if self.are_port_reference_terminals_connected(): + if output_aedb_path: + self.save_edb_as(output_aedb_path) + else: + self.save_edb_as(legacy_path) + working_cutout = True + break + self.close_edb() + self.edbpath = legacy_path + self.open_edb() + i += 1 + expansion = expansion_size * i + if working_cutout: + msg = f"Cutout completed in {i} iterations with expansion size of {expansion * 1e3}mm" + self.logger.info_timer(msg, start) + else: + msg = f"Cutout failed after {i} iterations and expansion size of {expansion * 1e3}mm" + self.logger.info_timer(msg, start) + return False + else: + result = self._create_cutout_multithread( + signal_list=signal_list, + reference_list=reference_list, + extent_type=extent_type, + expansion_size=expansion_size, + use_round_corner=use_round_corner, + number_of_threads=number_of_threads, + custom_extent=custom_extent, + output_aedb_path=output_aedb_path, + remove_single_pin_components=remove_single_pin_components, + use_pyaedt_extent_computing=use_pyaedt_extent_computing, + extent_defeature=extent_defeature, + custom_extent_units=custom_extent_units, + check_terminals=check_terminals, + include_pingroups=include_pingroups, + preserve_components_with_model=preserve_components_with_model, + include_partial=include_partial_instances, + simple_pad_check=simple_pad_check, + keep_lines_as_path=keep_lines_as_path, + inlcude_voids_in_extents=include_voids_in_extents, + ) + if result and not open_cutout_at_end and self.edbpath != legacy_path: + self.save_edb() + self.close_edb() + self.edbpath = legacy_path + self.open_edb() + return result + + def _create_cutout_legacy( + self, + signal_list=[], + reference_list=["GND"], + extent_type="Conforming", + expansion_size=0.002, + use_round_corner=False, + output_aedb_path=None, + open_cutout_at_end=True, + use_pyaedt_extent_computing=False, + remove_single_pin_components=False, + check_terminals=False, + include_pingroups=True, + inlcude_voids_in_extents=False, + ): + expansion_size = GrpcValue(expansion_size).value + + # validate nets in layout + net_signals = [net for net in self.layout.nets if net.name in signal_list] + + # validate references in layout + _netsClip = [net for net in self.layout.nets if net.name in reference_list] + + _poly = self._create_extent( + net_signals, + extent_type, + expansion_size, + use_round_corner, + use_pyaedt_extent_computing, + smart_cut=check_terminals, + reference_list=reference_list, + include_pingroups=include_pingroups, + inlcude_voids_in_extents=inlcude_voids_in_extents, + ) + _poly1 = GrpcPolygonData(arcs=_poly.arc_data, closed=True) + if inlcude_voids_in_extents: + for hole in _poly.holes: + if hole.area() >= 0.05 * _poly1.area(): + _poly1.holes.append(hole) + _poly = _poly1 + # Create new cutout cell/design + included_nets_list = signal_list + reference_list + included_nets = [net for net in self.layout.nets if net.name in included_nets_list] + _cutout = self.active_cell.cutout(included_nets, _netsClip, _poly, True) + # _cutout.simulation_setups = self.active_cell.simulation_setups see bug #433 status. + _dbCells = [_cutout] + if output_aedb_path: + db2 = self.create(output_aedb_path) + _success = db2.save() + _dbCells = _dbCells + db2.copy_cells(_dbCells) # Copies cutout cell/design to db2 project + if len(list(db2.circuit_cells)) > 0: + for net in db2.circuit_cells[0].layout.nets: + if not net.name in included_nets_list: + net.delete() + _success = db2.save() + for c in self.active_db.top_circuit_cells: + if c.name == _cutout.name: + c.delete() + if open_cutout_at_end: # pragma: no cover + self._db = db2 + self.edbpath = output_aedb_path + self._active_cell = self.top_circuit_cells[0] + self.edbpath = self.directory + self._init_objects() + if remove_single_pin_components: + self.components.delete_single_pin_rlc() + self.logger.info_timer("Single Pins components deleted") + self.components.refresh_components() + else: + if remove_single_pin_components: + try: + from ansys.edb.core.hierarchy.component_group import ( + ComponentGroup as GrpcComponentGroup, + ) + + layout = db2.circuit_cells[0].layout + _cmps = [l for l in layout.groups if isinstance(l, GrpcComponentGroup) and l.num_pins < 2] + for _cmp in _cmps: + _cmp.delete() + except: + self._logger.error("Failed to remove single pin components.") + db2.close() + source = os.path.join(output_aedb_path, "edb.def.tmp") + target = os.path.join(output_aedb_path, "edb.def") + self._wait_for_file_release(file_to_release=output_aedb_path) + if os.path.exists(source) and not os.path.exists(target): + try: + shutil.copy(source, target) + except: + pass + elif open_cutout_at_end: + self._active_cell = _cutout + self._init_objects() + if remove_single_pin_components: + self.components.delete_single_pin_rlc() + self.logger.info_timer("Single Pins components deleted") + self.components.refresh_components() + return [[pt.x.value, pt.y.value] for pt in _poly.without_arcs().points] + + def _create_cutout_multithread( + self, + signal_list=[], + reference_list=["GND"], + extent_type="Conforming", + expansion_size=0.002, + use_round_corner=False, + number_of_threads=4, + custom_extent=None, + output_aedb_path=None, + remove_single_pin_components=False, + use_pyaedt_extent_computing=False, + extent_defeature=0.0, + custom_extent_units="mm", + check_terminals=False, + include_pingroups=True, + preserve_components_with_model=False, + include_partial=False, + simple_pad_check=True, + keep_lines_as_path=False, + inlcude_voids_in_extents=False, + ): + from concurrent.futures import ThreadPoolExecutor + + if output_aedb_path: + self.save_edb_as(output_aedb_path) + self.logger.info("Cutout Multithread started.") + expansion_size = GrpcValue(expansion_size).value + + timer_start = self.logger.reset_timer() + if custom_extent: + if not reference_list and not signal_list: + reference_list = self.nets.netlist[::] + all_list = reference_list + else: + reference_list = reference_list + signal_list + all_list = reference_list + else: + all_list = signal_list + reference_list + pins_to_preserve = [] + nets_to_preserve = [] + if preserve_components_with_model: + for el in self.components.instances.values(): + if el.model_type in [ + "SPICEModel", + "SParameterModel", + "NetlistModel", + ] and list(set(el.nets[:]) & set(signal_list[:])): + pins_to_preserve.extend([i.edb_uid for i in el.pins.values()]) + nets_to_preserve.extend(el.nets) + if include_pingroups: + for pingroup in self.layout.pin_groups: + for pin in pingroup.pins: + if pin.net_name in reference_list: + pins_to_preserve.append(pin.edb_uid) + if check_terminals: + terms = [ + term for term in self.layout.terminals if term.boundary_type in get_terminal_supported_boundary_types() + ] + for term in terms: + if isinstance(term, PadstackInstanceTerminal): + if term.net.name in reference_list: + pins_to_preserve.append(term.edb_uid) + + for i in self.nets.nets.values(): + name = i.name + if name not in all_list and name not in nets_to_preserve: + i.delete() + reference_pinsts = [] + reference_prims = [] + reference_paths = [] + for i in self.padstacks.instances.values(): + net_name = i.net_name + id = i.id + if net_name not in all_list and id not in pins_to_preserve: + i.delete() + elif net_name in reference_list and id not in pins_to_preserve: + reference_pinsts.append(i) + for i in self.modeler.primitives: + if not i.is_null and not i.net.is_null: + if i.net.name not in all_list: + i.delete() + elif i.net.name in reference_list and not i.is_void: + if keep_lines_as_path and isinstance(i, Path): + reference_paths.append(i) + else: + reference_prims.append(i) + self.logger.info_timer("Net clean up") + self.logger.reset_timer() + + if custom_extent and isinstance(custom_extent, list): + if custom_extent[0] != custom_extent[-1]: + custom_extent.append(custom_extent[0]) + custom_extent = [ + [ + self.number_with_units(i[0], custom_extent_units), + self.number_with_units(i[1], custom_extent_units), + ] + for i in custom_extent + ] + _poly = GrpcPolygonData(points=custom_extent) + elif custom_extent: + _poly = custom_extent + else: + net_signals = [net for net in self.layout.nets if net.name in signal_list] + _poly = self._create_extent( + net_signals, + extent_type, + expansion_size, + use_round_corner, + use_pyaedt_extent_computing, + smart_cut=check_terminals, + reference_list=reference_list, + include_pingroups=include_pingroups, + pins_to_preserve=pins_to_preserve, + inlcude_voids_in_extents=inlcude_voids_in_extents, + ) + from ansys.edb.core.geometry.polygon_data import ( + ExtentType as GrpcExtentType, + ) + + if extent_type in ["Conforming", GrpcExtentType.CONFORMING, 1]: + if extent_defeature > 0: + _poly = _poly.defeature(extent_defeature) + _poly1 = GrpcPolygonData(arcs=_poly.arc_data, closed=True) + if inlcude_voids_in_extents: + for hole in list(_poly.holes): + if hole.area() >= 0.05 * _poly1.area(): + _poly1.holes.append(hole) + self.logger.info(f"Number of voids included:{len(list(_poly1.holes))}") + _poly = _poly1 + if not _poly.points: + self._logger.error("Failed to create Extent.") + return [] + self.logger.info_timer("Expanded Net Polygon Creation") + self.logger.reset_timer() + _poly_list = [_poly] + prims_to_delete = [] + poly_to_create = [] + pins_to_delete = [] + + def intersect(poly1, poly2): + if not isinstance(poly2, list): + poly2 = [poly2] + return poly1.intersect(poly1, poly2) + + def subtract(poly, voids): + return poly.subtract(poly, voids) + + def clip_path(path): + pdata = path.polygon_data + int_data = _poly.intersection_type(pdata) + if int_data == 0: + prims_to_delete.append(path) + return + result = path.set_clip_info(_poly, True) + if not result: + self.logger.info(f"Failed to clip path {path.id}. Clipping as polygon.") + reference_prims.append(path) + + def clean_prim(prim_1): # pragma: no cover + pdata = prim_1.polygon_data + int_data = _poly.intersection_type(pdata) + if int_data == 2: + if not inlcude_voids_in_extents: + return + skip = False + for hole in list(_poly.Holes): + if hole.intersection_type(pdata) == 0: + prims_to_delete.append(prim_1) + return + elif hole.intersection_type(pdata) == 1: + skip = True + if skip: + return + elif int_data == 0: + prims_to_delete.append(prim_1) + return + list_poly = intersect(_poly, pdata) + if list_poly: + net = prim_1.net.name + voids = prim_1.voids + for p in list_poly: + if not p.points: + continue + list_void = [] + if voids: + voids_data = [void.polygon_data for void in voids] + list_prims = subtract(p, voids_data) + for prim in list_prims: + if prim.points: + poly_to_create.append([prim, prim_1.layer.name, net, list_void]) + else: + poly_to_create.append([p, prim_1.layer.name, net, list_void]) + + prims_to_delete.append(prim_1) + + def pins_clean(pinst): + if not pinst.in_polygon(_poly, include_partial=include_partial, simple_check=simple_pad_check): + pins_to_delete.append(pinst) + + if not simple_pad_check: + pad_cores = 1 + else: + pad_cores = number_of_threads + with ThreadPoolExecutor(pad_cores) as pool: + pool.map(lambda item: pins_clean(item), reference_pinsts) + + for pin in pins_to_delete: + pin.delete() + + self.logger.info_timer(f"Padstack Instances removal completed. {len(pins_to_delete)} instances removed.") + self.logger.reset_timer() + + for item in reference_paths: + clip_path(item) + for prim in reference_prims: # removing multithreading as failing with new layer from primitive + clean_prim(prim) + + for el in poly_to_create: + self.modeler.create_polygon(el[0], el[1], net_name=el[2], voids=el[3]) + + for prim in prims_to_delete: + prim.delete() + + self.logger.info_timer(f"Primitives cleanup completed. {len(prims_to_delete)} primitives deleted.") + self.logger.reset_timer() + + i = 0 + for _, val in self.components.instances.items(): + if val.numpins == 0: + val.delete() + i += 1 + i += 1 + self.logger.info(f"Deleted {i} additional components") + if remove_single_pin_components: + self.components.delete_single_pin_rlc() + self.logger.info_timer("Single Pins components deleted") + + self.components.refresh_components() + if output_aedb_path: + self.save_edb() + self.logger.info_timer("Cutout completed.", timer_start) + self.logger.reset_timer() + return [[pt.x.value, pt.y.value] for pt in _poly.without_arcs().points] + + def get_conformal_polygon_from_netlist(self, netlist=None): + """Return an EDB conformal polygon based on a netlist. + + Parameters + ---------- + + netlist : List of net names. + list[str] + + Returns + ------- + :class:`Edb.Cell.Primitive.Polygon` + Edb polygon object. + + """ + from ansys.edb.core.geometry.polygon_data import ExtentType as GrpcExtentType + + temp_edb_path = self.edbpath[:-5] + "_temp_aedb.aedb" + shutil.copytree(self.edbpath, temp_edb_path) + temp_edb = EdbGrpc(temp_edb_path) + for via in list(temp_edb.padstacks.instances.values()): + via.pin.delete() + if netlist: + nets = [net for net in temp_edb.layout.nets if net.name in netlist] + _poly = temp_edb.layout.expanded_extent(nets, GrpcExtentType.CONFORMING, 0.0, True, True, 1) + else: + nets = [net for net in temp_edb.layout.nets if "gnd" in net.name.lower()] + _poly = temp_edb.layout.expanded_extent(nets, GrpcExtentType.CONFORMING, 0.0, True, True, 1) + temp_edb.close() + if _poly: + return _poly + else: + return False + + def number_with_units(self, value, units=None): + """Convert a number to a string with units. If value is a string, it's returned as is. + + Parameters + ---------- + value : float, int, str + Input number or string. + units : optional + Units for formatting. The default is ``None``, which uses ``"meter"``. + + Returns + ------- + str + String concatenating the value and unit. + + """ + if units is None: + units = "meter" + if isinstance(value, str): + return value + else: + return f"{value}{units}" + + def _create_cutout_on_point_list( + self, + point_list, + units="mm", + output_aedb_path=None, + open_cutout_at_end=True, + nets_to_include=None, + include_partial_instances=False, + keep_voids=True, + ): + from ansys.edb.core.geometry.point_data import PointData as GrpcPointData + + if point_list[0] != point_list[-1]: + point_list.append(point_list[0]) + point_list = [[self.number_with_units(i[0], units), self.number_with_units(i[1], units)] for i in point_list] + polygon_data = GrpcPolygonData(points=[GrpcPointData(pt) for pt in point_list]) + _ref_nets = [] + if nets_to_include: + self.logger.info(f"Creating cutout on {len(nets_to_include)} nets.") + else: + self.logger.info("Creating cutout on all nets.") # pragma: no cover + + # Check Padstack Instances overlapping the cutout + pinstance_to_add = [] + if include_partial_instances: + if nets_to_include: + pinst = [i for i in list(self.padstacks.instances.values()) if i.net_name in nets_to_include] + else: + pinst = [i for i in list(self.padstacks.instances.values())] + for p in pinst: + pin_position = p.position # check bug #434 status + if polygon_data.is_inside(p.position): # check bug #434 status + pinstance_to_add.append(p) + # validate references in layout + for _ref in self.nets.nets: + if nets_to_include: + if _ref in nets_to_include: + _ref_nets.append(self.nets.nets[_ref]) + else: + _ref_nets.append(self.nets.nets[_ref]) # pragma: no cover + if keep_voids: + voids = [p for p in self.modeler.circles if p.is_void] + voids2 = [p for p in self.modeler.polygons if p.is_void] + voids.extend(voids2) + else: + voids = [] + voids_to_add = [] + for circle in voids: + if polygon_data.get_intersection_type(circle.polygon_data) >= 3: + voids_to_add.append(circle) + + _netsClip = _ref_nets + # Create new cutout cell/design + _cutout = self.active_cell.cutout(_netsClip, _netsClip, polygon_data) + layout = _cutout.layout + cutout_obj_coll = layout.padstack_instances + ids = [] + for lobj in cutout_obj_coll: + ids.append(lobj.id) + if include_partial_instances: + from ansys.edb.core.geometry.point_data import PointData as GrpcPointData + from ansys.edb.core.primitive.primitive import ( + PadstackInstance as GrpcPadstackInstance, + ) + + p_missing = [i for i in pinstance_to_add if i.id not in ids] + self.logger.info(f"Added {len(p_missing)} padstack instances after cutout") + for p in p_missing: + position = GrpcPointData(p.position) + net = self.nets.find_or_create_net(p.net_name) + rotation = GrpcValue(p.rotation) + sign_layers = list(self.stackup.signal_layers.keys()) + if not p.start_layer: # pragma: no cover + fromlayer = self.stackup.signal_layers[sign_layers[0]] + else: + fromlayer = self.stackup.signal_layers[p.start_layer] + + if not p.stop_layer: # pragma: no cover + tolayer = self.stackup.signal_layers[sign_layers[-1]] + else: + tolayer = self.stackup.signal_layers[p.stop_layer] + for pad in list(self.padstacks.definitions.keys()): + if pad == p.padstack_definition: + padstack = self.padstacks.definitions[pad] + padstack_instance = GrpcPadstackInstance.create( + layout=_cutout.layout, + net=net, + name=p.name, + padstack_def=padstack, + position_x=position.x, + position_y=position.y, + rotation=rotation, + top_layer=fromlayer, + bottom_layer=tolayer, + layer_map=None, + solder_ball_layer=None, + ) + padstack_instance.is_layout_pin = p.is_pin + break + + for void_circle in voids_to_add: + if isinstance(void_circle, Circle): + res = void_circle.get_parameters() + cloned_circle = Circle.create( + layout=layout, + layer=void_circle.layer.name, + net=void_circle.net, + center_x=res[0].x, + center_y=res[0].y, + radius=res[1], + ) + cloned_circle.is_negative = True + elif isinstance(void_circle, Polygon): + cloned_polygon = Polygon.create( + layout, + void_circle.layer.name, + void_circle.net, + void_circle.polygon_data, + ) + cloned_polygon.is_negative = True + layers = [i for i in list(self.stackup.signal_layers.keys())] + for layer in layers: + layer_primitves = self.modeler.get_primitives(layer_name=layer) + if len(layer_primitves) == 0: + self.modeler.create_polygon(point_list, layer, net_name="DUMMY") + self.logger.info(f"Cutout {_cutout.name} created correctly") + for _setup in self.active_cell.simulation_setups: + # Add the create Simulation setup to cutout cell + # might need to add a clone setup method. + pass + + _dbCells = [_cutout] + if output_aedb_path: + db2 = self.create(output_aedb_path) + db2.save() + cell_copied = db2.copy_cells(_dbCells) # Copies cutout cell/design to db2 project + cell = cell_copied[0] + cell.name = os.path.basename(output_aedb_path[:-5]) + db2.save() + for c in list(self.active_db.top_circuit_cells): + if c.name == _cutout.name: + c.delete() + if open_cutout_at_end: # pragma: no cover + _success = db2.save() + self._db = db2 + self.edbpath = output_aedb_path + self._active_cell = cell + self.edbpath = self.directory + self._init_objects() + else: + db2.close() + source = os.path.join(output_aedb_path, "edb.def.tmp") + target = os.path.join(output_aedb_path, "edb.def") + self._wait_for_file_release(file_to_release=output_aedb_path) + if os.path.exists(source) and not os.path.exists(target): + try: + shutil.copy(source, target) + self.logger.warning("aedb def file manually created.") + except: + pass + return [[pt.x.value, pt.y.value] for pt in polygon_data.without_arcs().points] + + @staticmethod + def write_export3d_option_config_file(path_to_output, config_dictionaries=None): + """Write the options for a 3D export to a configuration file. + + Parameters + ---------- + path_to_output : str + Full path to the configuration file to save 3D export options to. + + config_dictionaries : dict, optional + Configuration dictionaries. The default is ``None``. + + """ + option_config = { + "UNITE_NETS": 1, + "ASSIGN_SOLDER_BALLS_AS_SOURCES": 0, + "Q3D_MERGE_SOURCES": 0, + "Q3D_MERGE_SINKS": 0, + "CREATE_PORTS_FOR_PWR_GND_NETS": 0, + "PORTS_FOR_PWR_GND_NETS": 0, + "GENERATE_TERMINALS": 0, + "SOLVE_CAPACITANCE": 0, + "SOLVE_DC_RESISTANCE": 0, + "SOLVE_DC_INDUCTANCE_RESISTANCE": 1, + "SOLVE_AC_INDUCTANCE_RESISTANCE": 0, + "CreateSources": 0, + "CreateSinks": 0, + "LAUNCH_Q3D": 0, + "LAUNCH_HFSS": 0, + } + if config_dictionaries: + for el, val in config_dictionaries.items(): + option_config[el] = val + with open(os.path.join(path_to_output, "options.config"), "w") as f: + for el, val in option_config.items(): + f.write(el + " " + str(val) + "\n") + return os.path.join(path_to_output, "options.config") + + def export_hfss( + self, + path_to_output, + net_list=None, + num_cores=None, + aedt_file_name=None, + hidden=False, + ): + """Export EDB to HFSS. + + Parameters + ---------- + path_to_output : str + Full path and name for saving the AEDT file. + net_list : list, optional + List of nets to export if only certain ones are to be exported. + The default is ``None``, in which case all nets are eported. + num_cores : int, optional + Number of cores to use for the export. The default is ``None``. + aedt_file_name : str, optional + Name of the AEDT output file without the ``.aedt`` extension. The default is ``None``, + in which case the default name is used. + hidden : bool, optional + Open Siwave in embedding mode. User will only see Siwave Icon but UI will be hidden. + + Returns + ------- + str + Full path to the AEDT file. + + Examples + -------- + + >>> from pyedb import Edb + >>> edb = Edb(edbpath=r"C:\temp\myproject.aedb", edbversion="2023.2") + + >>> options_config = {'UNITE_NETS' : 1, 'LAUNCH_Q3D' : 0} + >>> edb.write_export3d_option_config_file(r"C:\temp", options_config) + >>> edb.export_hfss(r"C:\temp") + """ + siwave_s = SiwaveSolve(self.edbpath, aedt_installer_path=self.base_path) + return siwave_s.export_3d_cad("HFSS", path_to_output, net_list, num_cores, aedt_file_name, hidden=hidden) + + def export_q3d( + self, + path_to_output, + net_list=None, + num_cores=None, + aedt_file_name=None, + hidden=False, + ): + """Export EDB to Q3D. + + Parameters + ---------- + path_to_output : str + Full path and name for saving the AEDT file. + net_list : list, optional + List of nets to export only if certain ones are to be exported. + The default is ``None``, in which case all nets are eported. + num_cores : int, optional + Number of cores to use for the export. The default is ``None``. + aedt_file_name : str, optional + Name of the AEDT output file without the ``.aedt`` extension. The default is ``None``, + in which case the default name is used. + hidden : bool, optional + Open Siwave in embedding mode. User will only see Siwave Icon but UI will be hidden. + + Returns + ------- + str + Full path to the AEDT file. + + Examples + -------- + + >>> from pyedb import Edb + >>> edb = Edb(edbpath=r"C:\temp\myproject.aedb", edbversion="2021.2") + >>> options_config = {'UNITE_NETS' : 1, 'LAUNCH_Q3D' : 0} + >>> edb.write_export3d_option_config_file(r"C:\temp", options_config) + >>> edb.export_q3d(r"C:\temp") + """ + + siwave_s = SiwaveSolve(self.edbpath, aedt_installer_path=self.base_path) + return siwave_s.export_3d_cad( + "Q3D", + path_to_output, + net_list, + num_cores=num_cores, + aedt_file_name=aedt_file_name, + hidden=hidden, + ) + + def export_maxwell( + self, + path_to_output, + net_list=None, + num_cores=None, + aedt_file_name=None, + hidden=False, + ): + """Export EDB to Maxwell 3D. + + Parameters + ---------- + path_to_output : str + Full path and name for saving the AEDT file. + net_list : list, optional + List of nets to export only if certain ones are to be + exported. The default is ``None``, in which case all nets are exported. + num_cores : int, optional + Number of cores to use for the export. The default is ``None.`` + aedt_file_name : str, optional + Name of the AEDT output file without the ``.aedt`` extension. The default is ``None``, + in which case the default name is used. + hidden : bool, optional + Open Siwave in embedding mode. User will only see Siwave Icon but UI will be hidden. + + Returns + ------- + str + Full path to the AEDT file. + + Examples + -------- + + >>> from pyedb import Edb + + >>> edb = Edb(edbpath=r"C:\temp\myproject.aedb", edbversion="2021.2") + + >>> options_config = {'UNITE_NETS' : 1, 'LAUNCH_Q3D' : 0} + >>> edb.write_export3d_option_config_file(r"C:\temp", options_config) + >>> edb.export_maxwell(r"C:\temp") + """ + siwave_s = SiwaveSolve(self.edbpath, aedt_installer_path=self.base_path) + return siwave_s.export_3d_cad( + "Maxwell", + path_to_output, + net_list, + num_cores=num_cores, + aedt_file_name=aedt_file_name, + hidden=hidden, + ) + + def solve_siwave(self): + """Close EDB and solve it with Siwave. + + Returns + ------- + str + Siwave project path. + """ + process = SiwaveSolve(self.edbpath, aedt_version=self.edbversion) + try: + self.close() + except: + pass + process.solve() + return self.edbpath[:-5] + ".siw" + + def export_siwave_dc_results( + self, + siwave_project, + solution_name, + output_folder=None, + html_report=True, + vias=True, + voltage_probes=True, + current_sources=True, + voltage_sources=True, + power_tree=True, + loop_res=True, + ): + """Close EDB and solve it with Siwave. + + Parameters + ---------- + siwave_project : str + Siwave full project name. + solution_name : str + Siwave DC Analysis name. + output_folder : str, optional + Ouptu folder where files will be downloaded. + html_report : bool, optional + Either if generate or not html report. Default is `True`. + vias : bool, optional + Either if generate or not vias report. Default is `True`. + voltage_probes : bool, optional + Either if generate or not voltage probe report. Default is `True`. + current_sources : bool, optional + Either if generate or not current source report. Default is `True`. + voltage_sources : bool, optional + Either if generate or not voltage source report. Default is `True`. + power_tree : bool, optional + Either if generate or not power tree image. Default is `True`. + loop_res : bool, optional + Either if generate or not loop resistance report. Default is `True`. + + Returns + ------- + list + List of files generated. + """ + process = SiwaveSolve(self.edbpath, aedt_version=self.edbversion) + try: + self.close() + except: + pass + return process.export_dc_report( + siwave_project, + solution_name, + output_folder, + html_report, + vias, + voltage_probes, + current_sources, + voltage_sources, + power_tree, + loop_res, + hidden=True, + ) + + def variable_exists(self, variable_name): + """Check if a variable exists or not. + + Returns + ------- + tuple of bool and VariableServer + It returns a booleand to check if the variable exists and the variable + server that should contain the variable. + """ + if "$" in variable_name: + if variable_name.index("$") == 0: + variables = self.active_db.get_all_variable_names() + else: + variables = self.active_cell.get_all_variable_names() + else: + variables = self.active_cell.get_all_variable_names() + if variable_name in variables: + return True + return False + + def get_variable(self, variable_name): + """Return Variable Value if variable exists. + + Parameters + ---------- + variable_name + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.edbvalue.EdbValue` + """ + if self.variable_exists(variable_name): + if "$" in variable_name: + if variable_name.index("$") == 0: + variable = next(var for var in self.active_db.get_all_variable_names()) + else: + variable = next(var for var in self.active_cell.get_all_variable_names()) + return self.db.get_variable_value(variable) + self.logger.info(f"Variable {variable_name} doesn't exists.") + return False + + def add_project_variable(self, variable_name, variable_value): + """Add a variable to edb database (project). The variable will have the prefix `$`. + + ..note:: + User can use also the setitem to create or assign a variable. See example below. + + Parameters + ---------- + variable_name : str + Name of the variable. Name can be provided without ``$`` prefix. + variable_value : str, float + Value of the variable with units. + + Returns + ------- + tuple + Tuple containing the ``AddVariable`` result and variable server. + + Examples + -------- + + >>> from pyedb import Edb + >>> edb_app = Edb() + >>> boolean_1, ant_length = edb_app.add_project_variable("my_local_variable", "1cm") + >>> print(edb_app["$my_local_variable"]) #using getitem + >>> edb_app["$my_local_variable"] = "1cm" #using setitem + + """ + if not variable_name.startswith("$"): + variable_name = f"${variable_name}" + if not self.variable_exists(variable_name): + return self.active_db.add_variable(variable_name, variable_value) + else: + self.logger.error(f"Variable {variable_name} already exists.") + return False + + def add_design_variable(self, variable_name, variable_value, is_parameter=False): + """Add a variable to edb. The variable can be a design one or a project variable (using ``$`` prefix). + + ..note:: + User can use also the setitem to create or assign a variable. See example below. + + Parameters + ---------- + variable_name : str + Name of the variable. To added the variable as a project variable, the name + must begin with ``$``. + variable_value : str, float + Value of the variable with units. + is_parameter : bool, optional + Whether to add the variable as a local variable. The default is ``False``. + When ``True``, the variable is added as a parameter default. + + Returns + ------- + tuple + Tuple containing the ``AddVariable`` result and variable server. + + Examples + -------- + + >>> from pyedb import Edb + >>> edb_app = Edb() + >>> boolean_1, ant_length = edb_app.add_design_variable("my_local_variable", "1cm") + >>> print(edb_app["my_local_variable"]) #using getitem + >>> edb_app["my_local_variable"] = "1cm" #using setitem + >>> boolean_2, para_length = edb_app.change_design_variable_value("my_parameter", "1m", is_parameter=True + >>> boolean_3, project_length = edb_app.change_design_variable_value("$my_project_variable", "1m") + + + """ + if variable_name.startswith("$"): + variable_name = variable_name[1:] + if not self.variable_exists(variable_name): + return self.active_cell.add_variable(variable_name, variable_value) + else: + self.logger.error(f"Variable {variable_name} already exists.") + return False + + def change_design_variable_value(self, variable_name, variable_value): + """Change a variable value. + + ..note:: + User can use also the getitem to read the variable value. See example below. + + Parameters + ---------- + variable_name : str + Name of the variable. + variable_value : str, float + Value of the variable with units. + + Returns + ------- + tuple + Tuple containing the ``SetVariableValue`` result and variable server. + + Examples + -------- + + >>> from pyedb import Edb + >>> edb_app = Edb() + >>> boolean, ant_length = edb_app.add_design_variable("ant_length", "1cm") + >>> boolean, ant_length = edb_app.change_design_variable_value("ant_length", "1m") + >>> print(edb_app["ant_length"]) #using getitem + """ + if self.variable_exists(variable_name): + if variable_name in self.db.get_all_variable_names(): + self.db.set_variable_value(variable_name, GrpcValue(variable_value)) + elif variable_name in self.active_cell.get_all_variable_names(): + self.active_cell.set_variable_value(variable_name, GrpcValue(variable_value)) + + def get_bounding_box(self): + """Get the layout bounding box. + + Returns + ------- + list[float] + Bounding box as a [lower-left X, lower-left Y, upper-right X, upper-right Y] in meters. + """ + lay_inst_polygon_data = [obj_inst.get_bbox() for obj_inst in self.layout_instance.query_layout_obj_instances()] + layout_bbox = GrpcPolygonData.bbox_of_polygons(lay_inst_polygon_data) + return [[layout_bbox[0].x.value, layout_bbox[0].y.value], [layout_bbox[1].x.value, layout_bbox[1].y.value]] + + # def build_simulation_project(self, simulation_setup): + # # type: (SimulationConfiguration) -> bool + # """Build a ready-to-solve simulation project. + # + # Parameters + # ---------- + # simulation_setup : :class:`pyedb.dotnet.database.edb_data.simulation_configuration.SimulationConfiguration`. + # SimulationConfiguration object that can be instantiated or directly loaded with a + # configuration file. + # + # Returns + # ------- + # bool + # ``True`` when successful, False when ``Failed``. + # + # Examples + # -------- + # + # >>> from pyedb import Edb + # >>> from pyedb.dotnet.database.edb_data.simulation_configuration import SimulationConfiguration + # >>> config_file = path_configuration_file + # >>> source_file = path_to_edb_folder + # >>> edb = Edb(source_file) + # >>> sim_setup = SimulationConfiguration(config_file) + # >>> edb.build_simulation_project(sim_setup) + # >>> edb.save_edb() + # >>> edb.close_edb() + # """ + # self.logger.info("Building simulation project.") + # from ansys.edb.core.layout.cell import CellType as GrpcCellType + # + # legacy_name = self.edbpath + # if simulation_setup.output_aedb: + # self.save_edb_as(simulation_setup.output_aedb) + # if simulation_setup.signal_layer_etching_instances: + # for layer in simulation_setup.signal_layer_etching_instances: + # if layer in self.stackup.layers: + # idx = simulation_setup.signal_layer_etching_instances.index(layer) + # if len(simulation_setup.etching_factor_instances) > idx: + # self.stackup[layer].etch_factor = float(simulation_setup.etching_factor_instances[idx]) + # + # if not simulation_setup.signal_nets and simulation_setup.components: + # nets_to_include = [] + # pnets = list(self.nets.power.keys())[:] + # for el in simulation_setup.components: + # nets_to_include.append([i for i in self.components[el].nets if i not in pnets]) + # simulation_setup.signal_nets = [ + # i + # for i in list(set.intersection(*map(set, nets_to_include))) + # if i not in simulation_setup.power_nets and i != "" + # ] + # self.nets.classify_nets(simulation_setup.power_nets, simulation_setup.signal_nets) + # if not simulation_setup.power_nets or not simulation_setup.signal_nets: + # self.logger.info("Disabling cutout as no signals or power nets have been defined.") + # simulation_setup.do_cutout_subdesign = False + # if simulation_setup.do_cutout_subdesign: + # self.logger.info(f"Cutting out using method: {simulation_setup.cutout_subdesign_type}") + # if simulation_setup.use_default_cutout: + # old_cell_name = self.active_cell.name + # if self.cutout( + # signal_list=simulation_setup.signal_nets, + # reference_list=simulation_setup.power_nets, + # expansion_size=simulation_setup.cutout_subdesign_expansion, + # use_round_corner=simulation_setup.cutout_subdesign_round_corner, + # extent_type=simulation_setup.cutout_subdesign_type, + # use_pyaedt_cutout=False, + # use_pyaedt_extent_computing=False, + # ): + # self.logger.info("Cutout processed.") + # old_cell = self.active_cell.find_by_name( + # self.db, + # GrpcCellType.CIRCUIT_CELL, + # old_cell_name, + # ) + # if old_cell: + # old_cell.delete() + # else: # pragma: no cover + # self.logger.error("Cutout failed.") + # else: + # self.logger.info(f"Cutting out using method: {simulation_setup.cutout_subdesign_type}") + # self.cutout( + # signal_list=simulation_setup.signal_nets, + # reference_list=simulation_setup.power_nets, + # expansion_size=simulation_setup.cutout_subdesign_expansion, + # use_round_corner=simulation_setup.cutout_subdesign_round_corner, + # extent_type=simulation_setup.cutout_subdesign_type, + # use_pyaedt_cutout=True, + # use_pyaedt_extent_computing=True, + # remove_single_pin_components=True, + # ) + # self.logger.info("Cutout processed.") + # else: + # if simulation_setup.include_only_selected_nets: + # included_nets = simulation_setup.signal_nets + simulation_setup.power_nets + # nets_to_remove = [net.name for net in list(self.nets.nets.values()) if not net.name in included_nets] + # self.nets.delete(nets_to_remove) + # self.logger.info("Deleting existing ports.") + # map(lambda port: port.Delete(), self.layout.terminals) + # map(lambda pg: pg.delete(), self.layout.pin_groups) + # if simulation_setup.solver_type == SolverType.Hfss3dLayout: + # if simulation_setup.generate_excitations: + # self.logger.info("Creating HFSS ports for signal nets.") + # source_type = SourceType.CoaxPort + # if not simulation_setup.generate_solder_balls: + # source_type = SourceType.CircPort + # for cmp in simulation_setup.components: + # if isinstance(cmp, str): # keep legacy component + # self.components.create_port_on_component( + # cmp, + # net_list=simulation_setup.signal_nets, + # do_pingroup=False, + # reference_net=simulation_setup.power_nets, + # port_type=source_type, + # ) + # elif isinstance(cmp, dict): + # if "refdes" in cmp: + # if not "solder_balls_height" in cmp: # pragma no cover + # cmp["solder_balls_height"] = None + # if not "solder_balls_size" in cmp: # pragma no cover + # cmp["solder_balls_size"] = None + # cmp["solder_balls_mid_size"] = None + # if not "solder_balls_mid_size" in cmp: # pragma no cover + # cmp["solder_balls_mid_size"] = None + # self.components.create_port_on_component( + # cmp["refdes"], + # net_list=simulation_setup.signal_nets, + # do_pingroup=False, + # reference_net=simulation_setup.power_nets, + # port_type=source_type, + # solder_balls_height=cmp["solder_balls_height"], + # solder_balls_size=cmp["solder_balls_size"], + # solder_balls_mid_size=cmp["solder_balls_mid_size"], + # ) + # if simulation_setup.generate_solder_balls and not self.hfss.set_coax_port_attributes( + # simulation_setup + # ): # pragma: no cover + # self.logger.error("Failed to configure coaxial port attributes.") + # self.logger.info(f"Number of ports: {self.hfss.get_ports_number()}") + # self.logger.info("Configure HFSS extents.") + # if simulation_setup.generate_solder_balls and simulation_setup.trim_reference_size: + # self.logger.info( + # f"Trimming the reference plane for coaxial ports: {bool(simulation_setup.trim_reference_size)}" + # ) + # self.hfss.trim_component_reference_size(simulation_setup) # pragma: no cover + # self.hfss.configure_hfss_extents(simulation_setup) + # if not self.hfss.configure_hfss_analysis_setup(simulation_setup): + # self.logger.error("Failed to configure HFSS simulation setup.") + # if simulation_setup.solver_type == SolverType.SiwaveSYZ: + # if simulation_setup.generate_excitations: + # for cmp in simulation_setup.components: + # if isinstance(cmp, str): # keep legacy + # self.components.create_port_on_component( + # cmp, + # net_list=simulation_setup.signal_nets, + # do_pingroup=simulation_setup.do_pingroup, + # reference_net=simulation_setup.power_nets, + # port_type=SourceType.CircPort, + # ) + # elif isinstance(cmp, dict): + # if "refdes" in cmp: # pragma no cover + # self.components.create_port_on_component( + # cmp["refdes"], + # net_list=simulation_setup.signal_nets, + # do_pingroup=simulation_setup.do_pingroup, + # reference_net=simulation_setup.power_nets, + # port_type=SourceType.CircPort, + # ) + # self.logger.info("Configuring analysis setup.") + # if not self.siwave.configure_siw_analysis_setup(simulation_setup): # pragma: no cover + # self.logger.error("Failed to configure Siwave simulation setup.") + # if simulation_setup.solver_type == SolverType.SiwaveDC: + # if simulation_setup.generate_excitations: + # self.components.create_source_on_component(simulation_setup.sources) + # if not self.siwave.configure_siw_analysis_setup(simulation_setup): # pragma: no cover + # self.logger.error("Failed to configure Siwave simulation setup.") + # self.padstacks.check_and_fix_via_plating() + # self.save_edb() + # if not simulation_setup.open_edb_after_build and simulation_setup.output_aedb: + # self.close_edb() + # self.edbpath = legacy_name + # self.open_edb() + # return True + + def get_statistics(self, compute_area=False): + """Get the EDBStatistics object. + + Returns + ------- + EDBStatistics object from the loaded layout. + """ + return self.modeler.get_layout_statistics(evaluate_area=compute_area, net_list=None) + + def are_port_reference_terminals_connected(self, common_reference=None): + """Check if all terminal references in design are connected. + If the reference nets are different, there is no hope for the terminal references to be connected. + After we have identified a common reference net we need to loop the terminals again to get + the correct reference terminals that uses that net. + + Parameters + ---------- + common_reference : str, optional + Common Reference name. If ``None`` it will be searched in ports terminal. + If a string is passed then all excitations must have such reference assigned. + + Returns + ------- + bool + Either if the ports are connected to reference_name or not. + + Examples + -------- + >>> from pyedb import Edb + >>>edb = Edb() + >>> edb.hfss.create_edge_port_vertical(prim_1_id, ["-66mm", "-4mm"], "port_ver") + >>> edb.hfss.create_edge_port_horizontal( + >>> ... prim_1_id, ["-60mm", "-4mm"], prim_2_id, ["-59mm", "-4mm"], "port_hori", 30, "Lower" + >>> ... ) + >>> edb.hfss.create_wave_port(traces[0].id, trace_paths[0][0], "wave_port") + >>> edb.cutout(["Net1"]) + >>> assert edb.are_port_reference_terminals_connected() + """ + all_sources = [i for i in self.excitations.values() if not isinstance(i, (WavePort, GapPort, BundleWavePort))] + all_sources.extend([i for i in self.sources.values()]) + if not all_sources: + return True + self.logger.reset_timer() + if not common_reference: + common_reference = list(set([i.reference_net.name for i in all_sources if i.reference_net.name])) + if len(common_reference) > 1: + self.logger.error("More than 1 reference found.") + return False + if not common_reference: + self.logger.error("No Reference found.") + return False + + common_reference = common_reference[0] + all_sources = [i for i in all_sources if i.net.name != common_reference] + + setList = [ + set(i.reference_object.get_connected_object_id_set()) + for i in all_sources + if i.reference_object and i.reference_net.name == common_reference + ] + if len(setList) != len(all_sources): + self.logger.error("No Reference found.") + return False + cmps = [ + i + for i in list(self.components.resistors.values()) + if i.numpins == 2 and common_reference in i.nets and self._decompose_variable_value(i.res_value) <= 1 + ] + cmps.extend( + [i for i in list(self.components.inductors.values()) if i.numpins == 2 and common_reference in i.nets] + ) + + for cmp in cmps: + found = False + ids = [i.id for i in cmp.pinlist] + for list_obj in setList: + if len(set(ids).intersection(list_obj)) == 1: + for list_obj2 in setList: + if list_obj2 != list_obj and len(set(ids).intersection(list_obj)) == 1: + if (ids[0] in list_obj and ids[1] in list_obj2) or ( + ids[1] in list_obj and ids[0] in list_obj2 + ): + setList[setList.index(list_obj)] = list_obj.union(list_obj2) + setList[setList.index(list_obj2)] = list_obj.union(list_obj2) + found = True + break + if found: + break + + # Get the set intersections for all the ID sets. + iDintersection = set.intersection(*setList) + self.logger.info_timer(f"Terminal reference primitive IDs total intersections = {len(iDintersection)}\n\n") + + # If the intersections are non-zero, the terminal references are connected. + return True if len(iDintersection) > 0 else False + + # def new_simulation_configuration(self, filename=None): + # # type: (str) -> SimulationConfiguration + # """New SimulationConfiguration Object. + # + # Parameters + # ---------- + # filename : str, optional + # Input config file. + # + # Returns + # ------- + # :class:`legacy.database.edb_data.simulation_configuration.SimulationConfiguration` + # """ + # return SimulationConfiguration(filename, self) + + @property + def setups(self): + """Get the dictionary of all EDB HFSS and SIwave setups. + + Returns + ------- + Dict[str, :class:`HfssSimulationSetup`] or + Dict[str, :class:`SiwaveSimulationSetup`] or + Dict[str, :class:`SIWaveDCIRSimulationSetup`] or + Dict[str, :class:`RaptorXSimulationSetup`] + + """ + self._setups = {} + for setup in self.active_cell.simulation_setups: + setup = setup.cast() + setup_type = setup.type.name + if setup_type == "HFSS": + self._setups[setup.name] = HfssSimulationSetup(self, setup) + elif setup_type == "SI_WAVE": + self._setups[setup.name] = SiwaveSimulationSetup(self, setup) + elif setup_type == "SI_WAVE_DCIR": + self._setups[setup.name] = SIWaveDCIRSimulationSetup(self, setup) + elif setup_type == "RAPTOR_X": + self._setups[setup.name] = RaptorXSimulationSetup(self, setup) + return self._setups + + @property + def hfss_setups(self): + """Active HFSS setup in EDB. + + Returns + ------- + Dict[str, :class:`legacy.database.edb_data.hfss_simulation_setup_data.HfssSimulationSetup`] + + """ + setups = {} + for setup in self.active_cell.simulation_setups: + if setup.type.name == "HFSS": + setups[setup.name] = HfssSimulationSetup(self, setup) + return setups + + @property + def siwave_dc_setups(self): + """Active Siwave DC IR Setups. + + Returns + ------- + Dict[str, :class:`legacy.database.edb_data.siwave_simulation_setup_data.SiwaveDCSimulationSetup`] + """ + return {name: i for name, i in self.setups.items() if isinstance(i, SIWaveDCIRSimulationSetup)} + + @property + def siwave_ac_setups(self): + """Active Siwave SYZ setups. + + Returns + ------- + Dict[str, :class:`legacy.database.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup`] + """ + return {name: i for name, i in self.setups.items() if isinstance(i, SiwaveSimulationSetup)} + + def create_hfss_setup(self, name=None, start_frequency="0GHz", stop_frequency="20GHz", step_frequency="10MHz"): + """Create an HFSS simulation setup from a template. + + . deprecated:: pyedb 0.30.0 + Use :func:`pyedb.grpc.core.hfss.add_setup` instead. + + Parameters + ---------- + name : str, optional + Setup name. + + Returns + ------- + :class:`legacy.database.edb_data.hfss_simulation_setup_data.HfssSimulationSetup` + + """ + warnings.warn( + "`create_hfss_setup` is deprecated and is now located here " "`pyedb.grpc.core.hfss.add_setup` instead.", + DeprecationWarning, + ) + return self._hfss.add_setup( + name=name, + distribution="linear", + start_freq=start_frequency, + stop_freq=stop_frequency, + step_freq=step_frequency, + ) + + def create_raptorx_setup(self, name=None): + """Create an RaptorX simulation setup from a template. + + Parameters + ---------- + name : str, optional + Setup name. + + Returns + ------- + :class:`legacy.database.edb_data.raptor_x_simulation_setup_data.RaptorXSimulationSetup` + + """ + from ansys.edb.core.simulation_setup.raptor_x_simulation_setup import ( + RaptorXSimulationSetup as GrpcRaptorXSimulationSetup, + ) + + if name in self.setups: + self.logger.error("Setup name already used in the layout") + return False + version = self.edbversion.split(".") + if int(version[0]) >= 2024 and int(version[-1]) >= 2 or int(version[0]) > 2024: + setup = GrpcRaptorXSimulationSetup.create(cell=self.active_cell, name=name) + return RaptorXSimulationSetup(self, setup) + else: + self.logger.error("RaptorX simulation only supported with Ansys release 2024R2 and higher") + return False + + def create_hfsspi_setup(self, name=None): + # """Create an HFSS PI simulation setup from a template. + # + # Parameters + # ---------- + # name : str, optional + # Setup name. + # + # Returns + # ------- + # :class:`legacy.database.edb_data.hfss_pi_simulation_setup_data.HFSSPISimulationSetup when succeeded, ``False`` + # when failed. + # + # """ + # if name in self.setups: + # self.logger.error("Setup name already used in the layout") + # return False + # version = self.edbversion.split(".") + # if float(self.edbversion) < 2024.2: + # self.logger.error("HFSSPI simulation only supported with Ansys release 2024R2 and higher") + # return False + # return HFSSPISimulationSetup(self, name=name) + + # TODO check HFSS-PI with Grpc. seems to defined at terminal level not setup. + pass + + def create_siwave_syz_setup(self, name=None, **kwargs): + """Create a setup from a template. + + Parameters + ---------- + name : str, optional + Setup name. + + Returns + ------- + :class:`pyedb.dotnet.database.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb() + >>> setup1 = edbapp.create_siwave_syz_setup("setup1") + >>> setup1.add_frequency_sweep(frequency_sweep=[ + ... ["linear count", "0", "1kHz", 1], + ... ["log scale", "1kHz", "0.1GHz", 10], + ... ["linear scale", "0.1GHz", "10GHz", "0.1GHz"], + ... ]) + """ + if not name: + name = generate_unique_name("Siwave_SYZ") + if name in self.setups: + return False + from ansys.edb.core.simulation_setup.siwave_simulation_setup import ( + SIWaveSimulationSetup as GrpcSIWaveSimulationSetup, + ) + + setup = SiwaveSimulationSetup(self, GrpcSIWaveSimulationSetup.create(cell=self.active_cell, name=name)) + for k, v in kwargs.items(): + setattr(setup, k, v) + return self.setups[name] + + def create_siwave_dc_setup(self, name=None, **kwargs): + """Create a setup from a template. + + Parameters + ---------- + name : str, optional + Setup name. + + Returns + ------- + :class:`legacy.database.edb_data.siwave_simulation_setup_data.SiwaveSYZSimulationSetup` + + Examples + -------- + >>> from pyedb import Edb + >>> edbapp = Edb() + >>> setup1 = edbapp.create_siwave_dc_setup("setup1") + >>> setup1.mesh_bondwires = True + + """ + if not name: + name = generate_unique_name("Siwave_DC") + if name in self.setups: + return False + setup = SIWaveDCIRSimulationSetup(self, GrpcSIWaveDCIRSimulationSetup.create(cell=self.active_cell, name=name)) + for k, v in kwargs.items(): + setattr(setup, k, v) + return setup + + def calculate_initial_extent(self, expansion_factor): + """Compute a float representing the larger number between the dielectric thickness or trace width + multiplied by the nW factor. The trace width search is limited to nets with ports attached. + + Parameters + ---------- + expansion_factor : float + Value for the width multiplier (nW factor). + + Returns + ------- + float + """ + nets = [] + for port in self.excitations.values(): + nets.append(port.net.name) + for port in self.sources.values(): + nets.append(port.net_name) + nets = list(set(nets)) + max_width = 0 + for net in nets: + for primitive in self.nets[net].primitives: + if primitive.type == "Path": + max_width = max(max_width, primitive.width) + + for layer in list(self.stackup.dielectric_layers.values()): + max_width = max(max_width, layer.thickness) + + max_width = max_width * expansion_factor + self.logger.info(f"The W factor is {expansion_factor}, The initial extent = {max_width}") + return max_width + + def copy_zones(self, working_directory=None): + """Copy multizone EDB project to one new edb per zone. + + Parameters + ---------- + working_directory : str + Directory path where all EDB project are copied, if empty will use the current EDB project. + + Returns + ------- + dict[str](int, EDB PolygonData) + Return a dictionary with edb path as key and tuple Zone Id as first item and EDB polygon Data defining + the region as second item. + + """ + if working_directory: + if not os.path.isdir(working_directory): + os.mkdir(working_directory) + else: + shutil.rmtree(working_directory) + os.mkdir(working_directory) + else: + working_directory = os.path.dirname(self.edbpath) + self.layout.synchronize_bend_manager() + zone_primitives = self.layout.zone_primitives + zone_ids = self.stackup.zone_ids + edb_zones = {} + if not self.setups: + self.siwave.add_siwave_syz_analysis() + self.save_edb() + for zone_primitive in zone_primitives: + if zone_primitive: + edb_zone_path = os.path.join(working_directory, f"{zone_primitive.id}_{os.path.basename(self.edbpath)}") + shutil.copytree(self.edbpath, edb_zone_path) + poly_data = zone_primitive.polygon_data + if self.version[0] >= 10: + edb_zones[edb_zone_path] = (zone_primitive.id, poly_data) + elif len(zone_primitives) == len(zone_ids): + edb_zones[edb_zone_path] = (zone_ids[0], poly_data) + else: + self.logger.info( + "Number of zone primitives is not equal to zone number. Zone information will be lost." + "Use Ansys 2024 R1 or later." + ) + edb_zones[edb_zone_path] = (-1, poly_data) + return edb_zones + + def cutout_multizone_layout(self, zone_dict, common_reference_net=None): + """Create a multizone project cutout. + + Parameters + ---------- + zone_dict : dict[str](EDB PolygonData) + Dictionary with EDB path as key and EDB PolygonData as value defining the zone region. + This dictionary is returned from the command copy_zones(): + >>> edb = Edb(edb_file) + >>> zone_dict = edb.copy_zones(r"C:\Temp\test") + + common_reference_net : str + the common reference net name. This net name must be provided to provide a valid project. + + Returns + ------- + dict[str][str] , list of str + first dictionary defined_ports with edb name as key and existing port name list as value. Those ports are the + ones defined before processing the multizone clipping. + second is the list of connected port. + + """ + terminals = {} + defined_ports = {} + project_connexions = None + for edb_path, zone_info in zone_dict.items(): + edb = EdbGrpc(edbversion=self.edbversion, edbpath=edb_path) + edb.cutout( + use_pyaedt_cutout=True, + custom_extent=zone_info[1], + open_cutout_at_end=True, + ) + if not zone_info[0] == -1: + layers_to_remove = [ + lay.name for lay in list(edb.stackup.layers.values()) if not lay.is_in_zone(zone_info[0]) + ] + for layer in layers_to_remove: + edb.stackup.remove_layer(layer) + edb.stackup.stackup_mode = "Laminate" + edb.cutout( + use_pyaedt_cutout=True, + custom_extent=zone_info[1], + open_cutout_at_end=True, + ) + edb.active_cell.name = os.path.splitext(os.path.basename(edb_path))[0] + if common_reference_net: + signal_nets = list(self.nets.signal.keys()) + defined_ports[os.path.splitext(os.path.basename(edb_path))[0]] = list(edb.excitations.keys()) + edb_terminals_info = edb.source_excitation.create_vertical_circuit_port_on_clipped_traces( + nets=signal_nets, + reference_net=common_reference_net, + user_defined_extent=zone_info[1], + ) + if edb_terminals_info: + terminals[os.path.splitext(os.path.basename(edb_path))[0]] = edb_terminals_info + project_connexions = self._get_connected_ports_from_multizone_cutout(terminals) + edb.save_edb() + edb.close_edb() + return defined_ports, project_connexions + + @staticmethod + def _get_connected_ports_from_multizone_cutout(terminal_info_dict): + """Return connected port list from clipped multizone layout. + + Parameters + terminal_info_dict : dict[str][str] + dictionary terminals with edb name as key and created ports name on clipped signal nets. + Dictionary is generated by the command cutout_multizone_layout: + >>> edb = Edb(edb_file) + >>> edb_zones = edb.copy_zones(r"C:\Temp\test") + >>> defined_ports, terminals_info = edb.cutout_multizone_layout(edb_zones, common_reference_net) + >>> project_connexions = get_connected_ports(terminals_info) + + Returns + ------- + list[str] + list of connected ports. + """ + if terminal_info_dict: + tolerance = 1e-8 + connected_ports_list = [] + project_list = list(terminal_info_dict.keys()) + project_combinations = list(combinations(range(0, len(project_list)), 2)) + for comb in project_combinations: + terminal_set1 = terminal_info_dict[project_list[comb[0]]] + terminal_set2 = terminal_info_dict[project_list[comb[1]]] + project1_nets = [t[0] for t in terminal_set1] + project2_nets = [t[0] for t in terminal_set2] + net_with_connected_ports = list(set(project1_nets).intersection(project2_nets)) + if net_with_connected_ports: + for net_name in net_with_connected_ports: + project1_port_info = [term_info for term_info in terminal_set1 if term_info[0] == net_name] + project2_port_info = [term_info for term_info in terminal_set2 if term_info[0] == net_name] + port_list = [p[3] for p in project1_port_info] + [p[3] for p in project2_port_info] + port_combinations = list(combinations(port_list, 2)) + for port_combination in port_combinations: + if not port_combination[0] == port_combination[1]: + port1 = [port for port in terminal_set1 if port[3] == port_combination[0]] + if not port1: + port1 = [port for port in terminal_set2 if port[3] == port_combination[0]] + port2 = [port for port in terminal_set2 if port[3] == port_combination[1]] + if not port2: + port2 = [port for port in terminal_set1 if port[3] == port_combination[1]] + port1 = port1[0] + port2 = port2[0] + if not port1[3] == port2[3]: + port_distance = GeometryOperators.points_distance(port1[1:3], port2[1:3]) + if port_distance < tolerance: + port1_connexion = None + port2_connexion = None + for ( + project_path, + port_info, + ) in terminal_info_dict.items(): + port1_map = [port for port in port_info if port[3] == port1[3]] + if port1_map: + port1_connexion = ( + project_path, + port1[3], + ) + port2_map = [port for port in port_info if port[3] == port2[3]] + if port2_map: + port2_connexion = ( + project_path, + port2[3], + ) + if port1_connexion and port2_connexion: + if ( + not port1_connexion[0] == port2_connexion[0] + or not port1_connexion[1] == port2_connexion[1] + ): + connected_ports_list.append((port1_connexion, port2_connexion)) + return connected_ports_list + + def create_port(self, terminal, ref_terminal=None, is_circuit_port=False, name=None): + """Create a port. + + Parameters + ---------- + terminal : class:`pyedb.dotnet.database.edb_data.terminals.EdgeTerminal`, + class:`pyedb.grpc.database.terminals.PadstackInstanceTerminal`, + class:`pyedb.grpc.database.terminals.PointTerminal`, + class:`pyedb.grpc.database.terminals.PinGroupTerminal`, + Positive terminal of the port. + ref_terminal : class:`pyedb.grpc.database.terminals.EdgeTerminal`, + class:`pyedb.grpc.database.terminals.PadstackInstanceTerminal`, + class:`pyedb.grpc.database.terminals.PointTerminal`, + class:`pyedb.grpc.database.terminals.PinGroupTerminal`, + optional + Negative terminal of the port. + is_circuit_port : bool, optional + Whether it is a circuit port. The default is ``False``. + name: str, optional + Name of the created port. The default is None, a random name is generated. + Returns + ------- + list: [:class:`pyedb.dotnet.database.edb_data.ports.GapPort`, + :class:`pyedb.dotnet.database.edb_data.ports.WavePort`,]. + """ + from ansys.edb.core.terminal.terminals import BoundaryType as GrpcBoundaryType + + if isinstance(terminal.boundary_type, GrpcBoundaryType): + terminal.boundary_type = GrpcBoundaryType.PORT + terminal.is_circuit_port = is_circuit_port + + if isinstance(ref_terminal.boundary_type, GrpcBoundaryType): + ref_terminal.boundary_type = GrpcBoundaryType.PORT + terminal.ref_terminal = ref_terminal + if name: + terminal.name = name + return self.ports[terminal.name] + + def create_voltage_probe(self, terminal, ref_terminal): + """Create a voltage probe. + + Parameters + ---------- + terminal : :class:`pyedb.grpc.database.terminals.EdgeTerminal`, + :class:`pyedb.grpc.database.terminals.PadstackInstanceTerminal`, + :class:`pyedb.grpc.database.terminals.PointTerminal`, + :class:`pyedb.grpc.database.terminals.PinGroupTerminal`, + Positive terminal of the port. + ref_terminal : :class:`pyedb.grpc.database.terminals.EdgeTerminal`, + :class:`pyedb.grpc.database.terminals.PadstackInstanceTerminal`, + :class:`pyedb.grpc.database.terminals.PointTerminal`, + :class:`pyedb.grpc.database.terminals.PinGroupTerminal`, + Negative terminal of the probe. + + Returns + ------- + pyedb.dotnet.database.edb_data.terminals.Terminal + """ + term = Terminal(self, terminal) + term.boundary_type = "voltage_probe" + + ref_term = Terminal(self, ref_terminal) + ref_term.boundary_type = "voltage_probe" + + term.ref_terminal = ref_terminal + return term + + def create_voltage_source(self, terminal, ref_terminal): + """Create a voltage source. + + Parameters + ---------- + terminal : :class:`pyedb.grpc.database.terminals.EdgeTerminal`, \ + :class:`pyedb.grpc.database.terminals.PadstackInstanceTerminal`, \ + :class:`pyedb.grpc.database.terminals.PointTerminal`, \ + :class:`pyedb.grpc.database.terminals.PinGroupTerminal` + Positive terminal of the port. + ref_terminal : class:`pyedb.grpc.database.terminals.EdgeTerminal`, \ + :class:`pyedb.grpc.database.terminals.PadstackInstanceTerminal`, \ + :class:`pyedb.grpc.database.terminals.PointTerminal`, \ + :class:`pyedb.grpc.database.terminals.PinGroupTerminal` + Negative terminal of the source. + + Returns + ------- + class:`legacy.database.edb_data.ports.ExcitationSources` + """ + term = Terminal(self, terminal) + term.boundary_type = "voltage_source" + + ref_term = Terminal(self, ref_terminal) + ref_term.boundary_type = "voltage_source" + + term.ref_terminal = ref_terminal + return term + + def create_current_source(self, terminal, ref_terminal): + """Create a current source. + + Parameters + ---------- + terminal : :class:`legacy.database.edb_data.terminals.EdgeTerminal`, + :class:`legacy.database.edb_data.terminals.PadstackInstanceTerminal`, + :class:`legacy.database.edb_data.terminals.PointTerminal`, + :class:`legacy.database.edb_data.terminals.PinGroupTerminal`, + Positive terminal of the port. + ref_terminal : class:`legacy.database.edb_data.terminals.EdgeTerminal`, + :class:`legacy.database.edb_data.terminals.PadstackInstanceTerminal`, + :class:`legacy.database.edb_data.terminals.PointTerminal`, + :class:`legacy.database.edb_data.terminals.PinGroupTerminal`, + Negative terminal of the source. + + Returns + ------- + :class:`legacy.database.edb_data.ports.ExcitationSources` + """ + term = Terminal(self, terminal) + term.boundary_type = "current_source" + + ref_term = Terminal(self, ref_terminal) + ref_term.boundary_type = "current_source" + + term.ref_terminal = ref_terminal + return term + + def get_point_terminal(self, name, net_name, location, layer): + """Place a voltage probe between two points. + + Parameters + ---------- + name : str, + Name of the terminal. + net_name : str + Name of the net. + location : list + Location of the terminal. + layer : str, + Layer of the terminal. + + Returns + ------- + :class:`legacy.database.edb_data.terminals.PointTerminal` + """ + from pyedb.grpc.database.terminal.point_terminal import PointTerminal + + return PointTerminal.create(layout=self.active_layout, name=name, net=net_name, layer=layer, point=location) + + def auto_parametrize_design( + self, + layers=True, + materials=True, + via_holes=True, + pads=True, + antipads=True, + traces=True, + layer_filter=None, + material_filter=None, + padstack_definition_filter=None, + trace_net_filter=None, + use_single_variable_for_padstack_definitions=True, + use_relative_variables=True, + output_aedb_path=None, + open_aedb_at_end=True, + expand_polygons_size=0, + expand_voids_size=0, + via_offset=True, + ): + """Assign automatically design and project variables with current values. + + Parameters + ---------- + layers : bool, optional + Enable layer thickness parametrization. Default value is ``True``. + materials : bool, optional + Enable material parametrization. Default value is ``True``. + via_holes : bool, optional + Enable via diameter parametrization. Default value is ``True``. + pads : bool, optional + Enable pads size parametrization. Default value is ``True``. + antipads : bool, optional + Enable anti pads size parametrization. Default value is ``True``. + traces : bool, optional + Enable trace width parametrization. Default value is ``True``. + layer_filter : str, List(str), optional + Enable layer filter. Default value is ``None``, all layers are parametrized. + material_filter : str, List(str), optional + Enable material filter. Default value is ``None``, all material are parametrized. + padstack_definition_filter : str, List(str), optional + Enable padstack definition filter. Default value is ``None``, all padsatcks are parametrized. + trace_net_filter : str, List(str), optional + Enable nets filter for trace width parametrization. Default value is ``None``, all layers are parametrized. + use_single_variable_for_padstack_definitions : bool, optional + Whether to use a single design variable for each padstack definition or a variable per pad layer. + Default value is ``True``. + use_relative_variables : bool, optional + Whether if use an absolute variable for each trace, padstacks and layers or a delta variable instead. + Default value is ``True``. + output_aedb_path : str, optional + Full path and name for the new AEDB file. If None, then current aedb will be cutout. + open_aedb_at_end : bool, optional + Whether to open the cutout at the end. The default is ``True``. + expand_polygons_size : float, optional + Expansion size on polygons. Polygons will be expanded in all directions. The default is ``0``. + expand_voids_size : float, optional + Expansion size on polygon voids. Polygons voids will be expanded in all directions. The default is ``0``. + via_offset : bool, optional + Whether if offset the via position or not. The default is ``True``. + + Returns + ------- + List(str) + List of all parameters name created. + """ + edb_original_path = self.edbpath + if output_aedb_path: + self.save_edb_as(output_aedb_path) + if isinstance(trace_net_filter, str): + trace_net_filter = [trace_net_filter] + parameters = [] + + def _apply_variable(orig_name, orig_value): + if use_relative_variables: + var = f"{orig_name}_delta" + else: + var = f"{orig_name}_value" + var = self._clean_string_for_variable_name(var) + if var not in self.variables: + if use_relative_variables: + if var.startswith("$"): + self.add_project_variable(var, 0.0) + else: + self.add_design_variable(var, 0.0) + else: + if var.startswith("$"): + self.add_project_variable(var, orig_value) + else: + self.add_design_variable(var, orig_value) + if use_relative_variables: + return f"{orig_value}+{var}", var + else: + return var, var + + if layers: + if not layer_filter: + _layers = self.stackup.layers + else: + if isinstance(layer_filter, str): + layer_filter = [layer_filter] + _layers = {k: v for k, v in self.stackup.layers.items() if k in layer_filter} + for layer_name, layer in _layers.items(): + var, val = _apply_variable(f"${layer_name}", layer.thickness) + layer.thickness = GrpcValue(var, self.active_db) + parameters.append(val) + if materials: + if not material_filter: + _materials = self.materials.materials + else: + _materials = {k: v for k, v in self.materials.materials.items() if k in material_filter} + for mat_name, material in _materials.items(): + if not material.conductivity or material.conductivity < 1e4: + var, val = _apply_variable(f"$epsr_{mat_name}", material.permittivity) + material.permittivity = GrpcValue(var, self.active_db) + parameters.append(val) + var, val = _apply_variable(f"$loss_tangent_{mat_name}", material.dielectric_loss_tangent) + material.dielectric_loss_tangent = GrpcValue(var, self.active_db) + parameters.append(val) + else: + var, val = _apply_variable(f"$sigma_{mat_name}", material.conductivity) + material.conductivity = GrpcValue(var, self.active_db) + parameters.append(val) + if traces: + if not trace_net_filter: + paths = self.modeler.paths + else: + paths = [path for path in self.modeler.paths if path.net_name in trace_net_filter] + for path in paths: + net_name = path.net_name + if use_relative_variables: + trace_width_variable = "trace" + elif net_name: + trace_width_variable = f"{path.net_name}_{path.aedt_name}" + else: + trace_width_variable = f"{path.aedt_name}" + var, val = _apply_variable(trace_width_variable, path.width) + path.width = GrpcValue(var, self.active_cell) + parameters.append(val) + if not padstack_definition_filter: + if trace_net_filter: + padstack_defs = {} + for net in trace_net_filter: + for via in self.nets[net].padstack_instances: + padstack_defs[via.padstack_definition] = self.padstacks.definitions[via.padstack_definition] + else: + used_padsatck_defs = list( + set( + [padstack_inst.padstack_definition for padstack_inst in list(self.padstacks.instances.values())] + ) + ) + padstack_defs = {k: v for k, v in self.padstacks.definitions.items() if k in used_padsatck_defs} + else: + padstack_defs = {k: v for k, v in self.padstacks.definitions.items() if k in padstack_definition_filter} + + for def_name, padstack_def in padstack_defs.items(): + if not padstack_def.start_layer == padstack_def.stop_layer: + if via_holes: # pragma no cover + if use_relative_variables: + hole_variable = "$hole_diameter" + else: + hole_variable = f"${def_name}_hole_diameter" + if padstack_def.hole_diameter: + var, val = _apply_variable(hole_variable, padstack_def.hole_diameter) + padstack_def.hole_properties = GrpcValue(var, self.active_db) + parameters.append(val) + if pads: + for layer, pad in padstack_def.pad_by_layer.items(): + if use_relative_variables: + pad_name = "$pad" + elif use_single_variable_for_padstack_definitions: + pad_name = f"${def_name}_pad" + else: + pad_name = f"${def_name}_{layer}_pad" + + if pad.geometry_type in [1, 2]: + var, val = _apply_variable(pad_name, pad.parameters_values_string[0]) + if pad.geometry_type == 1: + pad.parameters = {"Diameter": var} + else: + pad.parameters = {"Size": var} + parameters.append(val) + elif pad.geometry_type == 3: # pragma no cover + if use_relative_variables: + pad_name_x = "$pad_x" + pad_name_y = "$pad_y" + elif use_single_variable_for_padstack_definitions: + pad_name_x = f"${def_name}_pad_x" + pad_name_y = f"${def_name}_pad_y" + else: + pad_name_x = f"${def_name}_{layer}_pad_x" + pad_name_y = f"${def_name}_pad_y" + var, val = _apply_variable(pad_name_x, pad.parameters_values_string[0]) + var2, val2 = _apply_variable(pad_name_y, pad.parameters_values_string[1]) + + pad.parameters = {"XSize": var, "YSize": var2} + parameters.append(val) + parameters.append(val2) + if antipads: + for layer, antipad in padstack_def.antipad_by_layer.items(): + if antipad: + if use_relative_variables: + pad_name = "$antipad" + elif use_single_variable_for_padstack_definitions: + pad_name = f"${def_name}_antipad" + else: + pad_name = f"${def_name}_{layer}_antipad" + + if antipad.geometry_type in [1, 2]: + var, val = _apply_variable(pad_name, antipad.parameters_values_string[0]) + if antipad.geometry_type == 1: # pragma no cover + antipad.parameters = {"Diameter": var} + else: + antipad.parameters = {"Size": var} + parameters.append(val) + elif antipad.geometry_type == 3: # pragma no cover + if use_relative_variables: + pad_name_x = "$antipad_x" + pad_name_y = "$antipad_y" + elif use_single_variable_for_padstack_definitions: + pad_name_x = f"${def_name}_antipad_x" + pad_name_y = f"${def_name}_antipad_y" + else: + pad_name_x = f"${def_name}_{layer}_antipad_x" + pad_name_y = f"${def_name}_antipad_y" + + var, val = _apply_variable(pad_name_x, antipad.parameters_values_string[0]) + var2, val2 = _apply_variable(pad_name_y, antipad.parameters_values_string[1]) + antipad.parameters = {"XSize": var, "YSize": var2} + parameters.append(val) + parameters.append(val2) + + if via_offset: + var_x = "via_offset_x" + if var_x not in self.variables: + self.add_design_variable(var_x, 0.0) + var_y = "via_offset_y" + if var_y not in self.variables: + self.add_design_variable(var_y, 0.0) + for via in self.padstacks.instances.values(): + if not via.is_pin and (not trace_net_filter or (trace_net_filter and via.net_name in trace_net_filter)): + via.position = [f"{via.position[0]}+via_offset_x", f"{via.position[1]}+via_offset_y"] + + if expand_polygons_size: + for poly in self.modeler.polygons: + if not poly.is_void: + poly.expand(expand_polygons_size) + if expand_voids_size: + for poly in self.modeler.polygons: + if poly.is_void: + poly.expand(expand_voids_size, round_corners=False) + elif poly.has_voids: + for void in poly.voids: + void.expand(expand_voids_size, round_corners=False) + + if not open_aedb_at_end and self.edbpath != edb_original_path: + self.save_edb() + self.close_edb() + self.edbpath = edb_original_path + self.open_edb() + return parameters + + @staticmethod + def _clean_string_for_variable_name(variable_name): + """Remove forbidden character for variable name. + Parameters + ---------- + variable_name : str + Variable name. + Returns + ------- + str + Edited name. + """ + if "-" in variable_name: + variable_name = variable_name.replace("-", "_") + if "+" in variable_name: + variable_name = variable_name.replace("+", "p") + variable_name = re.sub(r"[() ]", "_", variable_name) + + return variable_name + + def create_model_for_arbitrary_wave_ports( + self, + temp_directory, + mounting_side="top", + signal_nets=None, + terminal_diameter=None, + output_edb=None, + launching_box_thickness="100um", + ): + """Generate EDB design to be consumed by PyAEDT to generate arbitrary wave ports shapes. + This model has to be considered as merged onto another one. The current opened design must have voids + surrounding the pad-stacks where wave ports terminal will be created. THe open design won't be edited, only + primitives like voids and pads-stack definition included in the voids are collected to generate a new design. + + Parameters + ---------- + temp_directory : str + Temporary directory used during the method execution. + + mounting_side : str + Gives the orientation to be considered for the current design. 2 options are available ``"top"`` and + ``"bottom". Default value is ``"top"``. If ``"top"`` is selected the method will voids at the top signal + layer, and the bottom layer if ``"bottom"`` is used. + + signal_nets : List[str], optional + Provides the nets to be included for the model creation. Default value is ``None``. If None is provided, + all nets will be included. + + terminal_diameter : float, str, optional + When ``None``, the terminal diameter is evaluated at each pads-tack instance found inside the voids. The top + or bottom layer pad diameter will be taken, depending on ``mounting_side`` selected. If value is provided, + it will overwrite the evaluated diameter. + + output_edb : str, optional + The output EDB absolute. If ``None`` the edb is created in the ``temp_directory`` as default name + `"waveport_model.aedb"`` + + launching_box_thickness : float, str, optional + Launching box thickness used for wave ports. Default value is ``"100um"``. + + Returns + ------- + bool + ``True`` when succeeded, ``False`` if failed. + """ + if not temp_directory: + self.logger.error("Temp directory must be provided when creating model foe arbitrary wave port") + return False + if mounting_side not in ["top", "bottom"]: + self.logger.error( + "Mounting side must be provided and only `top` or `bottom` are supported. Setting to " + "`top` will take the top layer from the current design as reference. Setting to `bottom` " + "will take the bottom one." + ) + if not output_edb: + output_edb = os.path.join(temp_directory, "waveport_model.aedb") + else: + output_edb = os.path.join(temp_directory, output_edb) + if os.path.isdir(temp_directory): + shutil.rmtree(temp_directory) + os.mkdir(temp_directory) + reference_layer = list(self.stackup.signal_layers.keys())[0] + if mounting_side.lower() == "bottom": + reference_layer = list(self.stackup.signal_layers.keys())[-1] + if not signal_nets: + signal_nets = list(self.nets.signal.keys()) + + used_padstack_defs = [] + padstack_instances_index = rtree.index.Index() + for padstack_inst in list(self.padstacks.instances.values()): + if not reference_layer in [padstack_inst.start_layer, padstack_inst.stop_layer]: + padstack_inst.delete() + else: + if padstack_inst.net.name in signal_nets: + padstack_instances_index.insert(padstack_inst.edb_uid, padstack_inst.position) + if not padstack_inst.padstack_def.name in used_padstack_defs: + used_padstack_defs.append(padstack_inst.padstack_def.name) + + polys = [ + poly + for poly in self.modeler.primitives + if poly.layer.name == reference_layer and self.modeler.primitives[0].type == "polygon" and poly.has_voids + ] + if not polys: + self.logger.error( + f"No polygon found with voids on layer {reference_layer} during model creation for " + f"arbitrary wave ports" + ) + return False + void_padstacks = [] + for poly in polys: + for void in poly.voids: + void_bbox = void.bbox + included_instances = list(padstack_instances_index.intersection(void_bbox)) + if included_instances: + void_padstacks.append((void, [self.padstacks.instances[edb_uid] for edb_uid in included_instances])) + + if not void_padstacks: + self.logger.error( + "No padstack instances found inside evaluated voids during model creation for arbitrary" "waveports" + ) + return False + cloned_edb = EdbGrpc(edbpath=output_edb, edbversion=self.edbversion, restart_rpc_server=True) + + cloned_edb.stackup.add_layer( + layer_name="ports", + layer_type="signal", + thickness=self.stackup.signal_layers[reference_layer].thickness, + material="pec", + ) + if launching_box_thickness: + launching_box_thickness = str(GrpcValue(launching_box_thickness)) + cloned_edb.stackup.add_layer( + layer_name="ref", + layer_type="signal", + thickness=0.0, + material="pec", + method=f"add_on_{mounting_side}", + base_layer="ports", + ) + cloned_edb.stackup.add_layer( + layer_name="port_pec", + layer_type="signal", + thickness=launching_box_thickness, + method=f"add_on_{mounting_side}", + material="pec", + base_layer="ports", + ) + for void_info in void_padstacks: + port_poly = cloned_edb.modeler.create_polygon( + points=void_info[0].cast().polygon_data, layer_name="ref", net_name="GND" + ) + pec_poly = cloned_edb.modeler.create_polygon( + points=port_poly.cast().polygon_data, layer_name="port_pec", net_name="GND" + ) + pec_poly.scale(1.5) + + for void_info in void_padstacks: + for inst in void_info[1]: + if not terminal_diameter: + pad_diameter = ( + self.padstacks.definitions[inst.padstack_def.name] + .pad_by_layer[reference_layer] + .parameters_values + ) + else: + pad_diameter = GrpcValue(terminal_diameter).value + _temp_circle = cloned_edb.modeler.create_circle( + layer_name="ports", + x=inst.position[0], + y=inst.position[1], + radius=pad_diameter[0] / 2, + net_name=inst.net_name, + ) + if not _temp_circle: + self.logger.error( + f"Failed to create circle for terminal during create_model_for_arbitrary_wave_ports" + ) + cloned_edb.save_as(output_edb) + cloned_edb.close(terminate_rpc_session=False) + return True + + @property + def definitions(self): + """Definitions class.""" + from pyedb.grpc.database.definitions import Definitions + + return Definitions(self) + + @property + def workflow(self): + """Workflow class.""" + return Workflow(self) diff --git a/src/pyedb/grpc/edb_init.py b/src/pyedb/grpc/edb_init.py new file mode 100644 index 0000000000..1e80b18c9c --- /dev/null +++ b/src/pyedb/grpc/edb_init.py @@ -0,0 +1,479 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +"""Database.""" +import os +import sys + +import ansys.edb.core.database as database + +from pyedb import __version__ +from pyedb.edb_logger import pyedb_logger +from pyedb.generic.general_methods import env_path, env_value, is_linux +from pyedb.grpc.rpc_session import RpcSession +from pyedb.misc.misc import list_installed_ansysem + + +class EdbInit(object): + """Edb Dot Net Class.""" + + def __init__(self, edbversion): + self.logger = pyedb_logger + self._db = None + if not edbversion: # pragma: no cover + try: + edbversion = "20{}.{}".format(list_installed_ansysem()[0][-3:-1], list_installed_ansysem()[0][-1:]) + self.logger.info("Edb version " + edbversion) + except IndexError: + raise Exception("No ANSYSEM_ROOTxxx is found.") + self.edbversion = edbversion + self.logger.info("Logger is initialized in EDB.") + self.logger.info("legacy v%s", __version__) + self.logger.info("Python version %s", sys.version) + self.session = None + if is_linux: + if env_value(self.edbversion) in os.environ: + self.base_path = env_path(self.edbversion) + sys.path.append(self.base_path) + else: + edb_path = os.getenv("PYAEDT_SERVER_AEDT_PATH") + if edb_path: + self.base_path = edb_path + sys.path.append(edb_path) + os.environ[env_value(self.edbversion)] = self.base_path + else: + self.base_path = env_path(self.edbversion) + sys.path.append(self.base_path) + os.environ["ECAD_TRANSLATORS_INSTALL_DIR"] = self.base_path + oa_directory = os.path.join(self.base_path, "common", "oa") + os.environ["ANSYS_OADIR"] = oa_directory + os.environ["PATH"] = "{};{}".format(os.environ["PATH"], self.base_path) + + @property + def db(self): + """Active database object.""" + return self._db + + def create(self, db_path, port=0, restart_rpc_server=False, kill_all_instances=False): + """Create a Database at the specified file location. + + Parameters + ---------- + db_path : str + Path to top-level database folder + + restart_rpc_server : optional, bool + Force restarting RPC server when `True`.Default value is `False` + + kill_all_instances : optional, bool. + Force killing all RPC server instances, must be used with caution. Default value is `False`. + + Returns + ------- + Database + """ + if not RpcSession.pid: + RpcSession.start( + edb_version=self.edbversion, + port=port, + restart_server=restart_rpc_server, + kill_all_instances=kill_all_instances, + ) + if not RpcSession.pid: + self.logger.error("Failed to start RPC server.") + return False + self._db = database.Database.create(db_path) + return self._db + + def open(self, db_path, read_only, port=0, restart_rpc_server=False, kill_all_instances=False): + """Open an existing Database at the specified file location. + + Parameters + ---------- + db_path : str + Path to top-level Database folder. + read_only : bool + Obtain read-only access. + port : optional, int. + Specify the port number.If not provided a randon free one is selected. Default value is `0`. + restart_rpc_server : optional, bool + Force restarting RPC server when `True`. Default value is `False`. + kill_all_instances : optional, bool. + Force killing all RPC server instances, must be used with caution. Default value is `False`. + + Returns + ------- + Database or None + The opened Database object, or None if not found. + """ + if restart_rpc_server: + RpcSession.pid = 0 + if not RpcSession.pid: + RpcSession.start( + edb_version=self.edbversion, + port=port, + restart_server=restart_rpc_server, + kill_all_instances=kill_all_instances, + ) + if not RpcSession.pid: + self.logger.error("Failed to start RPC server.") + return False + self._db = database.Database.open(db_path, read_only) + + def delete(self, db_path): + """Delete a database at the specified file location. + + Parameters + ---------- + db_path : str + Path to top-level database folder. + """ + return database.Database.delete(db_path) + + def save(self): + """Save any changes into a file.""" + return self._db.save() + + def close(self, terminate_rpc_session=True): + """Close the database. + + Parameters + ---------- + terminate_rpc_session : bool, optional + + + . note:: + Unsaved changes will be lost. + """ + self._db.close() + self._db = None + if terminate_rpc_session: + RpcSession.rpc_session.disconnect() + RpcSession.pid = 0 + return True + + @property + def top_circuit_cells(self): + """Get top circuit cells. + + Returns + ------- + list[:class:`Cell `] + """ + return [i for i in self._db.top_circuit_cells] + + @property + def circuit_cells(self): + """Get all circuit cells in the Database. + + Returns + ------- + list[:class:`Cell `] + """ + return [i for i in self._db.circuit_cells] + + @property + def footprint_cells(self): + """Get all footprint cells in the Database. + + Returns + ------- + list[:class:`Cell `] + """ + return [i for i in self._db.footprint_cells] + + @property + def edb_uid(self): + """Get ID of the database. + + Returns + ------- + int + The unique EDB id of the Database. + """ + return self._db.id + + @property + def is_read_only(self): + """Determine if the database is open in a read-only mode. + + Returns + ------- + bool + True if Database is open with read only access, otherwise False. + """ + return self._db.is_read_only + + def find_by_id(self, db_id): + """Find a database by ID. + + Parameters + ---------- + db_id : int + The Database's unique EDB id. + + Returns + ------- + Database + The Database or Null on failure. + """ + return database.Database.find_by_id(db_id) + + def save_as(self, path, version=""): + """Save this Database to a new location and older EDB version. + + Parameters + ---------- + path : str + New Database file location. + version : str + EDB version to save to. Empty string means current version. + """ + self._db.save_as(path, version) + + @property + def directory(self): + """Get the directory of the Database. + + Returns + ------- + str + Directory of the Database. + """ + return self._db.directory + + def get_product_property(self, prod_id, attr_it): + """Get the product-specific property value. + + Parameters + ---------- + prod_id : ProductIdType + Product ID. + attr_it : int + Attribute ID. + + Returns + ------- + str + Property value returned. + """ + return self._db.get_product_property(prod_id, attr_it) + + def set_product_property(self, prod_id, attr_it, prop_value): + """Set the product property associated with the given product and attribute ids. + + Parameters + ---------- + prod_id : ProductIdType + Product ID. + attr_it : int + Attribute ID. + prop_value : str + Product property's new value + """ + self._db.set_product_property(prod_id, attr_it, prop_value) + + def get_product_property_ids(self, prod_id): + """Get a list of attribute ids corresponding to a product property id. + + Parameters + ---------- + prod_id : ProductIdType + Product ID. + + Returns + ------- + list[int] + The attribute ids associated with this product property. + """ + return self._db.get_product_property_ids(prod_id) + + def import_material_from_control_file(self, control_file, schema_dir=None, append=True): + """Import materials from the provided control file. + + Parameters + ---------- + control_file : str + Control file name with full path. + schema_dir : str + Schema file path. + append : bool + True if the existing materials in Database are kept. False to remove existing materials in database. + """ + self._db.import_material_from_control_file(control_file, schema_dir, append) + + @property + def version(self): + """Get version of the Database. + + Returns + ------- + tuple(int, int) + A tuple of the version numbers [major, minor] + """ + major, minor = self._db.version + return major, minor + + def scale(self, scale_factor): + """Uniformly scale all geometry and their locations by a positive factor. + + Parameters + ---------- + scale_factor : float + Amount that coordinates are multiplied by. + """ + return self._db.scale(scale_factor) + + @property + def source(self): + """Get source name for this Database. + + This attribute is also used to set the source name. + + Returns + ------- + str + name of the source + """ + return self._db.source + + @source.setter + def source(self, source): + """Set source name of the database.""" + self._db.source = source + + @property + def source_version(self): + """Get the source version for this Database. + + This attribute is also used to set the version. + + Returns + ------- + str + version string + + """ + return self._db.source_version + + @source_version.setter + def source_version(self, source_version): + """Set source version of the database.""" + self._db.source_version = source_version + + def copy_cells(self, cells_to_copy): + """Copy Cells from other Databases or this Database into this Database. + + Parameters + ---------- + cells_to_copy : list[:class:`Cell `] + Cells to copy. + + Returns + ------- + list[:class:`Cell `] + New Cells created in this Database. + """ + if not isinstance(cells_to_copy, list): + cells_to_copy = [cells_to_copy] + return self._db.copy_cells(cells_to_copy) + + @property + def apd_bondwire_defs(self): + """Get all APD bondwire definitions in this Database. + + Returns + ------- + list[:class:`ApdBondwireDef `] + """ + return list(self._db.apd_bondwire_defs) + + @property + def jedec4_bondwire_defs(self): + """Get all JEDEC4 bondwire definitions in this Database. + + Returns + ------- + list[:class:`Jedec4BondwireDef `] + """ + return list(self._db.jedec4_bondwire_defs) + + @property + def jedec5_bondwire_defs(self): + """Get all JEDEC5 bondwire definitions in this Database. + + Returns + ------- + list[:class:`Jedec5BondwireDef `] + """ + return list(self._db.jedec5_bondwire_defs) + + @property + def padstack_defs(self): + """Get all Padstack definitions in this Database. + + Returns + ------- + list[:class:`PadstackDef `] + """ + return list(self._db.padstack_defs) + + @property + def package_defs(self): + """Get all Package definitions in this Database. + + Returns + ------- + list[:class:`PackageDef `] + """ + return list(self._db.package_defs) + + @property + def component_defs(self): + """Get all component definitions in the database. + + Returns + ------- + list[:class:`ComponentDef `] + """ + return list(self._db.component_defs) + + @property + def material_defs(self): + """Get all material definitions in the database. + + Returns + ------- + list[:class:`MaterialDef `] + """ + return list(self._db.material_defs) + + @property + def dataset_defs(self): + """Get all dataset definitions in the database. + + Returns + ------- + list[:class:`DatasetDef `] + """ + return list(self._db.dataset_defs) diff --git a/src/pyedb/grpc/rpc_session.py b/src/pyedb/grpc/rpc_session.py new file mode 100644 index 0000000000..4e9a637361 --- /dev/null +++ b/src/pyedb/grpc/rpc_session.py @@ -0,0 +1,165 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +from random import randint +import sys +import time + +from ansys.edb.core.session import launch_session +import psutil + +from pyedb import __version__ +from pyedb.edb_logger import pyedb_logger +from pyedb.generic.general_methods import env_path, env_value, is_linux +from pyedb.misc.misc import list_installed_ansysem + +latency_delay = 0.1 + + +class RpcSession: + """Static Class managing RPC server.""" + + pid = 0 + rpc_session = None + base_path = None + port = 10000 + + @staticmethod + def start(edb_version, port=0, restart_server=False, kill_all_instances=False): + """Start RPC-server, the server must be started before opening EDB. + + Parameters + ---------- + edb_version : str, optional. + Specify the Ansys version. If None, the latest installation will be detected on the local machine. + + port : int + Port number used for the RPC session. + + restart_server : bool, optional. + Force restarting the RPC server by killing the process in case EDB_RPC is already started. All open EDB + connection will be lost. This option must be used at the beginning of an application only to ensure the + server is properly started. + kill_all_instances : bool, optional. + Force killing all RPC sever instances, including zombie process. To be used with caution, default value is + `False`. + """ + if not port: + RpcSession.port = RpcSession.get_random_free_port() + else: + RpcSession.port = port + if not edb_version: # pragma: no cover + try: + edb_version = "20{}.{}".format(list_installed_ansysem()[0][-3:-1], list_installed_ansysem()[0][-1:]) + pyedb_logger.info("Edb version " + edb_version) + except IndexError: + raise Exception("No ANSYSEM_ROOTxxx is found.") + pyedb_logger.info("Logger is initialized in EDB.") + pyedb_logger.info("legacy v%s", __version__) + pyedb_logger.info("Python version %s", sys.version) + if is_linux: + if env_value(edb_version) in os.environ: + RpcSession.base_path = env_path(edb_version) + sys.path.append(RpcSession.base_path) + else: + edb_path = os.getenv("PYAEDT_SERVER_AEDT_PATH") + if edb_path: + RpcSession.base_path = edb_path + sys.path.append(edb_path) + os.environ[env_value(edb_version)] = RpcSession.base_path + else: + RpcSession.base_path = env_path(edb_version) + sys.path.append(RpcSession.base_path) + os.environ["ECAD_TRANSLATORS_INSTALL_DIR"] = RpcSession.base_path + oa_directory = os.path.join(RpcSession.base_path, "common", "oa") + os.environ["ANSYS_OADIR"] = oa_directory + os.environ["PATH"] = "{};{}".format(os.environ["PATH"], RpcSession.base_path) + + if RpcSession.pid: + if restart_server: + pyedb_logger.logger.info("Restarting RPC server") + if kill_all_instances: + RpcSession.__kill_all_instances() + else: + RpcSession.__kill() + RpcSession.__start_rpc_server() + else: + pyedb_logger.info(f"Server already running on port {RpcSession.port}") + else: + RpcSession.__start_rpc_server() + if RpcSession.rpc_session: + RpcSession.server_pid = RpcSession.rpc_session.local_server_proc.pid + pyedb_logger.info(f"Grpc session started: pid={RpcSession.server_pid}") + else: + pyedb_logger.error("Failed to start EDB_RPC_server process") + + @staticmethod + def __get_process_id(): + proc = [p for p in list(psutil.process_iter()) if "edb_rpc" in p.name().lower()] + time.sleep(latency_delay) + if proc: + RpcSession.pid = proc[-1].pid + else: + RpcSession.pid = 0 + + @staticmethod + def __start_rpc_server(): + RpcSession.rpc_session = launch_session(RpcSession.base_path, port_num=RpcSession.port) + time.sleep(latency_delay) + if RpcSession.rpc_session: + RpcSession.pid = RpcSession.rpc_session.local_server_proc.pid + pyedb_logger.logger.info("Grpc session started") + + @staticmethod + def __kill(): + p = psutil.Process(RpcSession.pid) + time.sleep(latency_delay) + p.terminate() + + @staticmethod + def __kill_all_instances(): + proc = [p.pid for p in list(psutil.process_iter()) if "edb_rpc" in p.name().lower()] + time.sleep(latency_delay) + for pid in proc: + p = psutil.Process(pid) + p.terminate() + + @staticmethod + def close(): + """Terminate the current RPC session. Must be executed at the end of the script to close properly the session. + If not executed, users should force restarting the process using the flag `restart_server`=`True`. + """ + if RpcSession.rpc_session: + RpcSession.rpc_session.disconnect() + time.sleep(latency_delay) + + @staticmethod + def get_random_free_port(): + port = randint(49152, 65535) + while True: + used_ports = [conn.laddr[1] for conn in psutil.net_connections()] + if port in used_ports: + port = randint(49152, 65535) + else: + break + return port diff --git a/src/pyedb/ipc2581/ecad/cad_data/assembly_drawing.py b/src/pyedb/ipc2581/ecad/cad_data/assembly_drawing.py index a10a308583..910734ac85 100644 --- a/src/pyedb/ipc2581/ecad/cad_data/assembly_drawing.py +++ b/src/pyedb/ipc2581/ecad/cad_data/assembly_drawing.py @@ -27,9 +27,10 @@ class AssemblyDrawing(object): """Class describing an IPC2581 assembly drawing.""" - def __init__(self, ipc): + def __init__(self, ipc, pedb): self._ipc = ipc - self.polygon = Polygon(self._ipc) + self._pedb = pedb + self.polygon = Polygon(self._ipc, pedb) self.line_ref = "" def write_xml(self, package): # pragma no cover diff --git a/src/pyedb/ipc2581/ecad/cad_data/feature.py b/src/pyedb/ipc2581/ecad/cad_data/feature.py index 6851517fcd..001f19a033 100644 --- a/src/pyedb/ipc2581/ecad/cad_data/feature.py +++ b/src/pyedb/ipc2581/ecad/cad_data/feature.py @@ -30,15 +30,16 @@ class Feature(object): """Class describing IPC2581 features.""" - def __init__(self, ipc): + def __init__(self, ipc, pedb): self._ipc = ipc + self._pedb = pedb self.feature_type = FeatureType().Polygon self.net = "" self.x = 0.0 self.y = 0.0 - self.polygon = Polygon(self._ipc) + self.polygon = Polygon(self._ipc, self._pedb) self._cutouts = [] - self.path = Path(self._ipc) + self.path = Path(self._ipc, pedb) # self.pad = PadstackDef() self.padstack_instance = PadstackInstance() self.drill = Drill() diff --git a/src/pyedb/ipc2581/ecad/cad_data/layer_feature.py b/src/pyedb/ipc2581/ecad/cad_data/layer_feature.py index 7885699341..effe4dbf69 100644 --- a/src/pyedb/ipc2581/ecad/cad_data/layer_feature.py +++ b/src/pyedb/ipc2581/ecad/cad_data/layer_feature.py @@ -29,8 +29,9 @@ class LayerFeature(object): """Class describing IPC2581 layer feature.""" - def __init__(self, ipc): + def __init__(self, ipc, pedb): self._ipc = ipc + self._pedb = pedb self.layer_name = "" self.color = "" self._features = [] @@ -48,12 +49,15 @@ def features(self, value): # pragma no cover def add_feature(self, obj_instance=None): # pragma no cover if obj_instance: - feature = Feature(self._ipc) - feature.net = obj_instance.net_name - if obj_instance.type == "Polygon": + feature = Feature(self._ipc, self._pedb) + if obj_instance.net_name: + feature.net = obj_instance.net_name + else: + feature.net = "" + if obj_instance.type.lower() == "polygon": feature.feature_type = FeatureType.Polygon feature.polygon.add_poly_step(obj_instance) - elif obj_instance.type == "Path": + elif obj_instance.type.lower() == "path": feature.feature_type = FeatureType.Path feature.path.add_path_step(obj_instance) self.features.append(feature) @@ -62,7 +66,7 @@ def add_feature(self, obj_instance=None): # pragma no cover def add_via_instance_feature(self, padstack_inst=None, padstackdef=None, layer_name=None): # pragma no cover if padstack_inst and padstackdef: - feature = Feature(self._ipc) + feature = Feature(self._ipc, self._pedb) def_name = padstackdef.name position = padstack_inst.position if padstack_inst.position else padstack_inst.position feature.padstack_instance.net = padstack_inst.net_name @@ -71,11 +75,14 @@ def add_via_instance_feature(self, padstack_inst=None, padstackdef=None, layer_n feature.feature_type = FeatureType.PadstackInstance feature.padstack_instance.x = self._ipc.from_meter_to_units(position[0], self._ipc.units) feature.padstack_instance.y = self._ipc.from_meter_to_units(position[1], self._ipc.units) - if padstackdef._hole_params is None: - hole_props = [i.ToDouble() for i in padstackdef.hole_params[2]] + if not self._pedb.grpc: + if padstackdef._hole_params is None: + hole_props = [i.ToDouble() for i in padstackdef.hole_params[2]] + else: + hole_props = [i.ToDouble() for i in padstackdef._hole_params[2]] + feature.padstack_instance.diameter = float(hole_props[0]) if hole_props else 0 else: - hole_props = [i.ToDouble() for i in padstackdef._hole_params[2]] - feature.padstack_instance.diameter = float(hole_props[0]) if hole_props else 0 + feature.padstack_instance.diameter = padstackdef.hole_diameter feature.padstack_instance.hole_name = def_name feature.padstack_instance.name = padstack_inst.name try: @@ -96,7 +103,7 @@ def add_via_instance_feature(self, padstack_inst=None, padstackdef=None, layer_n pass def add_drill_feature(self, via, diameter=0.0): # pragma no cover - feature = Feature(self._ipc) + feature = Feature(self._ipc, self._pedb) feature.feature_type = FeatureType.Drill feature.drill.net = via.net_name position = via._position if via._position else via.position @@ -113,14 +120,19 @@ def add_component_padstack_instance_feature( is_via = False if not pin.start_layer == pin.stop_layer: is_via = True - pin_net = pin._edb_object.GetNet().GetName() - pos_rot = pin._edb_padstackinstance.GetPositionAndRotationValue() - pin_rotation = pos_rot[2].ToDouble() - if pin._edb_padstackinstance.IsLayoutPin(): - out2 = pin._edb_padstackinstance.GetComponent().GetTransform().TransformPoint(pos_rot[1]) - pin_position = [out2.X.ToDouble(), out2.Y.ToDouble()] + if not self._pedb.grpc: + pin_net = pin._edb_object.GetNet().GetName() + pos_rot = pin._edb_padstackinstance.GetPositionAndRotationValue() + pin_rotation = pos_rot[2].ToDouble() + if pin._edb_padstackinstance.IsLayoutPin(): + out2 = pin._edb_padstackinstance.GetComponent().GetTransform().TransformPoint(pos_rot[1]) + pin_position = [out2.X.ToDouble(), out2.Y.ToDouble()] + else: + pin_position = [pos_rot[1].X.ToDouble(), pos_rot[1].Y.ToDouble()] else: - pin_position = [pos_rot[1].X.ToDouble(), pos_rot[1].Y.ToDouble()] + pin_net = pin.net_name + pin_rotation = pin.rotation + pin_position = pin.position pin_x = self._ipc.from_meter_to_units(pin_position[0], self._ipc.units) pin_y = self._ipc.from_meter_to_units(pin_position[1], self._ipc.units) cmp_rot_deg = component.rotation * 180 / math.pi @@ -131,11 +143,11 @@ def add_component_padstack_instance_feature( comp_placement_layer = component.placement_layer if comp_placement_layer == top_bottom_layers[-1]: mirror = True - feature = Feature(self._ipc) + feature = Feature(self._ipc, self._pedb) feature.feature_type = FeatureType.PadstackInstance feature.net = pin_net feature.padstack_instance.net = pin_net - feature.padstack_instance.pin = pin.pin.GetName() + feature.padstack_instance.pin = pin.name feature.padstack_instance.x = pin_x feature.padstack_instance.y = pin_y feature.padstack_instance.rotation = rotation diff --git a/src/pyedb/ipc2581/ecad/cad_data/outline.py b/src/pyedb/ipc2581/ecad/cad_data/outline.py index 5c3e351705..a19b471692 100644 --- a/src/pyedb/ipc2581/ecad/cad_data/outline.py +++ b/src/pyedb/ipc2581/ecad/cad_data/outline.py @@ -27,9 +27,10 @@ class Outline: """Class describing an IPC2581 outline.""" - def __init__(self, ipc): + def __init__(self, ipc, pedb): self._ipc = ipc - self.polygon = Polygon(self._ipc) + self._pedb = pedb + self.polygon = Polygon(self._ipc, pedb) self.line_ref = "" def write_xml(self, package): # pragma no cover diff --git a/src/pyedb/ipc2581/ecad/cad_data/package.py b/src/pyedb/ipc2581/ecad/cad_data/package.py index 1f8396e39a..b0275975ae 100644 --- a/src/pyedb/ipc2581/ecad/cad_data/package.py +++ b/src/pyedb/ipc2581/ecad/cad_data/package.py @@ -33,15 +33,16 @@ class Package(object): """Class describing an IPC2581 package definition.""" - def __init__(self, ipc): + def __init__(self, ipc, pedb): self._ipc = ipc + self._pedb = pedb self.name = "" self.type = "OTHER" self.pin_one = "1" self.pin_orientation = "OTHER" self.height = 0.1 - self.assembly_drawing = AssemblyDrawing(self._ipc) - self.outline = Outline(self._ipc) + self.assembly_drawing = AssemblyDrawing(self._ipc, pedb) + self.outline = Outline(self._ipc, pedb) self._pins = [] self.pickup_point = [0.0, 0.0] diff --git a/src/pyedb/ipc2581/ecad/cad_data/path.py b/src/pyedb/ipc2581/ecad/cad_data/path.py index 5cd7b4f9ce..68a61e6f4a 100644 --- a/src/pyedb/ipc2581/ecad/cad_data/path.py +++ b/src/pyedb/ipc2581/ecad/cad_data/path.py @@ -28,8 +28,9 @@ class Path(object): """Class describing an IPC2581 trace.""" - def __init__(self, ipc): + def __init__(self, ipc, pedb): self._ipc = ipc + self._pedb = pedb self.location_x = 0.0 self.location_y = 0.0 self.poly_steps = [] @@ -37,38 +38,22 @@ def __init__(self, ipc): self.width_ref_id = "" def add_path_step(self, path_step=None): # pragma no cover - arcs = path_step.primitive_object.GetCenterLine().GetArcData() - if not arcs: - return - self.line_width = self._ipc.from_meter_to_units(path_step.primitive_object.GetWidth(), self._ipc.units) - self.width_ref_id = "ROUND_{}".format(self.line_width) - if not self.width_ref_id in self._ipc.content.dict_line.dict_lines: - entry_line = EntryLine() - entry_line.line_width = self.line_width - self._ipc.content.dict_line.dict_lines[self.width_ref_id] = entry_line - # first point - arc = arcs[0] - new_segment_tep = PolyStep() - new_segment_tep.x = arc.Start.X.ToDouble() - new_segment_tep.y = arc.Start.Y.ToDouble() - self.poly_steps.append(new_segment_tep) - if arc.Height == 0: + if not self._pedb.grpc: + arcs = path_step.primitive_object.GetCenterLine().GetArcData() + if not arcs: + return + self.line_width = self._ipc.from_meter_to_units(path_step.primitive_object.GetWidth(), self._ipc.units) + self.width_ref_id = "ROUND_{}".format(self.line_width) + if not self.width_ref_id in self._ipc.content.dict_line.dict_lines: + entry_line = EntryLine() + entry_line.line_width = self.line_width + self._ipc.content.dict_line.dict_lines[self.width_ref_id] = entry_line + # first point + arc = arcs[0] new_segment_tep = PolyStep() - new_segment_tep.poly_type = PolyType.Segment - new_segment_tep.x = arc.End.X.ToDouble() - new_segment_tep.y = arc.End.Y.ToDouble() + new_segment_tep.x = arc.Start.X.ToDouble() + new_segment_tep.y = arc.Start.Y.ToDouble() self.poly_steps.append(new_segment_tep) - else: - arc_center = arc.GetCenter() - new_poly_step = PolyStep() - new_poly_step.poly_type = PolyType.Curve - new_poly_step.center_X = arc_center.X.ToDouble() - new_poly_step.center_y = arc_center.Y.ToDouble() - new_poly_step.x = arc.End.X.ToDouble() - new_poly_step.y = arc.End.Y.ToDouble() - new_poly_step.clock_wise = not arc.IsCCW() - self.poly_steps.append(new_poly_step) - for arc in list(arcs)[1:]: if arc.Height == 0: new_segment_tep = PolyStep() new_segment_tep.poly_type = PolyType.Segment @@ -85,6 +70,72 @@ def add_path_step(self, path_step=None): # pragma no cover new_poly_step.y = arc.End.Y.ToDouble() new_poly_step.clock_wise = not arc.IsCCW() self.poly_steps.append(new_poly_step) + for arc in list(arcs)[1:]: + if arc.Height == 0: + new_segment_tep = PolyStep() + new_segment_tep.poly_type = PolyType.Segment + new_segment_tep.x = arc.End.X.ToDouble() + new_segment_tep.y = arc.End.Y.ToDouble() + self.poly_steps.append(new_segment_tep) + else: + arc_center = arc.GetCenter() + new_poly_step = PolyStep() + new_poly_step.poly_type = PolyType.Curve + new_poly_step.center_X = arc_center.X.ToDouble() + new_poly_step.center_y = arc_center.Y.ToDouble() + new_poly_step.x = arc.End.X.ToDouble() + new_poly_step.y = arc.End.Y.ToDouble() + new_poly_step.clock_wise = not arc.IsCCW() + self.poly_steps.append(new_poly_step) + else: + arcs = path_step.cast().center_line.arc_data + if not arcs: + return + self.line_width = self._ipc.from_meter_to_units(path_step.cast().width.value, self._ipc.units) + self.width_ref_id = "ROUND_{}".format(self.line_width) + if not self.width_ref_id in self._ipc.content.dict_line.dict_lines: + entry_line = EntryLine() + entry_line.line_width = self.line_width + self._ipc.content.dict_line.dict_lines[self.width_ref_id] = entry_line + # first point + arc = arcs[0] + new_segment_tep = PolyStep() + new_segment_tep.x = arc.start.x.value + new_segment_tep.y = arc.start.y.value + self.poly_steps.append(new_segment_tep) + if arc.height == 0: + new_segment_tep = PolyStep() + new_segment_tep.poly_type = PolyType.Segment + new_segment_tep.x = arc.end.x.value + new_segment_tep.y = arc.end.y.value + self.poly_steps.append(new_segment_tep) + else: + arc_center = arc.center + new_poly_step = PolyStep() + new_poly_step.poly_type = PolyType.Curve + new_poly_step.center_X = arc_center.x.value + new_poly_step.center_y = arc_center.y.value + new_poly_step.x = arc.end.x.value + new_poly_step.y = arc.end.y.value + new_poly_step.clock_wise = not arc.is_ccw + self.poly_steps.append(new_poly_step) + for arc in list(arcs)[1:]: + if arc.height == 0: + new_segment_tep = PolyStep() + new_segment_tep.poly_type = PolyType.Segment + new_segment_tep.x = arc.end.x.value + new_segment_tep.y = arc.end.y.value + self.poly_steps.append(new_segment_tep) + else: + arc_center = arc.center + new_poly_step = PolyStep() + new_poly_step.poly_type = PolyType.Curve + new_poly_step.center_X = arc_center.x.value + new_poly_step.center_y = arc_center.y.value + new_poly_step.x = arc.end.x.value + new_poly_step.y = arc.end.y.value + new_poly_step.clock_wise = not arc.is_ccw + self.poly_steps.append(new_poly_step) def write_xml(self, net_root): # pragma no cover if not self.poly_steps: diff --git a/src/pyedb/ipc2581/ecad/cad_data/polygon.py b/src/pyedb/ipc2581/ecad/cad_data/polygon.py index 92a1e9659a..07689bb25a 100644 --- a/src/pyedb/ipc2581/ecad/cad_data/polygon.py +++ b/src/pyedb/ipc2581/ecad/cad_data/polygon.py @@ -26,8 +26,9 @@ class Polygon(object): - def __init__(self, ipc): + def __init__(self, ipc, pedb): self._ipc = ipc + self._pedb = pedb self.is_void = False self.poly_steps = [] self.solid_fill_id = "" @@ -35,65 +36,126 @@ def __init__(self, ipc): def add_poly_step(self, polygon=None): # pragma no cover if polygon: - polygon_data = polygon._edb_object.GetPolygonData() - if polygon_data.IsClosed(): - arcs = polygon_data.GetArcData() - if not arcs: - return - # begin - new_segment_tep = PolyStep() - new_segment_tep.poly_type = PolyType.Segment - new_segment_tep.x = arcs[0].Start.X.ToDouble() - new_segment_tep.y = arcs[0].Start.Y.ToDouble() - self.poly_steps.append(new_segment_tep) - for arc in arcs: - if arc.Height == 0: - new_segment_tep = PolyStep() - new_segment_tep.poly_type = PolyType.Segment - new_segment_tep.x = arc.End.X.ToDouble() - new_segment_tep.y = arc.End.Y.ToDouble() - self.poly_steps.append(new_segment_tep) - else: - arc_center = arc.GetCenter() - new_poly_step = PolyStep() - new_poly_step.poly_type = PolyType.Curve - new_poly_step.center_X = arc_center.X.ToDouble() - new_poly_step.center_y = arc_center.Y.ToDouble() - new_poly_step.x = arc.End.X.ToDouble() - new_poly_step.y = arc.End.Y.ToDouble() - new_poly_step.clock_wise = not arc.IsCCW() - self.poly_steps.append(new_poly_step) - for void in polygon.voids: - void_polygon_data = void._edb_object.GetPolygonData() - if void_polygon_data.IsClosed(): - void_arcs = void_polygon_data.GetArcData() - if not void_arcs: - return - void_polygon = Cutout(self._ipc) - self.cutout.append(void_polygon) - # begin - new_segment_tep = PolyStep() - new_segment_tep.poly_type = PolyType.Segment - new_segment_tep.x = void_arcs[0].Start.X.ToDouble() - new_segment_tep.y = void_arcs[0].Start.Y.ToDouble() - void_polygon.poly_steps.append(new_segment_tep) - for void_arc in void_arcs: - if void_arc.Height == 0: - new_segment_tep = PolyStep() - new_segment_tep.poly_type = PolyType.Segment - new_segment_tep.x = void_arc.End.X.ToDouble() - new_segment_tep.y = void_arc.End.Y.ToDouble() - void_polygon.poly_steps.append(new_segment_tep) - else: - arc_center = void_arc.GetCenter() - new_poly_step = PolyStep() - new_poly_step.poly_type = PolyType.Curve - new_poly_step.center_X = arc_center.X.ToDouble() - new_poly_step.center_y = arc_center.Y.ToDouble() - new_poly_step.x = void_arc.End.X.ToDouble() - new_poly_step.y = void_arc.End.Y.ToDouble() - new_poly_step.clock_wise = not void_arc.IsCCW() - void_polygon.poly_steps.append(new_poly_step) + if not self._pedb.grpc: + polygon_data = polygon._edb_object.GetPolygonData() + if polygon_data.IsClosed(): + arcs = polygon_data.GetArcData() + if not arcs: + return + # begin + new_segment_tep = PolyStep() + new_segment_tep.poly_type = PolyType.Segment + new_segment_tep.x = arcs[0].Start.X.ToDouble() + new_segment_tep.y = arcs[0].Start.Y.ToDouble() + self.poly_steps.append(new_segment_tep) + for arc in arcs: + if arc.Height == 0: + new_segment_tep = PolyStep() + new_segment_tep.poly_type = PolyType.Segment + new_segment_tep.x = arc.End.X.ToDouble() + new_segment_tep.y = arc.End.Y.ToDouble() + self.poly_steps.append(new_segment_tep) + else: + arc_center = arc.GetCenter() + new_poly_step = PolyStep() + new_poly_step.poly_type = PolyType.Curve + new_poly_step.center_X = arc_center.X.ToDouble() + new_poly_step.center_y = arc_center.Y.ToDouble() + new_poly_step.x = arc.End.X.ToDouble() + new_poly_step.y = arc.End.Y.ToDouble() + new_poly_step.clock_wise = not arc.IsCCW() + self.poly_steps.append(new_poly_step) + for void in polygon.voids: + void_polygon_data = void._edb_object.GetPolygonData() + if void_polygon_data.IsClosed(): + void_arcs = void_polygon_data.GetArcData() + if not void_arcs: + return + void_polygon = Cutout(self._ipc) + self.cutout.append(void_polygon) + # begin + new_segment_tep = PolyStep() + new_segment_tep.poly_type = PolyType.Segment + new_segment_tep.x = void_arcs[0].Start.X.ToDouble() + new_segment_tep.y = void_arcs[0].Start.Y.ToDouble() + void_polygon.poly_steps.append(new_segment_tep) + for void_arc in void_arcs: + if void_arc.Height == 0: + new_segment_tep = PolyStep() + new_segment_tep.poly_type = PolyType.Segment + new_segment_tep.x = void_arc.End.X.ToDouble() + new_segment_tep.y = void_arc.End.Y.ToDouble() + void_polygon.poly_steps.append(new_segment_tep) + else: + arc_center = void_arc.GetCenter() + new_poly_step = PolyStep() + new_poly_step.poly_type = PolyType.Curve + new_poly_step.center_X = arc_center.X.ToDouble() + new_poly_step.center_y = arc_center.Y.ToDouble() + new_poly_step.x = void_arc.End.X.ToDouble() + new_poly_step.y = void_arc.End.Y.ToDouble() + new_poly_step.clock_wise = not void_arc.IsCCW() + void_polygon.poly_steps.append(new_poly_step) + else: + polygon_data = polygon.polygon_data + if polygon_data.is_closed: + arcs = polygon_data.arc_data + if not arcs: + return + # begin + new_segment_tep = PolyStep() + new_segment_tep.poly_type = PolyType.Segment + new_segment_tep.x = arcs[0].start.x.value + new_segment_tep.y = arcs[0].start.y.value + self.poly_steps.append(new_segment_tep) + for arc in arcs: + if arc.height == 0: + new_segment_tep = PolyStep() + new_segment_tep.poly_type = PolyType.Segment + new_segment_tep.x = arc.end.x.value + new_segment_tep.y = arc.end.y.value + self.poly_steps.append(new_segment_tep) + else: + arc_center = arc.center + new_poly_step = PolyStep() + new_poly_step.poly_type = PolyType.Curve + new_poly_step.center_X = arc_center.x.value + new_poly_step.center_y = arc_center.y.value + new_poly_step.x = arc.end.x.value + new_poly_step.y = arc.end.y.value + new_poly_step.clock_wise = not arc.is_ccw + self.poly_steps.append(new_poly_step) + for void in polygon.voids: + void_polygon_data = void.polygon_data + if void_polygon_data.is_closed: + void_arcs = void_polygon_data.arc_data + if not void_arcs: + return + void_polygon = Cutout(self._ipc) + self.cutout.append(void_polygon) + # begin + new_segment_tep = PolyStep() + new_segment_tep.poly_type = PolyType.Segment + new_segment_tep.x = void_arcs[0].start.x.value + new_segment_tep.y = void_arcs[0].start.y.value + void_polygon.poly_steps.append(new_segment_tep) + for void_arc in void_arcs: + if void_arc.height == 0: + new_segment_tep = PolyStep() + new_segment_tep.poly_type = PolyType.Segment + new_segment_tep.x = void_arc.end.x.value + new_segment_tep.y = void_arc.end.y.value + void_polygon.poly_steps.append(new_segment_tep) + else: + arc_center = void_arc.center + new_poly_step = PolyStep() + new_poly_step.poly_type = PolyType.Curve + new_poly_step.center_X = arc_center.x.value + new_poly_step.center_y = arc_center.y.value + new_poly_step.x = void_arc.end.x.value + new_poly_step.y = void_arc.end.y.value + new_poly_step.clock_wise = not void_arc.is_ccw + void_polygon.poly_steps.append(new_poly_step) def add_cutout(self, cutout): # pragma no cover if not isinstance(cutout, Cutout): diff --git a/src/pyedb/ipc2581/ecad/cad_data/profile.py b/src/pyedb/ipc2581/ecad/cad_data/profile.py index 369c12f516..ed1a328d13 100644 --- a/src/pyedb/ipc2581/ecad/cad_data/profile.py +++ b/src/pyedb/ipc2581/ecad/cad_data/profile.py @@ -48,15 +48,16 @@ def xml_writer(self, step): # pragma no cover for poly in self.profile: for feature in poly.features: if feature.feature_type == 0: - polygon = ET.SubElement(profile, "Polygon") - polygon_begin = ET.SubElement(polygon, "PolyBegin") - polygon_begin.set( - "x", str(self._ipc.from_meter_to_units(feature.polygon.poly_steps[0].x, self._ipc.units)) - ) - polygon_begin.set( - "y", str(self._ipc.from_meter_to_units(feature.polygon.poly_steps[0].y, self._ipc.units)) - ) - for poly_step in feature.polygon.poly_steps[1:]: - poly_step.write_xml(polygon, self._ipc) - for cutout in feature.polygon.cutout: - cutout.write_xml(profile, self._ipc) + if feature.polygon.poly_steps: + polygon = ET.SubElement(profile, "Polygon") + polygon_begin = ET.SubElement(polygon, "PolyBegin") + polygon_begin.set( + "x", str(self._ipc.from_meter_to_units(feature.polygon.poly_steps[0].x, self._ipc.units)) + ) + polygon_begin.set( + "y", str(self._ipc.from_meter_to_units(feature.polygon.poly_steps[0].y, self._ipc.units)) + ) + for poly_step in feature.polygon.poly_steps[1:]: + poly_step.write_xml(polygon, self._ipc) + for cutout in feature.polygon.cutout: + cutout.write_xml(profile, self._ipc) diff --git a/src/pyedb/ipc2581/ecad/cad_data/step.py b/src/pyedb/ipc2581/ecad/cad_data/step.py index 1efa78d361..a3ecd12bf1 100644 --- a/src/pyedb/ipc2581/ecad/cad_data/step.py +++ b/src/pyedb/ipc2581/ecad/cad_data/step.py @@ -89,11 +89,21 @@ def add_logical_net(self, net=None): # pragma no cover net_name = net.name logical_net = LogicalNet() logical_net.name = net_name - net_pins = list(net._edb_object.PadstackInstances) + if not self._pedb.grpc: + net_pins = list(net._edb_object.PadstackInstances) + else: + net_pins = net.padstack_instances for pin in net_pins: new_pin_ref = logical_net.get_pin_ref_def() - new_pin_ref.pin = pin.GetName() - new_pin_ref.component_ref = pin.GetComponent().GetName() + if not self._pedb.grpc: + new_pin_ref.pin = pin.GetName() + new_pin_ref.component_ref = pin.GetComponent().GetName() + else: + new_pin_ref.pin = pin.name + if pin.component: + new_pin_ref.component_ref = pin.component.name + else: + new_pin_ref.component_ref = "" logical_net.pin_ref.append(new_pin_ref) self.logical_nets.append(logical_net) @@ -139,16 +149,21 @@ def add_component(self, component=None): # pragma no cover # adding component add package in Step if component: if not component.part_name in self._packages: - package = Package(self._ipc) + package = Package(self._ipc, self._pedb) package.add_component_outline(component) package.name = component.part_name package.height = "" package.type = component.type pin_number = 0 for _, pin in component.pins.items(): - geometry_type, pad_parameters, pos_x, pos_y, rot = self._pedb.padstacks.get_pad_parameters( - pin._edb_padstackinstance, component.placement_layer, 0 - ) + if not self._pedb.grpc: + geometry_type, pad_parameters, pos_x, pos_y, rot = self._pedb.padstacks.get_pad_parameters( + pin._edb_padstackinstance, component.placement_layer, 0 + ) + else: + geometry_type, pad_parameters, pos_x, pos_y, rot = self._pedb.padstacks.get_pad_parameters( + pin, component.placement_layer, 0 + ) 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) @@ -173,9 +188,9 @@ def add_component(self, component=None): # pragma no cover ipc_component = Component() ipc_component.type = component.type try: - ipc_component.value = component.value + ipc_component.value = str(component.value) except: - pass + self._pedb.logger.error(f"IPC export, failed loading component {component.refdes} value.") ipc_component.refdes = component.refdes center = component.center ipc_component.location = [ @@ -193,8 +208,12 @@ def layer_ranges( stop_layer, ): # pragma no cover started = False - start_layer_name = start_layer.GetName() - stop_layer_name = stop_layer.GetName() + if not self._pedb.grpc: + start_layer_name = start_layer.GetName() + stop_layer_name = stop_layer.GetName() + else: + start_layer_name = start_layer.name + stop_layer_name = stop_layer.name layer_list = [] for layer_name in self._ipc.layers_name: if started: @@ -215,7 +234,7 @@ def layer_ranges( def add_layer_feature(self, layer, polys): # pragma no cover layer_name = layer.name - layer_feature = LayerFeature(self._ipc) + layer_feature = LayerFeature(self._ipc, self._pedb) layer_feature.layer_name = layer_name layer_feature.color = layer.color @@ -225,7 +244,7 @@ def add_layer_feature(self, layer, polys): # pragma no cover self._ipc.ecad.cad_data.cad_data_step.layer_features.append(layer_feature) def add_profile(self, poly): # pragma no cover - profile = LayerFeature(self._ipc) + profile = LayerFeature(self._ipc, self._pedb) profile.layer_name = "profile" if poly: if not poly.is_void: @@ -235,14 +254,18 @@ def add_profile(self, poly): # pragma no cover def add_padstack_instances(self, padstack_instances, padstack_defs): # pragma no cover top_bottom_layers = self._ipc.top_bottom_layers layers = {j.layer_name: j for j in self._ipc.ecad.cad_data.cad_data_step.layer_features} - + layer_colors = {i: j.color for i, j in self._ipc._pedb.stackup.layers.items()} for padstack_instance in padstack_instances: - _, start_layer, stop_layer = padstack_instance._edb_padstackinstance.GetLayerRange() + if not self._pedb.grpc: + _, start_layer, stop_layer = padstack_instance._edb_padstackinstance.GetLayerRange() + else: + start_layer, stop_layer = padstack_instance.get_layer_range() for layer_name in self.layer_ranges(start_layer, stop_layer): if layer_name not in layers: layer_feature = LayerFeature(self._ipc) layer_feature.layer_name = layer_name - layer_feature.color = self._ipc._pedb.stackup[layer_name].color + # layer_feature.color = self._ipc._pedb.stackup[layer_name].color + layer_feature.color = layer_colors[layer_name] self._ipc.ecad.cad_data.cad_data_step.layer_features.append(layer_feature) layers[layer_name] = self._ipc.ecad.cad_data.cad_data_step.layer_features[-1] pdef_name = ( @@ -250,7 +273,13 @@ def add_padstack_instances(self, padstack_instances, padstack_defs): # pragma n ) if pdef_name in padstack_defs: padstack_def = padstack_defs[pdef_name] - comp_name = padstack_instance._edb_object.GetComponent().GetName() + if not self._pedb.grpc: + comp_name = padstack_instance._edb_object.GetComponent().GetName() + else: + if padstack_instance.component: + comp_name = padstack_instance.component.name + else: + comp_name = "" if padstack_instance.is_pin and comp_name: component_inst = self._pedb.components.instances[comp_name] layers[layer_name].add_component_padstack_instance_feature( @@ -261,15 +290,18 @@ def add_padstack_instances(self, padstack_instances, padstack_defs): # pragma n def add_drill_layer_feature(self, via_list=None, layer_feature_name=""): # pragma no cover if via_list: - drill_layer_feature = LayerFeature(self._ipc) + drill_layer_feature = LayerFeature(self._ipc, self._pedb.grpc) drill_layer_feature.is_drill_feature = True drill_layer_feature.layer_name = layer_feature_name for via in via_list: try: - via_diameter = via.pin.GetPadstackDef().GetData().GetHoleParameters()[2][0] + if not self._pedb.grpc: + via_diameter = via.pin.GetPadstackDef().GetData().GetHoleParameters()[2][0] + else: + via_diameter = via.definition.hole_diameter drill_layer_feature.add_drill_feature(via, via_diameter) except: - pass + self._pedb.logger.warning(f"Failed adding ipc drill on via {via.name}") self.layer_features.append(drill_layer_feature) def write_xml(self, cad_data): # pragma no cover diff --git a/src/pyedb/ipc2581/ipc2581.py b/src/pyedb/ipc2581/ipc2581.py index 5b14411288..f59614ea7f 100644 --- a/src/pyedb/ipc2581/ipc2581.py +++ b/src/pyedb/ipc2581/ipc2581.py @@ -73,9 +73,14 @@ def add_pdstack_definition(self): padstack_def = PadstackDef() padstack_def.name = padstack_name padstack_def.padstack_hole_def.name = padstack_name - if padstackdef.hole_properties: + if not self._pedb.grpc: + if padstackdef.hole_properties: + padstack_def.padstack_hole_def.diameter = self.from_meter_to_units( + padstackdef.hole_properties[0], self.units + ) + else: padstack_def.padstack_hole_def.diameter = self.from_meter_to_units( - padstackdef.hole_properties[0], self.units + padstackdef.hole_diameter, self.units ) for layer, pad in padstackdef.pad_by_layer.items(): if pad.parameters_values: @@ -124,7 +129,7 @@ def add_pdstack_definition(self): primitive_ref = "Default" padstack_def.add_padstack_pad_def(layer=layer, pad_use="REGULAR", primitive_ref=primitive_ref) for layer, antipad in padstackdef.antipad_by_layer.items(): - if antipad.parameters_values: + if antipad: if antipad.geometry_type == 1: primitive_ref = "CIRCLE_{}".format( self.from_meter_to_units(antipad.parameters_values[0], self.units) @@ -169,7 +174,7 @@ def add_pdstack_definition(self): primitive_ref = "Default" padstack_def.add_padstack_pad_def(layer=layer, pad_use="ANTIPAD", primitive_ref=primitive_ref) for layer, thermalpad in padstackdef.thermalpad_by_layer.items(): - if thermalpad.parameters_values: + if thermalpad: if thermalpad.geometry_type == 1: primitive_ref = "CIRCLE_{}".format( self.from_meter_to_units(thermalpad.parameters_values[0], self.units) @@ -240,7 +245,10 @@ def add_bom(self): self.bom.bom_items.append(bom_item) def add_layers_info(self): - self.design_name = self._pedb.layout.cell.GetName() + if not self._pedb.grpc: + self.design_name = self._pedb.layout.cell.GetName() + else: + self.design_name = self._pedb.layout.cell.name self.ecad.design_name = self.design_name self.ecad.cad_header.units = self.units self.ecad.cad_data.stackup.total_thickness = self.from_meter_to_units( @@ -264,31 +272,20 @@ def add_layers_info(self): loss_tg = 0 embedded = "NOT_EMBEDDED" # try: - material_name = self._pedb.stackup.layers[layer_name]._edb_layer.GetMaterial() - edb_material = self._pedb.edb_api.definition.MaterialDef.FindByName(self._pedb.active_db, material_name) + material_name = self._pedb.stackup.layers[layer_name].material + material = self._pedb.materials[material_name] material_type = "CONDUCTOR" if self._pedb.stackup.layers[layer_name].type == "dielectric": layer_type = "DIELPREG" material_type = "DIELECTRIC" - - permitivity = edb_material.GetProperty(self._pedb.edb_api.definition.MaterialPropertyId.Permittivity)[1] - if not isinstance(permitivity, float): - permitivity = permitivity.ToDouble() - loss_tg = edb_material.GetProperty( - self._pedb.edb_api.definition.MaterialPropertyId.DielectricLossTangent - )[1] - if not isinstance(loss_tg, float): - loss_tg = loss_tg.ToDouble() + permitivity = material.permittivity + loss_tg = material.loss_tangent conductivity = 0 if layer_type == "CONDUCTOR": - conductivity = edb_material.GetProperty(self._pedb.edb_api.definition.MaterialPropertyId.Conductivity)[ - 1 - ] - if not isinstance(conductivity, float): - conductivity = conductivity.ToDouble() + conductivity = material.conductivity self.ecad.cad_header.add_spec( name=layer_name, - material=self._pedb.stackup.layers[layer_name]._edb_layer.GetMaterial(), + material=material_name, layer_type=material_type, conductivity=str(conductivity), dielectric_constant=str(permitivity), @@ -351,35 +348,36 @@ def add_drills(self): self.ecad.cad_data.cad_data_step.add_drill_layer_feature(via_list, "DRILL_1-{}".format(l1)) def from_meter_to_units(self, value, units): - if isinstance(value, str): - value = float(value) - if isinstance(value, list): - returned_list = [] - for val in value: - if isinstance(val, str): - val = float(val) - if units.lower() == "mm": - returned_list.append(round(val * 1000, 4)) - if units.lower() == "um": - returned_list.append(round(val * 1e6, 4)) + if value: + if isinstance(value, str): + value = float(value) + if isinstance(value, list): + returned_list = [] + for val in value: + if isinstance(val, str): + val = float(val) + if units.lower() == "mm": + returned_list.append(round(val * 1000, 4)) + if units.lower() == "um": + returned_list.append(round(val * 1e6, 4)) + if units.lower() == "mils": + returned_list.append(round(val * 39370.079, 4)) + if units.lower() == "inch": + returned_list.append(round(val * 39.370079, 4)) + if units.lower() == "cm": + returned_list.append(round(val * 100, 4)) + return returned_list + else: + if units.lower() == "millimeter": + return round(value * 1000, 4) + if units.lower() == "micrometer": + return round(value * 1e6, 4) if units.lower() == "mils": - returned_list.append(round(val * 39370.079, 4)) + return round(value * 39370.079, 4) if units.lower() == "inch": - returned_list.append(round(val * 39.370079, 4)) - if units.lower() == "cm": - returned_list.append(round(val * 100, 4)) - return returned_list - else: - if units.lower() == "millimeter": - return round(value * 1000, 4) - if units.lower() == "micrometer": - return round(value * 1e6, 4) - if units.lower() == "mils": - return round(value * 39370.079, 4) - if units.lower() == "inch": - return round(value * 39.370079, 4) - if units.lower() == "centimeter": - return round(value * 100, 4) + return round(value * 39.370079, 4) + if units.lower() == "centimeter": + return round(value * 100, 4) def write_xml(self): if self.file_path: diff --git a/src/pyedb/modeler/geometry_operators.py b/src/pyedb/modeler/geometry_operators.py index 68452df7c8..b68a47c273 100644 --- a/src/pyedb/modeler/geometry_operators.py +++ b/src/pyedb/modeler/geometry_operators.py @@ -48,7 +48,7 @@ def parse_dim_arg(string, scale_to_unit=None, variable_manager=None): # pragma: 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:`pyedb.dotnet.application.Variables.VariableManager`, optional + variable_manager : :class:`pyedb.dotnet.database.Variables.VariableManager`, optional Try to parse formula and returns numeric value. The default is ``None``. diff --git a/tests/conftest.py b/tests/conftest.py index 60609be985..0ddbf4fa2d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,7 +56,7 @@ local_path = os.path.dirname(os.path.realpath(__file__)) # Initialize default desktop configuration -desktop_version = "2024.2" +desktop_version = "2025.2" if "ANSYSEM_ROOT{}".format(desktop_version[2:].replace(".", "")) not in list_installed_ansysem(): desktop_version = list_installed_ansysem()[0][12:].replace(".", "") desktop_version = "20{}.{}".format(desktop_version[:2], desktop_version[-1]) diff --git a/tests/grpc/__init__.py b/tests/grpc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/grpc/integration/__init__.py b/tests/grpc/integration/__init__.py new file mode 100644 index 0000000000..ac2de6d4ad --- /dev/null +++ b/tests/grpc/integration/__init__.py @@ -0,0 +1,3 @@ +"""Tests related to the interaction of multiple classes +from PyEDB, e.g. Edb and Ipc2581, ... +""" diff --git a/tests/grpc/system/__init__.py b/tests/grpc/system/__init__.py new file mode 100644 index 0000000000..98b5beab7c --- /dev/null +++ b/tests/grpc/system/__init__.py @@ -0,0 +1,3 @@ +"""Tests related to testing the system as a whole, e.g. exporting +the data of an aedb file to ipc2581, ... +""" diff --git a/tests/grpc/system/conftest.py b/tests/grpc/system/conftest.py new file mode 100644 index 0000000000..4e75df181b --- /dev/null +++ b/tests/grpc/system/conftest.py @@ -0,0 +1,149 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +""" + +import os +from os.path import dirname + +import pytest + +from pyedb.grpc.edb import EdbGrpc as Edb +from pyedb.misc.misc import list_installed_ansysem +from tests.conftest import generate_random_string + +example_models_path = os.path.join(dirname(dirname(dirname(os.path.realpath(__file__)))), "example_models") + +# Initialize default desktop configuration +desktop_version = "2025.1" +if "ANSYSEM_ROOT{}".format(desktop_version[2:].replace(".", "")) not in list_installed_ansysem(): + desktop_version = list_installed_ansysem()[0][12:].replace(".", "") + desktop_version = "20{}.{}".format(desktop_version[:2], desktop_version[-1]) + +test_subfolder = "TEDB" +test_project_name = "ANSYS-HSD_V1" +bom_example = "bom_example.csv" + + +class EdbExamples: + def __init__(self, local_scratch): + self.local_scratch = local_scratch + self.example_models_path = example_models_path + self.test_folder = "" + + def get_local_file_folder(self, name): + return os.path.join(self.local_scratch.path, name) + + def _create_test_folder(self): + """Create a local folder under `local_scratch`.""" + self.test_folder = os.path.join(self.local_scratch.path, generate_random_string(6)) + return self.test_folder + + def _copy_file_folder_into_local_folder(self, file_folder_path): + src = os.path.join(self.example_models_path, file_folder_path) + local_folder = self._create_test_folder() + file_folder_name = os.path.join(local_folder, os.path.split(src)[-1]) + dst = self.local_scratch.copyfolder(src, file_folder_name) + return dst + + def get_si_verse(self, edbapp=True, additional_files_folders="", version=None): + """Copy si_verse board file into local folder. A new temporary folder will be created.""" + aedb = self._copy_file_folder_into_local_folder("TEDB/ANSYS-HSD_V1.aedb") + if additional_files_folders: + files = ( + additional_files_folders if isinstance(additional_files_folders, list) else [additional_files_folders] + ) + for f in files: + src = os.path.join(self.example_models_path, f) + file_folder_name = os.path.join(self.test_folder, os.path.split(src)[-1]) + if os.path.isfile(src): + self.local_scratch.copyfile(src, file_folder_name) + else: + self.local_scratch.copyfolder(src, file_folder_name) + if edbapp: + version = desktop_version if version is None else version + return Edb(aedb, edbversion=version, restart_rpc_server=True, kill_all_instances=True) + else: + return aedb + + def create_empty_edb(self): + local_folder = self._create_test_folder() + aedb = os.path.join(local_folder, "new_layout.aedb") + return Edb(aedb, edbversion=desktop_version, restart_rpc_server=True, kill_all_instances=True) + + def get_multizone_pcb(self): + aedb = self._copy_file_folder_into_local_folder("multi_zone_project.aedb") + return Edb(aedb, edbversion=desktop_version, restart_rpc_server=True, kill_all_instances=True) + + def get_no_ref_pins_component(self): + aedb = self._copy_file_folder_into_local_folder("TEDB/component_no_ref_pins.aedb") + return Edb(aedb, edbversion=desktop_version, restart_rpc_server=True, kill_all_instances=True) + + +@pytest.fixture(scope="class") +def grpc_edb_app(add_grpc_edb): + app = add_grpc_edb(test_project_name, subfolder=test_subfolder) + return app + + +@pytest.fixture(scope="class") +def grpc_edb_app_without_material(add_grpc_edb): + app = add_grpc_edb() + return app + + +@pytest.fixture(scope="class", autouse=True) +def target_path(local_scratch): + example_project = os.path.join(example_models_path, test_subfolder, "example_package.aedb") + target_path = os.path.join(local_scratch.path, "example_package.aedb") + local_scratch.copyfolder(example_project, target_path) + return target_path + + +@pytest.fixture(scope="class", autouse=True) +def target_path2(local_scratch): + example_project2 = os.path.join(example_models_path, test_subfolder, "simple.aedb") + target_path2 = os.path.join(local_scratch.path, "simple_00.aedb") + local_scratch.copyfolder(example_project2, target_path2) + return target_path2 + + +@pytest.fixture(scope="class", autouse=True) +def target_path3(local_scratch): + example_project3 = os.path.join(example_models_path, test_subfolder, "ANSYS-HSD_V1_cut.aedb") + target_path3 = os.path.join(local_scratch.path, "test_plot.aedb") + local_scratch.copyfolder(example_project3, target_path3) + return target_path3 + + +@pytest.fixture(scope="class", autouse=True) +def target_path4(local_scratch): + example_project4 = os.path.join(example_models_path, test_subfolder, "Package.aedb") + target_path4 = os.path.join(local_scratch.path, "Package_00.aedb") + local_scratch.copyfolder(example_project4, target_path4) + return target_path4 + + +@pytest.fixture(scope="class", autouse=True) +def edb_examples(local_scratch): + return EdbExamples(local_scratch) diff --git a/tests/grpc/system/stackup_renamed.json b/tests/grpc/system/stackup_renamed.json new file mode 100644 index 0000000000..10e9c3c6ca --- /dev/null +++ b/tests/grpc/system/stackup_renamed.json @@ -0,0 +1,469 @@ +{ + "materials": { + "copper": { + "name": "copper", + "conductivity": 57000000.0, + "loss_tangent": 0.0, + "magnetic_loss_tangent": 0.0, + "mass_density": 0.0, + "permittivity": 1.0, + "permeability": 1.0, + "poisson_ratio": 0.0, + "specific_heat": 0.0, + "thermal_conductivity": 0.0, + "youngs_modulus": 0.0, + "thermal_expansion_coefficient": 0.0, + "dc_conductivity": null, + "dc_permittivity": null, + "dielectric_model_frequency": null, + "loss_tangent_at_frequency": null, + "permittivity_at_frequency": null + }, + "FR4_epoxy": { + "name": "FR4_epoxy", + "conductivity": 0.0, + "loss_tangent": 0.02, + "magnetic_loss_tangent": 0.0, + "mass_density": 0.0, + "permittivity": 4.4, + "permeability": 1.0, + "poisson_ratio": 0.0, + "specific_heat": 0.0, + "thermal_conductivity": 0.0, + "youngs_modulus": 0.0, + "thermal_expansion_coefficient": 0.0, + "dc_conductivity": null, + "dc_permittivity": null, + "dielectric_model_frequency": null, + "loss_tangent_at_frequency": null, + "permittivity_at_frequency": null + }, + "Megtron4": { + "name": "Megtron4", + "conductivity": 0.0, + "loss_tangent": 0.005, + "magnetic_loss_tangent": 0.0, + "mass_density": 0.0, + "permittivity": 3.77, + "permeability": 1.0, + "poisson_ratio": 0.0, + "specific_heat": 0.0, + "thermal_conductivity": 0.0, + "youngs_modulus": 0.0, + "thermal_expansion_coefficient": 0.0, + "dc_conductivity": null, + "dc_permittivity": null, + "dielectric_model_frequency": null, + "loss_tangent_at_frequency": null, + "permittivity_at_frequency": null + }, + "Megtron4_2": { + "name": "Megtron4_2", + "conductivity": 0.0, + "loss_tangent": 0.006, + "magnetic_loss_tangent": 0.0, + "mass_density": 0.0, + "permittivity": 3.47, + "permeability": 1.0, + "poisson_ratio": 0.0, + "specific_heat": 0.0, + "thermal_conductivity": 0.0, + "youngs_modulus": 0.0, + "thermal_expansion_coefficient": 0.0, + "dc_conductivity": null, + "dc_permittivity": null, + "dielectric_model_frequency": null, + "loss_tangent_at_frequency": null, + "permittivity_at_frequency": null + }, + "Megtron4_3": { + "name": "Megtron4_3", + "conductivity": 0.0, + "loss_tangent": 0.005, + "magnetic_loss_tangent": 0.0, + "mass_density": 0.0, + "permittivity": 4.2, + "permeability": 1.0, + "poisson_ratio": 0.0, + "specific_heat": 0.0, + "thermal_conductivity": 0.0, + "youngs_modulus": 0.0, + "thermal_expansion_coefficient": 0.0, + "dc_conductivity": null, + "dc_permittivity": null, + "dielectric_model_frequency": null, + "loss_tangent_at_frequency": null, + "permittivity_at_frequency": null + }, + "Solder Resist": { + "name": "Solder Resist", + "conductivity": 0.0, + "loss_tangent": 0.0, + "magnetic_loss_tangent": 0.0, + "mass_density": 0.0, + "permittivity": 3.0, + "permeability": 1.0, + "poisson_ratio": 0.0, + "specific_heat": 0.0, + "thermal_conductivity": 0.0, + "youngs_modulus": 0.0, + "thermal_expansion_coefficient": 0.0, + "dc_conductivity": null, + "dc_permittivity": null, + "dielectric_model_frequency": null, + "loss_tangent_at_frequency": null, + "permittivity_at_frequency": null + }, + "solder_mask": { + "name": "solder_mask", + "conductivity": 0.0, + "loss_tangent": 0.035, + "magnetic_loss_tangent": 0.0, + "mass_density": 0.0, + "permittivity": 3.1, + "permeability": 1.0, + "poisson_ratio": 0.0, + "specific_heat": 0.0, + "thermal_conductivity": 0.0, + "youngs_modulus": 0.0, + "thermal_expansion_coefficient": 0.0, + "dc_conductivity": null, + "dc_permittivity": null, + "dielectric_model_frequency": null, + "loss_tangent_at_frequency": null, + "permittivity_at_frequency": null + } + }, + "layers": { + "1_Top": { + "name": "1_Top", + "color": [ + 255, + 0, + 0 + ], + "type": "signal", + "material": "copper", + "dielectric_fill": "Solder Resist", + "thickness": 3.5e-05, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "DE1": { + "name": "DE1", + "color": [ + 128, + 128, + 128 + ], + "type": "dielectric", + "material": "Megtron4", + "dielectric_fill": null, + "thickness": 0.0001, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "Inner1(GND1)": { + "name": "Inner1(GND1)", + "color": [ + 128, + 128, + 0 + ], + "type": "signal", + "material": "copper", + "dielectric_fill": "Megtron4_2", + "thickness": 1.7000000000000003e-05, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "DE2": { + "name": "DE2", + "color": [ + 128, + 128, + 128 + ], + "type": "dielectric", + "material": "Megtron4_2", + "dielectric_fill": null, + "thickness": 8.8e-05, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "Inner2(PWR1)": { + "name": "Inner2(PWR1)", + "color": [ + 112, + 219, + 250 + ], + "type": "signal", + "material": "copper", + "dielectric_fill": "Megtron4_2", + "thickness": 1.7000000000000003e-05, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "DE3": { + "name": "DE3", + "color": [ + 128, + 128, + 128 + ], + "type": "dielectric", + "material": "Megtron4", + "dielectric_fill": null, + "thickness": 0.0001, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "Inner3(Sig1)": { + "name": "Inner3(Sig1)", + "color": [ + 255, + 0, + 255 + ], + "type": "signal", + "material": "copper", + "dielectric_fill": "Megtron4_3", + "thickness": 1.7000000000000003e-05, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "Megtron4-1mm": { + "name": "Megtron4-1mm", + "color": [ + 128, + 128, + 128 + ], + "type": "dielectric", + "material": "Megtron4_3", + "dielectric_fill": null, + "thickness": 0.001, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "Inner4(Sig2)": { + "name": "Inner4(Sig2)", + "color": [ + 128, + 0, + 128 + ], + "type": "signal", + "material": "copper", + "dielectric_fill": "Megtron4_3", + "thickness": 1.7000000000000003e-05, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "DE5": { + "name": "DE5", + "color": [ + 128, + 128, + 128 + ], + "type": "dielectric", + "material": "Megtron4", + "dielectric_fill": null, + "thickness": 0.0001, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "Inner5(PWR2)": { + "name": "Inner5(PWR2)", + "color": [ + 0, + 204, + 102 + ], + "type": "signal", + "material": "copper", + "dielectric_fill": "Megtron4_2", + "thickness": 1.7000000000000003e-05, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "DE6": { + "name": "DE6", + "color": [ + 128, + 128, + 128 + ], + "type": "dielectric", + "material": "Megtron4_2", + "dielectric_fill": null, + "thickness": 8.8e-05, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "Inner6(GND2)": { + "name": "Inner6(GND2)", + "color": [ + 0, + 128, + 128 + ], + "type": "signal", + "material": "copper", + "dielectric_fill": "Megtron4_2", + "thickness": 1.7000000000000003e-05, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "DE7": { + "name": "DE7", + "color": [ + 128, + 128, + 128 + ], + "type": "dielectric", + "material": "Megtron4", + "dielectric_fill": null, + "thickness": 0.0001, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + }, + "16_Bottom": { + "name": "16_Bottom", + "color": [ + 0, + 0, + 255 + ], + "type": "signal", + "material": "copper", + "dielectric_fill": "Solder Resist", + "thickness": 3.5000000000000004e-05, + "etch_factor": 0.0, + "roughness_enabled": false, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0 + } + } +} \ No newline at end of file diff --git a/tests/grpc/system/test_edb.py b/tests/grpc/system/test_edb.py new file mode 100644 index 0000000000..4e9483c94e --- /dev/null +++ b/tests/grpc/system/test_edb.py @@ -0,0 +1,1579 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb +""" + +import os + +import pytest + +from pyedb.generic.general_methods import is_linux +from pyedb.grpc.edb import EdbGrpc as Edb +from tests.conftest import desktop_version, local_path +from tests.legacy.system.conftest import test_subfolder + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch, target_path, target_path2, target_path4): + self.local_scratch = local_scratch + self.target_path = target_path + self.target_path2 = target_path2 + self.target_path4 = target_path4 + + def test_hfss_create_coax_port_on_component_from_hfss(self, edb_examples): + """Create a coaxial port on a component from its pin.""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.hfss.create_coax_port_on_component("U1", "DDR4_DQS0_P") + assert edbapp.hfss.create_coax_port_on_component("U1", ["DDR4_DQS0_P", "DDR4_DQS0_N"], True) + edbapp.close() + + def test_layout_bounding_box(self, edb_examples): + """Evaluate layout bounding box""" + # Done + edbapp = edb_examples.get_si_verse() + assert len(edbapp.get_bounding_box()) == 2 + assert edbapp.get_bounding_box() == [[-0.01426004895, -0.00455000106], [0.15010507444, 0.08000000002]] + edbapp.close() + + def test_siwave_create_circuit_port_on_net(self, edb_examples): + """Create a circuit port on a net.""" + # Done + edbapp = edb_examples.get_si_verse() + initial_len = len(edbapp.padstacks.pingroups) + assert edbapp.siwave.create_circuit_port_on_net("U1", "1V0", "U1", "GND", 50, "test") == "test" + p2 = edbapp.siwave.create_circuit_port_on_net("U1", "PLL_1V8", "U1", "GND", 50, "test") + assert p2 != "test" and "test" in p2 + pins = edbapp.components.get_pin_from_component("U1") + p3 = edbapp.siwave.create_circuit_port_on_pin(pins[200], pins[0], 45) + assert p3 != "" + p4 = edbapp.hfss.create_circuit_port_on_net("U1", "USB3_D_P") + assert len(edbapp.padstacks.pingroups) == initial_len + 6 + assert "GND" in p4 and "USB3_D_P" in p4 + + # TODO: Moves this piece of code in another place + assert "test" in edbapp.terminals + assert edbapp.siwave.create_pin_group_on_net("U1", "1V0", "PG_V1P0_S0") + assert edbapp.siwave.create_pin_group_on_net("U1", "GND", "U1_GND") + assert edbapp.siwave.create_circuit_port_on_pin_group("PG_V1P0_S0", "U1_GND", impedance=50, name="test_port") + edbapp.excitations["test_port"].name = "test_rename" + assert any(port for port in list(edbapp.excitations) if port == "test_rename") + edbapp.close() + + def test_siwave_create_voltage_source(self, edb_examples): + """Create a voltage source.""" + # Done + edbapp = edb_examples.get_si_verse() + assert "Vsource_" in edbapp.siwave.create_voltage_source_on_net("U1", "USB3_D_P", "U1", "GND", 3.3, 0) + assert len(edbapp.terminals) == 2 + assert list(edbapp.terminals.values())[0].magnitude == 3.3 + + pins = edbapp.components.get_pin_from_component("U1") + assert "VSource_" in edbapp.siwave.create_voltage_source_on_pin( + pins[300], pins[10], voltage_value=3.3, phase_value=1 + ) + assert len(edbapp.terminals) == 4 + assert list(edbapp.terminals.values())[2].phase == 1.0 + assert list(edbapp.terminals.values())[2].magnitude == 3.3 + + u6 = edbapp.components["U6"] + voltage_source = edbapp.create_voltage_source( + u6.pins["F2"].get_terminal(create_new_terminal=True), u6.pins["F1"].get_terminal(create_new_terminal=True) + ) + assert not voltage_source.is_null + edbapp.close() + + def test_siwave_create_current_source(self, edb_examples): + """Create a current source.""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.siwave.create_current_source_on_net("U1", "USB3_D_N", "U1", "GND", 0.1, 0) + pins = edbapp.components.get_pin_from_component("U1") + assert "I22" == edbapp.siwave.create_current_source_on_pin(pins[301], pins[10], 0.1, 0, "I22") + + assert edbapp.siwave.create_pin_group_on_net(reference_designator="U1", net_name="GND", group_name="gnd") + edbapp.siwave.create_pin_group(reference_designator="U1", pin_numbers=["A27", "A28"], group_name="vrm_pos") + edbapp.siwave.create_current_source_on_pin_group( + pos_pin_group_name="vrm_pos", neg_pin_group_name="gnd", name="vrm_current_source" + ) + + edbapp.siwave.create_pin_group(reference_designator="U1", pin_numbers=["R23", "P23"], group_name="sink_pos") + edbapp.siwave.create_pin_group_on_net(reference_designator="U1", net_name="GND", group_name="gnd2") + + # TODO: Moves this piece of code in another place + assert edbapp.siwave.create_voltage_source_on_pin_group("sink_pos", "gnd2", name="vrm_voltage_source") + edbapp.siwave.create_pin_group(reference_designator="U1", pin_numbers=["A27", "A28"], group_name="vp_pos") + assert edbapp.siwave.create_pin_group_on_net(reference_designator="U1", net_name="GND", group_name="vp_neg") + assert edbapp.siwave.pin_groups["vp_pos"] + assert edbapp.siwave.pin_groups["vp_pos"].pins + assert edbapp.siwave.create_voltage_probe_on_pin_group("vprobe", "vp_pos", "vp_neg") + assert edbapp.terminals["vprobe"] + edbapp.siwave.place_voltage_probe( + "vprobe_2", "1V0", ["112mm", "24mm"], "1_Top", "GND", ["112mm", "27mm"], "Inner1(GND1)" + ) + vprobe_2 = edbapp.terminals["vprobe_2"] + ref_term = vprobe_2.ref_terminal + assert isinstance(ref_term.location, list) + # ref_term.location = [0, 0] # position setter is crashing check pyedb-core bug #431 + assert ref_term.layer + ref_term.layer.name = "Inner1(GND1" + ref_term.layer.name = "test" + assert "test" in edbapp.stackup.layers + u6 = edbapp.components["U6"] + assert edbapp.create_current_source( + u6.pins["H8"].get_terminal(create_new_terminal=True), u6.pins["G9"].get_terminal(create_new_terminal=True) + ) + edbapp.close() + + def test_siwave_create_dc_terminal(self, edb_examples): + """Create a DC terminal.""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.siwave.create_dc_terminal("U1", "DDR4_DQ40", "dc_terminal1") == "dc_terminal1" + edbapp.close() + + def test_siwave_create_resistors_on_pin(self, edb_examples): + """Create a resistor on pin.""" + # Done + edbapp = edb_examples.get_si_verse() + pins = edbapp.components.get_pin_from_component("U1") + assert "RST4000" == edbapp.siwave.create_resistor_on_pin(pins[302], pins[10], 40, "RST4000") + edbapp.close() + + def test_siwave_add_syz_analsyis(self, edb_examples): + """Add a sywave AC analysis.""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.siwave.add_siwave_syz_analysis(start_freq="=1GHz", stop_freq="10GHz", step_freq="10MHz") + edbapp.close() + + def test_siwave_add_dc_analysis(self, edb_examples): + """Add a sywave DC analysis.""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.siwave.add_siwave_dc_analysis(name="Test_dc") + edbapp.close() + + def test_hfss_mesh_operations(self, edb_examples): + """Retrieve the trace width for traces with ports.""" + # Done + edbapp = edb_examples.get_si_verse() + edbapp.components.create_port_on_component( + "U1", + ["VDD_DDR"], + reference_net="GND", + port_type="circuit_port", + ) + mesh_ops = edbapp.hfss.get_trace_width_for_traces_with_ports() + assert len(mesh_ops) > 0 + edbapp.close() + + def test_add_variables(self, edb_examples): + """Add design and project variables.""" + # Done + edbapp = edb_examples.get_si_verse() + edbapp.add_design_variable("my_variable", "1mm") + assert "my_variable" in edbapp.active_cell.get_all_variable_names() + assert edbapp.modeler.parametrize_trace_width("DDR4_DQ25") + assert edbapp.modeler.parametrize_trace_width("DDR4_A2") + edbapp.add_design_variable("my_parameter", "2mm", True) + assert "my_parameter" in edbapp.active_cell.get_all_variable_names() + variable_value = edbapp.active_cell.get_variable_value("my_parameter").value + assert variable_value == 2e-3 + assert not edbapp.add_design_variable("my_parameter", "2mm", True) + edbapp.add_project_variable("$my_project_variable", "3mm") + assert edbapp.db.get_variable_value("$my_project_variable") == 3e-3 + assert not edbapp.add_project_variable("$my_project_variable", "3mm") + edbapp.close() + + def test_save_edb_as(self, edb_examples): + """Save edb as some file.""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.save_edb_as(os.path.join(self.local_scratch.path, "si_verse_new.aedb")) + assert os.path.exists(os.path.join(self.local_scratch.path, "si_verse_new.aedb", "edb.def")) + edbapp.close() + + def test_create_custom_cutout_0(self, edb_examples): + """Create custom cutout 0.""" + # Done + edbapp = edb_examples.get_si_verse() + output = os.path.join(self.local_scratch.path, "cutout.aedb") + assert edbapp.cutout( + ["DDR4_DQS0_P", "DDR4_DQS0_N"], + ["GND"], + output_aedb_path=output, + open_cutout_at_end=False, + use_pyaedt_extent_computing=True, + use_pyaedt_cutout=False, + ) + assert os.path.exists(os.path.join(output, "edb.def")) + bounding = edbapp.get_bounding_box() + assert bounding + + cutout_line_x = 41 + cutout_line_y = 30 + points = [[bounding[0][0], bounding[0][1]]] + points.append([cutout_line_x, bounding[0][1]]) + points.append([cutout_line_x, cutout_line_y]) + points.append([bounding[0][0], cutout_line_y]) + points.append([bounding[0][0], bounding[0][1]]) + + output = os.path.join(self.local_scratch.path, "cutout2.aedb") + assert edbapp.cutout( + custom_extent=points, + signal_list=["GND", "1V0"], + output_aedb_path=output, + open_cutout_at_end=False, + include_partial_instances=True, + use_pyaedt_cutout=False, + ) + assert os.path.exists(os.path.join(output, "edb.def")) + # edbapp.close() + + def test_create_custom_cutout_1(self, edb_examples): + """Create custom cutout 1.""" + # Done + edbapp = edb_examples.get_si_verse() + spice_path = os.path.join(local_path, "example_models", test_subfolder, "GRM32_DC0V_25degC.mod") + assert edbapp.components.instances["R8"].assign_spice_model(spice_path) + assert edbapp.nets.nets + assert edbapp.cutout( + signal_list=["1V0"], + reference_list=[ + "GND", + "LVDS_CH08_N", + "LVDS_CH08_P", + "LVDS_CH10_N", + "LVDS_CH10_P", + "LVDS_CH04_P", + "LVDS_CH04_N", + ], + extent_type="Bounding", + number_of_threads=4, + extent_defeature=0.001, + preserve_components_with_model=True, + keep_lines_as_path=True, + ) + assert "A0_N" not in edbapp.nets.nets + # assert isinstance(edbapp.layout_validation.disjoint_nets("GND", order_by_area=True), list) + # assert isinstance(edbapp.layout_validation.disjoint_nets("GND", keep_only_main_net=True), list) + # assert isinstance(edbapp.layout_validation.disjoint_nets("GND", clean_disjoints_less_than=0.005), list) + # assert edbapp.layout_validation.fix_self_intersections("PGND") + edbapp.close() + + def test_create_custom_cutout_2(self, edb_examples): + """Create custom cutout 2.""" + # Done + edbapp = edb_examples.get_si_verse() + bounding = edbapp.get_bounding_box() + assert bounding + cutout_line_x = 41 + cutout_line_y = 30 + points = [[bounding[0][0], bounding[0][1]]] + points.append([cutout_line_x, bounding[0][1]]) + points.append([cutout_line_x, cutout_line_y]) + points.append([bounding[0][0], cutout_line_y]) + points.append([bounding[0][0], bounding[0][1]]) + + assert edbapp.cutout( + signal_list=["1V0"], + reference_list=["GND"], + number_of_threads=4, + extent_type="ConvexHull", + custom_extent=points, + simple_pad_check=False, + ) + edbapp.close() + + def test_create_custom_cutout_3(self, edb_examples): + """Create custom cutout 3.""" + # Done + edbapp = edb_examples.get_si_verse() + edbapp.components.create_port_on_component( + "U1", + ["5V"], + reference_net="GND", + port_type="circuit_port", + ) + edbapp.components.create_port_on_component("U2", ["5V"], reference_net="GND") + edbapp.hfss.create_voltage_source_on_net("U4", "5V", "U4", "GND") + legacy_name = edbapp.edbpath + assert edbapp.cutout( + signal_list=["5V"], + reference_list=["GND"], + number_of_threads=4, + extent_type="ConvexHull", + use_pyaedt_extent_computing=True, + check_terminals=True, + ) + assert edbapp.edbpath == legacy_name + # assert edbapp.are_port_reference_terminals_connected(common_reference="GND") + + edbapp.close() + + def test_create_custom_cutout_4(self, edb_examples): + """Create custom cutout 4.""" + # Done + edbapp = edb_examples.get_si_verse() + edbapp.components.create_pingroup_from_pins( + [i for i in list(edbapp.components.instances["U1"].pins.values()) if i.net_name == "GND"] + ) + + assert edbapp.cutout( + signal_list=["DDR4_DQS0_P", "DDR4_DQS0_N"], + reference_list=["GND"], + number_of_threads=4, + extent_type="ConvexHull", + use_pyaedt_extent_computing=True, + include_pingroups=True, + check_terminals=True, + expansion_factor=4, + ) + edbapp.close() + + def test_export_to_hfss(self): + """Export EDB to HFSS.""" + # Done + edb = Edb( + edbpath=os.path.join(local_path, "example_models", test_subfolder, "simple.aedb"), + edbversion=desktop_version, + restart_rpc_server=True, + ) + options_config = {"UNITE_NETS": 1, "LAUNCH_Q3D": 0} + out = edb.write_export3d_option_config_file(self.local_scratch.path, options_config) + assert os.path.exists(out) + out = edb.export_hfss(self.local_scratch.path) + assert os.path.exists(out) + edb.close() + + def test_export_to_q3d(self): + """Export EDB to Q3D.""" + # Done + edb = Edb( + edbpath=os.path.join(local_path, "example_models", test_subfolder, "simple.aedb"), + edbversion=desktop_version, + restart_rpc_server=True, + ) + options_config = {"UNITE_NETS": 1, "LAUNCH_Q3D": 0} + out = edb.write_export3d_option_config_file(self.local_scratch.path, options_config) + assert os.path.exists(out) + out = edb.export_q3d(self.local_scratch.path, net_list=["ANALOG_A0", "ANALOG_A1", "ANALOG_A2"], hidden=True) + assert os.path.exists(out) + edb.close() + + def test_074_export_to_maxwell(self): + """Export EDB to Maxwell 3D.""" + + # Done + + edb = Edb( + edbpath=os.path.join(local_path, "example_models", test_subfolder, "simple.aedb"), + edbversion=desktop_version, + restart_rpc_server=True, + ) + options_config = {"UNITE_NETS": 1, "LAUNCH_MAXWELL": 0} + out = edb.write_export3d_option_config_file(self.local_scratch.path, options_config) + assert os.path.exists(out) + out = edb.export_maxwell(self.local_scratch.path, num_cores=6) + assert os.path.exists(out) + edb.close() + + def test_create_edge_port_on_polygon(self): + """Create lumped and vertical port.""" + # Done + edb = Edb( + edbpath=os.path.join(local_path, "example_models", test_subfolder, "edge_ports.aedb"), + edbversion=desktop_version, + restart_rpc_server=True, + ) + poly_list = [poly for poly in edb.layout.primitives if poly.primitive_type.value == 2] + port_poly = [poly for poly in poly_list if poly.edb_uid == 17][0] + ref_poly = [poly for poly in poly_list if poly.edb_uid == 19][0] + port_location = [-65e-3, -13e-3] + ref_location = [-63e-3, -13e-3] + assert edb.source_excitation.create_edge_port_on_polygon( + polygon=port_poly, + reference_polygon=ref_poly, + terminal_point=port_location, + reference_point=ref_location, + ) + port_poly = [poly for poly in poly_list if poly.edb_uid == 23][0] + ref_poly = [poly for poly in poly_list if poly.edb_uid == 22][0] + port_location = [-65e-3, -10e-3] + ref_location = [-65e-3, -10e-3] + assert edb.source_excitation.create_edge_port_on_polygon( + polygon=port_poly, + reference_polygon=ref_poly, + terminal_point=port_location, + reference_point=ref_location, + ) + port_poly = [poly for poly in poly_list if poly.edb_uid == 25][0] + port_location = [-65e-3, -7e-3] + assert edb.source_excitation.create_edge_port_on_polygon( + polygon=port_poly, terminal_point=port_location, reference_layer="gnd" + ) + sig = edb.modeler.create_trace([[0, 0], ["9mm", 0]], "sig2", "1mm", "SIG", "Flat", "Flat") + from pyedb.grpc.database.primitive.path import Path as PyEDBPath + + sig = PyEDBPath(edb, sig) + # TODO check bug #435 can't get product properties skipping wave port for now + assert sig.create_edge_port("pcb_port_1", "end", "Wave", None, 8, 8) + assert sig.create_edge_port("pcb_port_2", "start", "gap") + gap_port = edb.ports["pcb_port_2"] + assert gap_port.component.is_null + assert gap_port.magnitude == 0.0 + assert gap_port.phase == 0.0 + assert gap_port.impedance + assert not gap_port.deembed + gap_port.name = "gap_port" + assert gap_port.name == "gap_port" + assert gap_port.port_post_processing_prop.renormalization_impedance.value == 50 + gap_port.is_circuit_port = True + assert gap_port.is_circuit_port + edb.close() + + def test_edb_statistics(self, edb_examples): + """Get statistics.""" + # Done + edb = edb_examples.get_si_verse() + edb_stats = edb.get_statistics(compute_area=True) + assert edb_stats + assert edb_stats.num_layers + assert edb_stats.stackup_thickness + assert edb_stats.num_vias + assert edb_stats.occupying_ratio + assert edb_stats.occupying_surface + assert edb_stats.layout_size + assert edb_stats.num_polygons + assert edb_stats.num_traces + assert edb_stats.num_nets + assert edb_stats.num_discrete_components + assert edb_stats.num_inductors + assert edb_stats.num_capacitors + assert edb_stats.num_resistors + assert edb_stats.occupying_ratio["1_Top"] == 0.30168200230804587 + assert edb_stats.occupying_ratio["Inner1(GND1)"] == 0.9374673366306919 + assert edb_stats.occupying_ratio["16_Bottom"] == 0.20492545425825437 + edb.close() + + def test_hfss_set_bounding_box_extent(self, edb_examples): + """Configure HFSS with bounding box""" + + # obsolete check with config file 2.0 + + # edb = edb_examples.get_si_verse() + # #initial_extent_info = edb.active_cell.GetHFSSExtentInfo() + # assert edb.active_cell.hfss_extent_info.extent_type.name == "POLYGON" + # config = SimulationConfiguration() + # config.radiation_box = RadiationBoxType.BoundingBox + # assert edb.hfss.configure_hfss_extents(config) + # final_extent_info = edb.active_cell.GetHFSSExtentInfo() + # #assert final_extent_info.ExtentType == edb.u utility.HFSSExtentInfoType.BoundingBox + # edb.close() + + pass + + def test_create_rlc_component(self, edb_examples): + """Create rlc components from pin""" + # Done + edb = edb_examples.get_si_verse() + pins = edb.components.get_pin_from_component("U1", "1V0") + ref_pins = edb.components.get_pin_from_component("U1", "GND") + assert edb.components.create([pins[0], ref_pins[0]], "test_0rlc", r_value=1.67, l_value=1e-13, c_value=1e-11) + assert edb.components.create([pins[0], ref_pins[0]], "test_1rlc", r_value=None, l_value=1e-13, c_value=1e-11) + assert edb.components.create([pins[0], ref_pins[0]], "test_2rlc", r_value=None, c_value=1e-13) + edb.close() + + def test_create_rlc_boundary_on_pins(self, edb_examples): + """Create hfss rlc boundary on pins.""" + # Done + edb = edb_examples.get_si_verse() + pins = edb.components.get_pin_from_component("U1", "1V0") + ref_pins = edb.components.get_pin_from_component("U1", "GND") + assert edb.hfss.create_rlc_boundary_on_pins(pins[0], ref_pins[0], rvalue=1.05, lvalue=1.05e-12, cvalue=1.78e-13) + edb.close() + + def test_configure_hfss_analysis_setup_enforce_causality(self, edb_examples): + """Configure HFSS analysis setup.""" + # Done + edb = edb_examples.get_si_verse() + assert len(edb.active_cell.simulation_setups) == 0 + edb.hfss.add_setup() + assert edb.hfss_setups + assert len(edb.active_cell.simulation_setups) == 1 + assert list(edb.active_cell.simulation_setups)[0] + setup = list(edb.hfss_setups.values())[0] + setup.add_sweep() + assert len(setup.sweep_data) == 1 + assert not setup.sweep_data[0].interpolation_data.enforce_causality + sweeps = setup.sweep_data + for sweep in sweeps: + sweep.interpolation_data.enforce_causality = True + setup.sweep_data = sweeps + assert setup.sweep_data[0].interpolation_data.enforce_causality + edb.close() + + def test_configure_hfss_analysis_setup(self, edb_examples): + """Configure HFSS analysis setup.""" + # TODO adapt for config file 2.0 + edb = edb_examples.get_si_verse() + # sim_setup = SimulationConfiguration() + # sim_setup.mesh_sizefactor = 1.9 + # assert not sim_setup.do_lambda_refinement + # edb.hfss.configure_hfss_analysis_setup(sim_setup) + # mesh_size_factor = ( + # list(edb.active_cell.SimulationSetups)[0] + # .GetSimSetupInfo() + # .get_SimulationSettings() + # .get_InitialMeshSettings() + # .get_MeshSizefactor() + # ) + # assert mesh_size_factor == 1.9 + edb.close() + + def test_create_various_ports_0(self): + """Create various ports.""" + edb = Edb( + edbpath=os.path.join(local_path, "example_models", "edb_edge_ports.aedb"), + edbversion=desktop_version, + restart_rpc_server=True, + ) + prim_1_id = [i.id for i in edb.modeler.primitives if i.net.name == "trace_2"][0] + assert edb.source_excitation.create_edge_port_vertical(prim_1_id, ["-66mm", "-4mm"], "port_ver") + + prim_2_id = [i.id for i in edb.modeler.primitives if i.net.name == "trace_3"][0] + assert edb.source_excitation.create_edge_port_horizontal( + prim_1_id, ["-60mm", "-4mm"], prim_2_id, ["-59mm", "-4mm"], "port_hori", 30, "Lower" + ) + assert edb.source_excitation.get_ports_number() == 2 + port_ver = edb.ports["port_ver"] + assert not port_ver.is_null + assert not port_ver.is_circuit_port + assert port_ver.type.name == "EDGE" + + port_hori = edb.ports["port_hori"] + assert port_hori.reference_terminal + + kwargs = { + "layer_name": "Top", + "net_name": "SIGP", + "width": "0.1mm", + "start_cap_style": "Flat", + "end_cap_style": "Flat", + } + traces = [] + trace_paths = [ + [["-40mm", "-10mm"], ["-30mm", "-10mm"]], + [["-40mm", "-10.2mm"], ["-30mm", "-10.2mm"]], + [["-40mm", "-10.4mm"], ["-30mm", "-10.4mm"]], + ] + for p in trace_paths: + t = edb.modeler.create_trace(path_list=p, **kwargs) + traces.append(t) + + # TODO implement wave port with grPC + # wave_port = edb.source_excitation.create_bundle_wave_port["wave_port"] + # wave_port.horizontal_extent_factor = 10 + # wave_port.vertical_extent_factor = 10 + # assert wave_port.horizontal_extent_factor == 10 + # assert wave_port.vertical_extent_factor == 10 + # wave_port.radial_extent_factor = 1 + # assert wave_port.radial_extent_factor == 1 + # assert wave_port.pec_launch_width + # assert not wave_port.deembed + # assert wave_port.deembed_length == 0.0 + # assert wave_port.do_renormalize + # wave_port.do_renormalize = False + # assert not wave_port.do_renormalize + # assert edb.source_excitation.create_differential_wave_port( + # traces[1].id, + # trace_paths[0][0], + # traces[2].id, + # trace_paths[1][0], + # horizontal_extent_factor=8, + # port_name="df_port", + # ) + # assert edb.ports["df_port"] + # p, n = edb.ports["df_port"].terminals + # assert p.name == "df_port:T1" + # assert n.name == "df_port:T2" + # assert edb.ports["df_port"].decouple() + # p.couple_ports(n) + # + # traces_id = [i.id for i in traces] + # paths = [i[1] for i in trace_paths] + # df_port = edb.source_excitation.create_bundle_wave_port(traces_id, paths) + # assert df_port.name + # assert df_port.terminals + # df_port.horizontal_extent_factor = 10 + # df_port.vertical_extent_factor = 10 + # df_port.deembed = True + # df_port.deembed_length = "1mm" + # assert df_port.horizontal_extent_factor == 10 + # assert df_port.vertical_extent_factor == 10 + # assert df_port.deembed + # assert df_port.deembed_length == 1e-3 + edb.close() + + def test_create_various_ports_1(self): + """Create various ports.""" + edb = Edb( + edbpath=os.path.join(local_path, "example_models", "edb_edge_ports.aedb"), + edbversion=desktop_version, + restart_rpc_server=True, + ) + kwargs = { + "layer_name": "TOP", + "net_name": "SIGP", + "width": "0.1mm", + "start_cap_style": "Flat", + "end_cap_style": "Flat", + } + traces = [ + [["-40mm", "-10mm"], ["-30mm", "-10mm"]], + [["-40mm", "-10.2mm"], ["-30mm", "-10.2mm"]], + [["-40mm", "-10.4mm"], ["-30mm", "-10.4mm"]], + ] + edb_traces = [] + for p in traces: + t = edb.modeler.create_trace(path_list=p, **kwargs) + edb_traces.append(t) + assert edb_traces[0].length == 0.02 + + # TODO add wave port support + # assert edb.source_excitation.create_wave_port(traces[0], trace_pathes[0][0], "wave_port") + # + # assert edb.source_excitation.create_differential_wave_port( + # traces[0], + # trace_pathes[0][0], + # traces[1], + # trace_pathes[1][0], + # horizontal_extent_factor=8, + # ) + # + # paths = [i[1] for i in trace_pathes] + # assert edb.source_excitation.create_bundle_wave_port(traces, paths) + # p = edb.excitations["wave_port"] + # p.horizontal_extent_factor = 6 + # p.vertical_extent_factor = 5 + # p.pec_launch_width = "0.02mm" + # p.radial_extent_factor = 1 + # assert p.horizontal_extent_factor == 6 + # assert p.vertical_extent_factor == 5 + # assert p.pec_launch_width == "0.02mm" + # assert p.radial_extent_factor == 1 + edb.close() + + def test_set_all_antipad_values(self, edb_examples): + """Set all anti-pads from all pad-stack definition to the given value.""" + # Done + edb = edb_examples.get_si_verse() + assert edb.padstacks.set_all_antipad_value(0.0) + edb.close() + + def test_hfss_simulation_setup(self, edb_examples): + """Create a setup from a template and evaluate its properties.""" + # Done + edbapp = edb_examples.get_si_verse() + setup1 = edbapp.hfss.add_setup("setup1") + assert not edbapp.hfss.add_setup("setup1") + assert setup1.set_solution_single_frequency() + assert setup1.set_solution_multi_frequencies() + assert setup1.set_solution_broadband() + + setup1.settings.options.enhanced_low_frequency_accuracy = True + assert setup1.settings.options.enhanced_low_frequency_accuracy + setup1.settings.options.order_basis = setup1.settings.options.order_basis.FIRST_ORDER + assert setup1.settings.options.order_basis.name == "FIRST_ORDER" + setup1.settings.options.relative_residual = 0.0002 + assert setup1.settings.options.relative_residual == 0.0002 + setup1.settings.options.use_shell_elements = True + assert setup1.settings.options.use_shell_elements + + setup1b = edbapp.setups["setup1"] + assert not setup1.is_null + assert setup1b.add_adaptive_frequency_data("5GHz", "0.01") + setup1.settings.general.adaptive_solution_type = setup1.settings.general.adaptive_solution_type.BROADBAND + setup1.settings.options.max_refinement_per_pass = 20 + assert setup1.settings.options.max_refinement_per_pass == 20 + setup1.settings.options.min_passes = 2 + assert setup1.settings.options.min_passes == 2 + setup1.settings.general.save_fields = True + assert setup1.settings.general.save_fields + setup1.settings.general.save_rad_fields_only = True + assert setup1.settings.general.save_rad_fields_only + setup1.settings.general.use_parallel_refinement = True + assert setup1.settings.general.use_parallel_refinement + + assert edbapp.setups["setup1"].settings.general.adaptive_solution_type.name == "BROADBAND" + edbapp.setups["setup1"].settings.options.use_max_refinement = True + assert edbapp.setups["setup1"].settings.options.use_max_refinement + + edbapp.setups["setup1"].settings.advanced.defeature_absolute_length = "1um" + assert edbapp.setups["setup1"].settings.advanced.defeature_absolute_length == "1um" + edbapp.setups["setup1"].settings.advanced.defeature_ratio = 1e-5 + assert edbapp.setups["setup1"].settings.advanced.defeature_ratio == 1e-5 + edbapp.setups["setup1"].settings.advanced.healing_option = 0 + assert edbapp.setups["setup1"].settings.advanced.healing_option == 0 + edbapp.setups["setup1"].settings.advanced.remove_floating_geometry = True + assert edbapp.setups["setup1"].settings.advanced.remove_floating_geometry + edbapp.setups["setup1"].settings.advanced.small_void_area = 0.1 + assert edbapp.setups["setup1"].settings.advanced.small_void_area == 0.1 + edbapp.setups["setup1"].settings.advanced.union_polygons = False + assert not edbapp.setups["setup1"].settings.advanced.union_polygons + edbapp.setups["setup1"].settings.advanced.use_defeature = False + assert not edbapp.setups["setup1"].settings.advanced.use_defeature + edbapp.setups["setup1"].settings.advanced.use_defeature_absolute_length = True + assert edbapp.setups["setup1"].settings.advanced.use_defeature_absolute_length + + edbapp.setups["setup1"].settings.advanced.num_via_density = 1.0 + assert edbapp.setups["setup1"].settings.advanced.num_via_density == 1.0 + # if float(edbapp.edbversion) >= 2024.1: + # via_settings.via_mesh_plating = True + edbapp.setups["setup1"].settings.advanced.via_material = "pec" + assert edbapp.setups["setup1"].settings.advanced.via_material == "pec" + edbapp.setups["setup1"].settings.advanced.num_via_sides = 8 + assert edbapp.setups["setup1"].settings.advanced.num_via_sides == 8 + assert edbapp.setups["setup1"].settings.advanced.via_model_type.name == "MESH" + edbapp.setups["setup1"].settings.advanced_meshing.layer_snap_tol = "1e-6" + assert edbapp.setups["setup1"].settings.advanced_meshing.layer_snap_tol == "1e-6" + + edbapp.setups["setup1"].settings.advanced_meshing.arc_to_chord_error = "0.1" + assert edbapp.setups["setup1"].settings.advanced_meshing.arc_to_chord_error == "0.1" + edbapp.setups["setup1"].settings.advanced_meshing.max_num_arc_points = 12 + assert edbapp.setups["setup1"].settings.advanced_meshing.max_num_arc_points == 12 + + edbapp.setups["setup1"].settings.dcr.max_passes = 11 + assert edbapp.setups["setup1"].settings.dcr.max_passes == 11 + edbapp.setups["setup1"].settings.dcr.min_converged_passes = 2 + assert edbapp.setups["setup1"].settings.dcr.min_converged_passes == 2 + edbapp.setups["setup1"].settings.dcr.min_passes = 5 + assert edbapp.setups["setup1"].settings.dcr.min_passes == 5 + edbapp.setups["setup1"].settings.dcr.percent_error = 2.0 + assert edbapp.setups["setup1"].settings.dcr.percent_error == 2.0 + edbapp.setups["setup1"].settings.dcr.percent_refinement_per_pass = 20.0 + assert edbapp.setups["setup1"].settings.dcr.percent_refinement_per_pass == 20.0 + + edbapp.setups["setup1"].settings.solver.max_delta_z0 = 0.5 + assert edbapp.setups["setup1"].settings.solver.max_delta_z0 == 0.5 + edbapp.setups["setup1"].settings.solver.max_triangles_for_wave_port = 1000 + assert edbapp.setups["setup1"].settings.solver.max_triangles_for_wave_port == 1000 + edbapp.setups["setup1"].settings.solver.min_triangles_for_wave_port = 500 + assert edbapp.setups["setup1"].settings.solver.min_triangles_for_wave_port == 500 + edbapp.setups["setup1"].settings.solver.set_triangles_for_wave_port = True + assert edbapp.setups["setup1"].settings.solver.set_triangles_for_wave_port + edbapp.close() + + def test_hfss_simulation_setup_mesh_operation(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + setup = edbapp.create_hfss_setup(name="setup") + mop = setup.add_length_mesh_operation(net_layer_list={"GND": ["1_Top", "16_Bottom"]}, name="m1") + assert mop.enabled + assert mop.net_layer_info[0] == ("GND", "1_Top", True) + assert mop.net_layer_info[1] == ("GND", "16_Bottom", True) + assert mop.name == "m1" + assert mop.max_elements == "1000" + assert mop.restrict_max_elements + assert mop.restrict_max_length + assert mop.max_length == "1mm" + assert setup.mesh_operations + assert edbapp.setups["setup"].mesh_operations + + mop = edbapp.setups["setup"].add_skin_depth_mesh_operation({"GND": ["1_Top", "16_Bottom"]}) + assert mop.net_layer_info[0] == ("GND", "1_Top", True) + assert mop.net_layer_info[1] == ("GND", "16_Bottom", True) + assert mop.max_elements == "1000" + assert mop.restrict_max_elements + assert mop.skin_depth == "1um" + assert mop.surface_triangle_length == "1mm" + assert mop.number_of_layers == "2" + + mop.skin_depth = "5um" + mop.surface_triangle_length = "2mm" + mop.number_of_layer_elements = "3" + + assert mop.skin_depth == "5um" + assert mop.surface_triangle_length == "2mm" + assert mop.number_of_layer_elements == "3" + edbapp.close() + + def test_hfss_frequency_sweep(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + setup1 = edbapp.create_hfss_setup("setup1") + assert edbapp.setups["setup1"].name == "setup1" + setup1.add_sweep(name="sw1", distribution="linear_count", start_freq="1MHz", stop_freq="100MHz", step=10) + assert edbapp.setups["setup1"].sweep_data[0].name == "sw1" + assert edbapp.setups["setup1"].sweep_data[0].start_f == "1MHz" + assert edbapp.setups["setup1"].sweep_data[0].end_f == "100MHz" + assert edbapp.setups["setup1"].sweep_data[0].step == "10" + setup1.add_sweep(name="sw2", distribution="linear", start_freq="210MHz", stop_freq="300MHz", step="10MHz") + assert edbapp.setups["setup1"].sweep_data[0].name == "sw2" + setup1.add_sweep(name="sw3", distribution="log_scale", start_freq="1GHz", stop_freq="10GHz", step=10) + assert edbapp.setups["setup1"].sweep_data[0].name == "sw3" + setup1.sweep_data[2].use_q3d_for_dc = True + edbapp.close() + + def test_siwave_dc_simulation_setup(self, edb_examples): + """Create a dc simulation setup and evaluate its properties.""" + # TODO check with config file 2.0 + edbapp = edb_examples.get_si_verse() + setup1 = edbapp.create_siwave_dc_setup("DC1") + # setup1.dc_settings.restore_default() + # setup1.dc_advanced_settings.restore_default() + + # settings = edbapp.setups["DC1"].settings + # for k, v in setup1.dc_settings.defaults.items(): + # # NOTE: On Linux it seems that there is a strange behavior with use_dc_custom_settings + # # See https://github.com/ansys/pyedb/pull/791#issuecomment-2358036067 + # if k in ["compute_inductance", "plot_jv", "use_dc_custom_settings"]: + # continue + # assert settings["dc_settings"][k] == v + # + # for k, v in setup1.dc_advanced_settings.defaults.items(): + # assert settings["dc_advanced_settings"][k] == v + # + # for p in [0, 1, 2]: + # setup1.set_dc_slider(p) + # settings = edbapp.setups["DC1"].get_configurations() + # for k, v in setup1.dc_settings.dc_defaults.items(): + # assert settings["dc_settings"][k] == v[p] + # + # for k, v in setup1.dc_advanced_settings.dc_defaults.items(): + # assert settings["dc_advanced_settings"][k] == v[p] + edbapp.close() + + def test_siwave_ac_simulation_setup(self, edb_examples): + """Create an ac simulation setup and evaluate its properties.""" + # TODO check with config file 2.0 + # edb = edb_examples.get_si_verse() + # setup1 = edb.create_siwave_syz_setup("AC1") + # assert setup1.name == "AC1" + # assert setup1.enabled + # setup1.advanced_settings.restore_default() + # + # settings = edb.setups["AC1"].get_configurations() + # for k, v in setup1.advanced_settings.defaults.items(): + # if k in ["min_plane_area_to_mesh"]: + # continue + # assert settings["advanced_settings"][k] == v + # + # for p in [0, 1, 2]: + # setup1.set_si_slider(p) + # settings = edb.setups["AC1"].get_configurations() + # for k, v in setup1.advanced_settings.si_defaults.items(): + # assert settings["advanced_settings"][k] == v[p] + # + # for p in [0, 1, 2]: + # setup1.pi_slider_position = p + # settings = edb.setups["AC1"].get_configurations() + # for k, v in setup1.advanced_settings.pi_defaults.items(): + # assert settings["advanced_settings"][k] == v[p] + # + # sweep = setup1.add_sweep( + # name="sweep1", + # frequency_set=[ + # ["linear count", "0", "1kHz", 1], + # ["log scale", "1kHz", "0.1GHz", 10], + # ["linear scale", "0.1GHz", "10GHz", "0.1GHz"], + # ], + # ) + # assert 0 in sweep.frequencies + # assert not sweep.adaptive_sampling + # assert not sweep.adv_dc_extrapolation + # assert sweep.auto_s_mat_only_solve + # assert not sweep.enforce_causality + # assert not sweep.enforce_dc_and_causality + # assert sweep.enforce_passivity + # assert sweep.freq_sweep_type == "kInterpolatingSweep" + # assert sweep.interpolation_use_full_basis + # assert sweep.interpolation_use_port_impedance + # assert sweep.interpolation_use_prop_const + # assert sweep.max_solutions == 250 + # assert sweep.min_freq_s_mat_only_solve == "1MHz" + # assert not sweep.min_solved_freq + # assert sweep.passivity_tolerance == 0.0001 + # assert sweep.relative_s_error == 0.005 + # assert not sweep.save_fields + # assert not sweep.save_rad_fields_only + # assert not sweep.use_q3d_for_dc + # + # sweep.adaptive_sampling = True + # sweep.adv_dc_extrapolation = True + # sweep.compute_dc_point = True + # sweep.auto_s_mat_only_solve = False + # sweep.enforce_causality = True + # sweep.enforce_dc_and_causality = True + # sweep.enforce_passivity = False + # sweep.freq_sweep_type = "kDiscreteSweep" + # sweep.interpolation_use_full_basis = False + # sweep.interpolation_use_port_impedance = False + # sweep.interpolation_use_prop_const = False + # sweep.max_solutions = 200 + # sweep.min_freq_s_mat_only_solve = "2MHz" + # sweep.min_solved_freq = "1Hz" + # sweep.passivity_tolerance = 0.0002 + # sweep.relative_s_error = 0.004 + # sweep.save_fields = True + # sweep.save_rad_fields_only = True + # sweep.use_q3d_for_dc = True + # + # assert sweep.adaptive_sampling + # assert sweep.adv_dc_extrapolation + # assert sweep.compute_dc_point + # assert not sweep.auto_s_mat_only_solve + # assert sweep.enforce_causality + # assert sweep.enforce_dc_and_causality + # assert not sweep.enforce_passivity + # assert sweep.freq_sweep_type == "kDiscreteSweep" + # assert not sweep.interpolation_use_full_basis + # assert not sweep.interpolation_use_port_impedance + # assert not sweep.interpolation_use_prop_const + # assert sweep.max_solutions == 200 + # assert sweep.min_freq_s_mat_only_solve == "2MHz" + # assert sweep.min_solved_freq == "1Hz" + # assert sweep.passivity_tolerance == 0.0002 + # assert sweep.relative_s_error == 0.004 + # assert sweep.save_fields + # assert sweep.save_rad_fields_only + # assert sweep.use_q3d_for_dc + # edb.close() + pass + + def test_siwave_create_port_between_pin_and_layer(self, edb_examples): + """Create circuit port between pin and a reference layer.""" + # Done + + edbapp = edb_examples.get_si_verse() + assert edbapp.siwave.create_port_between_pin_and_layer( + component_name="U1", pins_name="A27", layer_name="16_Bottom", reference_net="GND" + ) + U7 = edbapp.components["U7"] + assert U7.pins["G7"].create_port() + assert U7.pins["F7"].create_port(reference=U7.pins["E7"]) + pin_group = edbapp.siwave.create_pin_group_on_net( + reference_designator="U7", net_name="GND", group_name="U7_GND" + ) + assert pin_group + U7.pins["R9"].create_port(name="test", reference=pin_group) + padstack_instance_terminals = [ + term for term in list(edbapp.terminals.values()) if term.type.name == "PADSTACK_INST" + ] + for term in padstack_instance_terminals: + assert term.position + pos_pin = edbapp.padstacks.get_pinlist_from_component_and_net("C173")[1] + neg_pin = edbapp.padstacks.get_pinlist_from_component_and_net("C172")[0] + edbapp.create_port( + pos_pin.get_terminal(create_new_terminal=True), + neg_pin.get_terminal(create_new_terminal=True), + is_circuit_port=True, + name="test1", + ) + assert edbapp.ports["test1"] + edbapp.ports["test1"].is_circuit_port = True + assert edbapp.ports["test1"].is_circuit_port + edbapp.close() + + def test_siwave_source_setter(self): + """Evaluate siwave sources property.""" + # Done + source_path = os.path.join(local_path, "example_models", test_subfolder, "test_sources.aedb") + target_path = os.path.join(self.local_scratch.path, "test_134_source_setter.aedb") + self.local_scratch.copyfolder(source_path, target_path) + edbapp = Edb(target_path, edbversion=desktop_version, restart_rpc_server=True) + sources = list(edbapp.siwave.sources.values()) + sources[0].magnitude = 1.45 + assert sources[0].magnitude.value == 1.45 + sources[1].magnitude = 1.45 + assert sources[1].magnitude.value == 1.45 + edbapp.close() + + def test_delete_pingroup(self): + """Delete siwave pin groups.""" + # Done + source_path = os.path.join(local_path, "example_models", test_subfolder, "test_pin_group.aedb") + target_path = os.path.join(self.local_scratch.path, "test_135_pin_group.aedb") + self.local_scratch.copyfolder(source_path, target_path) + edbapp = Edb(target_path, edbversion=desktop_version, restart_rpc_server=True) + for _, pingroup in edbapp.siwave.pin_groups.items(): + pingroup.delete() + assert not edbapp.siwave.pin_groups + edbapp.close() + + def test_create_padstack_instance(self, edb_examples): + """Create padstack instances.""" + # Done + edb = Edb(edbversion=desktop_version, restart_rpc_server=True) + edb.stackup.add_layer(layer_name="1_Top", fillMaterial="air", thickness="30um") + edb.stackup.add_layer(layer_name="contact", fillMaterial="air", thickness="100um", base_layer="1_Top") + + assert edb.padstacks.create( + pad_shape="Rectangle", + padstackname="pad", + x_size="350um", + y_size="500um", + holediam=0, + ) + pad_instance1 = edb.padstacks.place(position=["-0.65mm", "-0.665mm"], definition_name="pad") + assert pad_instance1 + pad_instance1.start_layer = "1_Top" + pad_instance1.stop_layer = "1_Top" + assert pad_instance1.start_layer == "1_Top" + assert pad_instance1.stop_layer == "1_Top" + + assert edb.padstacks.create(pad_shape="Circle", padstackname="pad2", paddiam="350um", holediam="15um") + pad_instance2 = edb.padstacks.place(position=["-0.65mm", "-0.665mm"], definition_name="pad2") + assert pad_instance2 + pad_instance2.start_layer = "1_Top" + pad_instance2.stop_layer = "1_Top" + assert pad_instance2.start_layer == "1_Top" + assert pad_instance2.stop_layer == "1_Top" + + assert edb.padstacks.create( + pad_shape="Circle", + padstackname="test2", + paddiam="400um", + holediam="200um", + antipad_shape="Rectangle", + anti_pad_x_size="700um", + anti_pad_y_size="800um", + start_layer="1_Top", + stop_layer="1_Top", + ) + + pad_instance3 = edb.padstacks.place(position=["-1.65mm", "-1.665mm"], definition_name="test2") + assert pad_instance3.start_layer == "1_Top" + assert pad_instance3.stop_layer == "1_Top" + # TODO check with dev the Property ID + # pad_instance3.dcir_equipotential_region = True + # assert pad_instance3.dcir_equipotential_region + # pad_instance3.dcir_equipotential_region = False + # assert not pad_instance3.dcir_equipotential_region + + trace = edb.modeler.create_trace([[0, 0], [0, 10e-3]], "1_Top", "0.1mm", "trace_with_via_fence") + edb.padstacks.create("via_0") + trace.create_via_fence("1mm", "1mm", "via_0") + edb.close() + + def test_stackup_properties(self): + """Evaluate stackup properties.""" + # Done + edb = Edb(edbversion=desktop_version, restart_rpc_server=True) + edb.stackup.add_layer(layer_name="gnd", fillMaterial="air", thickness="10um") + edb.stackup.add_layer(layer_name="diel1", fillMaterial="air", thickness="200um", base_layer="gnd") + edb.stackup.add_layer(layer_name="sig1", fillMaterial="air", thickness="10um", base_layer="diel1") + edb.stackup.add_layer(layer_name="diel2", fillMaterial="air", thickness="200um", base_layer="sig1") + edb.stackup.add_layer(layer_name="sig3", fillMaterial="air", thickness="10um", base_layer="diel2") + assert edb.stackup.thickness == 0.00043 + assert edb.stackup.num_layers == 5 + edb.close() + + def test_hfss_extent_info(self): + """HFSS extent information.""" + + # TODO check config file 2.0 + + # from pyedb.grpc.database.primitive.primitive import Primitive + # + # config = { + # "air_box_horizontal_extent_enabled": False, + # "air_box_horizontal_extent": 0.01, + # "air_box_positive_vertical_extent": 0.3, + # "air_box_positive_vertical_extent_enabled": False, + # "air_box_negative_vertical_extent": 0.1, + # "air_box_negative_vertical_extent_enabled": False, + # "base_polygon": self.edbapp.modeler.polygons[0], + # "dielectric_base_polygon": self.edbapp.modeler.polygons[1], + # "dielectric_extent_size": 0.1, + # "dielectric_extent_size_enabled": False, + # "dielectric_extent_type": "conforming", + # "extent_type": "conforming", + # "honor_user_dielectric": False, + # "is_pml_visible": False, + # "open_region_type": "pml", + # "operating_freq": "2GHz", + # "radiation_level": 1, + # "sync_air_box_vertical_extent": False, + # "use_open_region": False, + # "use_xy_data_extent_for_vertical_expansion": False, + # "truncate_air_box_at_ground": True, + # } + # hfss_extent_info = self.edbapp.hfss.hfss_extent_info + # hfss_extent_info.load_config(config) + # exported_config = hfss_extent_info.export_config() + # for i, j in exported_config.items(): + # if not i in config: + # continue + # if isinstance(j, Primitive): + # assert j.id == config[i].id + # elif isinstance(j, EdbValue): + # assert j.tofloat == hfss_extent_info._get_edb_value(config[i]).ToDouble() + # else: + # assert j == config[i] + pass + + def test_import_gds_from_tech(self): + """Use techfile.""" + from pyedb.grpc.database.control_file import ControlFile + + c_file_in = os.path.join( + local_path, "example_models", "cad", "GDS", "sky130_fictitious_dtc_example_control_no_map.xml" + ) + c_map = os.path.join(local_path, "example_models", "cad", "GDS", "dummy_layermap.map") + gds_in = os.path.join(local_path, "example_models", "cad", "GDS", "sky130_fictitious_dtc_example.gds") + gds_out = os.path.join(self.local_scratch.path, "sky130_fictitious_dtc_example.gds") + self.local_scratch.copyfile(gds_in, gds_out) + + c = ControlFile(c_file_in, layer_map=c_map) + setup = c.setups.add_setup("Setup1", "1GHz") + setup.add_sweep("Sweep1", "0.01GHz", "5GHz", "0.1GHz") + c.boundaries.units = "um" + c.stackup.units = "um" + c.boundaries.add_port("P1", x1=223.7, y1=222.6, layer1="Metal6", x2=223.7, y2=100, layer2="Metal6") + c.boundaries.add_extent() + comp = c.components.add_component("B1", "BGA", "IC", "Flip chip", "Cylinder") + comp.solder_diameter = "65um" + comp.add_pin("1", "81.28", "84.6", "met2") + comp.add_pin("2", "211.28", "84.6", "met2") + comp.add_pin("3", "211.28", "214.6", "met2") + comp.add_pin("4", "81.28", "214.6", "met2") + for via in c.stackup.vias: + via.create_via_group = True + via.snap_via_group = True + c.write_xml(os.path.join(self.local_scratch.path, "test_138.xml")) + c.import_options.import_dummy_nets = True + + # TODO check why GDS import fails with 2025.2. + + # edb = Edb(edbpath=gds_out, edbversion=desktop_version, + # technology_file=os.path.join(self.local_scratch.path, "test_138.xml"), restart_rpc_server=True + # ) + # + # assert edb + # assert "P1" in edb.excitations + # assert "Setup1" in edb.setups + # assert "B1" in edb.components.instances + # edb.close() + + def test_database_properties(self, edb_examples): + """Evaluate database properties.""" + # Done + edb = edb_examples.get_si_verse() + assert isinstance(edb.dataset_defs, list) + assert isinstance(edb.material_defs, list) + assert isinstance(edb.component_defs, list) + assert isinstance(edb.package_defs, list) + assert isinstance(edb.padstack_defs, list) + assert isinstance(edb.jedec5_bondwire_defs, list) + assert isinstance(edb.jedec4_bondwire_defs, list) + assert isinstance(edb.apd_bondwire_defs, list) + assert isinstance(edb.version, tuple) + assert isinstance(edb.footprint_cells, list) + edb.close() + + def test_backdrill_via_with_offset(self): + """Set backdrill from top.""" + + # TODO when material init is fixed + from ansys.edb.core.utility.value import Value as GrpcValue + + edb = Edb(edbversion=desktop_version, restart_rpc_server=True) + edb.stackup.add_layer(layer_name="bot") + edb.stackup.add_layer(layer_name="diel1", base_layer="bot", layer_type="dielectric", thickness="127um") + edb.stackup.add_layer(layer_name="signal1", base_layer="diel1") + edb.stackup.add_layer(layer_name="diel2", base_layer="signal1", layer_type="dielectric", thickness="127um") + edb.stackup.add_layer(layer_name="signal2", base_layer="diel2") + edb.stackup.add_layer(layer_name="diel3", base_layer="signal2", layer_type="dielectric", thickness="127um") + edb.stackup.add_layer(layer_name="top", base_layer="diel2") + edb.padstacks.create(padstackname="test1") + padstack_instance = edb.padstacks.place(position=[0, 0], net_name="test", definition_name="test1") + edb.padstacks.definitions["test1"].hole_range = "through" + drill_layer = edb.stackup.layers["signal1"] + drill_diameter = GrpcValue("200um") + drill_offset = GrpcValue("100um") + padstack_instance.set_back_drill_by_layer( + drill_to_layer=drill_layer, diameter=drill_diameter, offset=drill_offset + ) + assert padstack_instance.backdrill_type == "layer_drill" + assert padstack_instance.get_back_drill_by_layer() + layer, offset, diameter = padstack_instance.get_back_drill_by_layer() + assert layer == "signal1" + assert offset == 100e-6 + assert diameter == 200e-6 + # padstack_instance2 = edb.padstacks.place(position=[0.5, 0.5], net_name="test", definition_name="test1") + # padstack_instance2.set_back_drill_by_layer(drill_to_layer=drill_layer, + # diameter=drill_diameter, + # offset=drill_offset, + # from_bottom=False) + # assert padstack_instance2.get_back_drill_by_layer(from_bottom=False) + # layer2, offset2, diameter2 = padstack_instance2.get_back_drill_by_layer() + # assert layer2 == "signal1" + # assert offset2 == 100e-6 + # assert diameter2 == 200e-6 + edb.close() + + def test_add_layer_api_with_control_file(self): + """Add new layers with control file.""" + from pyedb.grpc.database.control_file import ControlFile + + # Done + ctrl = ControlFile() + # Material + ctrl.stackup.add_material(material_name="Copper", conductivity=5.56e7) + ctrl.stackup.add_material(material_name="BCB", permittivity=2.7) + ctrl.stackup.add_material(material_name="Silicon", conductivity=0.04) + ctrl.stackup.add_material(material_name="SiliconOxide", conductivity=4.4) + ctrl.stackup.units = "um" + assert len(ctrl.stackup.materials) == 4 + assert ctrl.stackup.units == "um" + # Dielectrics + ctrl.stackup.add_dielectric(material="Silicon", layer_name="Silicon", thickness=180) + ctrl.stackup.add_dielectric(layer_index=1, material="SiliconOxide", layer_name="USG1", thickness=1.2) + assert next(diel for diel in ctrl.stackup.dielectrics if diel.name == "USG1").properties["Index"] == 1 + ctrl.stackup.add_dielectric(material="BCB", layer_name="BCB2", thickness=9.5, base_layer="USG1") + ctrl.stackup.add_dielectric( + material="BCB", layer_name="BCB1", thickness=4.1, base_layer="BCB2", add_on_top=False + ) + ctrl.stackup.add_dielectric(layer_index=4, material="BCB", layer_name="BCB3", thickness=6.5) + assert ctrl.stackup.dielectrics[0].properties["Index"] == 0 + assert ctrl.stackup.dielectrics[1].properties["Index"] == 1 + assert ctrl.stackup.dielectrics[2].properties["Index"] == 3 + assert ctrl.stackup.dielectrics[3].properties["Index"] == 2 + assert ctrl.stackup.dielectrics[4].properties["Index"] == 4 + # Metal layer + ctrl.stackup.add_layer( + layer_name="9", elevation=185.3, material="Copper", target_layer="meta2", gds_type=0, thickness=6 + ) + assert [layer for layer in ctrl.stackup.layers if layer.name == "9"] + ctrl.stackup.add_layer( + layer_name="15", elevation=194.8, material="Copper", target_layer="meta3", gds_type=0, thickness=3 + ) + assert [layer for layer in ctrl.stackup.layers if layer.name == "15"] + # Via layer + ctrl.stackup.add_via( + layer_name="14", material="Copper", target_layer="via2", start_layer="meta2", stop_layer="meta3", gds_type=0 + ) + assert [layer for layer in ctrl.stackup.vias if layer.name == "14"] + # Port + ctrl.boundaries.add_port( + "test_port", x1=-21.1, y1=-288.7, layer1="meta3", x2=21.1, y2=-288.7, layer2="meta3", z0=50 + ) + assert ctrl.boundaries.ports + # setup using q3D for DC point + setup = ctrl.setups.add_setup("test_setup", "10GHz") + assert setup + setup.add_sweep( + name="test_sweep", + start="0GHz", + stop="20GHz", + step="10MHz", + sweep_type="Interpolating", + step_type="LinearStep", + use_q3d=True, + ) + assert setup.sweeps + + @pytest.mark.skipif(is_linux, reason="Failing download files") + def test_create_edb_with_dxf(self): + """Create EDB from dxf file.""" + # Done + src = os.path.join(local_path, "example_models", test_subfolder, "edb_test_82.dxf") + dxf_path = self.local_scratch.copyfile(src) + edb3 = Edb(dxf_path, edbversion=desktop_version, restart_rpc_server=True) + assert len(edb3.modeler.polygons) == 1 + assert edb3.modeler.polygons[0].polygon_data.points == [ + (0.0, 0.0), + (0.0, 0.0012), + (-0.0008, 0.0012), + (-0.0008, 0.0), + ] + edb3.close() + + @pytest.mark.skipif(is_linux, reason="Not supported in IPY") + def test_solve_siwave(self): + """Solve EDB with Siwave.""" + # Done + target_path = os.path.join(local_path, "example_models", "T40", "ANSYS-HSD_V1_DCIR.aedb") + out_edb = os.path.join(self.local_scratch.path, "to_be_solved.aedb") + self.local_scratch.copyfolder(target_path, out_edb) + edbapp = Edb(out_edb, edbversion=desktop_version, restart_rpc_server=True) + edbapp.siwave.create_exec_file(add_dc=True) + out = edbapp.solve_siwave() + assert os.path.exists(out) + res = edbapp.export_siwave_dc_results(out, "SIwaveDCIR1") + for i in res: + assert os.path.exists(i) + + def test_cutout_return_clipping_extent(self, edb_examples): + """""" + # Done + edbapp = edb_examples.get_si_verse() + extent = edbapp.cutout( + signal_list=["PCIe_Gen4_RX0_P", "PCIe_Gen4_RX0_N", "PCIe_Gen4_RX1_P", "PCIe_Gen4_RX1_N"], + reference_list=["GND"], + ) + assert extent + assert len(extent) == 55 + assert extent[0] == [0.011025799607142596, 0.04451508809926884] + assert extent[10] == [0.02214231174553801, 0.02851039223066996] + assert extent[20] == [0.06722930402216426, 0.02605468368384399] + assert extent[30] == [0.06793706871543964, 0.02961898967909681] + assert extent[40] == [0.0655032742298304, 0.03147893183305721] + assert extent[50] == [0.01143465157862367, 0.046365530038092975] + edbapp.close_edb() + + def test_move_and_edit_polygons(self): + """Move a polygon.""" + # Done + target_path = os.path.join(self.local_scratch.path, "test_move_edit_polygons", "test.aedb") + edbapp = Edb(target_path, edbversion=desktop_version, restart_rpc_server=True, kill_all_instances=True) + + edbapp.stackup.add_layer("GND") + edbapp.stackup.add_layer("Diel", "GND", layer_type="dielectric", thickness="0.1mm", material="FR4_epoxy") + edbapp.stackup.add_layer("TOP", "Diel", thickness="0.05mm") + points = [[0.0, -1e-3], [0.0, -10e-3], [100e-3, -10e-3], [100e-3, -1e-3], [0.0, -1e-3]] + polygon = edbapp.modeler.create_polygon(points, "TOP") + assert polygon.center == [0.05, -0.0055] + assert polygon.move(["1mm", 1e-3]) + assert round(polygon.center[0], 6) == 0.051 + assert round(polygon.center[1], 6) == -0.0045 + + assert polygon.rotate(angle=45) + assert polygon.bbox == [0.012462681128504282, -0.043037320277837944, 0.08953731887149571, 0.03403732027783795] + assert polygon.rotate(angle=34, center=[0, 0]) + assert polygon.bbox == [0.030839512681298656, -0.02515183168439915, 0.05875505700187538, 0.07472816760474396] + assert polygon.scale(factor=1.5) + assert polygon.bbox == [0.023860626601154476, -0.05012183150668493, 0.06573394308201956, 0.09969816742702975] + assert polygon.scale(factor=-0.5, center=[0, 0]) + assert polygon.bbox == [ + -0.03286697154100978, + -0.049849083713514875, + -0.011930313300577238, + 0.025060915753342464, + ] + assert polygon.move_layer("GND") + assert len(edbapp.modeler.polygons) == 1 + assert edbapp.modeler.polygons[0].layer_name == "GND" + edbapp.close() + + def test_multizone(self, edb_examples): + # Done + # edbapp = edb_examples.get_multizone_pcb() + # common_reference_net = "gnd" + # edb_zones = edbapp.copy_zones() + # assert edb_zones + # defined_ports, project_connexions = edbapp.cutout_multizone_layout(edb_zones, common_reference_net) + # assert defined_ports + # assert project_connexions + pass + + def test_icepak(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse(additional_files_folders=["siwave/icepak_component.pwrd"]) + edbapp.siwave.icepak_use_minimal_comp_defaults = True + assert edbapp.siwave.icepak_use_minimal_comp_defaults + edbapp.siwave.icepak_use_minimal_comp_defaults = False + assert not edbapp.siwave.icepak_use_minimal_comp_defaults + edbapp.siwave.icepak_component_file = edb_examples.get_local_file_folder("siwave/icepak_component.pwrd") + assert edbapp.siwave.icepak_component_file == edb_examples.get_local_file_folder("siwave/icepak_component.pwrd") + edbapp.close() + + def test_dcir_properties(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + setup = edbapp.create_siwave_dc_setup() + setup.settings.export_dc_thermal_data = True + assert setup.settings.export_dc_thermal_data + assert not setup.settings.import_thermal_data + setup.settings.dc_report_show_active_devices = True + assert setup.settings.dc_report_show_active_devices + assert not setup.settings.per_pin_use_pin_format + setup.settings.use_loop_res_for_per_pin = True + assert setup.settings.use_loop_res_for_per_pin + setup.settings.dc_report_config_file = edbapp.edbpath + assert setup.settings.dc_report_config_file + setup.settings.full_dc_report_path = edbapp.edbpath + assert setup.settings.full_dc_report_path + setup.settings.icepak_temp_file = edbapp.edbpath + assert setup.settings.icepak_temp_file + setup.settings.per_pin_res_path = edbapp.edbpath + assert setup.settings.per_pin_res_path + setup.settings.via_report_path = edbapp.edbpath + assert setup.settings.via_report_path + setup.settings.source_terms_to_ground = {"test": 1} + assert setup.settings.source_terms_to_ground + edbapp.close() + + def test_arbitrary_wave_ports(self): + # TODO check later when sever instances is improved. + example_folder = os.path.join(local_path, "example_models", test_subfolder) + source_path_edb = os.path.join(example_folder, "example_arbitrary_wave_ports.aedb") + target_path_edb = os.path.join(self.local_scratch.path, "test_wave_ports", "test.aedb") + self.local_scratch.copyfolder(source_path_edb, target_path_edb) + edbapp = Edb(target_path_edb, desktop_version, restart_rpc_server=True) + assert edbapp.create_model_for_arbitrary_wave_ports( + temp_directory=self.local_scratch.path, + output_edb="wave_ports.aedb", + mounting_side="top", + ) + edb_model = os.path.join(self.local_scratch.path, "wave_ports.aedb") + assert os.path.isdir(edb_model) + edbapp.close() + + def test_bondwire(self, edb_examples): + # TODO check bug #450 change trajectory and start end elevation. + # Done + edbapp = edb_examples.get_si_verse() + bondwire_1 = edbapp.modeler.create_bondwire( + definition_name="Default", + placement_layer="Postprocessing", + width="0.5mm", + material="copper", + start_layer_name="1_Top", + start_x="82mm", + start_y="30mm", + end_layer_name="1_Top", + end_x="71mm", + end_y="23mm", + bondwire_type="apd", + net="1V0", + start_cell_instance_name="test", + ) + bondwire_1.material = "Gold" + assert bondwire_1.material == "Gold" + bondwire_1.type = "jedec4" + assert bondwire_1.type == "jedec4" + bondwire_1.cross_section_type = "round" + assert bondwire_1.cross_section_type == "round" + bondwire_1.cross_section_height = "0.1mm" + assert bondwire_1.cross_section_height == 0.0001 + bondwire_1.set_definition_name("J4_LH10") + assert bondwire_1.get_definition_name() == "J4_LH10" + bondwire_1.trajectory = [1, 0.1, 0.2, 0.3] + assert bondwire_1.trajectory == [1, 0.1, 0.2, 0.3] + bondwire_1.width = "0.2mm" + assert bondwire_1.width == 0.0002 + # bondwire_1.start_elevation = "16_Bottom" + # bondwire_1.end_elevation = "16_Bottom" + # assert len(edbapp.layout.bondwires) == 1 + edbapp.close() + + def test_voltage_regulator(self, edb_examples): + # TODO is not working with EDB NET not implemented yet in Grpc + # edbapp = edb_examples.get_si_verse() + # positive_sensor_pin = edbapp.components["U1"].pins["A2"] + # negative_sensor_pin = edbapp.components["U1"].pins["A3"] + # vrm = edbapp.siwave.create_vrm_module( + # name="test", + # positive_sensor_pin=positive_sensor_pin, + # negative_sensor_pin=negative_sensor_pin, + # voltage="1.5V", + # load_regulation_current="0.5A", + # load_regulation_percent=0.2, + # ) + # assert vrm.component + # assert vrm.component.refdes == "U1" + # assert vrm.negative_remote_sense_pin + # assert vrm.negative_remote_sense_pin.name == "U1-A3" + # assert vrm.positive_remote_sense_pin + # assert vrm.positive_remote_sense_pin.name == "U1-A2" + # assert vrm.voltage == 1.5 + # assert vrm.is_active + # assert not vrm.is_null + # assert vrm.id + # assert edbapp.voltage_regulator_modules + # assert "test" in edbapp.voltage_regulator_modules + # edbapp.close() + pass + + def test_workflow(self, edb_examples): + # TODO check with config file 2.0 + + # edbapp = edb_examples.get_si_verse() + # path_bom = Path(edb_examples.test_folder) / "bom.csv" + # edbapp.workflow.export_bill_of_materials(path_bom) + # assert path_bom.exists() + # edbapp.close() + pass + + def test_create_port_on_component_no_ref_pins_in_component(self, edb_examples): + # Done + edbapp = edb_examples.get_no_ref_pins_component() + edbapp.components.create_port_on_component( + component="J2E2", + net_list=[ + "net1", + "net2", + "net3", + "net4", + "net5", + "net6", + "net7", + "net8", + "net9", + "net10", + "net11", + "net12", + "net13", + "net14", + "net15", + ], + port_type="circuit_port", + reference_net=["GND"], + extend_reference_pins_outside_component=True, + ) + assert len(edbapp.ports) == 15 + edbapp.close() + + def test_create_ping_group(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.modeler.create_pin_group( + name="test1", pins_by_id=[4294969495, 4294969494, 4294969496, 4294969497] + ) + + assert edbapp.modeler.create_pin_group( + name="test2", pins_by_id=[4294969502, 4294969503], pins_by_aedt_name=["U1-A11", "U1-A12", "U1-A13"] + ) + assert edbapp.modeler.create_pin_group( + name="test3", + pins_by_id=[4294969502, 4294969503], + pins_by_aedt_name=["U1-A11", "U1-A12", "U1-A13"], + pins_by_name=["A11", "A12", "A15", "A16"], + ) + edbapp.close() + + def test_create_edb_with_zip(self): + """Create EDB from zip file.""" + # Done + src = os.path.join(local_path, "example_models", "TEDB", "ANSYS-HSD_V1_0.zip") + zip_path = self.local_scratch.copyfile(src) + edb = Edb(zip_path, edbversion=desktop_version, restart_rpc_server=True) + assert edb.nets + assert edb.components + edb.close() diff --git a/tests/grpc/system/test_edb_components.py b/tests/grpc/system/test_edb_components.py new file mode 100644 index 0000000000..9a4c32591c --- /dev/null +++ b/tests/grpc/system/test_edb_components.py @@ -0,0 +1,636 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb components +""" +import math +import os + +import pytest + +from pyedb.grpc.database.hierarchy.component import Component +from pyedb.grpc.edb import EdbGrpc as Edb +from tests.conftest import desktop_version, local_path +from tests.legacy.system.conftest import test_subfolder + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + +bom_example = "bom_example.csv" + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch, target_path, target_path2, target_path4): + self.local_scratch = local_scratch + self.target_path = target_path + self.target_path2 = target_path2 + self.target_path4 = target_path4 + + def test_components_get_pin_from_component(self, edb_examples): + """Evaluate access to a pin from a component.""" + # Done + edb = edb_examples.get_si_verse() + comp = edb.components.get_component_by_name("J1") + assert comp is not None + pin = edb.components.get_pin_from_component("J1", pin_name="1") + assert pin[0].name == "1" + edb.close() + + def test_components_create_coax_port_on_component(self, edb_examples): + """Create a coaxial port on a component from its pin.""" + # Done + edb = edb_examples.get_si_verse() + coax_port = edb.components["U6"].pins["R3"].create_coax_port("coax_port") + coax_port.radial_extent_factor = 3 + assert coax_port.radial_extent_factor == 3 + assert coax_port.component + assert edb.components["U6"].pins["R3"].get_terminal() + assert edb.components["U6"].pins["R3"].id + assert edb.terminals + assert edb.ports + assert len(edb.components["U6"].pins["R3"].get_connected_objects()) == 1 + edb.close() + + def test_components_properties(self, edb_examples): + """Access components properties.""" + # Done + edb = edb_examples.get_si_verse() + assert len(edb.components.instances) > 2 + assert len(edb.components.inductors) > 0 + assert len(edb.components.resistors) > 0 + assert len(edb.components.capacitors) > 0 + assert len(edb.components.ICs) > 0 + assert len(edb.components.IOs) > 0 + assert len(edb.components.Others) > 0 + edb.close() + + def test_components_rlc_components_values(self, edb_examples): + """Update values of an RLC component.""" + # Done + edb = edb_examples.get_si_verse() + assert edb.components.set_component_rlc("C1", res_value=0.1, cap_value="5e-6", ind_value=1e-9, isparallel=False) + component = edb.components.instances["C1"] + assert component.rlc_values == [0.1, 1e-9, 5e-6] + assert edb.components.set_component_rlc("L10", res_value=1e-3, ind_value="10e-6", isparallel=True) + component = edb.components.instances["L10"] + assert component.rlc_values == [1e-3, 10e-6, 0.0] + edb.close() + + def test_components_r1_queries(self, edb_examples): + """Evaluate queries over component R1.""" + # Done + edb = edb_examples.get_si_verse() + assert "R1" in list(edb.components.instances.keys()) + assert not edb.components.instances["R1"].is_null + assert edb.components.instances["R1"].res_value == 6200 + assert edb.components.instances["R1"].placement_layer == "16_Bottom" + assert not edb.components.instances["R1"].component_def.is_null + assert edb.components.instances["R1"].location == [0.11167500144, 0.04072499856] + assert edb.components.instances["R1"].lower_elevation == 0.0 + assert edb.components.instances["R1"].upper_elevation == 35e-6 + assert edb.components.instances["R1"].top_bottom_association == 2 + assert len(edb.components.instances["R1"].pinlist) == 2 + assert edb.components.instances["R1"].pins + assert edb.components.instances["R1"].pins["1"].name == "1" + assert edb.components.instances["R1"].pins["1"].component_pin == "1" + + assert not edb.components.instances["R1"].pins["1"].component.is_null + assert edb.components.instances["R1"].pins["1"].placement_layer == edb.components.instances["R1"].layer.name + assert ( + edb.components.instances["R1"].pins["1"].layer.upper_elevation + == edb.components.instances["R1"].layer.upper_elevation + ) + assert ( + edb.components.instances["R1"].pins["1"].layer.top_bottom_association + == edb.components.instances["R1"].layer.top_bottom_association + ) + assert edb.components.instances["R1"].pins["1"].position == [0.111675, 0.039975] + assert edb.components.instances["R1"].pins["1"].rotation == -1.5707963267949 + edb.close() + + def test_components_create_clearance_on_component(self, edb_examples): + """Evaluate the creation of a clearance on soldermask.""" + # Done + edb = edb_examples.get_si_verse() + comp = edb.components.instances["U1"] + assert comp.create_clearance_on_component() + edb.close() + + def test_components_get_components_from_nets(self, edb_examples): + """Access to components from nets.""" + # Done + edb = edb_examples.get_si_verse() + assert edb.components.get_components_from_nets("DDR4_DQS0_P") + edb.close() + + def test_components_resistors(self, edb_examples): + """Evaluate component resistors.""" + # Done + edb = edb_examples.get_si_verse() + assert "R1" in list(edb.components.resistors.keys()) + assert "C1" not in list(edb.components.resistors.keys()) + assert "C1" in list(edb.components.capacitors.keys()) + assert "R1" not in list(edb.components.capacitors.keys()) + assert "L10" in list(edb.components.inductors.keys()) + assert "R1" not in list(edb.components.inductors.keys()) + assert "U1" in list(edb.components.ICs.keys()) + assert "R1" not in list(edb.components.ICs.keys()) + assert "X1" in list(edb.components.IOs.keys()) + assert "R1" not in list(edb.components.IOs.keys()) + assert "B1" in edb.components.Others + assert "R1" not in edb.components.Others + comp = edb.components.components_by_partname + assert "ALTR-FBGA24_A-130" in comp + assert len(comp["ALTR-FBGA24_A-130"]) == 1 + edb.components.get_through_resistor_list(10) + assert len(edb.components.get_rats()) > 0 + assert len(edb.components.get_component_net_connection_info("U1")) > 0 + edb.close() + + def test_components_get_pin_name_and_position(self, edb_examples): + """Retrieve component name and position.""" + # Done + edb = edb_examples.get_si_verse() + cmp_pinlist = edb.padstacks.get_pinlist_from_component_and_net("U6", "GND") + pin_name = edb.components.get_aedt_pin_name(cmp_pinlist[0]) + assert type(pin_name) is str + assert len(pin_name) > 0 + assert len(cmp_pinlist[0].position) == 2 + assert len(edb.components.get_pin_position(cmp_pinlist[0])) == 2 + edb.close() + + def test_components_get_pins_name_from_net(self, edb_examples): + """Retrieve pins belonging to a net.""" + # Done + edb = edb_examples.get_si_verse() + cmp_pinlist = edb.components.get_pin_from_component("U6") + assert len(edb.components.get_pins_name_from_net("GND", cmp_pinlist)) > 0 + assert len(edb.components.get_pins_name_from_net("5V", cmp_pinlist)) == 0 + edb.close() + + def test_components_delete_single_pin_rlc(self, edb_examples): + """Delete all RLC components with a single pin.""" + # Done + edb = edb_examples.get_si_verse() + assert len(edb.components.delete_single_pin_rlc()) == 0 + edb.close() + + def test_components_set_component_rlc(self, edb_examples): + """Update values for an RLC component.""" + # Done + edb = edb_examples.get_si_verse() + assert edb.components.set_component_rlc("R1", 30, 1e-9, 1e-12) + assert edb.components.disable_rlc_component("R1") + assert edb.components.delete("R1") + edb.close() + + def test_components_set_model(self, edb_examples): + """Assign component model.""" + # Done + edb = edb_examples.get_si_verse() + assert edb.components.set_component_model( + "C10", + modelpath=os.path.join( + local_path, + "example_models", + test_subfolder, + "GRM32ER72A225KA35_25C_0V.sp", + ), + modelname="GRM32ER72A225KA35_25C_0V", + ) + assert not edb.components.set_component_model( + "C100000", + modelpath=os.path.join( + local_path, + test_subfolder, + "GRM32ER72A225KA35_25C_0V.sp", + ), + modelname="GRM32ER72A225KA35_25C_0V", + ) + edb.close() + + def test_modeler_parametrize_layout(self, edb_examples): + """Parametrize a polygon""" + # Done + edb = edb_examples.get_si_verse() + assert len(edb.modeler.polygons) > 0 + for el in edb.modeler.polygons: + if el.edb_uid == 5953: + poly = el + for el in edb.modeler.polygons: + if el.edb_uid == 5954: + selection_poly = el + assert edb.modeler.parametrize_polygon(poly, selection_poly) + edb.close() + + def test_components_update_from_bom(self, edb_examples): + """Update components with values coming from a BOM file.""" + # Done + edb = edb_examples.get_si_verse() + assert edb.components.update_rlc_from_bom( + os.path.join(local_path, "example_models", test_subfolder, bom_example), + delimiter=",", + valuefield="Value", + comptype="Prod name", + refdes="RefDes", + ) + assert not edb.components.instances["R2"].enabled + edb.components.instances["R2"].enabled = True + assert edb.components.instances["R2"].enabled + edb.close() + + def test_components_export_bom(self, edb_examples): + """Export Bom file from layout.""" + # TODO check why add_member is failing + edb = edb_examples.get_si_verse() + edb.components.import_bom(os.path.join(local_path, "example_models", test_subfolder, "bom_example_2.csv")) + assert not edb.components.instances["R2"].enabled + assert edb.components.instances["U13"].partname == "SLAB-QFN-24-2550x2550TP_V" + + export_bom_path = os.path.join(self.local_scratch.path, "export_bom.csv") + assert edb.components.export_bom(export_bom_path) + edb.close() + + def test_components_create_component_from_pins(self, edb_examples): + """Create a component from a pin.""" + # TODO check bug 451 transform setter + edb = edb_examples.get_si_verse() + pins = edb.components.get_pin_from_component("R13") + component = edb.components.create(pins, "newcomp") + assert component + assert component.part_name == "newcomp" + assert len(component.pins) == 2 + edb.close() + + def test_convert_resistor_value(self): + """Convert a resistor value.""" + # Done + from pyedb.grpc.database.components import resistor_value_parser + + assert resistor_value_parser("100meg") + + def test_components_create_solder_ball_on_component(self, edb_examples): + """Set cylindrical solder balls on a given component""" + # Done + edb = edb_examples.get_si_verse() + assert edb.components.set_solder_ball("U1", shape="Spheroid") + assert edb.components.set_solder_ball("U6", sball_height=None) + assert edb.components.set_solder_ball( + "U6", sball_height="100um", auto_reference_size=False, chip_orientation="chip_up" + ) + edb.close() + + def test_components_short_component(self, edb_examples): + """Short pins of component with a trace.""" + # Done + edb = edb_examples.get_si_verse() + assert edb.components.short_component_pins("U12", width=0.2e-3) + assert edb.components.short_component_pins("U10", ["2", "5"]) + edb.close() + + def test_components_type(self, edb_examples): + """Retrieve components type.""" + # Done + edb = edb_examples.get_si_verse() + comp = edb.components["R4"] + comp.type = "resistor" + assert comp.type == "resistor" + comp.type = "inductor" + assert comp.type == "inductor" + comp.type = "capacitor" + assert comp.type == "capacitor" + comp.type = "io" + assert comp.type == "io" + comp.type = "ic" + assert comp.type == "ic" + comp.type = "other" + assert comp.type == "other" + edb.close() + + def test_componenets_deactivate_rlc(self, edb_examples): + """Deactivate RLC component and convert to a circuit port.""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.components.deactivate_rlc_component(component="C1", create_circuit_port=False) + assert edbapp.ports["C1"] + assert edbapp.components["C1"].is_enabled is False + assert edbapp.components.deactivate_rlc_component(component="C2", create_circuit_port=True) + edbapp.components["C2"].is_enabled = False + assert edbapp.components["C2"].is_enabled is False + edbapp.components["C2"].is_enabled = True + assert edbapp.components["C2"].is_enabled is True + pins = [*edbapp.components.instances["L10"].pins.values()] + edbapp.components.create_port_on_pins("L10", pins[0], pins[1]) + assert edbapp.components["L10"].is_enabled is False + assert "L10" in edbapp.ports.keys() + + def test_components_definitions(self, edb_examples): + """Evaluate components definition.""" + edbapp = edb_examples.get_si_verse() + assert edbapp.components.instances + assert edbapp.components.definitions + comp_def = edbapp.components.definitions["CAPC2012X12N"] + assert comp_def + comp_def.part_name = "CAPC2012X12N_new" + assert comp_def.part_name == "CAPC2012X12N_new" + assert len(comp_def.components) > 0 + cap = edbapp.components.definitions["CAPC2012X12N_new"] + assert cap.type == "capacitor" + cap.type = "resistor" + assert cap.type == "resistor" + + export_path = os.path.join(self.local_scratch.path, "comp_definition.csv") + # TODO check config file 2.0 + # assert edbapp.components.export_definition(export_path) + # assert edbapp.components.import_definition(export_path) + + # assert edbapp.components.definitions["CAPC3216X180X20ML20"].assign_rlc_model(1, 2, 3) + # sparam_path = os.path.join(local_path, "example_models", test_subfolder, "GRM32_DC0V_25degC_series.s2p") + # assert edbapp.components.definitions["CAPC3216X180X55ML20T25"].assign_s_param_model(sparam_path) + # ref_file = edbapp.components.definitions["CAPC3216X180X55ML20T25"].reference_file + # assert ref_file + # spice_path = os.path.join(local_path, "example_models", test_subfolder, "GRM32_DC0V_25degC.mod") + # assert edbapp.components.definitions["CAPMP7343X31N"].assign_spice_model(spice_path) + edbapp.close() + + def test_rlc_component_values_getter_setter(self, edb_examples): + """Evaluate component values getter and setter.""" + # Done + edbapp = edb_examples.get_si_verse() + components_to_change = [res for res in list(edbapp.components.Others.values()) if res.partname == "A93549-027"] + for res in components_to_change: + res.type = "Resistor" + res.res_value = [25, 0, 0] + res.res_value = 10 + assert res.res_value == 10 + res.rlc_values = [20, 1e-9, 1e-12] + assert res.res_value == 20 + assert res.ind_value == 1e-9 + assert res.cap_value == 1e-12 + res.res_value = 12.5 + assert res.res_value == 12.5 and res.ind_value == 1e-9 and res.cap_value == 1e-12 + res.ind_value = 5e-9 + assert res.res_value == 12.5 and res.ind_value == 5e-9 and res.cap_value == 1e-12 + res.cap_value = 8e-12 + assert res.res_value == 12.5 and res.ind_value == 5e-9 and res.cap_value == 8e-12 + edbapp.close() + + def test_create_port_on_pin(self, edb_examples): + """Create port on pins.""" + # Done + edbapp = edb_examples.get_si_verse() + pin = "A24" + ref_pins = [pin for pin in list(edbapp.components["U1"].pins.values()) if pin.net_name == "GND"] + assert edbapp.components.create_port_on_pins(refdes="U1", pins=pin, reference_pins=ref_pins) + assert edbapp.components.create_port_on_pins(refdes="U1", pins="C1", reference_pins=["A11"]) + assert edbapp.components.create_port_on_pins(refdes="U1", pins="C2", reference_pins=["A11"]) + assert edbapp.components.create_port_on_pins(refdes="U1", pins=["A24"], reference_pins=["A11", "A16"]) + assert edbapp.components.create_port_on_pins(refdes="U1", pins=["A26"], reference_pins=["A11", "A16", "A17"]) + assert edbapp.components.create_port_on_pins(refdes="U1", pins=["A28"], reference_pins=["A11", "A16"]) + edbapp.close() + + def test_replace_rlc_by_gap_boundaries(self, edb_examples): + """Replace RLC component by RLC gap boundaries.""" + # Done + edbapp = edb_examples.get_si_verse() + for refdes, cmp in edbapp.components.instances.items(): + edbapp.components.replace_rlc_by_gap_boundaries(refdes) + rlc_list = [term for term in edbapp.active_layout.terminals if term.boundary_type == "rlc"] + assert len(rlc_list) == 944 + edbapp.close() + + def test_components_get_component_placement_vector(self, edb_examples): + """Get the placement vector between 2 components.""" + # Done + edbapp = edb_examples.get_si_verse() + edb2 = Edb(self.target_path4, edbversion=desktop_version) + for _, cmp in edb2.components.instances.items(): + assert isinstance(cmp.solder_ball_placement, int) + mounted_cmp = edb2.components.get_component_by_name("BGA") + hosting_cmp = edbapp.components.get_component_by_name("U1") + ( + result, + vector, + rotation, + solder_ball_height, + ) = edbapp.components.get_component_placement_vector( + mounted_component=mounted_cmp, + hosting_component=hosting_cmp, + mounted_component_pin1="A10", + mounted_component_pin2="A12", + hosting_component_pin1="A2", + hosting_component_pin2="A4", + ) + assert result + assert abs(abs(rotation) - math.pi / 2) * 180 / math.pi == 90.0 + assert solder_ball_height == 0.00033 + assert len(vector) == 2 + edbapp.close(terminate_rpc_session=False) + edb2.close() + + def test_components_assign(self, edb_examples): + """Assign RLC model, S-parameter model and spice model.""" + # Done + edbapp = edb_examples.get_si_verse() + sparam_path = os.path.join(local_path, "example_models", test_subfolder, "GRM32_DC0V_25degC_series.s2p") + spice_path = os.path.join(local_path, "example_models", test_subfolder, "GRM32_DC0V_25degC.mod") + comp = edbapp.components.instances["R2"] + assert not comp.assign_rlc_model() + comp.assign_rlc_model(1, None, 3, False) + assert ( + not comp.is_parallel_rlc + and float(comp.res_value) == 1 + and float(comp.ind_value) == 0 + and float(comp.cap_value) == 3 + ) + comp.assign_rlc_model(1, 2, 3, True) + assert comp.is_parallel_rlc + assert ( + comp.is_parallel_rlc + and float(comp.res_value) == 1 + and float(comp.ind_value) == 2 + and float(comp.cap_value) == 3 + ) + assert comp.rlc_values + assert not comp.spice_model and not comp.s_param_model and not comp.netlist_model + comp.assign_s_param_model(sparam_path) + assert comp.s_param_model + assert not comp.s_param_model.is_null + comp.assign_spice_model(spice_path) + assert comp.spice_model + comp.type = "inductor" + comp.value = 10 # This command set the model back to ideal RLC + assert comp.type == "inductor" and comp.value == 10 and float(comp.ind_value) == 10 + + edbapp.components["C164"].assign_spice_model( + spice_path, sub_circuit_name="GRM32ER60J227ME05_DC0V_25degC", terminal_pairs=[["port1", 2], ["port2", 1]] + ) + edbapp.close() + + def test_components_bounding_box(self, edb_examples): + """Get component's bounding box.""" + # Done + edbapp = edb_examples.get_si_verse() + component = edbapp.components.instances["U1"] + assert component.bounding_box + assert isinstance(component.rotation, float) + edbapp.close() + + def test_pec_boundary_ports(self, edb_examples): + """Check pec boundary ports.""" + # Done + edbapp = edb_examples.get_si_verse() + edbapp.components.create_port_on_pins(refdes="U1", pins="AU38", reference_pins="AU37", pec_boundary=True) + assert edbapp.terminals["Port_GND_U1_AU38"].boundary_type == "pec" + assert edbapp.terminals["Port_GND_U1_AU38_ref"].boundary_type == "pec" + edbapp.components.deactivate_rlc_component(component="C5", create_circuit_port=True, pec_boundary=True) + edbapp.components.add_port_on_rlc_component(component="C65", circuit_ports=False, pec_boundary=True) + assert edbapp.terminals["C5"].boundary_type == "pec" + assert edbapp.terminals["C65"].boundary_type == "pec" + edbapp.close() + + def test_is_top_mounted(self, edb_examples): + """Check is_top_mounted property.""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.components.instances["U1"].is_top_mounted + assert not edbapp.components.instances["C347"].is_top_mounted + assert not edbapp.components.instances["R67"].is_top_mounted + edbapp.close_edb() + + def test_instances(self, edb_examples): + """Check instances access and values.""" + # Done + edbapp = edb_examples.get_si_verse() + comp_pins = edbapp.components.instances["U1"].pins + pins = [comp_pins["AM38"], comp_pins["AL37"]] + edbapp.components.create( + component_part_name="Test_part", component_name="Test", is_rlc=True, r_value=12.2, pins=pins + ) + assert edbapp.components.instances["Test"] + assert edbapp.components.instances["Test"].res_value == 12.2 + assert edbapp.components.instances["Test"].ind_value == 0 + assert edbapp.components.instances["Test"].cap_value == 0 + assert edbapp.components.instances["Test"].center == [0.07950000102, 0.03399999804] + edbapp.close_edb() + + def test_create_package_def(self, edb_examples): + """Check the creation of package definition.""" + # Done + edb = edb_examples.get_si_verse() + assert edb.components["C200"].create_package_def(component_part_name="SMTC-MECT-110-01-M-D-RA1_V") + assert not edb.components["C200"].create_package_def() + assert edb.components["C200"].package_def.name == "C200_CAPC3216X180X55ML20T25" + edb.close_edb() + + def test_solder_ball_getter_setter(self, edb_examples): + # Done + edb = edb_examples.get_si_verse() + cmp = edb.components.instances["X1"] + cmp.solder_ball_height = 0.0 + assert cmp.solder_ball_height == 0.0 + cmp.solder_ball_height = "100um" + assert cmp.solder_ball_height == 100e-6 + assert cmp.solder_ball_shape + cmp.solder_ball_shape = "cylinder" + assert cmp.solder_ball_shape == "cylinder" + cmp.solder_ball_shape = "spheroid" + assert cmp.solder_ball_shape == "spheroid" + cmp.solder_ball_shape = "cylinder" + assert cmp.solder_ball_diameter == (0.0, 0.0) + cmp.solder_ball_diameter = "200um" + diam1, diam2 = cmp.solder_ball_diameter + assert round(diam1, 6) == 200e-6 + assert round(diam2, 6) == 200e-6 + cmp.solder_ball_diameter = ("100um", "100um") + diam1, diam2 = cmp.solder_ball_diameter + assert round(diam1, 6) == 100e-6 + assert round(diam2, 6) == 100e-6 + + def test_create_pingroup_from_pins_types(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.components.create_pingroup_from_pins([*edbapp.components.instances["Q1"].pins.values()]) + assert edbapp.components._create_pin_group_terminal(edbapp.padstacks.pingroups[0], term_type="circuit") + edbapp.close() + + def test_component_lib(self): + # Done + edbapp = Edb() + comp_lib = edbapp.components.get_vendor_libraries() + assert len(comp_lib.capacitors) == 13 + assert len(comp_lib.inductors) == 7 + network = comp_lib.capacitors["AVX"]["AccuP01005"]["C005YJ0R1ABSTR"].s_parameters + test_esr = comp_lib.capacitors["AVX"]["AccuP01005"]["C005YJ0R1ABSTR"].esr + test_esl = comp_lib.capacitors["AVX"]["AccuP01005"]["C005YJ0R1ABSTR"].esl + assert round(test_esr, 4) == 1.7552 + assert round(test_esl, 12) == 2.59e-10 + assert network + assert network.frequency.npoints == 400 + network.write_touchstone(os.path.join(edbapp.directory, "test_export.s2p")) + assert os.path.isfile(os.path.join(edbapp.directory, "test_export.s2p")) + + def test_properties(self, edb_examples): + # TODO check with config file 2.0 + edbapp = edb_examples.get_si_verse() + pp = { + "pin_pair_model": [ + { + "first_pin": "2", + "second_pin": "1", + "is_parallel": True, + "resistance": "10ohm", + "resistance_enabled": True, + "inductance": "1nH", + "inductance_enabled": True, + "capacitance": "1nF", + "capacitance_enabled": True, + } + ] + } + edbapp.components["C378"].model_properties = pp + assert edbapp.components["C378"].model_properties == pp + edbapp.close() + + def test_ic_die_properties(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + component: Component = edbapp.components["U8"] + assert component.ic_die_properties.die_orientation == "chip_up" + component.ic_die_properties.die_orientation = "chip_down" + assert component.ic_die_properties.die_orientation == "chip_down" + assert component.ic_die_properties.die_type == "none" + assert component.ic_die_properties.height == 0.0 + component.ic_die_properties.height = 1e-3 + assert component.ic_die_properties.height == 1e-3 + edbapp.close() + + def test_rlc_component_302(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + pins = edbapp.components.get_pin_from_component("C31") + component = edbapp.components.create([pins[0], pins[1]], r_value=1.2, component_name="TEST", is_rlc=True) + assert component + assert component.name == "TEST" + assert component.location == [0.13275000120000002, 0.07350000032] + assert component.res_value == 1.2 + edbapp.close() diff --git a/tests/grpc/system/test_edb_definition.py b/tests/grpc/system/test_edb_definition.py new file mode 100644 index 0000000000..5e1d1a10a8 --- /dev/null +++ b/tests/grpc/system/test_edb_definition.py @@ -0,0 +1,89 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb component definitions +""" +import os + +import pytest + +from tests.conftest import local_path +from tests.legacy.system.conftest import test_subfolder + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch, target_path, target_path2, target_path4): + self.local_scratch = local_scratch + self.target_path = target_path + self.target_path2 = target_path2 + self.target_path4 = target_path4 + + def test_definitions(self, edb_examples): + edbapp = edb_examples.get_si_verse() + assert isinstance(edbapp.definitions.component, dict) + assert isinstance(edbapp.definitions.package, dict) + edbapp.close() + + def test_component_s_parameter(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + sparam_path = os.path.join(local_path, "example_models", test_subfolder, "GRM32_DC0V_25degC_series.s2p") + edbapp.definitions.component["CAPC3216X180X55ML20T25"].add_n_port_model(sparam_path, "GRM32_DC0V_25degC_series") + assert edbapp.definitions.component["CAPC3216X180X55ML20T25"].component_models + assert not edbapp.definitions.component["CAPC3216X180X55ML20T25"].component_models[0].is_null + assert edbapp.components["C200"].use_s_parameter_model("GRM32_DC0V_25degC_series") + # pp = {"pin_order": ["1", "2"]} + # edbapp.definitions.component["CAPC3216X180X55ML20T25"].set_properties(**pp) + # assert edbapp.definitions.component["CAPC3216X180X55ML20T25"].get_properties()["pin_order"] == ["1", "2"] + edbapp.close() + + def test_add_package_def(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + package = edbapp.definitions.add_package_def("package_1", "SMTC-MECT-110-01-M-D-RA1_V") + assert package + package.maximum_power = 1 + assert edbapp.definitions.package["package_1"].maximum_power == 1 + package.thermal_conductivity = 1 + assert edbapp.definitions.package["package_1"].thermal_conductivity == 1 + package.theta_jb = 1 + assert edbapp.definitions.package["package_1"].theta_jb == 1 + package.theta_jc = 1 + assert edbapp.definitions.package["package_1"].theta_jc == 1 + package.height = 1 + assert edbapp.definitions.package["package_1"].height == 1 + assert package.set_heatsink("1mm", "2mm", "x_oriented", "3mm", "4mm") + assert package.heat_sink.fin_base_height == 0.001 + assert package.heat_sink.fin_height == 0.002 + assert package.heat_sink.fin_orientation == "x_oriented" + assert package.heat_sink.fin_spacing == 0.003 + assert package.heat_sink.fin_thickness == 0.004 + package.name = "package_1b" + assert edbapp.definitions.package["package_1b"] + + assert edbapp.definitions.add_package_def("package_2", boundary_points=[["-1mm", "-1mm"], ["1mm", "1mm"]]) + edbapp.components["J5"].package_def = "package_2" + assert edbapp.components["J5"].package_def.name == "package_2" + edbapp.close() diff --git a/tests/grpc/system/test_edb_differential_pairs.py b/tests/grpc/system/test_edb_differential_pairs.py new file mode 100644 index 0000000000..607a3571c0 --- /dev/null +++ b/tests/grpc/system/test_edb_differential_pairs.py @@ -0,0 +1,48 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb differential pairs +""" + +import pytest + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch, target_path, target_path2, target_path4): + self.local_scratch = local_scratch + self.target_path = target_path + self.target_path2 = target_path2 + self.target_path4 = target_path4 + + def test_differential_pairs_queries(self, edb_examples): + """Evaluate differential pairs queries""" + # Done + edbapp = edb_examples.get_si_verse() + edbapp.differential_pairs.auto_identify() + diff_pair = edbapp.differential_pairs.create("new_pair1", "PCIe_Gen4_RX1_P", "PCIe_Gen4_RX1_N") + assert diff_pair.positive_net.name == "PCIe_Gen4_RX1_P" + assert diff_pair.negative_net.name == "PCIe_Gen4_RX1_N" + assert edbapp.differential_pairs.items["new_pair1"] + edbapp.close() diff --git a/tests/grpc/system/test_edb_extended_nets.py b/tests/grpc/system/test_edb_extended_nets.py new file mode 100644 index 0000000000..7ed93bcf84 --- /dev/null +++ b/tests/grpc/system/test_edb_extended_nets.py @@ -0,0 +1,53 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb extended nets +""" + +import pytest + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch, target_path, target_path2, target_path4): + self.local_scratch = local_scratch + self.target_path = target_path + self.target_path2 = target_path2 + self.target_path4 = target_path4 + + def test_nets_queries(self, edb_examples): + """Evaluate nets queries""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.extended_nets.auto_identify_signal() + assert edbapp.extended_nets.auto_identify_power() + extended_net_name, _ = next(iter(edbapp.extended_nets.items.items())) + assert edbapp.extended_nets.items[extended_net_name] + assert edbapp.extended_nets.items[extended_net_name].nets + assert edbapp.extended_nets.items[extended_net_name].components + assert edbapp.extended_nets.items[extended_net_name].rlc + assert edbapp.extended_nets.items[extended_net_name].serial_rlc + assert edbapp.extended_nets.items["1V0"].serial_rlc + assert edbapp.extended_nets.create("new_ex_net", "DDR4_A1") + edbapp.close() diff --git a/tests/grpc/system/test_edb_future_features_242.py b/tests/grpc/system/test_edb_future_features_242.py new file mode 100644 index 0000000000..198c87e2a4 --- /dev/null +++ b/tests/grpc/system/test_edb_future_features_242.py @@ -0,0 +1,159 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb +""" + +import pytest + +pytestmark = [pytest.mark.system, pytest.mark.legacy] +VERSION = 2025.2 + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self): + pass + + def test_add_raptorx_setup(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse(version=VERSION) + setup = edbapp.create_raptorx_setup("test") + assert "test" in edbapp.setups + setup.add_sweep(distribution="linear", start_freq="0.1GHz", stop_freq="10GHz", step="0.1GHz") + setup.enabled = False + assert not setup.enabled + assert len(setup.frequency_sweeps) == 1 + general_settings = setup.settings.general + assert general_settings.global_temperature == 22.0 + general_settings.global_temperature = 35.0 + assert edbapp.setups["test"].settings.general.global_temperature == 35.0 + assert general_settings.max_frequency == "10GHz" + general_settings.max_frequency = "20GHz" + assert general_settings.max_frequency == "20GHz" + advanced_settings = setup.settings.advanced + assert advanced_settings.auto_removal_sliver_poly == 0.001 + advanced_settings.auto_removal_sliver_poly = 0.002 + assert advanced_settings.auto_removal_sliver_poly == 0.002 + assert advanced_settings.cells_per_wavelength == 80 + advanced_settings.cells_per_wavelength = 60 + assert advanced_settings.cells_per_wavelength == 60 + assert advanced_settings.edge_mesh == "0.8um" + advanced_settings.edge_mesh = "1um" + assert advanced_settings.edge_mesh == "1um" + assert advanced_settings.eliminate_slit_per_holes == 5.0 + advanced_settings.eliminate_slit_per_holes = 4.0 + assert advanced_settings.eliminate_slit_per_holes == 4.0 + assert advanced_settings.mesh_frequency == "1GHz" + advanced_settings.mesh_frequency = "5GHz" + assert advanced_settings.mesh_frequency == "5GHz" + assert advanced_settings.override_shrink_factor == 1.0 + advanced_settings.override_shrink_factor = 1.5 + assert advanced_settings.override_shrink_factor == 1.5 + assert advanced_settings.plane_projection_factor == 1.0 + advanced_settings.plane_projection_factor = 1.4 + assert advanced_settings.plane_projection_factor == 1.4 + assert advanced_settings.use_accelerate_via_extraction + advanced_settings.use_accelerate_via_extraction = False + assert not advanced_settings.use_accelerate_via_extraction + assert not advanced_settings.use_auto_removal_sliver_poly + advanced_settings.use_auto_removal_sliver_poly = True + assert advanced_settings.use_auto_removal_sliver_poly + assert not advanced_settings.use_cells_per_wavelength + advanced_settings.use_cells_per_wavelength = True + assert advanced_settings.use_cells_per_wavelength + assert not advanced_settings.use_edge_mesh + advanced_settings.use_edge_mesh = True + assert advanced_settings.use_edge_mesh + assert not advanced_settings.use_eliminate_slit_per_holes + advanced_settings.use_eliminate_slit_per_holes = True + assert advanced_settings.use_eliminate_slit_per_holes + assert not advanced_settings.use_enable_advanced_cap_effects + advanced_settings.use_enable_advanced_cap_effects = True + assert advanced_settings.use_enable_advanced_cap_effects + assert not advanced_settings.use_enable_etch_transform + advanced_settings.use_enable_etch_transform = True + assert advanced_settings.use_enable_etch_transform + assert advanced_settings.use_enable_substrate_network_extraction + advanced_settings.use_enable_substrate_network_extraction = False + assert not advanced_settings.use_enable_substrate_network_extraction + assert not advanced_settings.use_extract_floating_metals_dummy + advanced_settings.use_extract_floating_metals_dummy = True + assert advanced_settings.use_extract_floating_metals_dummy + assert advanced_settings.use_extract_floating_metals_floating + advanced_settings.use_extract_floating_metals_floating = False + assert not advanced_settings.use_extract_floating_metals_floating + assert not advanced_settings.use_lde + advanced_settings.use_lde = True + assert advanced_settings.use_lde + assert not advanced_settings.use_mesh_frequency + advanced_settings.use_mesh_frequency = True + assert advanced_settings.use_mesh_frequency + assert not advanced_settings.use_override_shrink_factor + advanced_settings.use_override_shrink_factor = True + assert advanced_settings.use_override_shrink_factor + assert advanced_settings.use_plane_projection_factor + advanced_settings.use_plane_projection_factor = False + assert not advanced_settings.use_plane_projection_factor + assert not advanced_settings.use_relaxed_z_axis + advanced_settings.use_relaxed_z_axis = True + assert advanced_settings.use_relaxed_z_axis + edbapp.close() + + # def test_create_hfss_pi_setup(self, edb_examples): + # # TODO check HFSS PI later + # edbapp = edb_examples.get_si_verse(version=VERSION) + # setup = edbapp.create_hfsspi_setup("test") + # assert setup.get_simulation_settings() + # settings = { + # "auto_select_nets_for_simulation": True, + # "ignore_dummy_nets_for_selected_nets": False, + # "ignore_small_holes": 1, + # "ignore_small_holes_min_diameter": 1, + # "improved_loss_model": 2, + # "include_enhanced_bond_wire_modeling": True, + # "include_nets": ["GND"], + # "min_plane_area_to_mesh": "0.2mm2", + # "min_void_area_to_mesh": "0.02mm2", + # "model_type": 2, + # "perform_erc": True, + # "pi_slider_pos": 1, + # "rms_surface_roughness": "1", + # "signal_nets_conductor_modeling": 1, + # "signal_nets_error_tolerance": 0.02, + # "signal_nets_include_improved_dielectric_fill_refinement": True, + # "signal_nets_include_improved_loss_handling": True, + # "snap_length_threshold": "2.6um", + # "surface_roughness_model": 1, + # } + # setup.set_simulation_settings(settings) + # settings_get = edbapp.setups["test"].get_simulation_settings() + # for k, v in settings.items(): + # assert settings[k] == settings_get[k] + + # def test_create_hfss_pi_setup_add_sweep(self, edb_examples): + # # TODO check HFSS PI later + # edbapp = edb_examples.get_si_verse(version=VERSION) + # setup = edbapp.create_hfsspi_setup("test") + # setup.add_sweep(name="sweep1", frequency_sweep=["linear scale", "0.1GHz", "10GHz", "0.1GHz"]) + # assert setup.sweeps["sweep1"].frequencies + # edbapp.setups["test"].sweeps["sweep1"].adaptive_sampling = True diff --git a/tests/grpc/system/test_edb_ipc.py b/tests/grpc/system/test_edb_ipc.py new file mode 100644 index 0000000000..537286b2d9 --- /dev/null +++ b/tests/grpc/system/test_edb_ipc.py @@ -0,0 +1,77 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to the interaction between Edb and Ipc2581 +""" + +import os + +import pytest + +from pyedb.dotnet.edb import Edb +from tests.conftest import desktop_version + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch, target_path, target_path2, target_path4): + self.local_scratch = local_scratch + self.target_path = target_path + self.target_path2 = target_path2 + self.target_path4 = target_path4 + + def test_export_to_ipc2581_0(self, edb_examples): + """Export of a loaded aedb file to an XML IPC2581 file""" + # Done + edbapp = edb_examples.get_si_verse() + xml_file = os.path.join(edbapp.directory, "test.xml") + edbapp.export_to_ipc2581(xml_file) + assert os.path.exists(xml_file) + edbapp.export_to_ipc2581(xml_file, "mm") + assert os.path.exists(xml_file) + edbapp.close() + + @pytest.mark.xfail(reason="This test is expected to crash (sometimes) at `ipc_edb.close()`") + def test_export_to_ipc2581_1(self, edb_examples): + """Export of a loaded aedb file to an XML IPC2581 file""" + edbapp = edb_examples.get_si_verse() + xml_file = os.path.join(edbapp.directory, "test.xml") + edbapp.export_to_ipc2581(xml_file) + edbapp.close() + assert os.path.isfile(xml_file) + ipc_edb = Edb(xml_file, edbversion=desktop_version) + ipc_stats = ipc_edb.get_statistics() + assert ipc_stats.layout_size == (0.15, 0.0845) + assert ipc_stats.num_capacitors == 380 + assert ipc_stats.num_discrete_components == 31 + assert ipc_stats.num_inductors == 10 + assert ipc_stats.num_layers == 15 + assert ipc_stats.num_nets == 348 + assert ipc_stats.num_polygons == 139 + assert ipc_stats.num_resistors == 82 + assert ipc_stats.num_traces == 1565 + assert ipc_stats.num_traces == 1565 + assert ipc_stats.num_vias == 4730 + assert ipc_stats.stackup_thickness == 0.001748 + ipc_edb.close() diff --git a/tests/grpc/system/test_edb_layout.py b/tests/grpc/system/test_edb_layout.py new file mode 100644 index 0000000000..81068ca21e --- /dev/null +++ b/tests/grpc/system/test_edb_layout.py @@ -0,0 +1,38 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import pytest + +pytestmark = [pytest.mark.unit, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch): + pass + + def test_find(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.layout.find_primitive(layer_name="Inner5(PWR2)") + edbapp.close() diff --git a/tests/grpc/system/test_edb_materials.py b/tests/grpc/system/test_edb_materials.py new file mode 100644 index 0000000000..7f29c074c4 --- /dev/null +++ b/tests/grpc/system/test_edb_materials.py @@ -0,0 +1,337 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb +""" + +import os + +from ansys.edb.core.definition.djordjecvic_sarkar_model import ( + DjordjecvicSarkarModel as GrpcDjordjecvicSarkarModel, +) +from ansys.edb.core.definition.material_def import MaterialDef as GrpcMaterialDef +import pytest + +from pyedb.grpc.database.definition.materials import ( + Material, + MaterialProperties, + Materials, +) +from tests.conftest import local_path + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + +PROPERTIES = ( + "conductivity", + "dielectric_loss_tangent", + "magnetic_loss_tangent", + "mass_density", + "permittivity", + "permeability", + "poisson_ratio", + "specific_heat", + "thermal_conductivity", + "youngs_modulus", + "thermal_expansion_coefficient", +) +DC_PROPERTIES = ( + "dielectric_model_frequency", + "loss_tangent_at_frequency", + "permittivity_at_frequency", + "dc_conductivity", + "dc_permittivity", +) +FLOAT_VALUE = 12.0 +INT_VALUE = 12 +STR_VALUE = "12" +VALUES = (FLOAT_VALUE, INT_VALUE, STR_VALUE) +MATERIAL_NAME = "DummyMaterial" + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, edb_examples): + self.edbapp = edb_examples.get_si_verse() + material_def = GrpcMaterialDef.find_by_name(self.edbapp.active_db, MATERIAL_NAME) + if not material_def.is_null: + material_def.delete() + + def test_material_name(self): + """Evaluate material properties.""" + from ansys.edb.core.definition.material_def import ( + MaterialDef as GrpcMaterialDef, + ) + + material_def = GrpcMaterialDef.create(self.edbapp.active_db, MATERIAL_NAME) + material = Material(self.edbapp, material_def) + + assert MATERIAL_NAME == material.name + + def test_material_properties(self): + """Evaluate material properties.""" + + material_def = GrpcMaterialDef.create(self.edbapp.active_db, MATERIAL_NAME) + material = Material(self.edbapp, material_def) + + for property in PROPERTIES: + for value in VALUES: + setattr(material, property, value) + assert float(value) == getattr(material, property) + assert 12 == material.dielectric_loss_tangent + + def test_material_dc_properties(self): + """Evaluate material DC properties.""" + material_def = GrpcMaterialDef.create(self.edbapp.active_db, MATERIAL_NAME) + material_model = GrpcDjordjecvicSarkarModel.create() + material_def.dielectric_material_model = material_model + material = Material(self.edbapp, material_def) + + for property in DC_PROPERTIES: + for value in (INT_VALUE, FLOAT_VALUE): + setattr(material, property, value) + assert float(value) == float(getattr(material, property)) + # NOTE: Other properties do not accept EDB calls with string value + if property == "loss_tangent_at_frequency": + setattr(material, property, STR_VALUE) + assert float(STR_VALUE) == float(getattr(material, property)) + + def test_material_to_dict(self): + """Evaluate material conversion into a dictionary.""" + material_def = GrpcMaterialDef.create(self.edbapp.active_db, MATERIAL_NAME) + material = Material(self.edbapp, material_def) + for property in PROPERTIES: + setattr(material, property, FLOAT_VALUE) + expected_result = MaterialProperties( + **{field: FLOAT_VALUE for field in MaterialProperties.__annotations__} + ).model_dump() + expected_result["name"] = MATERIAL_NAME + # Material without DC model has None value for each DC properties + for property in DC_PROPERTIES: + expected_result[property] = None + + material_dict = material.to_dict() + assert expected_result == material_dict + + def test_material_with_dc_model_to_dict(self): + """Evaluate material conversion into a dictionary.""" + material_def = GrpcMaterialDef.create(self.edbapp.active_db, MATERIAL_NAME) + material_model = GrpcDjordjecvicSarkarModel.create() + material_def.dielectric_material_model = material_model + material = Material(self.edbapp, material_def) + for property in DC_PROPERTIES: + setattr(material, property, FLOAT_VALUE) + expected_result = MaterialProperties( + **{field: FLOAT_VALUE for field in MaterialProperties.__annotations__} + ).model_dump() + expected_result["name"] = MATERIAL_NAME + + material_dict = material.to_dict() + for property in DC_PROPERTIES: + assert expected_result[property] == material_dict[property] + + def test_material_update_properties(self): + """Evaluate material properties update.""" + material_def = GrpcMaterialDef.create(self.edbapp.active_db, MATERIAL_NAME) + material = Material(self.edbapp, material_def) + for property in PROPERTIES: + setattr(material, property, FLOAT_VALUE) + expected_value = FLOAT_VALUE + 1 + material_dict = MaterialProperties( + **{field: expected_value for field in MaterialProperties.__annotations__} + ).model_dump() + + material.update(material_dict) + # Dielectric model defined changing conductivity is not allowed + assert material.conductivity == 0.0044504017896274855 + assert material.dc_conductivity == 1e-12 + assert material.dielectric_material_model.dc_relative_permitivity == 5.0 + assert material.dielectric_material_model.loss_tangent_at_frequency == 0.02 + assert material.loss_tangent_at_frequency == 0.02 + assert material.mass_density == 13.0 + + def test_materials_syslib(self): + """Evaluate system library.""" + materials = Materials(self.edbapp) + + assert materials.syslib + + def test_materials_materials(self): + """Evaluate materials.""" + materials = Materials(self.edbapp) + assert materials.materials + + def test_materials_add_material(self): + """Evalue add material.""" + materials = Materials(self.edbapp) + + material = materials.add_material(MATERIAL_NAME, permittivity=12) + assert material + material.name == materials[MATERIAL_NAME].name + with pytest.raises(ValueError): + materials.add_material(MATERIAL_NAME, permittivity=12) + + def test_materials_add_conductor_material(self): + """Evalue add conductor material.""" + materials = Materials(self.edbapp) + + material = materials.add_conductor_material(MATERIAL_NAME, 12, permittivity=12) + assert material + _ = materials[MATERIAL_NAME] + with pytest.raises(ValueError): + materials.add_conductor_material(MATERIAL_NAME, 12, permittivity=12) + + def test_materials_add_dielectric_material(self): + """Evalue add dielectric material.""" + materials = Materials(self.edbapp) + + material = materials.add_dielectric_material(MATERIAL_NAME, 12, 12, conductivity=12) + assert material + _ = materials[MATERIAL_NAME] + with pytest.raises(ValueError): + materials.add_dielectric_material(MATERIAL_NAME, 12, 12, conductivity=12) + + def test_materials_add_djordjevicsarkar_dielectric(self): + """Evalue add djordjevicsarkar dielectric material.""" + materials = Materials(self.edbapp) + + material = materials.add_djordjevicsarkar_dielectric( + MATERIAL_NAME, 4.3, 0.02, 9, dc_conductivity=1e-12, dc_permittivity=5, conductivity=0 + ) + assert material + _ = materials[MATERIAL_NAME] + with pytest.raises(ValueError): + materials.add_djordjevicsarkar_dielectric( + MATERIAL_NAME, 4.3, 0.02, 9, dc_conductivity=1e-12, dc_permittivity=5, conductivity=0 + ) + + def test_materials_add_debye_material(self): + """Evalue add debye material material.""" + materials = Materials(self.edbapp) + + material = materials.add_debye_material(MATERIAL_NAME, 6, 4, 0.02, 0.05, 1e9, 10e9, conductivity=0) + assert material + _ = materials[MATERIAL_NAME] + with pytest.raises(ValueError): + materials.add_debye_material(MATERIAL_NAME, 6, 4, 0.02, 0.05, 1e9, 10e9, conductivity=0) + + def test_materials_add_multipole_debye_material(self): + """Evalue add multipole debye material.""" + materials = Materials(self.edbapp) + frequencies = [0, 2, 3, 4, 5, 6] + relative_permitivities = [1e9, 1.1e9, 1.2e9, 1.3e9, 1.5e9, 1.6e9] + loss_tangents = [0.025, 0.026, 0.027, 0.028, 0.029, 0.030] + + material = materials.add_multipole_debye_material( + MATERIAL_NAME, frequencies, relative_permitivities, loss_tangents, conductivity=0 + ) + assert material + _ = materials[MATERIAL_NAME] + with pytest.raises(ValueError): + materials.add_multipole_debye_material( + MATERIAL_NAME, frequencies, relative_permitivities, loss_tangents, conductivity=0 + ) + + def test_materials_duplicate(self): + """Evalue duplicate material.""" + materials = Materials(self.edbapp) + kwargs = MaterialProperties(**{field: 12 for field in MaterialProperties.__annotations__}).model_dump() + material = materials.add_material(MATERIAL_NAME, **kwargs) + other_name = "OtherMaterial" + + new_material = materials.duplicate(MATERIAL_NAME, other_name) + for mat_attribute in PROPERTIES: + assert getattr(material, mat_attribute) == getattr(new_material, mat_attribute) + with pytest.raises(ValueError): + materials.duplicate(MATERIAL_NAME, other_name) + + def test_materials_delete_material(self): + """Evaluate delete material.""" + materials = Materials(self.edbapp) + + _ = materials.add_material(MATERIAL_NAME) + materials.delete(MATERIAL_NAME) + assert MATERIAL_NAME not in materials + with pytest.raises(ValueError): + materials.delete(MATERIAL_NAME) + + def test_material_load_amat(self): + """Evaluate load material from an AMAT file.""" + materials = Materials(self.edbapp) + nb_materials = len(materials.materials) + mat_file = os.path.join(self.edbapp.base_path, "syslib", "Materials.amat") + + assert materials.load_amat(mat_file) + assert nb_materials != len(materials.materials) + assert 0.0013 == materials["Rogers RO3003 (tm)"].dielectric_loss_tangent + assert 3.0 == materials["Rogers RO3003 (tm)"].permittivity + + def test_materials_read_materials(self): + """Evaluate read materials.""" + materials = Materials(self.edbapp) + mat_file = os.path.join(local_path, "example_models", "syslib", "Materials.amat") + name_to_material = materials.read_materials(mat_file) + + key = "FC-78" + assert key in name_to_material + assert name_to_material[key]["thermal_conductivity"] == 0.062 + assert name_to_material[key]["mass_density"] == 1700 + assert name_to_material[key]["specific_heat"] == 1050 + assert name_to_material[key]["thermal_expansion_coefficient"] == 0.0016 + key = "Polyflon CuFlon (tm)" + assert key in name_to_material + assert name_to_material[key]["permittivity"] == 2.1 + assert name_to_material[key]["dielectric_loss_tangent"] == 0.00045 + key = "Water(@360K)" + assert key in name_to_material + assert name_to_material[key]["thermal_conductivity"] == 0.6743 + assert name_to_material[key]["mass_density"] == 967.4 + assert name_to_material[key]["specific_heat"] == 4206 + assert name_to_material[key]["thermal_expansion_coefficient"] == 0.0006979 + key = "steel_stainless" + assert name_to_material[key]["conductivity"] == 1100000 + assert name_to_material[key]["thermal_conductivity"] == 13.8 + assert name_to_material[key]["mass_density"] == 8055 + assert name_to_material[key]["specific_heat"] == 480 + assert name_to_material[key]["thermal_expansion_coefficient"] == 1.08e-005 + + def test_materials_load_conductor_material(self): + """Load conductor material.""" + materials = Materials(self.edbapp) + conductor_material_properties = {"name": MATERIAL_NAME, "conductivity": 2e4} + + assert MATERIAL_NAME not in materials + materials.load_material(conductor_material_properties) + material = materials[MATERIAL_NAME] + assert 2e4 == material.conductivity + + def test_materials_load_dielectric_material(self): + """Load dielectric material.""" + materials = Materials(self.edbapp) + dielectric_material_properties = {"name": MATERIAL_NAME, "permittivity": 12, "loss_tangent": 0.00045} + + assert MATERIAL_NAME not in materials + materials.load_material(dielectric_material_properties) + material = materials[MATERIAL_NAME] + assert 0.00045 == material.loss_tangent + assert 0.00045 == material.dielectric_loss_tangent + assert 12 == material.permittivity + self.edbapp.close() diff --git a/tests/grpc/system/test_edb_modeler.py b/tests/grpc/system/test_edb_modeler.py new file mode 100644 index 0000000000..375d4730cb --- /dev/null +++ b/tests/grpc/system/test_edb_modeler.py @@ -0,0 +1,582 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb modeler +""" + +import os + +import pytest + +from pyedb.generic.settings import settings +from pyedb.grpc.edb import EdbGrpc as Edb +from tests.conftest import desktop_version, local_path +from tests.legacy.system.conftest import test_subfolder + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch, target_path, target_path2, target_path4): + self.local_scratch = local_scratch + self.target_path = target_path + self.target_path2 = target_path2 + self.target_path4 = target_path4 + + def test_modeler_polygons(self, edb_examples): + """Evaluate modeler polygons""" + # Done + edbapp = edb_examples.get_si_verse() + assert len(edbapp.modeler.polygons) > 0 + assert not edbapp.modeler.polygons[0].is_void + + poly0 = edbapp.modeler.polygons[0] + assert edbapp.modeler.polygons[0].clone() + assert isinstance(poly0.voids, list) + assert isinstance(poly0.points_raw, list) + assert isinstance(poly0.points(), tuple) + assert isinstance(poly0.points()[0], list) + assert poly0.points()[0][0] >= 0.0 + assert poly0.points_raw[0].x.value >= 0.0 + assert poly0.type == "polygon" + assert not poly0.points_raw[0].is_arc + assert isinstance(poly0.voids, list) + # TODO check bug 455 + # assert isinstance(poly0.get_closest_point([0.07, 0.0027]), list) + assert isinstance(poly0.get_closest_arc_midpoint([0, 0]), list) + assert isinstance(poly0.arcs, list) + assert isinstance(poly0.longest_arc.length, float) + assert isinstance(poly0.shortest_arc.length, float) + assert not poly0.in_polygon([0, 0]) + # assert isinstance(poly0.arcs[0].center, list) + # assert isinstance(poly0.arcs[0].radius, float) + assert poly0.arcs[0].is_segment() + assert not poly0.arcs[0].is_point() + assert not poly0.arcs[0].is_ccw() + # assert isinstance(poly0.arcs[0].points_raw, list) + assert isinstance(poly0.arcs[0].points, list) + assert isinstance(poly0.intersection_type(poly0), int) + assert poly0.is_intersecting(poly0) + poly_3022 = edbapp.modeler.get_primitive(3022) + assert edbapp.modeler.get_primitive(3023) + assert poly_3022.aedt_name == "poly void_2425" + poly_3022.aedt_name = "poly3022" + assert poly_3022.aedt_name == "poly3022" + poly_with_voids = [poly for poly in edbapp.modeler.polygons if poly.has_voids] + assert poly_with_voids + for k in poly_with_voids[0].voids: + assert k.id + assert k.expand(0.0005) + poly_167 = [i for i in edbapp.modeler.paths if i.edb_uid == 167][0] + assert poly_167.expand(0.0005) + edbapp.close() + + def test_modeler_paths(self, edb_examples): + """Evaluate modeler paths""" + # Done + edbapp = edb_examples.get_si_verse() + assert len(edbapp.modeler.paths) > 0 + path = edbapp.modeler.paths[0] + assert path.type == "path" + assert path.clone() + assert isinstance(path.width, float) + path.width = "1mm" + assert path.width == 0.001 + assert edbapp.modeler["line_167"].type == "path" + assert edbapp.modeler["poly_3022"].type == "polygon" + line_number = len(edbapp.modeler.primitives) + edbapp.modeler["line_167"].delete() + assert edbapp.modeler._primitives == [] + assert line_number == len(edbapp.modeler.primitives) + 1 + assert edbapp.modeler["poly_3022"].type == "polygon" + edbapp.close() + + def test_modeler_primitives_by_layer(self, edb_examples): + """Evaluate modeler primitives by layer""" + # Done + edbapp = edb_examples.get_si_verse() + primmitive = edbapp.modeler.primitives_by_layer["1_Top"][0] + assert primmitive.layer_name == "1_Top" + assert not primmitive.is_negative + assert not primmitive.is_void + primmitive.is_negative = True + assert primmitive.is_negative + primmitive.is_negative = False + assert not primmitive.has_voids + assert not primmitive.is_parameterized + # assert isinstance(primmitive.get_hfss_prop(), tuple) + assert not primmitive.is_zone_primitive + assert primmitive.can_be_zone_primitive + edbapp.close() + + def test_modeler_primitives(self, edb_examples): + """Evaluate modeler primitives""" + # Done + edbapp = edb_examples.get_si_verse() + assert len(edbapp.modeler.rectangles) > 0 + assert len(edbapp.modeler.circles) > 0 + assert len(edbapp.layout.bondwires) == 0 + assert "1_Top" in edbapp.modeler.polygons_by_layer.keys() + assert len(edbapp.modeler.polygons_by_layer["1_Top"]) > 0 + assert len(edbapp.modeler.polygons_by_layer["DE1"]) == 0 + assert edbapp.modeler.rectangles[0].type == "rectangle" + assert edbapp.modeler.circles[0].type == "circle" + edbapp.close() + + def test_modeler_get_polygons_bounding(self, edb_examples): + """Retrieve polygons bounding box.""" + # Done + edbapp = edb_examples.get_si_verse() + polys = edbapp.modeler.get_polygons_by_layer("GND") + for poly in polys: + bounding = edbapp.modeler.get_polygon_bounding_box(poly) + assert len(bounding) == 4 + edbapp.close() + + def test_modeler_get_polygons_by_layer_and_nets(self, edb_examples): + """Retrieve polygons by layer and nets.""" + # Done + edbapp = edb_examples.get_si_verse() + nets = ["GND", "1V0"] + polys = edbapp.modeler.get_polygons_by_layer("16_Bottom", nets) + assert polys + edbapp.close() + + def test_modeler_get_polygons_points(self, edb_examples): + """Retrieve polygons points.""" + # Done + edbapp = edb_examples.get_si_verse() + polys = edbapp.modeler.get_polygons_by_layer("GND") + for poly in polys: + points = edbapp.modeler.get_polygon_points(poly) + assert points + edbapp.close() + + def test_modeler_create_polygon(self, edb_examples): + """Create a polygon based on a shape or points.""" + edbapp = edb_examples.get_si_verse() + settings.enable_error_handler = True + points = [ + [-0.025, -0.02], + [0.025, -0.02], + [0.025, 0.02], + [-0.025, 0.02], + [-0.025, -0.02], + ] + plane = edbapp.modeler.create_polygon(points=points, layer_name="1_Top") + + points = [ + [-0.001, -0.001], + [0.001, 0.001], + [0.0015, 0.0015, 0.0001], + [-0.001, 0.0015], + [-0.001, -0.001], + ] + void1 = edbapp.modeler.create_polygon(points=points, layer_name="1_Top") + void2 = edbapp.modeler.create_rectangle( + lower_left_point=[-0.002, 0.0], upper_right_point=[-0.015, 0.0005], layer_name="1_Top" + ) + assert edbapp.modeler.create_polygon(points=plane.polygon_data, layer_name="1_Top", voids=[void1, void2]) + edbapp["polygon_pts_x"] = -1.025 + edbapp["polygon_pts_y"] = -1.02 + points = [ + ["polygon_pts_x", "polygon_pts_y"], + [1.025, -1.02], + [1.025, 1.02], + [-1.025, 1.02], + [-1.025, -1.02], + ] + assert edbapp.modeler.create_polygon(points, "1_Top") + settings.enable_error_handler = False + points = [[-0.025, -0.02], [0.025, -0.02], [-0.025, -0.02], [0.025, 0.02], [-0.025, 0.02], [-0.025, -0.02]] + poly = edbapp.modeler.create_polygon(points=points, layer_name="1_Top") + assert poly.has_self_intersections + assert poly.fix_self_intersections() == [] + assert not poly.has_self_intersections + edbapp.close() + + def test_modeler_create_polygon_from_shape(self, edb_examples): + """Create polygon from shape.""" + # Done + edbapp = edb_examples.get_si_verse() + edbapp.modeler.create_polygon( + points=[[0.0, 0.0], [0.0, 10e-3], [10e-3, 10e-3], [10e-3, 0]], layer_name="1_Top", net_name="test" + ) + poly_test = [poly for poly in edbapp.modeler.polygons if poly.net_name == "test"] + assert len(poly_test) == 1 + assert poly_test[0].center == [0.005, 0.005] + assert poly_test[0].bbox == [0.0, 0.0, 0.01, 0.01] + assert poly_test[0].move_layer("16_Bottom") + poly_test = [poly for poly in edbapp.modeler.polygons if poly.net_name == "test"] + assert len(poly_test) == 1 + assert poly_test[0].layer_name == "16_Bottom" + edbapp.close() + + def test_modeler_create_trace(self, edb_examples): + """Create a trace based on a list of points.""" + # Done + edbapp = edb_examples.get_si_verse() + points = [ + [-0.025, -0.02], + [0.025, -0.02], + [0.025, 0.02], + ] + trace = edbapp.modeler.create_trace(points, "1_Top") + assert trace + assert isinstance(trace.get_center_line(), list) + assert isinstance(trace.get_center_line(), list) + # TODO fixing parameters first + # edbapp["delta_x"] = "1mm" + # assert trace.add_point("delta_x", "1mm", True) + # assert trace.get_center_line(True)[-1][0] == "(delta_x)+(0.025)" + # TODO check issue #475 center_line has no setter + # assert trace.add_point(0.001, 0.002) + # assert trace.get_center_line()[-1] == [0.001, 0.002] + edbapp.close() + + def test_modeler_add_void(self, edb_examples): + """Add a void into a shape.""" + # Done + edbapp = edb_examples.get_si_verse() + plane_shape = edbapp.modeler.create_rectangle( + lower_left_point=["-5mm", "-5mm"], upper_right_point=["5mm", "5mm"], layer_name="1_Top" + ) + plane = edbapp.modeler.create_polygon(plane_shape.polygon_data, "1_Top", net_name="GND") + void = edbapp.modeler.create_trace(path_list=[["0", "0"], ["0", "1mm"]], layer_name="1_Top", width="0.1mm") + assert edbapp.modeler.add_void(plane, void) + plane.add_void(void) + edbapp.close() + + def test_modeler_fix_circle_void(self, edb_examples): + """Fix issues when circle void are clipped due to a bug in EDB.""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.modeler.fix_circle_void_for_clipping() + edbapp.close() + + def test_modeler_primitives_area(self, edb_examples): + """Access primitives total area.""" + # Done + edbapp = edb_examples.get_si_verse() + i = 0 + while i < 2: + prim = edbapp.modeler.primitives[i] + assert prim.area(True) > 0 + i += 1 + assert prim.bbox + assert prim.center + # TODO check bug #455 + # assert prim.get_closest_point((0, 0)) + assert prim.polygon_data + assert edbapp.modeler.paths[0].length + edbapp.close() + + def test_modeler_create_rectangle(self, edb_examples): + """Create rectangle.""" + # Done + edbapp = edb_examples.get_si_verse() + rect = edbapp.modeler.create_rectangle( + layer_name="1_Top", lower_left_point=["0", "0"], upper_right_point=["2mm", "3mm"] + ) + assert rect + rect.is_negative = True + assert rect.is_negative + rect.is_negative = False + assert not rect.is_negative + assert edbapp.modeler.create_rectangle( + layer_name="1_Top", + center_point=["0", "0"], + width="4mm", + height="5mm", + representation_type="center_width_height", + ) + edbapp.close() + + def test_modeler_create_circle(self, edb_examples): + """Create circle.""" + # Done + edbapp = edb_examples.get_si_verse() + poly = edbapp.modeler.create_polygon(points=[[0, 0], [100, 0], [100, 100], [0, 100]], layer_name="1_Top") + assert poly + poly.add_void([[20, 20], [20, 30], [100, 30], [100, 20]]) + poly2 = edbapp.modeler.create_polygon(points=[[60, 60], [60, 150], [150, 150], [150, 60]], layer_name="1_Top") + new_polys = poly.subtract(poly2) + assert len(new_polys) == 1 + circle = edbapp.modeler.create_circle("1_Top", 40, 40, 15) + assert circle + intersection = new_polys[0].intersect(circle) + assert len(intersection) == 1 + circle2 = edbapp.modeler.create_circle("1_Top", 20, 20, 15) + assert circle2.unite(intersection) + edbapp.close() + + def test_modeler_defeature(self, edb_examples): + """Defeature the polygon.""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.modeler.defeature_polygon(edbapp.modeler.primitives_by_net["GND"][-1], 0.0001) + edbapp.close() + + def test_modeler_primitives_boolean_operation(self): + """Evaluate modeler primitives boolean operations.""" + from pyedb.grpc.edb import EdbGrpc as Edb + + # TODO check bug #464. + edb = Edb(restart_rpc_server=True) + edb.stackup.add_layer(layer_name="test") + x = edb.modeler.create_polygon(layer_name="test", points=[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]) + assert not x.is_null + x_hole1 = edb.modeler.create_polygon(layer_name="test", points=[[1.0, 1.0], [4.5, 1.0], [4.5, 9.0], [1.0, 9.0]]) + x_hole2 = edb.modeler.create_polygon(layer_name="test", points=[[4.5, 1.0], [9.0, 1.0], [9.0, 9.0], [4.5, 9.0]]) + x = x.subtract([x_hole1, x_hole2])[0] + assert not x.is_null + y = edb.modeler.create_polygon(layer_name="test", points=[[4.0, 3.0], [6.0, 3.0], [6.0, 6.0], [4.0, 6.0]]) + z = x.subtract(y) + assert not z[0].is_null + # edb.stackup.add_layer(layer_name="test") + x = edb.modeler.create_polygon(layer_name="test", points=[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]) + x_hole = edb.modeler.create_polygon(layer_name="test", points=[[1.0, 1.0], [9.0, 1.0], [9.0, 9.0], [1.0, 9.0]]) + y = x.subtract(x_hole)[0] + z = edb.modeler.create_polygon(layer_name="test", points=[[-15.0, 5.0], [15.0, 5.0], [15.0, 6.0], [-15.0, 6.0]]) + assert y.intersect(z) + + edb.stackup.add_layer(layer_name="test2") + x = edb.modeler.create_polygon(layer_name="test2", points=[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]) + x_hole = edb.modeler.create_polygon(layer_name="test2", points=[[1.0, 1.0], [9.0, 1.0], [9.0, 9.0], [1.0, 9.0]]) + y = x.subtract(x_hole)[0] + assert y.voids + y_clone = y.clone() + assert y_clone.voids + edb.close() + + def test_modeler_path_convert_to_polygon(self): + # Done + target_path = os.path.join(local_path, "example_models", "convert_and_merge_path.aedb") + edbapp = Edb(target_path, edbversion=desktop_version, restart_rpc_server=True) + for path in edbapp.modeler.paths: + assert path.convert_to_polygon() + # cannot merge one net only - see test: test_unite_polygon for reference + edbapp.close() + + def test_156_check_path_length(self): + """""" + # Done + source_path = os.path.join(local_path, "example_models", test_subfolder, "test_path_length.aedb") + target_path = os.path.join(self.local_scratch.path, "test_path_length", "test.aedb") + self.local_scratch.copyfolder(source_path, target_path) + edbapp = Edb(target_path, desktop_version, restart_rpc_server=True) + net1 = [path for path in edbapp.modeler.paths if path.net_name == "loop1"] + net1_length = 0 + for path in net1: + net1_length += path.length + assert net1_length == 0.018144801000000002 + net2 = [path for path in edbapp.modeler.paths if path.net_name == "line1"] + net2_length = 0 + for path in net2: + net2_length += path.length + assert net2_length == 0.007 + net3 = [path for path in edbapp.modeler.paths if path.net_name == "lin2"] + net3_length = 0 + for path in net3: + net3_length += path.length + assert net3_length == 0.048605551 + net4 = [path for path in edbapp.modeler.paths if path.net_name == "lin3"] + net4_length = 0 + for path in net4: + net4_length += path.length + assert net4_length == 7.6e-3 + net5 = [path for path in edbapp.modeler.paths if path.net_name == "lin4"] + net5_length = 0 + for path in net5: + net5_length += path.length + assert net5_length == 0.026285626 + edbapp.close_edb() + + def test_duplicate(self): + # Done + edbapp = Edb() + edbapp["$H"] = "0.65mil" + assert edbapp["$H"] == 1.651e-5 + edbapp["$S_D"] = "10.65mil" + edbapp["$T"] = "21.3mil" + edbapp["$Antipad_R"] = "24mil" + edbapp["Via_S"] = "40mil" + edbapp.stackup.add_layer("bot_gnd", thickness="0.65mil") + edbapp.stackup.add_layer("d1", layer_type="dielectric", thickness="$S_D", material="FR4_epoxy") + edbapp.stackup.add_layer("trace2", thickness="$H") + edbapp.stackup.add_layer("d2", layer_type="dielectric", thickness="$T-$S_D", material="FR4_epoxy") + edbapp.stackup.add_layer("mid_gnd", thickness="0.65mil") + edbapp.stackup.add_layer("d3", layer_type="dielectric", thickness="13mil", material="FR4_epoxy") + edbapp.stackup.add_layer("top_gnd", thickness="0.65mil") + edbapp.stackup.add_layer("d4", layer_type="dielectric", thickness="13mil", material="FR4_epoxy") + edbapp.stackup.add_layer("trace1", thickness="$H") + r1 = edbapp.modeler.create_rectangle( + center_point=([0, 0]), + width="200mil", + height="200mil", + layer_name="top_gnd", + representation_type="CenterWidthHeight", + net_name="r1", + ) + r2 = edbapp.modeler.create_rectangle( + center_point=([0, 0]), + width="40mil", + height="$Antipad_R*2", + layer_name="top_gnd", + representation_type="CenterWidthHeight", + net_name="r2", + ) + assert r2 + assert r1.subtract(r2) + lay_list = ["bot_gnd", "mid_gnd"] + assert edbapp.modeler.primitives[0].duplicate_across_layers(lay_list) + assert edbapp.modeler.primitives_by_layer["mid_gnd"] + assert edbapp.modeler.primitives_by_layer["bot_gnd"] + edbapp.close() + + def test_unite_polygon(self): + # Done + edbapp = Edb() + edbapp["$H"] = "0.65mil" + edbapp["Via_S"] = "40mil" + edbapp["MS_W"] = "4.75mil" + edbapp["MS_S"] = "5mil" + edbapp["SL_W"] = "6.75mil" + edbapp["SL_S"] = "8mil" + edbapp.stackup.add_layer("trace1", thickness="$H") + t1_1 = edbapp.modeler.create_trace( + width="MS_W", + layer_name="trace1", + path_list=[("-Via_S/2", "0"), ("-MS_S/2-MS_W/2", "-16 mil"), ("-MS_S/2-MS_W/2", "-100 mil")], + start_cap_style="FLat", + end_cap_style="FLat", + net_name="t1_1", + ) + t2_1 = edbapp.modeler.create_trace( + width="MS_W", + layer_name="trace1", + path_list=[("-Via_S/2", "0"), ("-SL_S/2-SL_W/2", "16 mil"), ("-SL_S/2-SL_W/2", "100 mil")], + start_cap_style="FLat", + end_cap_style="FLat", + net_name="t2_1", + ) + t3_1 = edbapp.modeler.create_trace( + width="MS_W", + layer_name="trace1", + path_list=[("-Via_S/2", "0"), ("-SL_S/2-SL_W/2", "16 mil"), ("SL_S/2+MS_W/2", "100 mil")], + start_cap_style="FLat", + end_cap_style="FLat", + net_name="t3_1", + ) + t1_1.convert_to_polygon() + t2_1.convert_to_polygon() + t3_1.convert_to_polygon() + net_list = ["t1_1", "t2_1"] + assert len(edbapp.modeler.polygons) == 3 + edbapp.nets.merge_nets_polygons(net_names_list=net_list) + assert len(edbapp.modeler.polygons) == 2 + edbapp.modeler.unite_polygons_on_layer("trace1") + assert len(edbapp.modeler.polygons) == 1 + edbapp.close() + + def test_layer_name(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.modeler.polygons[50].layer_name == "1_Top" + edbapp.modeler.polygons[50].layer_name = "16_Bottom" + assert edbapp.modeler.polygons[50].layer_name == "16_Bottom" + edbapp.close() + + def test_287_circuit_ports(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + cap = edbapp.components.capacitors["C1"] + assert edbapp.siwave.create_circuit_port_on_pin(pos_pin=cap.pins["1"], neg_pin=cap.pins["2"]) + assert edbapp.components.capacitors["C3"].pins + assert edbapp.padstacks.pins + edbapp.close() + + def test_get_primitives_by_point_layer_and_nets(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + primitives = edbapp.modeler.get_primitive_by_layer_and_point(layer="Inner1(GND1)", point=[20e-3, 30e-3]) + assert primitives + assert len(primitives) == 1 + assert primitives[0].type == "polygon" + primitives = edbapp.modeler.get_primitive_by_layer_and_point(point=[20e-3, 30e-3]) + assert len(primitives) == 3 + edbapp.close() + + def test_arbitrary_wave_ports(self): + # Done + example_folder = os.path.join(local_path, "example_models", test_subfolder) + source_path_edb = os.path.join(example_folder, "example_arbitrary_wave_ports.aedb") + target_path_edb = os.path.join(self.local_scratch.path, "test_wave_ports", "test.aedb") + self.local_scratch.copyfolder(source_path_edb, target_path_edb) + edbapp = Edb(target_path_edb, desktop_version, restart_rpc_server=True) + edbapp.create_model_for_arbitrary_wave_ports( + temp_directory=self.local_scratch.path, + output_edb="wave_ports.aedb", + mounting_side="top", + ) + edbapp.close() + edb_model = os.path.join(self.local_scratch.path, "wave_ports.aedb") + test_edb = Edb(edbpath=edb_model, edbversion=desktop_version) + assert len(list(test_edb.nets.signal.keys())) == 13 + assert len(list(test_edb.stackup.layers.keys())) == 3 + assert "ref" in test_edb.stackup.layers + assert len(test_edb.modeler.polygons) == 12 + test_edb.close() + + def test_path_center_line(self): + # TODO wait Material class done. + edb = Edb() + edb.stackup.add_layer("GND", "Gap") + edb.stackup.add_layer("Substrat", "GND", layer_type="dielectric", thickness="0.2mm", material="Duroid (tm)") + edb.stackup.add_layer("TOP", "Substrat") + trace_length = 10e-3 + trace_width = 200e-6 + trace_gap = 1e-3 + edb.modeler.create_trace( + path_list=[[-trace_gap / 2, 0.0], [-trace_gap / 2, trace_length]], + layer_name="TOP", + width=trace_width, + net_name="signal1", + start_cap_style="Flat", + end_cap_style="Flat", + ) + centerline = edb.modeler.paths[0].center_line + assert centerline == [[-0.0005, 0.0], [-0.0005, 0.01]] + # TODO check enhancement request + # https://github.com/ansys/pyedb-core/issues/457 + # edb.modeler.paths[0].set_center_line([[0.0, 0.0], [0.0, 5e-3]]) # Path does not have center_lin setter. + # assert edb.modeler.paths[0].center_line == [[0.0, 0.0], [0.0, 5e-3]] + + def test_polygon_data_refactoring_bounding_box(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + poly_with_voids = [pp for pp in edbapp.modeler.polygons if pp.has_voids] + for poly in poly_with_voids: + for void in poly.voids: + assert void.bbox + edbapp.close() diff --git a/tests/grpc/system/test_edb_net_classes.py b/tests/grpc/system/test_edb_net_classes.py new file mode 100644 index 0000000000..d446b0f23f --- /dev/null +++ b/tests/grpc/system/test_edb_net_classes.py @@ -0,0 +1,52 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb net classes +""" + +import pytest + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch, target_path, target_path2, target_path4): + self.local_scratch = local_scratch + self.target_path = target_path + self.target_path2 = target_path2 + self.target_path4 = target_path4 + + def test_net_classes_queries(self, edb_examples): + """Evaluate net classes queries""" + from pyedb.grpc.database.nets.net_class import NetClass + + edbapp = edb_examples.get_si_verse() + assert edbapp.net_classes + net_class = NetClass.create(edbapp.layout, "DDR4_ADD") + net_class.add_net(edbapp.nets.nets["DDR4_A0"]) + net_class.add_net(edbapp.nets.nets["DDR4_A1"]) + assert edbapp.net_classes["DDR4_ADD"].name == "DDR4_ADD" + assert edbapp.net_classes["DDR4_ADD"].nets + edbapp.net_classes["DDR4_ADD"].name = "DDR4_ADD_RENAMED" + assert not edbapp.net_classes["DDR4_ADD_RENAMED"].is_null + edbapp.close() diff --git a/tests/grpc/system/test_edb_nets.py b/tests/grpc/system/test_edb_nets.py new file mode 100644 index 0000000000..88a8b2aded --- /dev/null +++ b/tests/grpc/system/test_edb_nets.py @@ -0,0 +1,262 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb nets +""" + +import os + +import pytest + +from pyedb.grpc.edb import EdbGrpc as Edb +from tests.conftest import desktop_version, local_path +from tests.legacy.system.conftest import test_subfolder + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch, target_path, target_path2, target_path4): + self.local_scratch = local_scratch + self.target_path = target_path + self.target_path2 = target_path2 + self.target_path4 = target_path4 + + def test_nets_queries(self, edb_examples): + """Evaluate nets queries""" + # Done + edbapp = edb_examples.get_si_verse() + assert len(edbapp.nets.netlist) > 0 + signalnets = edbapp.nets.signal + assert not signalnets[list(signalnets.keys())[0]].is_power_ground + assert len(list(signalnets[list(signalnets.keys())[0]].primitives)) > 0 + assert len(signalnets) > 2 + + powernets = edbapp.nets.power + assert len(powernets) > 2 + assert powernets["AVCC_1V3"].is_power_ground + powernets["AVCC_1V3"].is_power_ground = False + assert not powernets["AVCC_1V3"].is_power_ground + powernets["AVCC_1V3"].is_power_ground = True + assert powernets["AVCC_1V3"].name == "AVCC_1V3" + assert powernets["AVCC_1V3"].is_power_ground + assert len(list(powernets["AVCC_1V3"].components.keys())) > 0 + assert len(powernets["AVCC_1V3"].primitives) > 0 + + assert edbapp.nets.find_or_create_net("GND") + assert edbapp.nets.find_or_create_net(start_with="gn") + assert edbapp.nets.find_or_create_net(start_with="g", end_with="d") + assert edbapp.nets.find_or_create_net(end_with="d") + assert edbapp.nets.find_or_create_net(contain="usb") + assert not edbapp.nets.nets["AVCC_1V3"].extended_net + edbapp.extended_nets.auto_identify_power() + assert edbapp.nets.nets["AVCC_1V3"].extended_net + edbapp.close() + + def test_nets_get_power_tree(self, edb_examples): + """Evaluate nets get powertree.""" + # Done + edbapp = edb_examples.get_si_verse() + OUTPUT_NET = "5V" + GROUND_NETS = ["GND", "PGND"] + ( + component_list, + component_list_columns, + net_group, + ) = edbapp.nets.get_powertree(OUTPUT_NET, GROUND_NETS) + assert component_list + assert component_list_columns + assert net_group + edbapp.close() + + def test_nets_delete(self, edb_examples): + """Delete a net.""" + # Done + edbapp = edb_examples.get_si_verse() + assert "JTAG_TCK" in edbapp.nets.nets + edbapp.nets.nets["JTAG_TCK"].delete() + assert "JTAG_TCK" not in edbapp.nets.nets + edbapp.close() + + def test_nets_classify_nets(self, edb_examples): + """Reassign power based on list of nets.""" + # Done + edbapp = edb_examples.get_si_verse() + assert "SFPA_SDA" in edbapp.nets.signal + assert "SFPA_SCL" in edbapp.nets.signal + assert "SFPA_VCCR" in edbapp.nets.power + + assert edbapp.nets.classify_nets(["SFPA_SDA", "SFPA_SCL"], ["SFPA_VCCR"]) + assert "SFPA_SDA" in edbapp.nets.power + assert "SFPA_SDA" not in edbapp.nets.signal + assert "SFPA_SCL" in edbapp.nets.power + assert "SFPA_SCL" not in edbapp.nets.signal + assert "SFPA_VCCR" not in edbapp.nets.power + assert "SFPA_VCCR" in edbapp.nets.signal + + assert edbapp.nets.classify_nets(["SFPA_VCCR"], ["SFPA_SDA", "SFPA_SCL"]) + assert "SFPA_SDA" in edbapp.nets.signal + assert "SFPA_SCL" in edbapp.nets.signal + assert "SFPA_VCCR" in edbapp.nets.power + edbapp.close() + + def test_nets_arc_data(self, edb_examples): + """Evaluate primitive arc data.""" + # Done + edbapp = edb_examples.get_si_verse() + assert len(edbapp.nets.nets["1.2V_DVDDL"].primitives[0].arcs) > 0 + assert edbapp.nets.nets["1.2V_DVDDL"].primitives[0].arcs[0].start + assert edbapp.nets.nets["1.2V_DVDDL"].primitives[0].arcs[0].end + assert edbapp.nets.nets["1.2V_DVDDL"].primitives[0].arcs[0].height + edbapp.close() + + @pytest.mark.slow + def test_nets_dc_shorts(self, edb_examples): + # TODO get_connected_object return empty list. + edbapp = edb_examples.get_si_verse() + # dc_shorts = edbapp.layout_validation.dc_shorts() + # assert dc_shorts + # edbapp.nets.nets["DDR4_A0"].name = "DDR4$A0" + # edbapp.layout_validation.illegal_net_names(True) + # edbapp.layout_validation.illegal_rlc_values(True) + # + # # assert len(dc_shorts) == 20 + # assert ["SFPA_Tx_Fault", "PCIe_Gen4_CLKREQ_L"] in dc_shorts + # assert ["VDD_DDR", "GND"] in dc_shorts + # assert len(edbapp.nets["DDR4_DM3"].find_dc_short()) > 0 + # edbapp.nets["DDR4_DM3"].find_dc_short(True) + # assert len(edbapp.nets["DDR4_DM3"].find_dc_short()) == 0 + edbapp.close() + + def test_nets_eligible_power_nets(self, edb_examples): + """Evaluate eligible power nets.""" + # Done + edbapp = edb_examples.get_si_verse() + assert "GND" in [i.name for i in edbapp.nets.eligible_power_nets()] + edbapp.close() + + def test_nets_merge_polygon(self): + """Convert paths from net into polygons.""" + # Done + source_path = os.path.join(local_path, "example_models", test_subfolder, "test_merge_polygon.aedb") + target_path = os.path.join(self.local_scratch.path, "test_merge_polygon", "test.aedb") + self.local_scratch.copyfolder(source_path, target_path) + edbapp = Edb(target_path, desktop_version, restart_rpc_server=True) + assert edbapp.nets.merge_nets_polygons(["net1", "net2"]) + edbapp.close_edb() + + def test_layout_auto_parametrization_0(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + parameters = edbapp.auto_parametrize_design( + layers=True, + layer_filter="1_Top", + materials=False, + via_holes=False, + pads=False, + antipads=False, + traces=False, + use_relative_variables=False, + open_aedb_at_end=False, + ) + assert "$1_Top_value" in parameters + edbapp.close_edb() + + def test_layout_auto_parametrization_1(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + edbapp.auto_parametrize_design( + layers=True, materials=False, via_holes=False, pads=False, antipads=False, traces=False, via_offset=False + ) + assert len(list(edbapp.variables.keys())) == len(list(edbapp.stackup.layers.keys())) + edbapp.close_edb() + + def test_layout_auto_parametrization_2(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + edbapp.auto_parametrize_design( + layers=False, + materials=True, + via_holes=False, + pads=False, + antipads=False, + traces=False, + material_filter=["copper"], + expand_voids_size=0.0001, + expand_polygons_size=0.0001, + via_offset=True, + ) + assert "via_offset_x" in edbapp.variables + assert "$sigma_copper_delta" in edbapp.variables + edbapp.close_edb() + + def test_layout_auto_parametrization_3(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + edbapp.auto_parametrize_design( + layers=False, materials=True, via_holes=False, pads=False, antipads=False, traces=False + ) + assert len(list(edbapp.variables.values())) == 13 + edbapp.close_edb() + + def test_layout_auto_parametrization_4(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + edbapp.auto_parametrize_design( + layers=False, materials=False, via_holes=True, pads=False, antipads=False, traces=False + ) + assert len(list(edbapp.variables.values())) == 3 + edbapp.close_edb() + + def test_layout_auto_parametrization_5(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + edbapp.auto_parametrize_design( + layers=False, materials=False, via_holes=False, pads=True, antipads=False, traces=False + ) + assert len(list(edbapp.variables.values())) == 5 + edbapp.close_edb() + + def test_layout_auto_parametrization_6(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + edbapp.auto_parametrize_design( + layers=False, materials=False, via_holes=False, pads=False, antipads=True, traces=False + ) + assert len(list(edbapp.variables.values())) == 2 + edbapp.close_edb() + + def test_layout_auto_parametrization_7(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + edbapp.auto_parametrize_design( + layers=False, + materials=False, + via_holes=False, + pads=False, + antipads=False, + traces=True, + trace_net_filter=["SFPA_Tx_Fault", "SFPA_Tx_Disable", "SFPA_SDA", "SFPA_SCL", "SFPA_Rx_LOS"], + ) + assert len(list(edbapp.variables.keys())) == 3 + edbapp.close_edb() diff --git a/tests/grpc/system/test_edb_padstacks.py b/tests/grpc/system/test_edb_padstacks.py new file mode 100644 index 0000000000..b3452a43b6 --- /dev/null +++ b/tests/grpc/system/test_edb_padstacks.py @@ -0,0 +1,510 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb padstacks +""" +import os + +import pytest + +from pyedb.grpc.edb import EdbGrpc as Edb +from tests.conftest import desktop_version, local_path +from tests.legacy.system.conftest import test_subfolder + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch, target_path, target_path3, target_path4): + self.local_scratch = local_scratch + self.target_path = target_path + self.target_path3 = target_path3 + self.target_path4 = target_path4 + + def test_get_pad_parameters(self, edb_examples): + """Access to pad parameters.""" + # Done + edbapp = edb_examples.get_si_verse() + pin = edbapp.components.get_pin_from_component("J1", pin_name="1") + parameters = edbapp.padstacks.get_pad_parameters(pin=pin[0], layername="1_Top", pad_type="regular_pad") + assert isinstance(parameters[1], list) + assert isinstance(parameters[0], str) + edbapp.close() + + def test_get_vias_from_nets(self, edb_examples): + """Use padstacks' get_via_instance_from_net method.""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.padstacks.get_via_instance_from_net("GND") + assert not edbapp.padstacks.get_via_instance_from_net(["GND2"]) + edbapp.close() + + def test_create_with_packstack_name(self, edb_examples): + """Create a padstack""" + # Create myVia + edbapp = edb_examples.get_si_verse() + edbapp.padstacks.create(padstackname="myVia") + assert "myVia" in list(edbapp.padstacks.definitions.keys()) + edbapp.padstacks.definitions["myVia"].hole_range = "begin_on_upper_pad" + assert edbapp.padstacks.definitions["myVia"].hole_range == "begin_on_upper_pad" + edbapp.padstacks.definitions["myVia"].hole_range = "through" + assert edbapp.padstacks.definitions["myVia"].hole_range == "through" + # Create myVia_bullet + edbapp.padstacks.create(padstackname="myVia_bullet", antipad_shape="Bullet") + assert isinstance(edbapp.padstacks.definitions["myVia"].instances, list) + assert "myVia_bullet" in list(edbapp.padstacks.definitions.keys()) + edbapp.add_design_variable("via_x", 5e-3) + edbapp["via_y"] = "1mm" + assert edbapp["via_y"] == 1e-3 + assert edbapp.padstacks.place(["via_x", "via_x+via_y"], "myVia", via_name="via_test1") + assert edbapp.padstacks.place(["via_x", "via_x+via_y*2"], "myVia_bullet") + edbapp.padstacks["via_test1"].net_name = "GND" + assert edbapp.padstacks["via_test1"].net_name == "GND" + padstack = edbapp.padstacks.place(["via_x", "via_x+via_y*3"], "myVia", is_pin=True) + padstack_instance = edbapp.padstacks.instances[padstack.edb_uid] + assert padstack_instance.is_pin + assert padstack_instance.position + assert padstack_instance.start_layer in padstack_instance.layer_range_names + assert padstack_instance.stop_layer in padstack_instance.layer_range_names + padstack_instance.position = [0.001, 0.002] + assert padstack_instance.position == [0.001, 0.002] + assert padstack_instance.parametrize_position() + assert isinstance(padstack_instance.rotation, float) + edbapp.padstacks.create_circular_padstack(padstackname="mycircularvia") + assert "mycircularvia" in list(edbapp.padstacks.definitions.keys()) + assert not padstack_instance.backdrill_top + assert not padstack_instance.backdrill_bottom + padstack_instance.delete() + via = edbapp.padstacks.place([0, 0], "myVia") + via.set_back_drill_by_layer(drill_to_layer="Inner4(Sig2)", diameter=0.5e-3, offset=0.0, from_bottom=True) + assert via.get_back_drill_by_layer()[0] == "Inner4(Sig2)" + assert via.get_back_drill_by_layer()[1] == 0.0 + assert via.get_back_drill_by_layer()[2] == 5e-4 + assert via.backdrill_bottom + + # via = edbapp.padstacks.instances_by_name["Via1266"] + # via.backdrill_parameters = { + # "from_bottom": {"drill_to_layer": "Inner5(PWR2)", "diameter": "0.4mm", "stub_length": "0.1mm"}, + # "from_top": {"drill_to_layer": "Inner2(PWR1)", "diameter": "0.41mm", "stub_length": "0.11mm"}, + # } + # assert via.backdrill_parameters == { + # "from_bottom": {"drill_to_layer": "Inner5(PWR2)", "diameter": "0.4mm", "stub_length": "0.1mm"}, + # "from_top": {"drill_to_layer": "Inner2(PWR1)", "diameter": "0.41mm", "stub_length": "0.11mm"}, + # } + edbapp.close() + + def test_padstacks_get_nets_from_pin_list(self, edb_examples): + """Retrieve pin list from component and net.""" + # Done + edbapp = edb_examples.get_si_verse() + cmp_pinlist = edbapp.padstacks.get_pinlist_from_component_and_net("U1", "GND") + assert cmp_pinlist[0].net.name + edbapp.close() + + def test_padstack_properties_getter(self, edb_examples): + """Evaluate properties""" + # Done + edbapp = edb_examples.get_si_verse() + for name in list(edbapp.padstacks.definitions.keys()): + padstack = edbapp.padstacks.definitions[name] + assert padstack.hole_plating_thickness is not None or False + assert padstack.hole_plating_ratio is not None or False + assert padstack.start_layer is not None or False + assert padstack.stop_layer is not None or False + assert padstack.material is not None or False + assert padstack.hole_finished_size is not None or False + assert padstack.hole_rotation is not None or False + assert padstack.hole_offset_x is not None or False + assert padstack.hole_offset_y is not None or False + pad = padstack.pad_by_layer[padstack.stop_layer] + if not pad.shape == "NoGeometry": + assert pad.parameters_values is not None or False + assert pad.offset_x is not None or False + assert pad.offset_y is not None or False + assert isinstance(pad.geometry_type, int) + polygon = pad.polygon_data + if polygon: + assert polygon.bbox() + edbapp.close() + + def test_padstack_properties_setter(self, edb_examples): + """Set padstack properties""" + edbapp = edb_examples.get_si_verse() + pad = edbapp.padstacks.definitions["c180h127"] + hole_pad = 8 + tol = 1e-12 + pad.hole_properties = hole_pad + pad.hole_offset_x = 0 + pad.hole_offset_y = 1 + pad.hole_rotation = 0 + pad.hole_plating_ratio = 90 + assert pad.hole_plating_ratio == 90 + pad.hole_plating_thickness = 0.3 + assert abs(pad.hole_plating_thickness - 0.3) <= tol + pad.material = "copper" + offset_x = 7 + offset_y = 1 + # pad.pad_by_layer[pad.stop_layer].shape = "circle" + pad.pad_by_layer[pad.stop_layer].offset_x = offset_x + pad.pad_by_layer[pad.stop_layer].offset_y = offset_y + assert pad.pad_by_layer[pad.stop_layer].offset_x == 7 + assert pad.pad_by_layer[pad.stop_layer].offset_y == 1 + # pad.pad_by_layer[pad.stop_layer].parameters = {"Diameter": 8} + # assert pad.pad_by_layer[pad.stop_layer].parameters["Diameter"].tofloat == 8 + # pad.pad_by_layer[pad.stop_layer].parameters = {"Diameter": 1} + # pad.pad_by_layer[pad.stop_layer].shape = "Square" + # pad.pad_by_layer[pad.stop_layer].parameters = {"Size": 1} + # pad.pad_by_layer[pad.stop_layer].shape = "Rectangle" + # pad.pad_by_layer[pad.stop_layer].parameters = {"XSize": 1, "YSize": 1} + # pad.pad_by_layer[pad.stop_layer].shape = "Oval" + # pad.pad_by_layer[pad.stop_layer].parameters = {"XSize": 1, "YSize": 1, "CornerRadius": 1} + # pad.pad_by_layer[pad.stop_layer].parameters = {"XSize": 1, "YSize": 1, "CornerRadius": 1} + # pad.pad_by_layer[pad.stop_layer].parameters = [1, 1, 1] + edbapp.close() + + def test_padstack_get_instance(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.padstacks.get_instances(name="Via1961") + assert edbapp.padstacks.get_instances(definition_name="v35h15") + assert edbapp.padstacks.get_instances(net_name="1V0") + assert edbapp.padstacks.get_instances(component_reference_designator="U7") + """Access padstack instance by name.""" + padstack_instances = edbapp.padstacks.get_instances(net_name="GND") + assert len(padstack_instances) + padstack_1 = padstack_instances[0] + assert padstack_1.id + assert isinstance(padstack_1.bounding_box, list) + for v in padstack_instances: + if not v.is_pin: + v.name = "TestInst" + assert v.name == "TestInst" + break + edbapp.close() + + def test_padstack_duplicate_padstack(self, edb_examples): + """Duplicate a padstack.""" + # Done + edbapp = edb_examples.get_si_verse() + edbapp.padstacks.duplicate( + target_padstack_name="c180h127", + new_padstack_name="c180h127_NEW", + ) + assert edbapp.padstacks.definitions["c180h127_NEW"] + edbapp.close() + + def test_padstack_set_pad_property(self, edb_examples): + """Set pad and antipad properties of the padstack.""" + # Done + edbapp = edb_examples.get_si_verse() + edbapp.padstacks.set_pad_property( + padstack_name="c180h127", + layer_name="new", + pad_shape="Circle", + pad_params="800um", + ) + assert edbapp.padstacks.definitions["c180h127"].pad_by_layer["new"] + edbapp.close() + + def test_microvias(self): + """Convert padstack to microvias 3D objects.""" + # TODO later + # source_path = os.path.join(local_path, "example_models", test_subfolder, "padstacks.aedb") + # target_path = os.path.join(self.local_scratch.path, "test_128_microvias.aedb") + # self.local_scratch.copyfolder(source_path, target_path) + # edbapp = Edb(target_path, edbversion=desktop_version, restart_rpc_server=True, kill_all_instances=True) + # assert edbapp.padstacks.definitions["Padstack_Circle"].convert_to_3d_microvias(False) + # assert edbapp.padstacks.definitions["Padstack_Rectangle"].convert_to_3d_microvias(False, hole_wall_angle=10) + # assert edbapp.padstacks.definitions["Padstack_Polygon_p12"].convert_to_3d_microvias(False) + # assert edbapp.padstacks.definitions["MyVia"].convert_to_3d_microvias( + # convert_only_signal_vias=False, delete_padstack_def=False + # ) + # assert edbapp.padstacks.definitions["MyVia_square"].convert_to_3d_microvias( + # convert_only_signal_vias=False, delete_padstack_def=False + # ) + # assert edbapp.padstacks.definitions["MyVia_rectangle"].convert_to_3d_microvias( + # convert_only_signal_vias=False, delete_padstack_def=False + # ) + # assert not edbapp.padstacks.definitions["MyVia_poly"].convert_to_3d_microvias( + # convert_only_signal_vias=False, delete_padstack_def=False + # ) + # edbapp.close() + pass + + def test_split_microvias(self): + """Convert padstack definition to multiple microvias definitions.""" + edbapp = Edb(self.target_path4, edbversion=desktop_version, restart_rpc_server=True, kill_all_instances=True) + assert len(edbapp.padstacks.definitions["C4_POWER_1"].split_to_microvias()) > 0 + edbapp.close() + + def test_padstack_plating_ratio_fixing(self, edb_examples): + """Fix hole plating ratio.""" + # Done + edbapp = edb_examples.get_si_verse() + assert edbapp.padstacks.check_and_fix_via_plating() + edbapp.close() + + def test_padstack_search_reference_pins(self, edb_examples): + """Search for reference pins using given criteria.""" + # Done + edbapp = edb_examples.get_si_verse() + pin = edbapp.components.instances["J5"].pins["19"] + assert pin + ref_pins = pin.get_reference_pins(reference_net="GND", search_radius=5e-3, max_limit=0, component_only=True) + assert len(ref_pins) == 3 + reference_pins = edbapp.padstacks.get_reference_pins( + positive_pin=pin, reference_net="GND", search_radius=5e-3, max_limit=0, component_only=True + ) + assert len(reference_pins) == 3 + reference_pins = edbapp.padstacks.get_reference_pins( + positive_pin=pin, reference_net="GND", search_radius=5e-3, max_limit=2, component_only=True + ) + assert len(reference_pins) == 2 + reference_pins = edbapp.padstacks.get_reference_pins( + positive_pin=pin, reference_net="GND", search_radius=5e-3, max_limit=0, component_only=False + ) + assert len(reference_pins) == 11 + edbapp.close() + + def test_vias_metal_volume(self, edb_examples): + """Metal volume of via hole instance.""" + # Done + edbapp = edb_examples.get_si_verse() + vias = [via for via in list(edbapp.padstacks.instances.values()) if not via.start_layer == via.stop_layer] + assert vias[0].metal_volume + assert vias[1].metal_volume + edbapp.close() + + def test_padstacks_create_rectangle_in_pad(self): + """Create a rectangle inscribed inside a padstack instance pad.""" + example_model = os.path.join(local_path, "example_models", test_subfolder, "padstacks.aedb") + self.local_scratch.copyfolder( + example_model, + os.path.join(self.local_scratch.path, "padstacks2.aedb"), + ) + edb = Edb(edbpath=os.path.join(self.local_scratch.path, "padstacks2.aedb"), edbversion=desktop_version) + for test_prop in (edb.padstacks.instances, edb.padstacks.instances): + padstack_instances = list(test_prop.values()) + for padstack_instance in padstack_instances: + result = padstack_instance.create_rectangle_in_pad("s", partition_max_order=8) + if result: + if padstack_instance.padstack_definition != "Padstack_None": + assert result.points() + else: + assert not result.points() + edb.close() + + def test_padstaks_plot_on_matplotlib(self): + """Plot a Net to Matplotlib 2D Chart.""" + # Done + edb_plot = Edb(self.target_path3, edbversion=desktop_version, restart_rpc_server=True) + + local_png1 = os.path.join(self.local_scratch.path, "test1.png") + edb_plot.nets.plot( + nets=None, + layers=None, + save_plot=local_png1, + plot_components_on_top=True, + plot_components_on_bottom=True, + outline=[[-10e-3, -10e-3], [110e-3, -10e-3], [110e-3, 70e-3], [-10e-3, 70e-3]], + ) + assert os.path.exists(local_png1) + assert edb_plot.modeler.primitives[0].plot(show=False) + local_png2 = os.path.join(self.local_scratch.path, "test2.png") + edb_plot.nets.plot( + nets="DDR4_DQS7_N", + layers=None, + save_plot=local_png2, + plot_components_on_top=True, + plot_components_on_bottom=True, + ) + assert os.path.exists(local_png2) + + local_png3 = os.path.join(self.local_scratch.path, "test3.png") + edb_plot.nets.plot( + nets=["DDR4_DQ57", "DDR4_DQ56"], + layers="1_Top", + color_by_net=True, + save_plot=local_png3, + plot_components=True, + plot_vias=True, + ) + assert os.path.exists(local_png3) + + local_png4 = os.path.join(self.local_scratch.path, "test4.png") + edb_plot.stackup.plot( + save_plot=local_png4, + plot_definitions=list(edb_plot.padstacks.definitions.keys())[0], + ) + assert os.path.exists(local_png4) + + local_png5 = os.path.join(self.local_scratch.path, "test5.png") + edb_plot.stackup.plot( + scale_elevation=False, + save_plot=local_png5, + plot_definitions=list(edb_plot.padstacks.definitions.keys())[0], + ) + assert os.path.exists(local_png4) + edb_plot.close() + + def test_update_padstacks_after_layer_name_changed(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + signal_layer_list = list(edbapp.stackup.signal_layers.values()) + old_layers = [] + for n_layer, layer in enumerate(signal_layer_list): + new_name = f"new_signal_name_{n_layer}" + old_layers.append(layer.name) + layer.name = new_name + for layer_name in list(edbapp.stackup.layers.keys()): + print(f"New layer name is {layer_name}") + for padstack_inst in list(edbapp.padstacks.instances.values())[:100]: + padsatck_layers = padstack_inst.layer_range_names + assert padsatck_layers not in old_layers + edbapp.close_edb() + + def test_hole(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + edbapp.padstacks.definitions["v35h15"].hole_diameter = "0.16mm" + assert edbapp.padstacks.definitions["v35h15"].hole_diameter == 0.00016 + edbapp.close() + + def test_padstack_instances_rtree_index(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + index = edbapp.padstacks.get_padstack_instances_rtree_index() + assert index.bounds == [-0.013785, -0.00225, 0.148, 0.078] + stats = edbapp.get_statistics() + bbox = (0.0, 0.0, stats.layout_size[0], stats.layout_size[1]) + test = list(index.intersection(bbox)) + assert len(test) == 5689 + index = edbapp.padstacks.get_padstack_instances_rtree_index(nets="GND") + test = list(index.intersection(bbox)) + assert len(test) == 2048 + test = edbapp.padstacks.get_padstack_instances_intersecting_bounding_box( + bounding_box=[0, 0, 0.05, 0.08], nets="GND" + ) + assert len(test) == 194 + edbapp.close() + + def test_polygon_based_padstack(self, edb_examples): + # Done + edbapp = edb_examples.get_si_verse() + polygon_data = edbapp.modeler.paths[0].polygon_data + edbapp.padstacks.create( + padstackname="test", + pad_shape="Polygon", + antipad_shape="Polygon", + pad_polygon=polygon_data, + antipad_polygon=polygon_data, + ) + edbapp.padstacks.create( + padstackname="test2", + pad_shape="Polygon", + antipad_shape="Polygon", + pad_polygon=[ + [-0.025, -0.02], + [0.025, -0.02], + [0.025, 0.02], + [-0.025, 0.02], + [-0.025, -0.02], + ], + antipad_polygon=[ + [-0.025, -0.02], + [0.025, -0.02], + [0.025, 0.02], + [-0.025, 0.02], + [-0.025, -0.02], + ], + ) + assert edbapp.padstacks.definitions["test"] + assert edbapp.padstacks.definitions["test2"] + edbapp.close() + + def test_via_fence(self): + # TODO check bug #466 status polygon based via + # source_path = os.path.join(local_path, "example_models", test_subfolder, "via_fence_generic_project.aedb") + # target_path1 = os.path.join(self.local_scratch.path, "test_pvia_fence", "via_fence1.aedb") + # target_path2 = os.path.join(self.local_scratch.path, "test_pvia_fence", "via_fence2.aedb") + # self.local_scratch.copyfolder(source_path, target_path1) + # self.local_scratch.copyfolder(source_path, target_path2) + # edbapp = Edb(target_path1, edbversion=desktop_version, restart_rpc_server=True) + # assert edbapp.padstacks.merge_via_along_lines(net_name="GND", distance_threshold=2e-3, minimum_via_number=6) + # assert not edbapp.padstacks.merge_via_along_lines( + # net_name="test_dummy", distance_threshold=2e-3, minimum_via_number=6 + # ) + # assert "main_via" in edbapp.padstacks.definitions + # assert "via_central" in edbapp.padstacks.definitions + # edbapp.close(terminate_rpc_session=False) + # edbapp = Edb(target_path2, edbversion=desktop_version) + # assert edbapp.padstacks.merge_via_along_lines( + # net_name="GND", distance_threshold=2e-3, minimum_via_number=6, selected_angles=[0, 180] + # ) + # assert "main_via" in edbapp.padstacks.definitions + # assert "via_central" in edbapp.padstacks.definitions + # edbapp.close() + pass + + # def test_pad_parameter(self, edb_examples): + # edbapp = edb_examples.get_si_verse() + # o_pad_params = edbapp.padstacks.definitions["v35h15"].pad_by_layer + # assert o_pad_params["1_Top"][0].name == "PADGEOMTYPE_CIRCLE" + # + # i_pad_params = {} + # i_pad_params["regular_pad"] = [ + # {"layer_name": "1_Top", "shape": "circle", "offset_x": "0.1mm", "rotation": "0", "diameter": "0.5mm"} + # ] + # i_pad_params["anti_pad"] = [{"layer_name": "1_Top", "shape": "circle", "diameter": "1mm"}] + # i_pad_params["thermal_pad"] = [ + # { + # "layer_name": "1_Top", + # "shape": "round90", + # "inner": "1mm", + # "channel_width": "0.2mm", + # "isolation_gap": "0.3mm", + # } + # ] + # edbapp.padstacks.definitions["v35h15"].pad_parameters = i_pad_params + # o2_pad_params = edbapp.padstacks.definitions["v35h15"].pad_parameters + # assert o2_pad_params["regular_pad"][0]["diameter"] == "0.5mm" + # assert o2_pad_params["regular_pad"][0]["offset_x"] == "0.1mm" + # assert o2_pad_params["anti_pad"][0]["diameter"] == "1mm" + # assert o2_pad_params["thermal_pad"][0]["inner"] == "1mm" + # assert o2_pad_params["thermal_pad"][0]["channel_width"] == "0.2mm" + # + # def test_pad_parameter2(self, edb_examples): + # edbapp = edb_examples.get_si_verse() + # o_hole_params = edbapp.padstacks.definitions["v35h15"].hole_parameters + # assert o_hole_params["shape"] == "circle" + # edbapp.padstacks.definitions["v35h15"].hole_parameters = {"shape": "circle", "diameter": "0.2mm"} + # assert edbapp.padstacks.definitions["v35h15"].hole_parameters["diameter"] == "0.2mm" + + def test_via_merge(self, edb_examples): + # TODO + # edbapp = edb_examples.get_si_verse() + # polygon = [[[118e-3, 60e-3], [125e-3, 60e-3], [124e-3, 56e-3], [118e-3, 56e-3]]] + # result = edbapp.padstacks.merge_via(contour_boxes=polygon, start_layer="1_Top", stop_layer="16_Bottom") + # assert len(result) == 1 + # edbapp.close() + pass diff --git a/tests/grpc/system/test_edb_stackup.py b/tests/grpc/system/test_edb_stackup.py new file mode 100644 index 0000000000..1fdff2fa08 --- /dev/null +++ b/tests/grpc/system/test_edb_stackup.py @@ -0,0 +1,1128 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb stackup +""" + +import math +import os + +import pytest + +from pyedb.dotnet.edb import Edb +from tests.conftest import desktop_version, local_path +from tests.legacy.system.conftest import test_subfolder + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch, target_path, target_path2, target_path4): + self.local_scratch = local_scratch + self.target_path = target_path + self.target_path2 = target_path2 + self.target_path4 = target_path4 + + def test_stackup_get_signal_layers(self, edb_examples): + """Report residual copper area per layer.""" + edbapp = edb_examples.get_si_verse() + assert edbapp.stackup.residual_copper_area_per_layer() + edbapp.close() + + def test_stackup_limits(self, edb_examples): + """Retrieve stackup limits.""" + edbapp = edb_examples.get_si_verse() + assert edbapp.stackup.limits() + edbapp.close() + + def test_stackup_add_outline(self): + """Add an outline layer named ``"Outline1"`` if it is not present.""" + edbapp = Edb( + edbversion=desktop_version, + ) + assert edbapp.stackup.add_outline_layer() + assert "Outline" in edbapp.stackup.non_stackup_layers + edbapp.stackup.add_layer("1_Top") + assert edbapp.stackup.layers["1_Top"].thickness == 3.5e-05 + edbapp.stackup.layers["1_Top"].thickness = 4e-5 + assert edbapp.stackup.layers["1_Top"].thickness == 4e-05 + edbapp.close() + + def test_stackup_create_symmetric_stackup(self): + """Create a symmetric stackup.""" + app_edb = Edb(edbversion=desktop_version) + assert not app_edb.stackup.create_symmetric_stackup(9) + assert app_edb.stackup.create_symmetric_stackup(8) + app_edb.close() + + app_edb = Edb(edbversion=desktop_version) + assert app_edb.stackup.create_symmetric_stackup(8, soldermask=False) + app_edb.close() + + def test_stackup_place_a3dcomp_3d_placement(self): + """Place a 3D Component into current layout.""" + source_path = os.path.join(local_path, "example_models", test_subfolder, "lam_for_bottom_place.aedb") + target_path = os.path.join(self.local_scratch.path, "output.aedb") + self.local_scratch.copyfolder(source_path, target_path) + laminate_edb = Edb(target_path, edbversion=desktop_version) + chip_a3dcomp = os.path.join(local_path, "example_models", test_subfolder, "chip.a3dcomp") + try: + layout = laminate_edb.active_layout + cell_instances = list(layout.CellInstances) + assert len(cell_instances) == 0 + assert laminate_edb.stackup.place_a3dcomp_3d_placement( + chip_a3dcomp, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + place_on_top=True, + ) + cell_instances = list(layout.CellInstances) + assert len(cell_instances) == 1 + cell_instance = cell_instances[0] + assert cell_instance.Is3DPlacement() + if desktop_version > "2023.1": + ( + res, + local_origin, + rotation_axis_from, + rotation_axis_to, + angle, + loc, + _, + ) = cell_instance.Get3DTransformation() + else: + ( + res, + local_origin, + rotation_axis_from, + rotation_axis_to, + angle, + loc, + ) = cell_instance.Get3DTransformation() + assert res + zero_value = laminate_edb.edb_value(0) + one_value = laminate_edb.edb_value(1) + origin_point = laminate_edb.edb_api.geometry.point3d_data(zero_value, zero_value, zero_value) + x_axis_point = laminate_edb.edb_api.geometry.point3d_data(one_value, zero_value, zero_value) + assert local_origin.IsEqual(origin_point) + assert rotation_axis_from.IsEqual(x_axis_point) + assert rotation_axis_to.IsEqual(x_axis_point) + assert angle.IsEqual(zero_value) + assert loc.IsEqual( + laminate_edb.edb_api.geometry.point3d_data(zero_value, zero_value, laminate_edb.edb_value(170e-6)) + ) + assert laminate_edb.save_edb() + finally: + laminate_edb.close() + + def test_stackup_place_a3dcomp_3d_placement_on_bottom(self): + """Place a 3D Component into current layout.""" + source_path = os.path.join(local_path, "example_models", test_subfolder, "lam_for_bottom_place.aedb") + target_path = os.path.join(self.local_scratch.path, "output.aedb") + self.local_scratch.copyfolder(source_path, target_path) + laminate_edb = Edb(target_path, edbversion=desktop_version) + chip_a3dcomp = os.path.join(local_path, "example_models", test_subfolder, "chip.a3dcomp") + try: + layout = laminate_edb.active_layout + cell_instances = list(layout.CellInstances) + assert len(cell_instances) == 0 + assert laminate_edb.stackup.place_a3dcomp_3d_placement( + chip_a3dcomp, + angle=90.0, + offset_x=0.5e-3, + offset_y=-0.5e-3, + place_on_top=False, + ) + cell_instances = list(layout.CellInstances) + assert len(cell_instances) == 1 + cell_instance = cell_instances[0] + assert cell_instance.Is3DPlacement() + if desktop_version > "2023.1": + ( + res, + local_origin, + rotation_axis_from, + rotation_axis_to, + angle, + loc, + mirror, + ) = cell_instance.Get3DTransformation() + else: + ( + res, + local_origin, + rotation_axis_from, + rotation_axis_to, + angle, + loc, + ) = cell_instance.Get3DTransformation() + assert res + zero_value = laminate_edb.edb_value(0) + one_value = laminate_edb.edb_value(1) + flip_angle_value = laminate_edb.edb_value("180deg") + origin_point = laminate_edb.edb_api.geometry.point3d_data(zero_value, zero_value, zero_value) + x_axis_point = laminate_edb.edb_api.geometry.point3d_data(one_value, zero_value, zero_value) + assert local_origin.IsEqual(origin_point) + assert rotation_axis_from.IsEqual(x_axis_point) + assert rotation_axis_to.IsEqual( + laminate_edb.edb_api.geometry.point3d_data(zero_value, laminate_edb.edb_value(-1.0), zero_value) + ) + assert angle.IsEqual(flip_angle_value) + assert loc.IsEqual( + laminate_edb.edb_api.geometry.point3d_data( + laminate_edb.edb_value(0.5e-3), + laminate_edb.edb_value(-0.5e-3), + zero_value, + ) + ) + assert laminate_edb.save_edb() + finally: + laminate_edb.close() + + def test_stackup_properties_0(self): + """Evaluate various stackup properties.""" + source_path = os.path.join(local_path, "example_models", test_subfolder, "ANSYS-HSD_V1.aedb") + target_path = os.path.join(self.local_scratch.path, "test_0124.aedb") + self.local_scratch.copyfolder(source_path, target_path) + edbapp = Edb(target_path, edbversion=desktop_version) + assert isinstance(edbapp.stackup.layers, dict) + assert isinstance(edbapp.stackup.signal_layers, dict) + assert isinstance(edbapp.stackup.dielectric_layers, dict) + assert isinstance(edbapp.stackup.non_stackup_layers, dict) + assert not edbapp.stackup["Outline"].is_stackup_layer + assert edbapp.stackup["1_Top"].conductivity + assert edbapp.stackup["DE1"].permittivity + assert edbapp.stackup.add_layer("new_layer") + new_layer = edbapp.stackup["new_layer"] + assert new_layer.is_stackup_layer + assert not new_layer.is_negative + new_layer.name = "renamed_layer" + assert new_layer.name == "renamed_layer" + rename_layer = edbapp.stackup["renamed_layer"] + rename_layer.thickness = 50e-6 + assert rename_layer.thickness == 50e-6 + rename_layer.etch_factor = 0 + rename_layer.etch_factor = 2 + assert rename_layer.etch_factor == 2 + assert rename_layer.material + assert rename_layer.type + assert rename_layer.dielectric_fill + + rename_layer.roughness_enabled = True + assert rename_layer.roughness_enabled + rename_layer.roughness_enabled = False + assert not rename_layer.roughness_enabled + assert rename_layer.assign_roughness_model("groisse", groisse_roughness="2um") + assert rename_layer.assign_roughness_model(apply_on_surface="1_Top") + assert rename_layer.assign_roughness_model(apply_on_surface="bottom") + assert rename_layer.assign_roughness_model(apply_on_surface="side") + assert edbapp.stackup.add_layer("new_above", "1_Top", "insert_above") + assert edbapp.stackup.add_layer("new_below", "1_Top", "insert_below") + assert edbapp.stackup.add_layer("new_bottom", "1_Top", "add_on_bottom", "dielectric") + assert edbapp.stackup.remove_layer("new_bottom") + assert "new_bottom" not in edbapp.stackup.layers + + assert edbapp.stackup["1_Top"].color + edbapp.stackup["1_Top"].color = [0, 120, 0] + assert edbapp.stackup["1_Top"].color == [0, 120, 0] + edbapp.stackup["1_Top"].transparency = 10 + assert edbapp.stackup["1_Top"].transparency == 10 + assert edbapp.stackup.mode == "Laminate" + edbapp.stackup.mode = "Overlapping" + assert edbapp.stackup.mode == "Overlapping" + edbapp.stackup.mode = "MultiZone" + assert edbapp.stackup.mode == "MultiZone" + edbapp.stackup.mode = "Overlapping" + assert edbapp.stackup.mode == "Overlapping" + assert edbapp.stackup.add_layer("new_bottom", "1_Top", "add_at_elevation", "dielectric", elevation=0.0003) + edbapp.close() + + def test_stackup_properties_1(self): + """Evaluate various stackup properties.""" + edbapp = Edb(edbversion=desktop_version) + import_method = edbapp.stackup.load + export_method = edbapp.stackup.export + + assert import_method(os.path.join(local_path, "example_models", test_subfolder, "ansys_pcb_stackup.csv")) + assert "18_Bottom" in edbapp.stackup.layers.keys() + assert edbapp.stackup.add_layer("19_Bottom", None, "add_on_top", material="iron") + export_stackup_path = os.path.join(self.local_scratch.path, "export_galileo_stackup.csv") + assert export_method(export_stackup_path) + assert os.path.exists(export_stackup_path) + + edbapp.close() + + def test_stackup_properties_2(self): + """Evaluate various stackup properties.""" + edbapp = Edb(edbversion=desktop_version) + import_method = edbapp.stackup.load + export_method = edbapp.stackup.export + + assert import_method(os.path.join(local_path, "example_models", test_subfolder, "ansys_pcb_stackup.csv")) + assert "18_Bottom" in edbapp.stackup.layers.keys() + assert edbapp.stackup.add_layer("19_Bottom", None, "add_on_top", material="iron") + export_stackup_path = os.path.join(self.local_scratch.path, "export_galileo_stackup.csv") + assert export_method(export_stackup_path) + assert os.path.exists(export_stackup_path) + edbapp.close() + + def test_stackup_layer_properties(self, edb_examples): + """Evaluate various layer properties.""" + # TODO + # edbapp = edb_examples.get_si_verse() + # edbapp.stackup.load(os.path.join(local_path, "example_models", test_subfolder, "ansys_pcb_stackup.xml")) + # layer = edbapp.stackup["1_Top"] + # layer.name = "TOP" + # assert layer.name == "TOP" + # layer.type = "dielectric" + # assert layer.type == "dielectric" + # layer.type = "signal" + # layer.color = (0, 0, 0) + # assert layer.color == (0, 0, 0) + # layer.transparency = 0 + # assert layer.transparency == 0 + # layer.etch_factor = 2 + # assert layer.etch_factor == 2 + # layer.thickness = 50e-6 + # assert layer.thickness == 50e-6 + # assert layer.lower_elevation + # assert layer.upper_elevation + # layer.is_negative = True + # assert layer.is_negative + # assert not layer.is_via_layer + # assert layer.material == "copper" + # edbapp.close() + pass + + def test_stackup_load_json(self): + """Import stackup from a file.""" + source_path = os.path.join(local_path, "example_models", test_subfolder, "ANSYS-HSD_V1.aedb") + fpath = os.path.join(local_path, "example_models", test_subfolder, "stackup.json") + edbapp = Edb(source_path, edbversion=desktop_version) + edbapp.stackup.load(fpath) + edbapp.close() + + def test_stackup_export_json(self): + """Export stackup into a JSON file.""" + import json + + MATERIAL_MEGTRON_4 = { + "name": "Megtron4", + "conductivity": 0.0, + "dielectric_loss_tangent": 0.005, + "magnetic_loss_tangent": 0.0, + "mass_density": 0.0, + "permittivity": 3.77, + "permeability": 0.0, + "poisson_ratio": 0.0, + "specific_heat": 0.0, + "thermal_conductivity": 0.0, + "youngs_modulus": 0.0, + "thermal_expansion_coefficient": 0.0, + "dc_conductivity": None, + "dc_permittivity": None, + "dielectric_model_frequency": None, + "loss_tangent_at_frequency": None, + "permittivity_at_frequency": None, + } + LAYER_DE_2 = { + "name": "DE2", + "color": [128, 128, 128], + "type": "dielectric", + "material": "Megtron4_2", + "dielectric_fill": None, + "thickness": 8.8e-05, + "etch_factor": 0.0, + "roughness_enabled": False, + "top_hallhuray_nodule_radius": 0.0, + "top_hallhuray_surface_ratio": 0.0, + "bottom_hallhuray_nodule_radius": 0.0, + "bottom_hallhuray_surface_ratio": 0.0, + "side_hallhuray_nodule_radius": 0.0, + "side_hallhuray_surface_ratio": 0.0, + "upper_elevation": 0.0, + "lower_elevation": 0.0, + } + source_path = os.path.join(local_path, "example_models", test_subfolder, "ANSYS-HSD_V1.aedb") + edbapp = Edb(source_path, edbversion=desktop_version) + json_path = os.path.join(self.local_scratch.path, "exported_stackup.json") + + assert edbapp.stackup.export(json_path) + with open(json_path, "r") as json_file: + data = json.load(json_file) + # Check material + assert MATERIAL_MEGTRON_4 == data["materials"]["Megtron4"] + # Check layer + assert LAYER_DE_2 == data["layers"]["DE2"] + edbapp.close() + + def test_stackup_load_xml(self, edb_examples): + # TODO + # edbapp = edb_examples.get_si_verse() + # assert edbapp.stackup.load(os.path.join(local_path, "example_models",test_subfolder, "ansys_pcb_stackup.xml")) + # assert "Inner1" in list(edbapp.stackup.layers.keys()) # Renamed layer + # assert "DE1" not in edbapp.stackup.layers.keys() # Removed layer + # assert edbapp.stackup.export(os.path.join(self.local_scratch.path, "stackup.xml")) + # assert round(edbapp.stackup.signal_layers["1_Top"].thickness, 6) == 3.5e-5 + pass + + def test_stackup_load_layer_renamed(self): + """Import stackup from a file.""" + source_path = os.path.join(local_path, "example_models", test_subfolder, "ANSYS-HSD_V1.aedb") + fpath = os.path.join(local_path, "example_models", test_subfolder, "stackup_renamed.json") + edbapp = Edb(source_path, edbversion=desktop_version) + edbapp.stackup.load(fpath, rename=True) + assert "1_Top_renamed" in edbapp.stackup.layers + assert "DE1_renamed" in edbapp.stackup.layers + assert "16_Bottom_renamed" in edbapp.stackup.layers + edbapp.close() + + def test_stackup_place_in_3d_with_flipped_stackup(self): + """Place into another cell using 3d placement method with and + without flipping the current layer stackup. + """ + edb_path = os.path.join(self.target_path2, "edb.def") + edb1 = Edb(edb_path, edbversion=desktop_version) + + edb2 = Edb(self.target_path, edbversion=desktop_version) + assert edb2.stackup.place_in_layout_3d_placement( + edb1, + angle=0.0, + offset_x="41.783mm", + offset_y="35.179mm", + flipped_stackup=False, + place_on_top=False, + solder_height=0.0, + ) + edb2.close() + edb2 = Edb(self.target_path, edbversion=desktop_version) + assert edb2.stackup.place_in_layout_3d_placement( + edb1, + angle=0.0, + offset_x="41.783mm", + offset_y="35.179mm", + flipped_stackup=True, + place_on_top=False, + solder_height=0.0, + ) + edb2.close() + edb2 = Edb(self.target_path, edbversion=desktop_version) + assert edb2.stackup.place_in_layout_3d_placement( + edb1, + angle=0.0, + offset_x="41.783mm", + offset_y="35.179mm", + flipped_stackup=False, + place_on_top=True, + solder_height=0.0, + ) + edb2.close() + edb2 = Edb(self.target_path, edbversion=desktop_version) + assert edb2.stackup.place_in_layout_3d_placement( + edb1, + angle=0.0, + offset_x="41.783mm", + offset_y="35.179mm", + flipped_stackup=True, + place_on_top=True, + solder_height=0.0, + ) + edb2.close() + edb1.close() + + def test_stackup_place_instance_with_flipped_stackup(self): + """Place into another cell using 3d placement method with and + without flipping the current layer stackup. + """ + edb_path = os.path.join(self.target_path2, "edb.def") + edb1 = Edb(edb_path, edbversion=desktop_version) + + edb2 = Edb(self.target_path, edbversion=desktop_version) + assert edb1.stackup.place_instance( + edb2, + angle=0.0, + offset_x="41.783mm", + offset_y="35.179mm", + flipped_stackup=False, + place_on_top=False, + solder_height=0.0, + ) + assert edb1.stackup.place_instance( + edb2, + angle=0.0, + offset_x="41.783mm", + offset_y="35.179mm", + flipped_stackup=True, + place_on_top=False, + solder_height=0.0, + ) + assert edb1.stackup.place_instance( + edb2, + angle=0.0, + offset_x="41.783mm", + offset_y="35.179mm", + flipped_stackup=False, + place_on_top=True, + solder_height=0.0, + ) + assert edb1.stackup.place_instance( + edb2, + angle=0.0, + offset_x="41.783mm", + offset_y="35.179mm", + flipped_stackup=True, + place_on_top=True, + solder_height=0.0, + ) + edb2.close() + edb1.close() + + def test_stackup_place_in_layout_with_flipped_stackup(self): + """Place into another cell using layer placement method with and + without flipping the current layer stackup. + """ + # TODO + # edb2 = Edb(self.target_path, edbversion=desktop_version) + # assert edb2.stackup.place_in_layout( + # self.edbapp, + # angle=0.0, + # offset_x="41.783mm", + # offset_y="35.179mm", + # flipped_stackup=True, + # place_on_top=True, + # ) + # edb2.close() + pass + + def test_stackup_place_on_top_of_lam_with_mold(self): + """Place on top lam with mold using 3d placement method""" + laminateEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "lam_with_mold.aedb"), + edbversion=desktop_version, + ) + chipEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "chip.aedb"), + edbversion=desktop_version, + ) + try: + cellInstances = laminateEdb.layout.cell_instances + assert len(cellInstances) == 0 + assert chipEdb.stackup.place_in_layout_3d_placement( + laminateEdb, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + flipped_stackup=False, + place_on_top=True, + ) + merged_cell = chipEdb.edb_api.cell.cell.FindByName( + chipEdb.active_db, chipEdb.edb_api.cell.CellType.CircuitCell, "lam_with_mold" + ) + assert not merged_cell.IsNull() + layout = merged_cell.GetLayout() + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 1 + cellInstance = cellInstances[0] + assert cellInstance.Is3DPlacement() + if desktop_version > "2023.1": + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + _, + ) = cellInstance.Get3DTransformation() + else: + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + ) = cellInstance.Get3DTransformation() + assert res + zeroValue = chipEdb.edb_value(0) + originPoint = chipEdb.point_3d(0.0, 0.0, 0.0) + xAxisPoint = chipEdb.point_3d(1.0, 0.0, 0.0) + assert localOrigin.IsEqual(originPoint) + assert rotAxisFrom.IsEqual(xAxisPoint) + assert rotAxisTo.IsEqual(xAxisPoint) + assert angle.IsEqual(zeroValue) + assert loc.IsEqual(chipEdb.point_3d(0.0, 0.0, chipEdb.edb_value(170e-6))) + finally: + chipEdb.close() + laminateEdb.close() + + def test_stackup_place_on_bottom_of_lam_with_mold(self): + """Place on lam with mold using 3d placement method""" + + laminateEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "lam_with_mold.aedb"), + edbversion=desktop_version, + ) + chipEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "chip_flipped_stackup.aedb"), + edbversion=desktop_version, + ) + try: + layout = laminateEdb.active_layout + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 0 + assert chipEdb.stackup.place_in_layout_3d_placement( + laminateEdb, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + flipped_stackup=False, + place_on_top=False, + ) + merged_cell = chipEdb.edb_api.cell.cell.FindByName( + chipEdb.active_db, chipEdb.edb_api.cell.CellType.CircuitCell, "lam_with_mold" + ) + assert not merged_cell.IsNull() + layout = merged_cell.GetLayout() + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 1 + cellInstance = cellInstances[0] + assert cellInstance.Is3DPlacement() + if desktop_version > "2023.1": + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + _, + ) = cellInstance.Get3DTransformation() + else: + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + ) = cellInstance.Get3DTransformation() + assert res + zeroValue = chipEdb.edb_value(0) + originPoint = chipEdb.point_3d(0.0, 0.0, 0.0) + xAxisPoint = chipEdb.point_3d(1.0, 0.0, 0.0) + assert localOrigin.IsEqual(originPoint) + assert rotAxisFrom.IsEqual(xAxisPoint) + assert rotAxisTo.IsEqual(xAxisPoint) + assert angle.IsEqual(zeroValue) + assert loc.IsEqual(chipEdb.point_3d(0.0, 0.0, chipEdb.edb_value(-90e-6))) + finally: + chipEdb.close() + laminateEdb.close() + + def test_stackup_place_on_top_of_lam_with_mold_solder(self): + """Place on top of lam with mold solder using 3d placement method.""" + laminateEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "lam_with_mold.aedb"), + edbversion=desktop_version, + ) + chipEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "chip_solder.aedb"), + edbversion=desktop_version, + ) + try: + layout = laminateEdb.active_layout + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 0 + assert chipEdb.stackup.place_in_layout_3d_placement( + laminateEdb, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + flipped_stackup=False, + place_on_top=True, + ) + merged_cell = chipEdb.edb_api.cell.cell.FindByName( + chipEdb.active_db, chipEdb.edb_api.cell.CellType.CircuitCell, "lam_with_mold" + ) + assert not merged_cell.IsNull() + layout = merged_cell.GetLayout() + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 1 + cellInstance = cellInstances[0] + assert cellInstance.Is3DPlacement() + if desktop_version > "2023.1": + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + _, + ) = cellInstance.Get3DTransformation() + else: + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + ) = cellInstance.Get3DTransformation() + assert res + zeroValue = chipEdb.edb_value(0) + oneValue = chipEdb.edb_value(1) + originPoint = chipEdb.edb_api.geometry.point3d_data(zeroValue, zeroValue, zeroValue) + xAxisPoint = chipEdb.edb_api.geometry.point3d_data(oneValue, zeroValue, zeroValue) + assert localOrigin.IsEqual(originPoint) + assert rotAxisFrom.IsEqual(xAxisPoint) + assert rotAxisTo.IsEqual(xAxisPoint) + assert angle.IsEqual(zeroValue) + assert loc.IsEqual(chipEdb.edb_api.geometry.point3d_data(zeroValue, zeroValue, chipEdb.edb_value(190e-6))) + finally: + chipEdb.close() + laminateEdb.close() + + def test_stackup_place_on_bottom_of_lam_with_mold_solder(self): + """Place on bottom of lam with mold solder using 3d placement method.""" + + laminateEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "lam_with_mold.aedb"), + edbversion=desktop_version, + ) + chipEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "chip_solder.aedb"), + edbversion=desktop_version, + ) + try: + layout = laminateEdb.active_layout + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 0 + assert chipEdb.stackup.place_in_layout_3d_placement( + laminateEdb, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + flipped_stackup=True, + place_on_top=False, + ) + merged_cell = chipEdb.edb_api.cell.cell.FindByName( + chipEdb.active_db, chipEdb.edb_api.cell.CellType.CircuitCell, "lam_with_mold" + ) + assert not merged_cell.IsNull() + layout = merged_cell.GetLayout() + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 1 + cellInstance = cellInstances[0] + assert cellInstance.Is3DPlacement() + if desktop_version > "2023.1": + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + _, + ) = cellInstance.Get3DTransformation() + else: + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + ) = cellInstance.Get3DTransformation() + assert res + zeroValue = chipEdb.edb_value(0) + oneValue = chipEdb.edb_value(1) + originPoint = chipEdb.edb_api.geometry.point3d_data(zeroValue, zeroValue, zeroValue) + xAxisPoint = chipEdb.edb_api.geometry.point3d_data(oneValue, zeroValue, zeroValue) + assert localOrigin.IsEqual(originPoint) + assert rotAxisFrom.IsEqual(xAxisPoint) + assert rotAxisTo.IsEqual(xAxisPoint) + assert angle.IsEqual(chipEdb.edb_value(math.pi)) + assert loc.IsEqual(chipEdb.edb_api.geometry.point3d_data(zeroValue, zeroValue, chipEdb.edb_value(-20e-6))) + finally: + chipEdb.close() + laminateEdb.close() + + def test_stackup_place_on_top_with_zoffset_chip(self): + """Place on top of lam with mold chip zoffset using 3d placement method.""" + laminateEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "lam_with_mold.aedb"), + edbversion=desktop_version, + ) + chipEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "chip_zoffset.aedb"), + edbversion=desktop_version, + ) + try: + layout = laminateEdb.active_layout + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 0 + assert chipEdb.stackup.place_in_layout_3d_placement( + laminateEdb, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + flipped_stackup=False, + place_on_top=True, + ) + merged_cell = chipEdb.edb_api.cell.cell.FindByName( + chipEdb.active_db, chipEdb.edb_api.cell.CellType.CircuitCell, "lam_with_mold" + ) + assert not merged_cell.IsNull() + layout = merged_cell.GetLayout() + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 1 + cellInstance = cellInstances[0] + assert cellInstance.Is3DPlacement() + if desktop_version > "2023.1": + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + _, + ) = cellInstance.Get3DTransformation() + else: + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + ) = cellInstance.Get3DTransformation() + assert res + zeroValue = chipEdb.edb_value(0) + oneValue = chipEdb.edb_value(1) + originPoint = chipEdb.edb_api.geometry.point3d_data(zeroValue, zeroValue, zeroValue) + xAxisPoint = chipEdb.edb_api.geometry.point3d_data(oneValue, zeroValue, zeroValue) + assert localOrigin.IsEqual(originPoint) + assert rotAxisFrom.IsEqual(xAxisPoint) + assert rotAxisTo.IsEqual(xAxisPoint) + assert angle.IsEqual(zeroValue) + assert loc.IsEqual(chipEdb.edb_api.geometry.point3d_data(zeroValue, zeroValue, chipEdb.edb_value(160e-6))) + finally: + chipEdb.close() + laminateEdb.close() + + def test_stackup_place_on_bottom_with_zoffset_chip(self): + """Place on bottom of lam with mold chip zoffset using 3d placement method.""" + + laminateEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "lam_with_mold.aedb"), + edbversion=desktop_version, + ) + chipEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "chip_zoffset.aedb"), + edbversion=desktop_version, + ) + try: + layout = laminateEdb.active_layout + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 0 + assert chipEdb.stackup.place_in_layout_3d_placement( + laminateEdb, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + flipped_stackup=True, + place_on_top=False, + ) + merged_cell = chipEdb.edb_api.cell.cell.FindByName( + chipEdb.active_db, chipEdb.edb_api.cell.CellType.CircuitCell, "lam_with_mold" + ) + assert not merged_cell.IsNull() + layout = merged_cell.GetLayout() + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 1 + cellInstance = cellInstances[0] + assert cellInstance.Is3DPlacement() + if desktop_version > "2023.1": + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + _, + ) = cellInstance.Get3DTransformation() + else: + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + ) = cellInstance.Get3DTransformation() + assert res + zeroValue = chipEdb.edb_value(0) + oneValue = chipEdb.edb_value(1) + originPoint = chipEdb.edb_api.geometry.point3d_data(zeroValue, zeroValue, zeroValue) + xAxisPoint = chipEdb.edb_api.geometry.point3d_data(oneValue, zeroValue, zeroValue) + assert localOrigin.IsEqual(originPoint) + assert rotAxisFrom.IsEqual(xAxisPoint) + assert rotAxisTo.IsEqual(xAxisPoint) + assert angle.IsEqual(chipEdb.edb_value(math.pi)) + assert loc.IsEqual(chipEdb.edb_api.geometry.point3d_data(zeroValue, zeroValue, chipEdb.edb_value(10e-6))) + finally: + chipEdb.close() + laminateEdb.close() + + def test_stackup_place_on_top_with_zoffset_solder_chip(self): + """Place on top of lam with mold chip zoffset using 3d placement method.""" + laminateEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "lam_with_mold.aedb"), + edbversion=desktop_version, + ) + chipEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "chip_zoffset_solder.aedb"), + edbversion=desktop_version, + ) + try: + layout = laminateEdb.active_layout + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 0 + assert chipEdb.stackup.place_in_layout_3d_placement( + laminateEdb, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + flipped_stackup=False, + place_on_top=True, + ) + merged_cell = chipEdb.edb_api.cell.cell.FindByName( + chipEdb.active_db, chipEdb.edb_api.cell.CellType.CircuitCell, "lam_with_mold" + ) + assert not merged_cell.IsNull() + layout = merged_cell.GetLayout() + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 1 + cellInstance = cellInstances[0] + assert cellInstance.Is3DPlacement() + if desktop_version > "2023.1": + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + _, + ) = cellInstance.Get3DTransformation() + else: + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + ) = cellInstance.Get3DTransformation() + assert res + zeroValue = chipEdb.edb_value(0) + oneValue = chipEdb.edb_value(1) + originPoint = chipEdb.edb_api.geometry.point3d_data(zeroValue, zeroValue, zeroValue) + xAxisPoint = chipEdb.edb_api.geometry.point3d_data(oneValue, zeroValue, zeroValue) + assert localOrigin.IsEqual(originPoint) + assert rotAxisFrom.IsEqual(xAxisPoint) + assert rotAxisTo.IsEqual(xAxisPoint) + assert angle.IsEqual(zeroValue) + assert loc.IsEqual(chipEdb.edb_api.geometry.point3d_data(zeroValue, zeroValue, chipEdb.edb_value(150e-6))) + finally: + chipEdb.close() + laminateEdb.close() + + def test_stackup_place_on_bottom_with_zoffset_solder_chip(self): + """Place on bottom of lam with mold chip zoffset using 3d placement method.""" + + laminateEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "lam_with_mold.aedb"), + edbversion=desktop_version, + ) + chipEdb = Edb( + os.path.join(local_path, "example_models", test_subfolder, "chip_zoffset_solder.aedb"), + edbversion=desktop_version, + ) + try: + layout = laminateEdb.active_layout + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 0 + assert chipEdb.stackup.place_in_layout_3d_placement( + laminateEdb, + angle=0.0, + offset_x=0.0, + offset_y=0.0, + flipped_stackup=True, + place_on_top=False, + ) + merged_cell = chipEdb.edb_api.cell.cell.FindByName( + chipEdb.active_db, chipEdb.edb_api.cell.CellType.CircuitCell, "lam_with_mold" + ) + assert not merged_cell.IsNull() + layout = merged_cell.GetLayout() + cellInstances = list(layout.CellInstances) + assert len(cellInstances) == 1 + cellInstance = cellInstances[0] + assert cellInstance.Is3DPlacement() + if desktop_version > "2023.1": + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + _, + ) = cellInstance.Get3DTransformation() + else: + ( + res, + localOrigin, + rotAxisFrom, + rotAxisTo, + angle, + loc, + ) = cellInstance.Get3DTransformation() + assert res + zeroValue = chipEdb.edb_value(0) + oneValue = chipEdb.edb_value(1) + originPoint = chipEdb.edb_api.geometry.point3d_data(zeroValue, zeroValue, zeroValue) + xAxisPoint = chipEdb.edb_api.geometry.point3d_data(oneValue, zeroValue, zeroValue) + assert localOrigin.IsEqual(originPoint) + assert rotAxisFrom.IsEqual(xAxisPoint) + assert rotAxisTo.IsEqual(xAxisPoint) + assert angle.IsEqual(chipEdb.edb_value(math.pi)) + assert loc.IsEqual(chipEdb.edb_api.geometry.point3d_data(zeroValue, zeroValue, chipEdb.edb_value(20e-6))) + finally: + chipEdb.close() + laminateEdb.close() + + def test_18_stackup(self): + # TODO + # def validate_material(pedb_materials, material, delta): + # pedb_mat = pedb_materials[material["name"]] + # if not material["dielectric_model_frequency"]: + # assert (pedb_mat.conductivity - material["conductivity"]) < delta + # assert (pedb_mat.permittivity - material["permittivity"]) < delta + # assert (pedb_mat.dielectric_loss_tangent - material["dielectric_loss_tangent"]) < delta + # assert (pedb_mat.permeability - material["permeability"]) < delta + # assert (pedb_mat.magnetic_loss_tangent - material["magnetic_loss_tangent"]) < delta + # assert (pedb_mat.mass_density - material["mass_density"]) < delta + # assert (pedb_mat.poisson_ratio - material["poisson_ratio"]) < delta + # assert (pedb_mat.specific_heat - material["specific_heat"]) < delta + # assert (pedb_mat.thermal_conductivity - material["thermal_conductivity"]) < delta + # assert (pedb_mat.youngs_modulus - material["youngs_modulus"]) < delta + # assert (pedb_mat.thermal_expansion_coefficient - material["thermal_expansion_coefficient"]) < delta + # if material["dc_conductivity"] is not None: + # assert (pedb_mat.dc_conductivity - material["dc_conductivity"]) < delta + # else: + # assert pedb_mat.dc_conductivity == material["dc_conductivity"] + # if material["dc_permittivity"] is not None: + # assert (pedb_mat.dc_permittivity - material["dc_permittivity"]) < delta + # else: + # assert pedb_mat.dc_permittivity == material["dc_permittivity"] + # if material["dielectric_model_frequency"] is not None: + # assert (pedb_mat.dielectric_model_frequency - material["dielectric_model_frequency"]) < delta + # else: + # assert pedb_mat.dielectric_model_frequency == material["dielectric_model_frequency"] + # if material["loss_tangent_at_frequency"] is not None: + # assert (pedb_mat.loss_tangent_at_frequency - material["loss_tangent_at_frequency"]) < delta + # else: + # assert pedb_mat.loss_tangent_at_frequency == material["loss_tangent_at_frequency"] + # if material["permittivity_at_frequency"] is not None: + # assert (pedb_mat.permittivity_at_frequency - material["permittivity_at_frequency"]) < delta + # else: + # assert pedb_mat.permittivity_at_frequency == material["permittivity_at_frequency"] + # + # import json + # + # target_path = os.path.join(local_path, "example_models", test_subfolder, "ANSYS-HSD_V1.aedb") + # out_edb = os.path.join(self.local_scratch.path, "ANSYS-HSD_V1_test.aedb") + # self.local_scratch.copyfolder(target_path, out_edb) + # json_path = os.path.join(local_path, "example_models", test_subfolder, "test_mat.json") + # edbapp = Edb(out_edb, edbversion=desktop_version) + # edbapp.stackup.load(json_path) + # edbapp.save_edb() + # delta = 1e-6 + # f = open(json_path) + # json_dict = json.load(f) + # dict_materials = json_dict["materials"] + # for material_dict in dict_materials.values(): + # validate_material(edbapp.materials, material_dict, delta) + # for k, v in json_dict.items(): + # if k == "layers": + # for layer_name, layer in v.items(): + # pedb_lay = edbapp.stackup.layers[layer_name] + # assert list(pedb_lay.color) == layer["color"] + # assert pedb_lay.type == layer["type"] + # if isinstance(layer["material"], str): + # assert pedb_lay.material.lower() == layer["material"].lower() + # else: + # assert 0 == validate_material(edbapp.materials, layer["material"], delta) + # if isinstance(layer["dielectric_fill"], str) or layer["dielectric_fill"] is None: + # assert pedb_lay.dielectric_fill == layer["dielectric_fill"] + # else: + # assert 0 == validate_material(edbapp.materials, layer["dielectric_fill"], delta) + # assert (pedb_lay.thickness - layer["thickness"]) < delta + # assert (pedb_lay.etch_factor - layer["etch_factor"]) < delta + # assert pedb_lay.roughness_enabled == layer["roughness_enabled"] + # if layer["roughness_enabled"]: + # assert (pedb_lay.top_hallhuray_nodule_radius - layer["top_hallhuray_nodule_radius"]) < delta + # assert (pedb_lay.top_hallhuray_surface_ratio - layer["top_hallhuray_surface_ratio"]) < delta + # assert ( + # pedb_lay.bottom_hallhuray_nodule_radius - layer["bottom_hallhuray_nodule_radius"] + # ) < delta + # assert ( + # pedb_lay.bottom_hallhuray_surface_ratio - layer["bottom_hallhuray_surface_ratio"] + # ) < delta + # assert (pedb_lay.side_hallhuray_nodule_radius - layer["side_hallhuray_nodule_radius"]) < delta + # assert (pedb_lay.side_hallhuray_surface_ratio - layer["side_hallhuray_surface_ratio"]) < delta + # edbapp.close() + pass + + def test_19(self, edb_examples): + edbapp = edb_examples.get_si_verse() + assert edbapp.stackup.add_layer_top(name="add_layer_top") + assert list(edbapp.stackup.layers.values())[0].name == "add_layer_top" + assert edbapp.stackup.add_layer_bottom(name="add_layer_bottom") + assert list(edbapp.stackup.layers.values())[-1].name == "add_layer_bottom" + assert edbapp.stackup.add_layer_below(name="add_layer_below", base_layer_name="1_Top") + base_layer = edbapp.stackup.layers["1_Top"] + l_id = edbapp.stackup.layers_by_id.index([base_layer.id, base_layer.name]) + assert edbapp.stackup.layers_by_id[l_id + 1][1] == "add_layer_below" + assert edbapp.stackup.add_layer_above(name="add_layer_above", base_layer_name="1_Top") + base_layer = edbapp.stackup.layers["1_Top"] + l_id = edbapp.stackup.layers_by_id.index([base_layer.id, base_layer.name]) + assert edbapp.stackup.layers_by_id[l_id - 1][1] == "add_layer_above" + edbapp.close() diff --git a/tests/grpc/system/test_emi_scanner.py b/tests/grpc/system/test_emi_scanner.py new file mode 100644 index 0000000000..709afb98a2 --- /dev/null +++ b/tests/grpc/system/test_emi_scanner.py @@ -0,0 +1,72 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to the interaction between Edb and Ipc2581 +""" + +from pathlib import Path + +import pytest + +from pyedb.misc.siw_feature_config.emc_rule_checker_settings import ( + EMCRuleCheckerSettings, +) +from tests.conftest import local_path + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch, target_path, target_path2, target_path4): + self.local_scratch = local_scratch + self.local_temp_dir = Path(self.local_scratch.path) + self.fdir_model = Path(local_path) / "example_models" / "TEDB" + print(self.local_temp_dir) + + def test_001_read_write_xml(self): + emi_scanner = EMCRuleCheckerSettings() + emi_scanner.read_xml(self.fdir_model / "emi_scanner.tgs") + emi_scanner.write_xml(self.local_temp_dir / "test_001_write_xml.tgs") + + def test_002_json(self): + emi_scanner = EMCRuleCheckerSettings() + emi_scanner.read_xml(self.fdir_model / "emi_scanner.tgs") + emi_scanner.write_json(self.local_temp_dir / "test_002_write_json.json") + + def test_003_system(self): + emi_scanner = EMCRuleCheckerSettings() + emi_scanner.add_net("CHASSIS2") + emi_scanner.add_net("LVDS_CH01_P", diff_mate_name="LVDS_CH01_N", net_type="Differential") + emi_scanner.add_component( + comp_name="U2", + comp_value="", + device_name="SQFP28X28_208", + is_clock_driver="0", + is_high_speed="0", + is_ic="1", + is_oscillator="0", + x_loc="-21.59", + y_loc="-41.91", + cap_type="Decoupling", + ) + emi_scanner.write_xml(self.local_temp_dir / "test_003.tgs") diff --git a/tests/grpc/system/test_siwave.py b/tests/grpc/system/test_siwave.py new file mode 100644 index 0000000000..c6726be675 --- /dev/null +++ b/tests/grpc/system/test_siwave.py @@ -0,0 +1,104 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import json +import os +import time + +import pytest + +from pyedb.siwave import Siwave +from tests.conftest import desktop_version, local_path + +pytestmark = [pytest.mark.unit, pytest.mark.legacy] + + +@pytest.mark.skipif(True, reason="skipping test on CI because they fail in non-graphical") +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch): + self.local_scratch = local_scratch + + def test_siwave(self): + """Create Siwave.""" + + siw = Siwave(desktop_version) + time.sleep(10) + example_project = os.path.join(local_path, "example_models", "siwave", "siw_dc.siw") + target_path = os.path.join(self.local_scratch.path, "siw_dc.siw") + self.local_scratch.copyfile(example_project, target_path) + assert siw + assert siw.close_project() + siw.open_project(target_path) + siw.run_dc_simulation() + export_report = os.path.join(siw.results_directory, "test.htm") + assert siw.export_siwave_report("DC IR Sim 3", export_report) + assert siw.export_dc_simulation_report("DC IR Sim 3", os.path.join(siw.results_directory, "test2")) + export_data = os.path.join(siw.results_directory, "test.txt") + assert siw.export_element_data("DC IR Sim 3", export_data) + export_icepak = os.path.join(siw.results_directory, "icepak.aedt") + assert siw.export_icepak_project(export_icepak, "DC IR Sim 3") + assert siw.quit_application() + + def test_configuration(self, edb_examples): + edbapp = edb_examples.get_si_verse(edbapp=False) + data = { + "ports": [ + { + "name": "CIRCUIT_X1_B8_GND", + "reference_designator": "X1", + "type": "circuit", + "positive_terminal": {"pin": "B8"}, + "negative_terminal": {"net": "GND"}, + } + ], + "operations": { + "cutout": { + "custom_extent": [ + [77, 54], + [5, 54], + [5, 20], + [77, 20], + ], + "custom_extent_units": "mm", + } + }, + } + + cfg_json = os.path.join(edb_examples.test_folder, "cfg.json") + with open(cfg_json, "w") as f: + json.dump(data, f) + + siw = Siwave(desktop_version) + siw.import_edb(edbapp) + siw.load_configuration(cfg_json) + cfg_json_2 = os.path.join(edb_examples.test_folder, "cfg2.json") + siw.export_configuration(cfg_json_2) + siw.quit_application() + with open(cfg_json_2, "r") as f: + json_data = json.load(f) + assert json_data["ports"][0]["name"] == "CIRCUIT_X1_B8_GND" + + siw = Siwave(desktop_version) + siw.import_edb(edbapp) + siw.load_configuration(cfg_json_2) + siw.quit_application() diff --git a/tests/grpc/system/test_siwave_features.py b/tests/grpc/system/test_siwave_features.py new file mode 100644 index 0000000000..d70cb6e175 --- /dev/null +++ b/tests/grpc/system/test_siwave_features.py @@ -0,0 +1,120 @@ +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests related to Edb +""" + +import os + +import pytest + +from pyedb.generic.general_methods import ET + +pytestmark = [pytest.mark.system, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, edb_examples, local_scratch, target_path, target_path2, target_path4): + self.edbapp = edb_examples.get_si_verse() + self.local_scratch = local_scratch + self.target_path = target_path + self.target_path2 = target_path2 + self.target_path4 = target_path4 + + def test_create_impedance_scan(self): + xtalk_scan = self.edbapp.siwave.create_impedance_crosstalk_scan(scan_type="impedance") + for net in list(self.edbapp.nets.signal.keys()): + xtalk_scan.impedance_scan.add_single_ended_net( + name=net, nominal_impedance=45.0, warning_threshold=40.0, violation_threshold=30.0 + ) + xtalk_scan.file_path = os.path.join(self.local_scratch.path, "test_impedance_scan.xml") + assert xtalk_scan.write_xml() + tree = ET.parse(xtalk_scan.file_path) + root = tree.getroot() + nets = [child for child in root[0] if "SingleEndedNets" in child.tag][0] + assert len(nets) == 342 + for net in nets: + net_dict = net.attrib + assert net_dict["Name"] + assert float(net_dict["NominalZ0"]) == 45.0 + assert float(net_dict["WarningThreshold"]) == 40.0 + assert float(net_dict["ViolationThreshold"]) == 30.0 + + def test_create_frequency_xtalk_scan(self): + xtalk_scan = self.edbapp.siwave.create_impedance_crosstalk_scan(scan_type="frequency_xtalk") + for net in list(self.edbapp.nets.signal.keys()): + xtalk_scan.frequency_xtalk_scan.add_single_ended_net( + name=net, + next_warning_threshold=5.0, + next_violation_threshold=8.0, + fext_warning_threshold_warning=8.0, + fext_violation_threshold=10.0, + ) + + xtalk_scan.file_path = os.path.join(self.local_scratch.path, "test_impedance_scan.xml") + assert xtalk_scan.write_xml() + tree = ET.parse(xtalk_scan.file_path) + root = tree.getroot() + nets = [child for child in root[0] if "SingleEndedNets" in child.tag][0] + assert len(nets) == 342 + for net in nets: + net_dict = net.attrib + assert net_dict["Name"] + assert float(net_dict["FEXTWarningThreshold"]) == 8.0 + assert float(net_dict["FEXTViolationThreshold"]) == 10.0 + assert float(net_dict["NEXTWarningThreshold"]) == 5.0 + assert float(net_dict["NEXTViolationThreshold"]) == 8.0 + + def test_create_time_xtalk_scan(self): + xtalk_scan = self.edbapp.siwave.create_impedance_crosstalk_scan(scan_type="time_xtalk") + for net in list(self.edbapp.nets.signal.keys()): + xtalk_scan.time_xtalk_scan.add_single_ended_net( + name=net, driver_rise_time="132ps", voltage="2.4V", driver_impedance=45.0, termination_impedance=51.0 + ) + driver_pins = [pin for pin in list(self.edbapp.components["U1"].pins.values()) if "DDR4" in pin.net_name] + receiver_pins = [pin for pin in list(self.edbapp.components["U1"].pins.values()) if pin.net_name == "GND"] + for pin in driver_pins: + xtalk_scan.time_xtalk_scan.add_driver_pins( + name=pin.name, ref_des="U1", rise_time="20ps", voltage="1.2V", impedance=120.0 + ) + for pin in receiver_pins: + xtalk_scan.time_xtalk_scan.add_receiver_pin(name=pin.name, ref_des="U1", impedance=80.0) + xtalk_scan.file_path = os.path.join(self.local_scratch.path, "test_impedance_scan.xml") + assert xtalk_scan.write_xml() + tree = ET.parse(xtalk_scan.file_path) + root = tree.getroot() + nets = [child for child in root[0] if "SingleEndedNets" in child.tag][0] + assert len(nets) == 342 + driver_pins = [child for child in root[0] if "DriverPins" in child.tag][0] + assert len(driver_pins) == 120 + receiver_pins = [child for child in root[0] if "ReceiverPins" in child.tag][0] + assert len(receiver_pins) == 403 + for net in nets: + net_dict = net.attrib + assert net_dict["Name"] + assert net_dict["DriverRiseTime"] == "132ps" + assert net_dict["Voltage"] == "2.4V" + assert float(net_dict["DriverImpedance"]) == 45.0 + assert float(net_dict["TerminationImpedance"]) == 51.0 + for pin in driver_pins: + pin_dict = pin.attrib + assert pin_dict["Name"] + assert pin_dict["RefDes"] == "U1" + assert pin_dict["DriverRiseTime"] == "20ps" + assert pin_dict["Voltage"] == "1.2V" + assert float(pin_dict["DriverImpedance"]) == 120.0 + for pin in receiver_pins: + pin_dict = pin.attrib + assert pin_dict["Name"] + assert float(pin_dict["ReceiverImpedance"]) == 80.0 diff --git a/tests/grpc/system/wave_ports.aedb/edb.def b/tests/grpc/system/wave_ports.aedb/edb.def new file mode 100644 index 0000000000..ffab73fba2 Binary files /dev/null and b/tests/grpc/system/wave_ports.aedb/edb.def differ diff --git a/tests/grpc/system/wave_ports.aedb/stride/model.index b/tests/grpc/system/wave_ports.aedb/stride/model.index new file mode 100644 index 0000000000..ce85c0d511 --- /dev/null +++ b/tests/grpc/system/wave_ports.aedb/stride/model.index @@ -0,0 +1,2 @@ +$begin 'Models' +$end 'Models' diff --git a/tests/grpc/unit/__init__.py b/tests/grpc/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/grpc/unit/conftest.py b/tests/grpc/unit/conftest.py new file mode 100644 index 0000000000..cada8e557a --- /dev/null +++ b/tests/grpc/unit/conftest.py @@ -0,0 +1,62 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +""" + +import csv +import os +from os.path import dirname + +import pytest + +example_models_path = os.path.join(dirname(dirname(dirname(os.path.realpath(__file__)))), "example_models") + +test_subfolder = "misc" + + +@pytest.fixture(scope="function", autouse=False) +def points_for_line_detection(): + csv_file = os.path.join(example_models_path, test_subfolder, "points_for_line_detection.csv") + + points = [] + with open(csv_file, mode="r") as file: + csv_reader = csv.reader(file) + for row in csv_reader: + x, y = map(float, row) + points.append((x, y)) + + return points + + +@pytest.fixture(scope="function", autouse=False) +def points_for_line_detection_135(): + csv_file = os.path.join(example_models_path, test_subfolder, "points_for_line_detection_135.csv") + + points = [] + with open(csv_file, mode="r") as file: + csv_reader = csv.reader(file) + for row in csv_reader: + x, y = map(float, row) + points.append((x, y)) + + return points diff --git a/tests/grpc/unit/test_edb.py b/tests/grpc/unit/test_edb.py new file mode 100644 index 0000000000..c563fb7d54 --- /dev/null +++ b/tests/grpc/unit/test_edb.py @@ -0,0 +1,236 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import os + +from mock import MagicMock, PropertyMock, patch +import pytest + +from pyedb.dotnet.edb import Edb +from tests.conftest import desktop_version + +pytestmark = [pytest.mark.unit, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self, local_scratch): + self.local_scratch = local_scratch + + def test_create_edb(self): + """Create EDB.""" + edb = Edb( + os.path.join(self.local_scratch.path, "temp.aedb"), + edbversion=desktop_version, + ) + assert edb + assert edb.active_layout + edb.close() + + def test_create_edb_without_path(self): + """Create EDB without path.""" + import time + + edbapp_without_path = Edb(edbversion=desktop_version, isreadonly=False) + time.sleep(2) + edbapp_without_path.close() + + def test_variables_value(self): + """Evaluate variables value.""" + from pyedb.generic.general_methods import check_numeric_equivalence + + edb = Edb( + os.path.join(self.local_scratch.path, "temp.aedb"), + edbversion=desktop_version, + ) + edb["var1"] = 0.01 + edb["var2"] = "10um" + edb["var3"] = [0.03, "test description"] + edb["$var4"] = ["1mm", "Project variable."] + edb["$var5"] = 0.1 + assert edb["var1"].value == 0.01 + assert check_numeric_equivalence(edb["var2"].value, 1.0e-5) + assert edb["var3"].value == 0.03 + assert edb["var3"].description == "test description" + assert edb["$var4"].value == 0.001 + assert edb["$var4"].description == "Project variable." + assert edb["$var5"].value == 0.1 + assert edb.project_variables["$var5"].delete() + + def test_add_design_variable(self): + """Add a variable value.""" + edb = Edb( + os.path.join(self.local_scratch.path, "temp.aedb"), + edbversion=desktop_version, + ) + is_added, _ = edb.add_design_variable("ant_length", "1cm") + assert is_added + is_added, _ = edb.add_design_variable("ant_length", "1cm") + assert not is_added + is_added, _ = edb.add_design_variable("my_parameter_default", "1mm", is_parameter=True) + assert is_added + is_added, _ = edb.add_design_variable("my_parameter_default", "1mm", is_parameter=True) + assert not is_added + is_added, _ = edb.add_design_variable("$my_project_variable", "1mm") + assert is_added + is_added, _ = edb.add_design_variable("$my_project_variable", "1mm") + assert not is_added + + def test_add_design_variable_with_setitem(self): + """Add a variable value.""" + edb = Edb( + os.path.join(self.local_scratch.path, "temp.aedb"), + edbversion=desktop_version, + ) + edb["ant_length"] = "1cm" + assert edb.variable_exists("ant_length")[0] + assert edb["ant_length"].value == 0.01 + + def test_change_design_variable_value(self): + """Change a variable value.""" + edb = Edb( + os.path.join(self.local_scratch.path, "temp.aedb"), + edbversion=desktop_version, + ) + edb.add_design_variable("ant_length", "1cm") + edb.add_design_variable("my_parameter_default", "1mm", is_parameter=True) + edb.add_design_variable("$my_project_variable", "1mm") + + is_changed, _ = edb.change_design_variable_value("ant_length", "1m") + assert is_changed + is_changed, _ = edb.change_design_variable_value("elephant_length", "1m") + assert not is_changed + is_changed, _ = edb.change_design_variable_value("my_parameter_default", "1m") + assert is_changed + is_changed, _ = edb.change_design_variable_value("$my_project_variable", "1m") + assert is_changed + is_changed, _ = edb.change_design_variable_value("$my_parameter", "1m") + assert not is_changed + + def test_change_design_variable_value_with_setitem(self): + """Change a variable value.""" + edb = Edb( + os.path.join(self.local_scratch.path, "temp.aedb"), + edbversion=desktop_version, + ) + edb["ant_length"] = "1cm" + assert edb["ant_length"].value == 0.01 + edb["ant_length"] = "2cm" + assert edb["ant_length"].value == 0.02 + + def test_create_padstack_instance(self): + """Create padstack instances.""" + edb = Edb( + os.path.join(self.local_scratch.path, "temp.aedb"), + edbversion=desktop_version, + ) + + pad_name = edb.padstacks.create( + pad_shape="Rectangle", + padstackname="pad", + x_size="350um", + y_size="500um", + holediam=0, + ) + assert pad_name == "pad" + + pad_name = edb.padstacks.create(pad_shape="Circle", padstackname="pad2", paddiam="350um", holediam="15um") + assert pad_name == "pad2" + + pad_name = edb.padstacks.create( + pad_shape="Circle", + padstackname="test2", + paddiam="400um", + holediam="200um", + antipad_shape="Rectangle", + anti_pad_x_size="700um", + anti_pad_y_size="800um", + start_layer="1_Top", + stop_layer="1_Top", + ) + pad_name == "test2" + edb.close() + + @patch("os.path.isfile") + @patch("os.unlink") + @patch( + "pyedb.dotnet.database.dotnet.database.EdbDotNet.logger", + new_callable=PropertyMock, + ) + def test_conflict_files_removal_success(self, mock_logger, mock_unlink, mock_isfile): + logger_mock = MagicMock() + mock_logger.return_value = logger_mock + mock_isfile.side_effect = lambda file: file.endswith((".aedt", ".aedt.lock")) + + edbpath = "file.edb" + aedt_file = os.path.splitext(edbpath)[0] + ".aedt" + files = [aedt_file, aedt_file + ".lock"] + _ = Edb(edbpath, remove_existing_aedt=True) + + for file in files: + mock_unlink.assert_any_call(file) + logger_mock.info.assert_any_call(f"Deleted AEDT project-related file {file}.") + + @patch("os.path.isfile") + @patch("os.unlink") + @patch( + "pyedb.dotnet.database.dotnet.database.EdbDotNet.logger", + new_callable=PropertyMock, + ) + def test_conflict_files_removal_failure(self, mock_logger, mock_unlink, mock_isfile): + logger_mock = MagicMock() + mock_logger.return_value = logger_mock + mock_isfile.side_effect = lambda file: file.endswith((".aedt", ".aedt.lock")) + mock_unlink.side_effect = Exception("Could not delete file") + + edbpath = "file.edb" + aedt_file = os.path.splitext(edbpath)[0] + ".aedt" + files = [aedt_file, aedt_file + ".lock"] + _ = Edb(edbpath, remove_existing_aedt=True) + + for file in files: + mock_unlink.assert_any_call(file) + logger_mock.info.assert_any_call(f"Failed to delete AEDT project-related file {file}.") + + @patch("os.path.isfile") + @patch("os.unlink") + @patch( + "pyedb.dotnet.database.dotnet.database.EdbDotNet.logger", + new_callable=PropertyMock, + ) + def test_conflict_files_leave_in_place(self, mock_logger, mock_unlink, mock_isfile): + logger_mock = MagicMock() + mock_logger.return_value = logger_mock + mock_isfile.side_effect = lambda file: file.endswith((".aedt", ".aedt.lock")) + mock_unlink.side_effect = Exception("Could not delete file") + + edbpath = "file.edb" + aedt_file = os.path.splitext(edbpath)[0] + ".aedt" + files = [aedt_file, aedt_file + ".lock"] + _ = Edb(edbpath) + + mock_unlink.assert_not_called() + for file in files: + logger_mock.warning.assert_any_call( + f"AEDT project-related file {file} exists and may need to be deleted before opening the EDB in HFSS 3D Layout." # noqa: E501 + ) diff --git a/tests/grpc/unit/test_edbsiwave.py b/tests/grpc/unit/test_edbsiwave.py new file mode 100644 index 0000000000..0ac34c9607 --- /dev/null +++ b/tests/grpc/unit/test_edbsiwave.py @@ -0,0 +1,42 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os + +from mock import Mock +import pytest + +from pyedb.dotnet.database.siwave import EdbSiwave + +pytestmark = [pytest.mark.unit, pytest.mark.no_licence, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self): + self.edb = Mock() + self.edb.edbpath = os.path.join(os.path.expanduser("~"), "fake_edb.aedb") + self.siwave = EdbSiwave(self.edb) + + def test_siwave_add_syz_analsyis(self): + """Add a sywave AC analysis.""" + assert self.siwave.add_siwave_syz_analysis() diff --git a/tests/grpc/unit/test_geometry_oprators.py b/tests/grpc/unit/test_geometry_oprators.py new file mode 100644 index 0000000000..e6aec94060 --- /dev/null +++ b/tests/grpc/unit/test_geometry_oprators.py @@ -0,0 +1,62 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import pytest + +from pyedb.modeler.geometry_operators import GeometryOperators as go + +pytestmark = [pytest.mark.unit, pytest.mark.no_licence, pytest.mark.legacy] + + +class TestClass: + def test_find_points_along_lines(self, points_for_line_detection): + distance_threshold = 0.015 + minimum_number_of_points = 10 + + lines, lines_idx, nppoints, nplines, nslines, nlines = go.find_points_along_lines( + points=points_for_line_detection, + minimum_number_of_points=minimum_number_of_points, + distance_threshold=distance_threshold, + return_additional_info=True, + ) + assert len(lines) == 20 + assert nppoints == 800 + assert nplines == 28 + assert nslines == 8 + assert nlines == 20 + + def test_find_points_along_lines_2(self, points_for_line_detection_135): + distance_threshold = 0.015 + minimum_number_of_points = 10 + + lines, lines_idx, nppoints, nplines, nslines, nlines = go.find_points_along_lines( + points=points_for_line_detection_135, + minimum_number_of_points=minimum_number_of_points, + distance_threshold=distance_threshold, + selected_angles=[0, 135], + return_additional_info=True, + ) + assert len(lines) == 21 + assert nppoints == 1200 + assert nplines == 24 + assert nslines == 7 + assert nlines == 21 diff --git a/tests/grpc/unit/test_materials.py b/tests/grpc/unit/test_materials.py new file mode 100644 index 0000000000..dd17079f9b --- /dev/null +++ b/tests/grpc/unit/test_materials.py @@ -0,0 +1,103 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import builtins +from unittest.mock import mock_open + +from mock import MagicMock, PropertyMock, patch +import pytest + +from pyedb.dotnet.database.materials import Materials + +pytestmark = [pytest.mark.unit, pytest.mark.no_licence, pytest.mark.legacy] + +MATERIALS = """ +$begin 'Polyflon CuFlon (tm)' + $begin 'AttachedData' + $begin 'MatAppearanceData' + property_data='appearance_data' + Red=230 + Green=225 + Blue=220 + $end 'MatAppearanceData' + $end 'AttachedData' + simple('permittivity', 2.1) + simple('dielectric_loss_tangent', 0.00045) + ModTime=1499970477 +$end 'Polyflon CuFlon (tm)' +$begin 'Water(@360K)' + $begin 'MaterialDef' + $begin 'Water(@360K)' + CoordinateSystemType='Cartesian' + BulkOrSurfaceType=1 + $begin 'PhysicsTypes' + set('Thermal') + $end 'PhysicsTypes' + $begin 'AttachedData' + $begin 'MatAppearanceData' + property_data='appearance_data' + Red=0 + Green=128 + Blue=255 + Transparency=0.8 + $end 'MatAppearanceData' + $end 'AttachedData' + thermal_conductivity='0.6743' + mass_density='967.4' + specific_heat='4206' + thermal_expansion_coeffcient='0.0006979' + $begin 'thermal_material_type' + property_type='ChoiceProperty' + Choice='Fluid' + $end 'thermal_material_type' + $begin 'clarity_type' + property_type='ChoiceProperty' + Choice='Transparent' + $end 'clarity_type' + material_refractive_index='1.333' + diffusivity='1.657e-007' + molecular_mass='0.018015' + viscosity='0.000324' + ModTime=1592011950 + $end 'Water(@360K)' + $end 'MaterialDef' +$end 'Water(@360K)' +""" + + +@patch("pyedb.dotnet.database.materials.Materials.materials", new_callable=PropertyMock) +@patch.object(builtins, "open", new_callable=mock_open, read_data=MATERIALS) +def test_materials_read_materials(mock_file_open, mock_materials_property): + """Read materials from an AMAT file.""" + mock_materials_property.return_value = ["copper"] + materials = Materials(MagicMock()) + expected_res = { + "Polyflon CuFlon (tm)": {"permittivity": 2.1, "dielectric_loss_tangent": 0.00045}, + "Water(@360K)": { + "thermal_conductivity": 0.6743, + "mass_density": 967.4, + "specific_heat": 4206.0, + "thermal_expansion_coefficient": 0.0006979, + }, + } + mats = materials.read_materials("some path") + assert mats == expected_res diff --git a/tests/grpc/unit/test_padstack.py b/tests/grpc/unit/test_padstack.py new file mode 100644 index 0000000000..fd7f3ba56a --- /dev/null +++ b/tests/grpc/unit/test_padstack.py @@ -0,0 +1,56 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from mock import MagicMock, PropertyMock, patch +import pytest + +from pyedb.dotnet.database.padstack import EdbPadstacks + +pytestmark = [pytest.mark.unit, pytest.mark.no_licence, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self): + self.padstacks = EdbPadstacks(MagicMock()) + + # for padstack_def in list(self.definitions.values()): + # if padstack_def.hole_plating_ratio <= minimum_value_to_replace: + # padstack_def.hole_plating_ratio = default_plating_ratio + # self._logger.info( + # "Padstack definition with zero plating ratio, defaulting to 20%".format(padstack_def.name) + # ) + # def test_132_via_plating_ratio_check(self): + # assert self.edbapp.padstacks.check_and_fix_via_plating() + # minimum_value_to_replace=0.0, default_plating_ratio=0.2 + + @patch("pyedb.dotnet.database.padstack.EdbPadstacks.definitions", new_callable=PropertyMock) + def test_padstack_plating_ratio_fixing(self, mock_definitions): + """Fix hole plating ratio.""" + mock_definitions.return_value = { + "definition_0": MagicMock(hole_plating_ratio=-0.1), + "definition_1": MagicMock(hole_plating_ratio=0.3), + } + assert self.padstacks["definition_0"].hole_plating_ratio == -0.1 + self.padstacks.check_and_fix_via_plating() + assert self.padstacks["definition_0"].hole_plating_ratio == 0.2 + assert self.padstacks["definition_1"].hole_plating_ratio == 0.3 diff --git a/tests/grpc/unit/test_simulation_configuration.py b/tests/grpc/unit/test_simulation_configuration.py new file mode 100644 index 0000000000..6b1f55875f --- /dev/null +++ b/tests/grpc/unit/test_simulation_configuration.py @@ -0,0 +1,76 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os + +import pytest + +from pyedb.dotnet.database.edb_data.simulation_configuration import ( + SimulationConfiguration, +) +from pyedb.generic.constants import SourceType + +pytestmark = [pytest.mark.unit, pytest.mark.no_licence, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init( + self, + local_scratch, + ): + self.local_scratch = local_scratch + + def test_simulation_configuration_export_import(self): + """Export and import simulation file.""" + sim_config = SimulationConfiguration() + assert sim_config.output_aedb is None + sim_config.output_aedb = os.path.join(self.local_scratch.path, "test.aedb") + assert sim_config.output_aedb == os.path.join(self.local_scratch.path, "test.aedb") + json_file = os.path.join(self.local_scratch.path, "test.json") + sim_config._filename = json_file + sim_config.arc_angle = "90deg" + assert sim_config.export_json(json_file) + + test_0import = SimulationConfiguration() + assert test_0import.import_json(json_file) + assert test_0import.arc_angle == "90deg" + assert test_0import._filename == json_file + + def test_simulation_configuration_add_rlc(self): + """Add voltage source.""" + sim_config = SimulationConfiguration() + sim_config.add_rlc( + "test", + r_value=1.5, + c_value=1e-13, + l_value=1e-10, + positive_node_net="test_0net", + positive_node_component="U2", + negative_node_net="neg_net", + negative_node_component="U2", + ) + assert sim_config.sources + assert sim_config.sources[0].source_type == SourceType.Rlc + assert sim_config.sources[0].r_value == 1.5 + assert sim_config.sources[0].l_value == 1e-10 + assert sim_config.sources[0].c_value == 1e-13 diff --git a/tests/grpc/unit/test_source.py b/tests/grpc/unit/test_source.py new file mode 100644 index 0000000000..c6dfb2a10c --- /dev/null +++ b/tests/grpc/unit/test_source.py @@ -0,0 +1,45 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import pytest + +from pyedb.dotnet.database.edb_data.sources import Source + +pytestmark = [pytest.mark.unit, pytest.mark.no_licence, pytest.mark.legacy] + + +class TestClass: + # @pytest.fixture(autouse=True) + # def init(self,local_scratch,): + # self.local_scratch = local_scratch + + def test_source_change_values(self): + """Create source and change its values""" + source = Source() + source.l_value = 1e-9 + assert source.l_value == 1e-9 + source.r_value = 1.3 + assert source.r_value == 1.3 + source.c_value = 1e-13 + assert source.c_value == 1e-13 + source.create_physical_resistor = True + assert source.create_physical_resistor diff --git a/tests/grpc/unit/test_stackup.py b/tests/grpc/unit/test_stackup.py new file mode 100644 index 0000000000..a33b438136 --- /dev/null +++ b/tests/grpc/unit/test_stackup.py @@ -0,0 +1,112 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from mock import MagicMock, PropertyMock, patch +import pytest + +from pyedb.dotnet.database.stackup import Stackup + +pytestmark = [pytest.mark.unit, pytest.mark.no_licence, pytest.mark.legacy] + + +class TestClass: + @pytest.fixture(autouse=True) + def init(self): + self.stackup = Stackup(MagicMock()) + + def test_stackup_int_to_layer_types(self): + """Evaluate mapping from integer to layer type.""" + signal_layer = self.stackup._int_to_layer_types(0) + assert signal_layer == self.stackup.layer_types.SignalLayer + dielectric_layer = self.stackup._int_to_layer_types(1) + assert dielectric_layer == self.stackup.layer_types.DielectricLayer + conducting_layer = self.stackup._int_to_layer_types(2) + assert conducting_layer == self.stackup.layer_types.ConductingLayer + airlines_layer = self.stackup._int_to_layer_types(3) + assert airlines_layer == self.stackup.layer_types.AirlinesLayer + errors_layer = self.stackup._int_to_layer_types(4) + assert errors_layer == self.stackup.layer_types.ErrorsLayer + symbol_layer = self.stackup._int_to_layer_types(5) + assert symbol_layer == self.stackup.layer_types.SymbolLayer + measure_layer = self.stackup._int_to_layer_types(6) + assert measure_layer == self.stackup.layer_types.MeasureLayer + assembly_layer = self.stackup._int_to_layer_types(8) + assert assembly_layer == self.stackup.layer_types.AssemblyLayer + silkscreen_layer = self.stackup._int_to_layer_types(9) + assert silkscreen_layer == self.stackup.layer_types.SilkscreenLayer + solder_mask_layer = self.stackup._int_to_layer_types(10) + assert solder_mask_layer == self.stackup.layer_types.SolderMaskLayer + solder_paste_layer = self.stackup._int_to_layer_types(11) + assert solder_paste_layer == self.stackup.layer_types.SolderPasteLayer + glue_layer = self.stackup._int_to_layer_types(12) + assert glue_layer == self.stackup.layer_types.GlueLayer + wirebond_layer = self.stackup._int_to_layer_types(13) + assert wirebond_layer == self.stackup.layer_types.WirebondLayer + user_layer = self.stackup._int_to_layer_types(14) + assert user_layer == self.stackup.layer_types.UserLayer + siwave_hfss_solver_regions = self.stackup._int_to_layer_types(16) + assert siwave_hfss_solver_regions == self.stackup.layer_types.SIwaveHFSSSolverRegions + outline_layer = self.stackup._int_to_layer_types(18) + assert outline_layer == self.stackup.layer_types.OutlineLayer + + def test_stackup_layer_types_to_int(self): + """Evaluate mapping from layer type to int.""" + signal_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.SignalLayer) + assert signal_layer == 0 + dielectric_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.DielectricLayer) + assert dielectric_layer == 1 + conducting_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.ConductingLayer) + assert conducting_layer == 2 + airlines_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.AirlinesLayer) + assert airlines_layer == 3 + errors_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.ErrorsLayer) + assert errors_layer == 4 + symbol_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.SymbolLayer) + assert symbol_layer == 5 + measure_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.MeasureLayer) + assert measure_layer == 6 + assembly_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.AssemblyLayer) + assert assembly_layer == 8 + silkscreen_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.SilkscreenLayer) + assert silkscreen_layer == 9 + solder_mask_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.SolderMaskLayer) + assert solder_mask_layer == 10 + solder_paste_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.SolderPasteLayer) + assert solder_paste_layer == 11 + glue_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.GlueLayer) + assert glue_layer == 12 + wirebond_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.WirebondLayer) + assert wirebond_layer == 13 + user_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.UserLayer) + assert user_layer == 14 + siwave_hfss_solver_regions = self.stackup._layer_types_to_int(self.stackup.layer_types.SIwaveHFSSSolverRegions) + assert siwave_hfss_solver_regions == 16 + outline_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.OutlineLayer) + assert outline_layer == 18 + + @patch("pyedb.dotnet.database.stackup.Stackup.layers", new_callable=PropertyMock) + def test_110_layout_tchickness(self, mock_stackup_layers): + """""" + mock_stackup_layers.return_value = {"layer": MagicMock(upper_elevation=42, lower_elevation=0)} + assert self.stackup.get_layout_thickness() == 42 + mock_stackup_layers.return_value = {"layer": MagicMock(upper_elevation=0, lower_elevation=0)} + assert self.stackup.get_layout_thickness() == 0 diff --git a/tests/legacy/system/test_edb.py b/tests/legacy/system/test_edb.py index 211440484d..45c22c2906 100644 --- a/tests/legacy/system/test_edb.py +++ b/tests/legacy/system/test_edb.py @@ -28,11 +28,11 @@ import pytest -from pyedb.dotnet.edb import Edb -from pyedb.dotnet.edb_core.edb_data.edbvalue import EdbValue -from pyedb.dotnet.edb_core.edb_data.simulation_configuration import ( +from pyedb.dotnet.database.edb_data.edbvalue import EdbValue +from pyedb.dotnet.database.edb_data.simulation_configuration import ( SimulationConfiguration, ) +from pyedb.dotnet.edb import Edb from pyedb.generic.constants import RadiationBoxType, SourceType from pyedb.generic.general_methods import is_linux, isclose from tests.conftest import desktop_version, local_path @@ -1290,7 +1290,7 @@ def test_stackup_properties(self): def test_hfss_extent_info(self): """HFSS extent information.""" - from pyedb.dotnet.edb_core.cell.primitive.primitive import Primitive + from pyedb.dotnet.database.cell.primitive.primitive import Primitive config = { "air_box_horizontal_extent_enabled": False, @@ -1330,7 +1330,7 @@ def test_hfss_extent_info(self): def test_import_gds_from_tech(self): """Use techfile.""" - from pyedb.dotnet.edb_core.edb_data.control_file import ControlFile + from pyedb.dotnet.database.edb_data.control_file import ControlFile c_file_in = os.path.join( local_path, "example_models", "cad", "GDS", "sky130_fictitious_dtc_example_control_no_map.xml" @@ -1415,7 +1415,7 @@ def test_backdrill_via_with_offset(self): def test_add_via_with_options_control_file(self): """Add new via layer with option in control file.""" - from pyedb.dotnet.edb_core.edb_data.control_file import ControlFile + from pyedb.dotnet.database.edb_data.control_file import ControlFile ctrl = ControlFile() ctrl.stackup.add_layer( @@ -1450,7 +1450,7 @@ def test_add_via_with_options_control_file(self): def test_add_layer_api_with_control_file(self): """Add new layers with control file.""" - from pyedb.dotnet.edb_core.edb_data.control_file import ControlFile + from pyedb.dotnet.database.edb_data.control_file import ControlFile ctrl = ControlFile() # Material diff --git a/tests/legacy/system/test_edb_components.py b/tests/legacy/system/test_edb_components.py index 43380a41e7..e23d1cc22d 100644 --- a/tests/legacy/system/test_edb_components.py +++ b/tests/legacy/system/test_edb_components.py @@ -277,7 +277,7 @@ def test_components_create_component_from_pins(self): def test_convert_resistor_value(self): """Convert a resistor value.""" - from pyedb.dotnet.edb_core.components import resistor_value_parser + from pyedb.dotnet.database.components import resistor_value_parser assert resistor_value_parser("100meg") diff --git a/tests/legacy/system/test_edb_configuration_1p0.py b/tests/legacy/system/test_edb_configuration_1p0.py index ed7df8cc90..e9fea9a9b6 100644 --- a/tests/legacy/system/test_edb_configuration_1p0.py +++ b/tests/legacy/system/test_edb_configuration_1p0.py @@ -24,10 +24,10 @@ import pytest -from pyedb.dotnet.edb import Edb -from pyedb.dotnet.edb_core.edb_data.simulation_configuration import ( +from pyedb.dotnet.database.edb_data.simulation_configuration import ( SimulationConfiguration, ) +from pyedb.dotnet.edb import Edb from pyedb.generic.constants import SolverType from tests.conftest import desktop_version, local_path from tests.legacy.system.conftest import test_subfolder diff --git a/tests/legacy/system/test_edb_materials.py b/tests/legacy/system/test_edb_materials.py index fc6ab5d18a..819facbb86 100644 --- a/tests/legacy/system/test_edb_materials.py +++ b/tests/legacy/system/test_edb_materials.py @@ -27,7 +27,7 @@ import pytest -from pyedb.dotnet.edb_core.materials import ( +from pyedb.dotnet.database.materials import ( PERMEABILITY_DEFAULT_VALUE, Material, MaterialProperties, diff --git a/tests/legacy/unit/test_edb.py b/tests/legacy/unit/test_edb.py index b74f24cbb6..c563fb7d54 100644 --- a/tests/legacy/unit/test_edb.py +++ b/tests/legacy/unit/test_edb.py @@ -174,7 +174,7 @@ def test_create_padstack_instance(self): @patch("os.path.isfile") @patch("os.unlink") @patch( - "pyedb.dotnet.edb_core.dotnet.database.EdbDotNet.logger", + "pyedb.dotnet.database.dotnet.database.EdbDotNet.logger", new_callable=PropertyMock, ) def test_conflict_files_removal_success(self, mock_logger, mock_unlink, mock_isfile): @@ -194,7 +194,7 @@ def test_conflict_files_removal_success(self, mock_logger, mock_unlink, mock_isf @patch("os.path.isfile") @patch("os.unlink") @patch( - "pyedb.dotnet.edb_core.dotnet.database.EdbDotNet.logger", + "pyedb.dotnet.database.dotnet.database.EdbDotNet.logger", new_callable=PropertyMock, ) def test_conflict_files_removal_failure(self, mock_logger, mock_unlink, mock_isfile): @@ -215,7 +215,7 @@ def test_conflict_files_removal_failure(self, mock_logger, mock_unlink, mock_isf @patch("os.path.isfile") @patch("os.unlink") @patch( - "pyedb.dotnet.edb_core.dotnet.database.EdbDotNet.logger", + "pyedb.dotnet.database.dotnet.database.EdbDotNet.logger", new_callable=PropertyMock, ) def test_conflict_files_leave_in_place(self, mock_logger, mock_unlink, mock_isfile): diff --git a/tests/legacy/unit/test_edbsiwave.py b/tests/legacy/unit/test_edbsiwave.py index ae21dcf309..28bd21beed 100644 --- a/tests/legacy/unit/test_edbsiwave.py +++ b/tests/legacy/unit/test_edbsiwave.py @@ -25,7 +25,7 @@ from mock import Mock import pytest -from pyedb.dotnet.edb_core.siwave import EdbSiwave +from pyedb.dotnet.database.siwave import EdbSiwave pytestmark = [pytest.mark.unit, pytest.mark.no_licence, pytest.mark.legacy] diff --git a/tests/legacy/unit/test_materials.py b/tests/legacy/unit/test_materials.py index 45814e7f5f..dd17079f9b 100644 --- a/tests/legacy/unit/test_materials.py +++ b/tests/legacy/unit/test_materials.py @@ -26,7 +26,7 @@ from mock import MagicMock, PropertyMock, patch import pytest -from pyedb.dotnet.edb_core.materials import Materials +from pyedb.dotnet.database.materials import Materials pytestmark = [pytest.mark.unit, pytest.mark.no_licence, pytest.mark.legacy] @@ -84,7 +84,7 @@ """ -@patch("pyedb.dotnet.edb_core.materials.Materials.materials", new_callable=PropertyMock) +@patch("pyedb.dotnet.database.materials.Materials.materials", new_callable=PropertyMock) @patch.object(builtins, "open", new_callable=mock_open, read_data=MATERIALS) def test_materials_read_materials(mock_file_open, mock_materials_property): """Read materials from an AMAT file.""" diff --git a/tests/legacy/unit/test_padstack.py b/tests/legacy/unit/test_padstack.py index f3cec97e47..8aa4ac5cb2 100644 --- a/tests/legacy/unit/test_padstack.py +++ b/tests/legacy/unit/test_padstack.py @@ -24,7 +24,7 @@ from mock import MagicMock, PropertyMock, patch import pytest -from pyedb.dotnet.edb_core.padstack import EdbPadstacks +from pyedb.dotnet.database.padstack import EdbPadstacks pytestmark = [pytest.mark.unit, pytest.mark.no_licence, pytest.mark.legacy] @@ -44,7 +44,7 @@ def init(self): # assert self.edbapp.padstacks.check_and_fix_via_plating() # minimum_value_to_replace=0.0, default_plating_ratio=0.2 - @patch("pyedb.dotnet.edb_core.padstack.EdbPadstacks.definitions", new_callable=PropertyMock) + @patch("pyedb.dotnet.database.padstack.EdbPadstacks.definitions", new_callable=PropertyMock) def test_padstack_plating_ratio_fixing(self, mock_definitions): """Fix hole plating ratio.""" mock_definitions.return_value = { diff --git a/tests/legacy/unit/test_simulation_configuration.py b/tests/legacy/unit/test_simulation_configuration.py index ce0efe6315..6b1f55875f 100644 --- a/tests/legacy/unit/test_simulation_configuration.py +++ b/tests/legacy/unit/test_simulation_configuration.py @@ -24,7 +24,7 @@ import pytest -from pyedb.dotnet.edb_core.edb_data.simulation_configuration import ( +from pyedb.dotnet.database.edb_data.simulation_configuration import ( SimulationConfiguration, ) from pyedb.generic.constants import SourceType diff --git a/tests/legacy/unit/test_source.py b/tests/legacy/unit/test_source.py index 472c5d6ea5..c6dfb2a10c 100644 --- a/tests/legacy/unit/test_source.py +++ b/tests/legacy/unit/test_source.py @@ -22,7 +22,7 @@ import pytest -from pyedb.dotnet.edb_core.edb_data.sources import Source +from pyedb.dotnet.database.edb_data.sources import Source pytestmark = [pytest.mark.unit, pytest.mark.no_licence, pytest.mark.legacy] diff --git a/tests/legacy/unit/test_stackup.py b/tests/legacy/unit/test_stackup.py index 27bec46cb3..a33b438136 100644 --- a/tests/legacy/unit/test_stackup.py +++ b/tests/legacy/unit/test_stackup.py @@ -23,7 +23,7 @@ from mock import MagicMock, PropertyMock, patch import pytest -from pyedb.dotnet.edb_core.stackup import Stackup +from pyedb.dotnet.database.stackup import Stackup pytestmark = [pytest.mark.unit, pytest.mark.no_licence, pytest.mark.legacy] @@ -103,7 +103,7 @@ def test_stackup_layer_types_to_int(self): outline_layer = self.stackup._layer_types_to_int(self.stackup.layer_types.OutlineLayer) assert outline_layer == 18 - @patch("pyedb.dotnet.edb_core.stackup.Stackup.layers", new_callable=PropertyMock) + @patch("pyedb.dotnet.database.stackup.Stackup.layers", new_callable=PropertyMock) def test_110_layout_tchickness(self, mock_stackup_layers): """""" mock_stackup_layers.return_value = {"layer": MagicMock(upper_elevation=42, lower_elevation=0)}