Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
891778a
padstack_instance.py
svandenb-dev Aug 12, 2025
ce002e1
cutout refactored pass #1
svandenb-dev Aug 13, 2025
60b5f85
cutout refactored pass #1
svandenb-dev Aug 13, 2025
a6d8805
cutout refactored pass #2
svandenb-dev Aug 13, 2025
1d2aa64
cutout refactored pass #2
svandenb-dev Aug 13, 2025
98079e2
cutout refactored pass #3
svandenb-dev Aug 13, 2025
7179363
cutout refactored pass #4
svandenb-dev Aug 14, 2025
577dce5
cutout refactored pass #5
svandenb-dev Aug 14, 2025
b90ff64
cutout refactored pass #6 shapely added
svandenb-dev Aug 14, 2025
c04cd14
cutout refactored pass #6 numpy
svandenb-dev Aug 14, 2025
c95ca0e
cutout refactored pass #7 numpy
svandenb-dev Aug 14, 2025
046cf43
cutout refactored pass #8 moved to utility
svandenb-dev Aug 15, 2025
825fdd9
cutout refactored pass #9 improved
svandenb-dev Aug 15, 2025
d4d0d78
cutout refactored pass #10 improved
svandenb-dev Aug 15, 2025
1f885d8
cutout refactored pass #11 numpy free (numpy does not improve)
svandenb-dev Aug 15, 2025
573bd7d
restored
svandenb-dev Aug 15, 2025
e4d294f
restored + extent with shapely added
svandenb-dev Aug 15, 2025
27acbc4
latest fixed
svandenb-dev Aug 16, 2025
5e36918
expansion factor added
svandenb-dev Aug 16, 2025
3182b43
backward compatibility pass #1
svandenb-dev Aug 16, 2025
bdf8858
backward compatibility pass #2
svandenb-dev Aug 16, 2025
f96226f
backward compatibility pass #3
svandenb-dev Aug 16, 2025
6255b85
backward compatibility pass #4
svandenb-dev Aug 16, 2025
b02c24f
backward compatibility pass #5
svandenb-dev Aug 16, 2025
a31dc16
backward compatibility pass #6
svandenb-dev Aug 16, 2025
c9b251d
backward compatibility pass #7
svandenb-dev Aug 16, 2025
fe5a674
backward compatibility pass #8
svandenb-dev Aug 25, 2025
e0c114c
Merge branch 'main' into grpc-cutout-refactoring
svandenb-dev Aug 25, 2025
1755ddc
dotnet cutout failure fixed
svandenb-dev Aug 25, 2025
96cb8e5
Merge remote-tracking branch 'origin/grpc-cutout-refactoring' into gr…
svandenb-dev Aug 25, 2025
7372722
Merge branch 'main' into grpc-cutout-refactoring
svandenb-dev Aug 26, 2025
fae351b
docstring added
svandenb-dev Sep 1, 2025
9671659
try except removed
svandenb-dev Sep 1, 2025
f593dd5
net_name replaced
svandenb-dev Sep 1, 2025
773bf78
Merge branch 'main' into grpc-cutout-refactoring
svandenb-dev Sep 1, 2025
42b6728
Merge branch 'main' into grpc-cutout-refactoring
hui-zhou-a Sep 6, 2025
0554572
Merge branch 'main' into grpc-cutout-refactoring
svandenb-dev Sep 13, 2025
5718d90
updated with modeler last changes
svandenb-dev Sep 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/pyedb/dotnet/edb.py
Original file line number Diff line number Diff line change
Expand Up @@ -2068,6 +2068,7 @@ def cutout(


"""
expansion_factor = self.value(expansion_factor)
if expansion_factor > 0:
expansion_size = self.calculate_initial_extent(expansion_factor)
if signal_list is None:
Expand Down
15 changes: 8 additions & 7 deletions src/pyedb/grpc/database/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -1328,13 +1328,14 @@ def delete_single_pin_rlc(self, deactivate_only: bool = False) -> List[str]:
"""
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 hasattr(val, "pins") and val.pins:
if val.num_pins == 1 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)))
Expand Down
199 changes: 198 additions & 1 deletion src/pyedb/grpc/database/modeler.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"""

import math
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional, Tuple, Union
import warnings

from ansys.edb.core.geometry.arc_data import ArcData as GrpcArcData
from ansys.edb.core.geometry.point_data import PointData as GrpcPointData
Expand All @@ -49,6 +50,7 @@
from pyedb.grpc.database.primitive.rectangle import Rectangle
from pyedb.grpc.database.utility.layout_statistics import LayoutStatistics
from pyedb.grpc.database.utility.value import Value
from pyedb.misc.decorators import deprecate_argument_name


class Modeler(object):
Expand Down Expand Up @@ -173,6 +175,18 @@ def _remove_primitive(self, prim: Primitive):
if not layer_dict:
self._primitives_by_layer_and_net.pop(layer_name, None)

def delete_batch_primitives(self, prim_list: List[Primitive]) -> None:
"""Delete a batch of primitives and update caches.

Parameters
----------
prim_list : list
List of primitive objects to delete.
"""
self._reload_all()
for prim in prim_list:
prim._edb_object.delete()

@property
def primitives(self) -> list[Primitive]:
if not self._primitives:
Expand Down Expand Up @@ -1484,3 +1498,186 @@ def add_void(shape: "Primitive", void_shape: Union["Primitive", List["Primitive"
if not flag:
return flag
return True

@deprecate_argument_name(
{
"signal_list": "signal_nets",
"reference_list": "reference_nets",
"expansion_size": "expansion",
"point_list": "extent_points",
}
)
def cutout(
self,
signal_nets: "list[str] | None" = None,
reference_nets: "list[str] | None" = None,
extent_points: List[Tuple[float, float]] = None,
expansion: Union[float, str] = "2mm",
extent_type: str = "bounding_box",
number_of_threads: int = None,
custom_extent_units: str = "mm",
include_partial_instances: bool = False,
check_terminals: bool = False,
preserve_components_with_model: bool = False,
simple_pad_check: bool = True,
keep_lines_as_paths: bool = False,
**kw,
) -> "list[Tuple[float, float]]":
"""
Create an EDB cutout and return the polygon that defines the resulting extent.

Two mutually exclusive modes are available:

1. Net-driven mode
Automatically derives the cutout polygon from the geometries that belong to
the requested nets.
Example:
>>> edb.cutout(
... signal_net=["DDR4_D0", "DDR4_D1"],
... reference_nets=["GND", "VDD"],
... expansion=1e-3,
... extent_type="convex_hull",
... )

2. Polygon-driven mode
Uses an explicitly supplied polygon.
Example:
>>> edb.cutout(
... point_list=[(0, 0), (5e-3, 0), (5e-3, 5e-3), ([0, 5e-3)]],
... expansion=0,
... )

Parameters
----------
signal_nets : list[str] | None, default None
Nets to be kept inside the cutout. Must be provided in net-driven mode.
reference_nets : list[str] | None, default None
Additional reference nets that are required for a meaningful cutout
(e.g., ground or supply nets). Ignored in polygon-driven mode.
extent_points : List[Tuple[float, float]] | None, default None
Sequence of (x, y) coordinates (in meters) that define the user-supplied
polygon. Must be provided in polygon-driven mode.
expansion : float or str default "2mm"
Extra margin (in meters) to enlarge the automatically computed bounding
polygon.
extent_type: {"bounding_box", "convex_hull", "conforming"}, default "bounding_box"
Strategy used to compute the extent when operating in net-driven mode. 'conforming' raises a warning
as it is not the recommended way to compute the extent and increase computational time.
number_of_threads : int | None, default None
Number of parallel threads to use while computing the cutout. If *None*,
the implementation chooses the maximum number of threads available but will be limited by python GIL.
Therefore, cpu usage does not go above 25 percent if you have multiple cores.
custom_extent_units : str, default "mm"
Unit string used when interpreting legacy ``custom_extent`` arguments
supplied through ``**kw``.
include_partial_instances : bool, default False
If *True*, any component or via that intersects the extent polygon—even
partially—is retained; otherwise only fully contained instances are kept.
check_terminals : bool, default False
If *True*, the routine verifies that every signal net terminal is still
connected after the cutout is performed and raises a warning if not.
preserve_components_with_model : bool, default False
If *True*, components that have a 3-D model are always preserved,
regardless of their location relative to the extent.
simple_pad_check : bool, default True
Use the faster, less accurate pad/via geometry check when deciding which
objects lie inside the cutout.
keep_lines_as_paths : bool, default False
If *True*, trace geometries remain as path objects instead of being
converted to polygons. This can reduce memory usage at the cost of
slightly increased geometric complexity.
**kw : dict
Legacy keyword arguments (deprecated): ``use_pyaedt_cutout``,
``remove_single_pin_components``, ``custom_extent``, ``keep_voids``,
``output_aedb_path``, etc. A warning is emitted for every legacy key
that is encountered.

Returns
-------
list[Tuple[float, float]]
The final polygon that defines the cutout extent, represented as a list
of (x, y) coordinates in meters.

Raises
------
ValueError
If neither ``signal_net`` nor ``point_list`` is provided, or if both are
provided simultaneously.

Examples
--------
Net-driven cutout with a 1 mm expansion around the convex hull of all
geometries belonging to the differential pair nets:

>>> extent = edb.cutout(
... signal_net=["USB_DP", "USB_DN"],
... reference_nets=["GND"],
... expansion="1mm",
... extent_type="convex_hull",
... )

Polygon-driven cutout using an L-shaped boundary:

>>> outline = [(0, 0), (5e-3, 0), (5e-3, 2e-3), (2e-3, 2e-3),
... (2e-3, 5e-3), (0, 5e-3])]
>>> extent = edb.cutout(point_list=outline)
"""
# TODO add support for include_partial_instances,
# TODO add support for check_terminals
# TODO add support for preserve_components_with_model
# TODO add support for simple_pad_check
# TODO add support for keep_lines_as_paths

from pyedb.grpc.database.utility.cutout import cutout_worker, extent_from_nets

if getattr(kw, "use_round_corner", None):
warnings.warn("Argument `use_round_corner` is deprecated. ", DeprecationWarning)
if getattr(kw, "use_pyaedt_cutout", None):
warnings.warn("Argument `use_pyaedt_cutout` is deprecated. ", DeprecationWarning)
if getattr(kw, "use_pyaedt_extent_computing", None):
warnings.warn("Argument `use_pyaedt_extent_computing` is deprecated. ", DeprecationWarning)
if getattr(kw, "extent_defeature", None):
warnings.warn("Argument `extent_defeature` is deprecated. ", DeprecationWarning)
if getattr(kw, "remove_single_pin_components", None):
warnings.warn("Argument `remove_single_pin_components` is deprecated. ", DeprecationWarning)
if getattr(kw, "custom_extent", None):
warnings.warn("Argument `custom_extent` is deprecated. ", DeprecationWarning)
if getattr(kw, "keep_voids", None):
warnings.warn("Argument `keep_voids` is deprecated. ", DeprecationWarning)
if getattr(kw, "include_pingroups", None):
warnings.warn("Argument `include_pingroups` is deprecated. ", DeprecationWarning)
if getattr(kw, "expansion_factor", None):
warnings.warn("Argument `expansion_factor` is deprecated. ", DeprecationWarning)
if getattr(kw, "maximum_iterations", None):
warnings.warn("Argument `maximum_iterations` is deprecated. ", DeprecationWarning)
if getattr(kw, "include_voids_in_extents", None):
warnings.warn("Argument `include_voids_in_extents` is deprecated. ", DeprecationWarning)
if getattr(kw, "output_aedb_path", None):
warnings.warn("Argument `output_aedb_path` is deprecated. ", DeprecationWarning)
if getattr(kw, "open_cutout_at_end", None):
warnings.warn("Argument `open_cutout_at_end` is deprecated. ", DeprecationWarning)

if extent_points:
if custom_extent_units:
for pt in extent_points:
if not isinstance(pt, list) or len(pt) != 2:
raise ValueError(f"Invalid point {pt} in point_list. Expected a list of [x, y] coordinates.")
extent_points = [
[Value(f"{pt[0]}{custom_extent_units}"), Value(f"{pt[1]}{custom_extent_units}")]
for pt in extent_points
]
else:
warnings.warn("No custom_extent_units provided, using default 'meter'.", UserWarning)

warnings.warn("Using polygon driven cutout mode.", UserWarning)
else:
if signal_nets and reference_nets:
extent_points = extent_from_nets(
self._pedb,
signal_nets or [],
expansion,
extent_type,
**kw,
)
cutout_worker(self._pedb, extent_points, signal_nets, reference_nets, number_of_threads, **kw)
return extent_points
35 changes: 20 additions & 15 deletions src/pyedb/grpc/database/primitive/padstack_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ def bounding_box(self) -> list[float]:
return self._bounding_box
return self._bounding_box

def in_polygon(self, polygon_data, include_partial=True) -> bool:
def in_polygon(self, polygon_data, include_partial=True, arbitrary_extent_value=300e-6) -> bool:
"""Check if padstack Instance is in given polygon data.

Parameters
Expand All @@ -416,30 +416,35 @@ def in_polygon(self, polygon_data, include_partial=True) -> bool:
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.
arbitrary_extent_value : float, optional
When ``include_partial`` is ``True``, an arbitrary value is used to create a bounding box for the padstack
instance to check for intersection and save computation time during the cutout. The default is ``300e-6``.

Returns
-------
bool
``True`` when successful, ``False`` when failed.
"""
int_val = 1 if polygon_data.point_in_polygon(GrpcPointData(self.position)) else 0
int_val = 1 if polygon_data.is_inside(GrpcPointData(self.position)) else 0
if int_val == 0:
if include_partial:
# pad-stack instance bbox is slow we take an arbitrary value e.g. 300e-6
arbitrary_value = arbitrary_extent_value
position = self.position
inst_bbox = [
position[0] - arbitrary_value / 2,
position[1] - arbitrary_value / 2,
position[0] + arbitrary_value / 2,
position[1] + arbitrary_value / 2,
]
int_val = polygon_data.intersection_type(GrpcPolygonData(inst_bbox)).value
if int_val == 0: # fully outside
return False
elif int_val in [2, 3]: # fully or partially inside
return True
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) -> str:
Expand Down
8 changes: 5 additions & 3 deletions src/pyedb/grpc/database/primitive/polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ def in_polygon(
return False

def add_void(self, polygon):
if isinstance(polygon, list) or isinstance(polygon, GrpcPolygonData):
polygon = self._pedb.modeler.create_polygon(points=polygon, layer_name=self.layer.name)
return self._edb_object.add_void(polygon)
if isinstance(polygon, list):
polygon = self._pedb.modeler.create_polygon(
points=polygon, layer_name=self.layer.name, net_name=self.net_name
)
self._edb_object.add_void(polygon)
Loading
Loading